commit 8a38081c04d53f60ab21faec763029746dbc6b1b Author: woodser Date: Tue May 4 20:20:01 2021 -0400 Bisq diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..81dc8b4ad3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +continuation_indent_size = 8 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.diff] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab + +[*.bat] +end_of_line = crlf + +[build.gradle] +continuation_indent_size = 4 + +[.idea/codeStyles/*.xml] +indent_size = 2 +insert_final_newline = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..923d54457a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Auto detect text files and normalize line endings to LF +# This will handle all files NOT found below +* text=auto +# These text files should retain Windows line endings (CRLF) +*.bat text eol=crlf +# These binary files should be left untouched +# (binary is a macro for -text -diff) +*.bmp binary +*.gif binary +*.ico binary +*.jar binary +*.jpg binary +*.jpeg binary +*.png binary +p2p/src/main/resources/*BTC_MAINNET filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..ff49e05a1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,45 @@ +--- +name: "\U0001F41B Bug report" +about: Report a bug or a technical issue + +--- + + + +### Description + + + +#### Version + + + +### Steps to reproduce + + + +### Expected behaviour + + + +### Actual behaviour + + + +### Screenshots + + + +#### Device or machine + + + + + +#### Additional info + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..43c42f28c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💡 Feature Request + url: https://github.com/bisq-network/bisq/discussions + about: Request new features or upgrades to existing tools + - name: 💬 Community Support Chat + url: https://keybase.io/team/bisq + about: Ask general questions and get community support diff --git a/.github/ISSUE_TEMPLATE/new_asset.md b/.github/ISSUE_TEMPLATE/new_asset.md new file mode 100644 index 0000000000..7f69fae833 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new_asset.md @@ -0,0 +1,18 @@ +Please fill in the following data to request for a new asset to be listed on Bisq. For more details, be sure to read [the full documentation](https://docs.bisq.network/exchange/howto/list-asset.html) on adding a new asset. + +### 1. Asset name + + +### 2. Ticker +_Your asset's ticker must not conflict with any national currency tickers (per [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)) or any of the [top 100 cryptocurrency tickers](https://coinmarketcap.com/coins/)._ + + +### 3. Block explorer URL +_Your asset's block explorer must be active and publicly available so that transactions can be verified with a receiver's address...if this isn't possible, [see workarounds here](file:///home/steve/wb/bisq/bisq-docs/build/asciidoc/html5/exchange/howto/list-asset.html#arbitrators-must-be-able-to-look-up-transactions-in-the-asset-block-explorer)._ + +### 4. Additional technical requirements (yes/no) +_Your asset should not have any additional requirements (for example, needing input fields for anything other than an address)._ + + +### 5. Initial coin offering (yes/no) +_Bisq will not list your token if it has taken part in an initial coin offering (ICO)_ diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml new file mode 100644 index 0000000000..3c462f9a6d --- /dev/null +++ b/.github/boring-cyborg.yml @@ -0,0 +1,15 @@ +labelPRBasedOnFilePath: + in:altcoins: + - assets/**/* + + is:no-priority: + - assets/**/* + +firstPRWelcomeComment: > + **Thanks for opening this pull request!**

Please check out our [contributor checklist](https://docs.bisq.network/contributor-checklist.html) and check if *Travis* or *Codacy* found any issues with your PR. Also make sure your commits are signed, and that you applied [Bisq's code style](https://github.com/bisq-network/style/issues) and [formatting](.editorconfig).

A maintainer will add an `is:priority` label to your PR if it is up for compensation. Please see our [Bisq Q1 2020 Update post](https://bisq.network/blog/q1-2020-update/) for more details. + +firstPRMergeComment: > + Awesome work, congrats on your first merged pull request! + +firstIssueWelcomeComment: > + **Thanks for opening your first issue here!**

Be sure to follow the issue template. Your issue will be reviewed by a maintainer and labeled for further action. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..16e5fa4216 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,35 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 90 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - a:bug + - re:security + - re:privacy + - re:Tor + - in:dao + - $BSQ bounty + - good first issue + - Epic + - a:feature + - is:priority +# Label to use when marking an issue as stale +staleLabel: was:dropped +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue has been automatically closed because of inactivity. + Feel free to reopen it if you think it is still relevant. + +pulls: + daysUntilStale: 30 + markComment: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..f3d52c3073 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +*/docs +*/log +*/bin +*/out +.idea +!.idea/copyright/bisq_Affero_GPLv3.xml +!.idea/copyright/profiles_settings.xml +!.idea/codeStyleSettings.xml +*.iml +*.spvchain +*.wallet +*.ser +*.log +*.sw[op] +.DS_Store +.gradle +build +.classpath +.project +.settings +*.java.hsp +*.java.hsw +*.~ava +/bundles +/bisq-* +/lib +/xchange +desktop.ini +*/target/* +*.class +deploy +*/releases/* +/monitor/TorHiddenServiceStartupTimeTests/* +/monitor/monitor-tor/* +.java-version +.localnet +/apitest/src/main/resources/dao-setup* diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..d38834b5ad --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000..79ee123c2b --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/fileTemplates/includes/File Header.java b/.idea/fileTemplates/includes/File Header.java new file mode 100644 index 0000000000..397d96c8a2 --- /dev/null +++ b/.idea/fileTemplates/includes/File Header.java @@ -0,0 +1,16 @@ +/* + * 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 . + */ \ No newline at end of file diff --git a/.idea/fileTemplates/internal/AnnotationType.java b/.idea/fileTemplates/internal/AnnotationType.java new file mode 100644 index 0000000000..471086f12f --- /dev/null +++ b/.idea/fileTemplates/internal/AnnotationType.java @@ -0,0 +1,4 @@ +#parse("File Header.java") +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +public @interface ${NAME} { +} diff --git a/.idea/fileTemplates/internal/Class.java b/.idea/fileTemplates/internal/Class.java new file mode 100644 index 0000000000..352bcb1f43 --- /dev/null +++ b/.idea/fileTemplates/internal/Class.java @@ -0,0 +1,4 @@ +#parse("File Header.java") +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +class ${NAME} { +} diff --git a/.idea/fileTemplates/internal/Enum.java b/.idea/fileTemplates/internal/Enum.java new file mode 100644 index 0000000000..cec22e643b --- /dev/null +++ b/.idea/fileTemplates/internal/Enum.java @@ -0,0 +1,4 @@ +#parse("File Header.java") +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +public enum ${NAME} { +} diff --git a/.idea/fileTemplates/internal/Interface.java b/.idea/fileTemplates/internal/Interface.java new file mode 100644 index 0000000000..aaa2a5b461 --- /dev/null +++ b/.idea/fileTemplates/internal/Interface.java @@ -0,0 +1,4 @@ +#parse("File Header.java") +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +public interface ${NAME} { +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..c214da055b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: java +jdk: + - openjdk11 + +cache: + directories: + - .git/lfs +git: + lfs_skip_smudge: true + +install: + - git lfs pull + +before_install: + grep -v '^#' assets/src/main/resources/META-INF/services/bisq.asset.Asset | sort --check --dictionary-order --ignore-case diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..c5ee421e25 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,14 @@ +# This doc specifies who gets requested to review GitHub pull requests. +# See https://help.github.com/articles/about-codeowners/. + +/core/main/java/bisq/core/dao/ @ManfredKarrer + +# For seednode configuration changes +/seednode/bisq-seednode.env @wiz +/seednode/bisq-seednode.service @wiz +/seednode/bitcoin.conf @wiz +/seednode/bitcoin.service @wiz +/seednode/docker-compose.yml @wiz +/seednode/install_seednode_debian.sh @wiz +/seednode/torrc @wiz +/seednode/uninstall_seednode_debian.sh @wiz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..c7e0a43c23 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing to Bisq + +Anyone is welcome to contribute to Bisq. This document provides an overview of how we work. If you're looking for somewhere to start contributing, check out [critical bugs](https://bisq.wiki/Critical_Bugs) or see [good first issue](https://github.com/bisq-network/bisq/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue") list. + + +## Communication Channels + +Most communication about Bisq happens on [Keybase](https://keybase.io). + +Install Keybase and enter "bisq" from the teams tab. This is an "open" team, which means the admins will auto-accept any request to join, and you can get in fast. + +Discussion about code changes happens in GitHub issues and pull requests. + +Discussion about larger changes to the way Bisq works happens in issues the [bisq-network/proposals](https://github.com/bisq-network/proposals/issues) repository. See https://docs.bisq.network/proposals.html for details. + + +## Contributor Workflow + +All Bisq contributors submit changes via pull requests. The workflow is as follows: + + - Fork the repository + - Create a topic branch from the `master` branch + - Commit patches + - Squash redundant or unnecessary commits + - Submit a pull request from your topic branch back to the `master` branch of the main repository + - Make changes to the pull request if reviewers request them and __**request a re-review**__ + +Pull requests should be focused on a single change. Do not mix, for example, refactorings with a bug fix or implementation of a new feature. This practice makes it easier for fellow contributors to review each pull request on its merits and to give a clear ACK/NACK (see below). + + +## Reviewing Pull Requests + +Bisq follows the review workflow established by the Bitcoin Core project. The following is adapted from the [Bitcoin Core contributor documentation](https://github.com/bitcoin/bitcoin/blob/master/CONTRIBUTING.md#peer-review): + +Anyone may participate in peer review which is expressed by comments in the pull request. Typically reviewers will review the code for obvious errors, as well as test out the patch set and opine on the technical merits of the patch. Project maintainers take into account the peer review when determining if there is consensus to merge a pull request (remember that discussions may have been spread out over GitHub, mailing list and IRC discussions). The following language is used within pull-request comments: + + - `ACK` means "I have tested the code and I agree it should be merged"; + - `NACK` means "I disagree this should be merged", and must be accompanied by sound technical justification. NACKs without accompanying reasoning may be disregarded; + - `utACK` means "I have not tested the code, but I have reviewed it and it looks OK, I agree it can be merged"; + - `Concept ACK` means "I agree in the general principle of this pull request"; + - `Nit` refers to trivial, often non-blocking issues. + +Please note that Pull Requests marked `NACK` and/or GitHub's `Change requested` are closed after 30 days if not addressed. + + +## Compensation + +Bisq is not a company, but operates as a _decentralized autonomous organization_ (DAO). + +Since our [Q1 2020 update](https://bisq.network/blog/q1-2020-update/) contributions are NOT eligible for compensation unless they are allocated as part of the development budget. Fixes for [critical bugs](https://bisq.wiki/Critical_Bugs) are eligible for compensation when delivered. +In any case please contact the team lead for development (@ripcurlx) upfront if you want to get compensated for your contributions. + +For any work that was approved and merged into Bisq's `master` branch, you can [submit a compensation request](https://docs.bisq.network/dao/phase-zero.html#how-to-request-compensation) and earn BSQ (the Bisq DAO native token). Learn more about the Bisq DAO and BSQ [here](https://docs.bisq.network/dao/phase-zero.html). + + +## Style and Coding Conventions + +### Configure Git user name and email metadata + +See https://help.github.com/articles/setting-your-username-in-git/ for instructions. + +### Write well-formed commit messages + +From https://chris.beams.io/posts/git-commit/#seven-rules: + + 1. Separate subject from body with a blank line + 2. Limit the subject line to 50 characters (*) + 3. Capitalize the subject line + 4. Do not end the subject line with a period + 5. Use the imperative mood in the subject line + 6. Wrap the body at 72 characters (*) + 7. Use the body to explain what and why vs. how + +*) See [here](https://stackoverflow.com/a/45563628/8340320) for how to enforce these two checks in IntelliJ IDEA. + +See also [bisq-network/style#9](https://github.com/bisq-network/style/issues/9). + +### Sign your commits with GPG + +See https://github.com/blog/2144-gpg-signature-verification for background and +https://help.github.com/articles/signing-commits-with-gpg/ for instructions. + +### Use an editor that supports Editorconfig + +The [.editorconfig](.editorconfig) settings in this repository ensure consistent management of whitespace, line endings and more. Most modern editors support it natively or with plugin. See http://editorconfig.org for details. See also [bisq-network/style#10](https://github.com/bisq-network/style/issues/10). + +### Keep the git history clean + +It's very important to keep the git history clear, light and easily browsable. This means contributors must make sure their pull requests include only meaningful commits (if they are redundant or were added after a review, they should be removed) and _no merge commits_. + +### Additional style guidelines + +See the issues in the [bisq-network/style](https://github.com/bisq-network/style/issues) repository. + + +## See also + + - [contributor checklist](https://docs.bisq.network/contributor-checklist.html) + - [developer docs](docs#readme) including build and dev environment setup instructions + - [project management process](https://bisq.wiki/Project_management) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..dba13ed2dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +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 new file mode 100644 index 0000000000..9fac1408c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,279 @@ +# +# INTRODUCTION +# +# This makefile is designed to help Bisq contributors get up and running +# as quickly as possible with a local regtest Bisq network deployment, +# or 'localnet' for short. A localnet is a complete and self-contained +# "mini Bisq network" suitable for development and end-to-end testing +# efforts. +# +# +# REQUIREMENTS +# +# You'll need the following to proceed: +# +# - Linux, macOS or similar *nix with standard tools like `make` +# - bitcoind and bitcoin-cli (`brew install bitcoin` on macOS) +# - JDK 11 to build and run Bisq binaries; see +# https://jdk.java.net/archive/ +# +# +# USAGE +# +# The following commands (and a couple manual instructions) will get +# your localnet up and running quickly. +# +# STEP 1: Build all Bisq binaries and set up localnet resources. This +# will take a few minutes the first time through. +# +# $ make +# +# Notes: +# +# - When complete, you'll have a number of scripts available in the +# root directory. They will be used in the make targets below to start +# the various Bisq seed and desktop nodes that will make up your +# localnet: +# +# $ ls -1 bisq-* +# bisq-desktop +# bisq-monitor +# bisq-pricenode +# bisq-relay +# bisq-seednode +# bisq-statsnode +# +# - You will see a new '.localnet' directory containing the data dirs +# for your regtest Bitcoin and Bisq nodes. Once you've deployed them in +# the step below, the directory will look as follows: +# +# $ tree -d -L 1 .localnet +# .localnet +# ├── alice +# ├── bitcoind +# ├── bob +# ├── mediator +# ├── seednode +# └── seednode2 +# +# STEP 2: Deploy the Bitcoin and Bisq nodes that make up the localnet. +# Run each of the following in a SEPARATE TERMINAL WINDOW, as they are +# long-running processes. +# +# $ make bitcoind +# $ make seednode +# $ make seednode2 +# $ make mediator +# $ make alice +# $ make bob +# +# Tip: Those familiar with the `screen` terminal multiplexer can +# automate the above by running the `deploy` target found below. +# +# Notes: +# +# - The 'seednode' targets launch headless Bisq nodes that help +# desktop nodes discover other peers, as well as storing and +# forwarding p2p network messages for nodes as they go on and +# offline. +# +# - As you run the 'mediator', 'alice' and 'bob' targets above, +# you'll see a Bisq desktop node window appear for each. The Alice +# and Bob instances represent two traders who can make and take +# offers with one another. The Mediator instance represents a Bisq +# contributor who can help resolve any technical problems or disputes +# that come up between the two traders. +# +# STEP 3: Configure the mediator Bisq node. In order to make and take +# offers, Alice and Bob will need to have a mediator and a refund agent +# registered on the network. Follow the instructions below to complete +# that process: +# +# a) Go to the Account screen in the Mediator instance and press CMD+D +# and a popup will appear. Click 'Unlock' and then click 'Register' to +# register the instance as a mediator. +# +# b) While still in the Account screen, press CMD+N and follow the same +# steps as above to register the instance as a refund agent. +# +# When the steps above are complete, your localnet should be up and +# ready to use. You can now test in isolation all Bisq features and use +# cases. +# + +# Set up everything necessary for deploying your localnet. This is the +# default target. +setup: build .localnet + +clean: clean-build clean-localnet + +clean-build: + ./gradlew clean + +clean-localnet: + rm -rf .localnet ./dao-setup + +# Build Bisq binaries and shell scripts used in the targets below +build: seednode/build desktop/build + +seednode/build: + ./gradlew :seednode:build + +desktop/build: + ./gradlew :desktop:build + +# Unpack and customize a Bitcoin regtest node and Alice and Bob Bisq +# nodes that have been preconfigured with a blockchain containing the +# BSQ genesis transaction +.localnet: + # Unpack the old dao-setup.zip and move things around for more + # concise and intuitive naming. This is a temporary measure until we + # clean these resources up more thoroughly. + unzip docs/dao-setup.zip + mv dao-setup .localnet + mv .localnet/Bitcoin-regtest .localnet/bitcoind + mv .localnet/bisq-BTC_REGTEST_Alice_dao .localnet/alice + mv .localnet/bisq-BTC_REGTEST_Bob_dao .localnet/bob + # Remove the preconfigured bitcoin.conf in favor of explicitly + # parameterizing the invocation of bitcoind in the target below + rm -v .localnet/bitcoind/bitcoin.conf + # Avoid spurious 'runCommand' errors in the bitcoind log when nc + # fails to bind to one of the listed block notification ports + echo exit 0 >> .localnet/bitcoind/blocknotify + +# Alias '.localnet' to 'localnet' so the target is discoverable in tab +# completion +localnet: .localnet + +# Deploy a complete localnet by running all required Bitcoin and Bisq +# nodes, each in their own named screen window. If you are not a screen +# user, you'll need to manually run each of the targets listed below +# commands manually in a separate terminal or as background jobs. +deploy: setup + # create a new screen session named 'localnet' + screen -dmS localnet + # deploy each node in its own named screen window + for target in \ + bitcoind \ + seednode \ + seednode2 \ + alice \ + bob \ + mediator; do \ + screen -S localnet -X screen -t $$target; \ + screen -S localnet -p $$target -X stuff "make $$target\n"; \ + done; + # give bitcoind rpc server time to start + sleep 5 + # generate a block to ensure Bisq nodes get dao-synced + make block + +# Undeploy a running localnet by killing all Bitcoin and Bisq +# node processes, then killing the localnet screen session altogether +undeploy: + # kill all Bitcoind and Bisq nodes running in screen windows + screen -S localnet -X at "#" stuff "^C" + # quit all screen windows which results in killing the session + screen -S localnet -X at "#" kill + +bitcoind: .localnet + bitcoind \ + -regtest \ + -prune=0 \ + -txindex=1 \ + -peerbloomfilters=1 \ + -server \ + -rpcuser=bisqdao \ + -rpcpassword=bsq \ + -datadir=.localnet/bitcoind \ + -blocknotify='.localnet/bitcoind/blocknotify %s' + +seednode: seednode/build + ./bisq-seednode \ + --baseCurrencyNetwork=BTC_REGTEST \ + --useLocalhostForP2P=true \ + --useDevPrivilegeKeys=true \ + --fullDaoNode=true \ + --rpcUser=bisqdao \ + --rpcPassword=bsq \ + --rpcBlockNotificationPort=5120 \ + --nodePort=2002 \ + --userDataDir=.localnet \ + --appName=seednode + +seednode2: seednode/build + ./bisq-seednode \ + --baseCurrencyNetwork=BTC_REGTEST \ + --useLocalhostForP2P=true \ + --useDevPrivilegeKeys=true \ + --fullDaoNode=true \ + --rpcUser=bisqdao \ + --rpcPassword=bsq \ + --rpcBlockNotificationPort=5121 \ + --nodePort=3002 \ + --userDataDir=.localnet \ + --appName=seednode2 + +mediator: desktop/build + ./bisq-desktop \ + --baseCurrencyNetwork=BTC_REGTEST \ + --useLocalhostForP2P=true \ + --useDevPrivilegeKeys=true \ + --nodePort=4444 \ + --appDataDir=.localnet/mediator \ + --appName=Mediator + +alice: setup + ./bisq-desktop \ + --baseCurrencyNetwork=BTC_REGTEST \ + --useLocalhostForP2P=true \ + --useDevPrivilegeKeys=true \ + --nodePort=5555 \ + --fullDaoNode=true \ + --rpcUser=bisqdao \ + --rpcPassword=bsq \ + --rpcBlockNotificationPort=5122 \ + --genesisBlockHeight=111 \ + --genesisTxId=30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf \ + --appDataDir=.localnet/alice \ + --appName=Alice + +bob: setup + ./bisq-desktop \ + --baseCurrencyNetwork=BTC_REGTEST \ + --useLocalhostForP2P=true \ + --useDevPrivilegeKeys=true \ + --nodePort=6666 \ + --appDataDir=.localnet/bob \ + --appName=Bob + +# Generate a new block on your Bitcoin regtest network. Requires that +# bitcoind is already running. See the `bitcoind` target above. +block: + bitcoin-cli \ + -regtest \ + -rpcuser=bisqdao \ + -rpcpassword=bsq \ + getnewaddress \ + | xargs bitcoin-cli \ + -regtest \ + -rpcuser=bisqdao \ + -rpcpassword=bsq \ + generatetoaddress 1 + +# Generate more than 1 block. +# Instead of running `make block` 24 times, +# you can now run `make blocks n=24` +blocks: + bitcoin-cli \ + -regtest \ + -rpcuser=bisqdao \ + -rpcpassword=bsq \ + getnewaddress \ + | xargs bitcoin-cli \ + -regtest \ + -rpcuser=bisqdao \ + -rpcpassword=bsq \ + generatetoaddress $(n) + +.PHONY: build seednode diff --git a/README.md b/README.md new file mode 100644 index 0000000000..b76c4d0ef9 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Bisq + +[![Build Status](https://travis-ci.org/bisq-network/bisq.svg?branch=master)](https://travis-ci.org/bisq-network/bisq) + + +## What is Bisq? + +Bisq is a safe, private and decentralized way to exchange bitcoin for national currencies and other digital assets. Bisq uses peer-to-peer networking and multi-signature escrow to facilitate trading without a third party. Bisq is non-custodial and incorporates a human arbitration system to resolve disputes. + +To learn more, see the doc and video at https://bisq.network/intro. + + +## Get started using Bisq + +Follow the step-by-step instructions at https://bisq.network/get-started. + + +## Contribute to Bisq + +See [CONTRIBUTING.md](CONTRIBUTING.md) and the [developer docs](docs/README.md). diff --git a/apitest/dao-setup.gradle b/apitest/dao-setup.gradle new file mode 100644 index 0000000000..5f55ce72e5 --- /dev/null +++ b/apitest/dao-setup.gradle @@ -0,0 +1,83 @@ +// This gradle file contains tasks to install and clean dao-setup files downloaded from +// https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip +// These tasks are not run by the default build, but they can can be run during a full +// or partial builds, or by themselves. +// To run a full Bisq clean build, test, and install dao-setup files: +// ./gradlew clean build :apitest:installDaoSetup +// To install or re-install dao-setup file only: +// ./gradlew :apitest:installDaoSetup -x test +// To clean installed dao-setup files: +// ./gradlew :apitest:cleanDaoSetup -x test +// +// The :apitest subproject will not run on Windows, and these tasks have not been +// tested on Windows. +def buildResourcesDir = project(":apitest").buildDir.path + '/resources/main' + +// This task requires ant in the system $PATH. +task installDaoSetup(dependsOn: 'cleanDaoSetup') { + doLast { + println "Installing dao-setup directories in build dir $buildResourcesDir ..." + def src = 'https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip' + def destfile = project.rootDir.path + '/apitest/src/main/resources/dao-setup.zip' + def url = new URL(src) + def f = new File(destfile) + if (f.exists()) { + println "File $destfile already exists, skipping download." + } else { + if (!f.parentFile.exists()) + mkdir "$buildResourcesDir" + + println "Downloading $url to $buildResourcesDir ..." + url.withInputStream { i -> f.withOutputStream { it << i } } + } + + // We need an ant task for unzipping the dao-setup.zip file. + println "Unzipping $destfile to $buildResourcesDir ..." + ant.unzip(src: 'src/main/resources/dao-setup.zip', + dest: 'src/main/resources', + overwrite: "true") { + // Warning: overwrite: "true" does not work if empty dirs exist, so the + // cleanDaoSetup task should be run before trying to re-install fresh + // dao-setup files. + patternset() { + include(name: '**') + exclude(name: '**/bitcoin.conf') // installed at runtime with correct blocknotify script path + exclude(name: '**/blocknotify') // installed from src/main/resources to allow port configs + } + mapper(type: "identity") + } + + // Copy files from unzip target dir 'dao-setup' to build/resources/main. + def daoSetupSrc = project.rootDir.path + '/apitest/src/main/resources/dao-setup' + def daoSetupDest = buildResourcesDir + '/dao-setup' + println "Copying $daoSetupSrc to $daoSetupDest ..." + copy { + from daoSetupSrc + into daoSetupDest + } + + // Move dao-setup files from build/resources/main/dao-setup to build/resources/main + file(buildResourcesDir + '/dao-setup/Bitcoin-regtest') + .renameTo(file(buildResourcesDir + '/Bitcoin-regtest')) + file(buildResourcesDir + '/dao-setup/bisq-BTC_REGTEST_Alice_dao') + .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Alice_dao')) + file(buildResourcesDir + '/dao-setup/bisq-BTC_REGTEST_Bob_dao') + .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Bob_dao')) + delete file(buildResourcesDir + '/dao-setup') + } +} + +task cleanDaoSetup { + doLast { + // When re-installing dao-setup files before re-running tests, the bitcoin + // datadir and dao-setup dirs have to be cleaned first. This task allows + // you to re-install dao-setup files and re-run tests without having to + // re-compile any code. + println "Deleting dao-setup directories in build dir $buildResourcesDir ..." + delete file(buildResourcesDir + '/Bitcoin-regtest') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Seed_2002') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Arb_dao') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Alice_dao') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Bob_dao') + } +} diff --git a/apitest/docs/README.md b/apitest/docs/README.md new file mode 100644 index 0000000000..1f24def3e4 --- /dev/null +++ b/apitest/docs/README.md @@ -0,0 +1,6 @@ +# Bisq apitest docs + + - [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij. + - [test-categories.md](test-categories.md): How to categorize a test case as `method`, `scenario` or `e2e`. + - [regtest-port-conflicts.md](regtest-port-conflicts.md): Avoid port conflicts when running multiple bitcoin-core apps in regtest mode. + - [api-beta-test-guide.md](api-beta-test-guide.md): How to run the test harness and tutorial script, and beta test the API with the new CLI. diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md new file mode 100644 index 0000000000..4a110a303a --- /dev/null +++ b/apitest/docs/api-beta-test-guide.md @@ -0,0 +1,499 @@ +# Bisq API Beta Testing Guide + +This guide explains how Bisq Api beta testers can quickly get a test harness running, watch a regtest trade simulation, +and use the CLI to execute trades between Bob and Alice. + +Knowledge of Git, Java, and installing bitcoin-core is required. + +## System Requirements + +**Hardware**: A reasonably fast development machine is recommended, with at least 16 Gb RAM and 8 cores. +None of the headless apps use a lot of RAM, but build times can be long on machines with less RAM and fewer cores. +In addition, a slow machine may run into race conditions if asynchronous wallet changes are not persisted to disk +fast enough. Test harness startup and shutdown times may also not happen fast enough, and require test harness +option adjustments to compensate. + +**OS**: Linux or Mac OSX + +**Shell**: Bash + +**Java SDK**: Version 10, 11, or 12 + +**Bitcoin-Core**: Version 0.19, 0.20, or 0.21 + +**Git Client** + +## Clone and Build Source Code + +Beta testing can be done with no knowledge of how git works, but you need a git client to get the source code. + +Clone the Bisq master branch into a project folder of your choice. In this document, the root project folder is +called `api-beta-test`. +``` +$ git clone https://github.com/bisq-network/bisq.git api-beta-test +``` + +Change your current working directory to `api-beta-test`, build the source, and download / install Bisq’s +pre-configured DAO / dev / regtest setup files. +``` +$ cd api-beta-test +$ ./gradlew clean build :apitest:installDaoSetup -x test # if you want to skip Bisq tests +$ ./gradlew clean build :apitest:installDaoSetup # if you want to run Bisq tests +``` + +## Running Api Test Harness + +If your bitcoin-core binaries are in your system `PATH`, start bitcoind in regtest-mode, Bisq seednode and arbitration +node daemons, plus Bob & Alice daemons in a bash terminal with the following bash command: +``` +$ ./bisq-apitest --apiPassword=xyz \ + --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \ + --shutdownAfterTests=false +``` + +If your bitcoin-core binaries are not in your system `PATH`, you can specify the bitcoin-core bin directory with the +`-–bitcoinPath=` option: +``` +$ ./bisq-apitest --apiPassword=xyz \ + --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \ + --shutdownAfterTests=false \ + --bitcoinPath=/bin +``` + +If your bitcoin-core binaries are not statically linked to your BerkleyDB library, you can specify the path to it +with the `–-berkeleyDbLibPath=` option: +``` +$ ./bisq-apitest --apiPassword=xyz \ + --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \ + --shutdownAfterTests=false \ + --bitcoinPath=/bin \ + --berkeleyDbLibPath= +``` + +Alternatively, you can specify any or all of these bisq-apitest options in a properties file located in +`apitest/src/main/resources/apitest.properties`. + +In this example, a beta tester uses the `apitest.properties` below, instead of `bisq-cli` options. +``` +supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon +apiPassword=xyz +shutdownAfterTests=false +bitcoinPath=/home/beta-tester/path-to-my-bitcoin-core/bin +``` + +Start up the test harness with without command options: +``` +$ ./bisq-apitest +``` + +If you edit `apitest.properties`, do not forget to re-build the source. You do not need to do a full clean and +build, or run tests. The following build command should finish quickly. +``` +$ ./gradlew build :apitest:installDaoSetup -x test +``` + +You should see the test harness startup bitcoin-core and other Bisq daemons in your console, run a +`bitcoin-cli getwalletinfo` command, and generate a regtest btc block. + +After the test harness tells you how to shut it down by entering `^C`, the test harness is ready to use. + +## Running Trade Simulation Script + +_Warning: again, it is assumed the beta tester has a reasonably fast machine, or the scripted wait times -- for +the other side to perform his step in the protocol, and for btc block generation and asynchronous processing +of new btc blocks by test daemons -- may not be long enough._ + +### System Requirements + +Same as described at the top of this document, but your bitcoin-core’s `bitcoin-cli` binary must be in the system +`PATH`. (The script generates regtest blocks with it.) + +### Description + +The regtest trade simulation script `apitest/scripts/trade-simulation.sh` is a useful introduction to the Bisq Api. +The bash script’s output is intended to serve as a tutorial, showing how the CLI can be used to create payment +accounts for Bob and Alice, create an offer, take the offer, and complete a trade. +(The bash script itself is not intended to be as useful as the output.) The output is generated too quickly to +follow in real time, so let the script complete before studying the output from start to finish. + +The script takes four options: +``` +-d= The trade direciton, BUY or SELL. +-c= The two letter country code, US, FR, AT, RU, etc. +-f= The offer’s fixed price. + OR (-f and -m options mutually exclusive, use one or the other) +-m= The offer’s margin (%) from market price. +-a= The amount of btc to buy or sell. +``` + +### Examples + +This simulation creates US / USD face-to-face payment accounts for Bob and Alice. Alice (always the trade maker) +creates a SELL / USD offer for the amount of 0.1 BTC, at a price 2% below the current market price. +Bob (always the taker), will use his face-to-face account to take the offer, then the two sides will complete +the trade, checking their trade status along the way, and their BSQ / BTC balances when the trade is closed. +``` +$ apitest/scripts/trade-simulation.sh -d sell -c us -m 2.00 -a 0.1 +``` + +In the next example, Bob and Alice create Austrian face-to-face payment accounts. Alice creates a BUY/ EUR +offer to buy 0.125 BTC at a fixed price of 30,800 EUR. +``` +$ apitest/scripts/trade-simulation.sh -d buy -c at -f 30800 -a 0.125 +``` + +## Manual Testing + +The test harness used by the simulation script described in the previous section can also be used for manual CLI +testing, and you can leave it running as you try the commands described below. + +The Api’s default server listening port is `9998`, and you do not need to specify a `–port=` option in a +CLI command unless you change the server’s `–apiPort=`. In the test harness, Alice’s Api port is +`9998`, Bob’s is `9999`. When you manually test the Api using the test harness, be aware of the port numbers being +used in the CLI commands, so you know which server (Bob’s or Alice’s) the CLI is sending requests to. + +### CLI Help + +Useful information can be found using the CLI’s `--help` option. + +For list of supported CLI commands: +``` +$ ./bisq-cli --help (the –password option is not needed because there is no server request) +``` + +For help with a specific CLI command: +``` +$ ./bisq-cli --password=xyz --help getbalance +OR +$ ./bisq-cli --password=xyz getbalance --help +``` +The position of `--help` option does not matter. If a supported positional command option is present, +method help will be returned from the server. Also note an api password is required to get help from the server. + +### Working With Encrypted Wallet + +There is no need to secure your regtest Bisq wallet with an encryption password when running these examples, +but you should encrypt your mainnet wallet as you probably already do when using the Bisq UI to transact in +real BTC. This section explains how to encrypt your Bisq wallet with the CLI, and unlock it before performing wallet +related operations such as creating and taking offers, checking balances, and sending BSQ and BTC to external wallets. + +Encrypt your wallet with a password: +``` +$ ./bisq-cli --password=xyz setwalletpassword --wallet-password= +``` + +Set a new password on your already encrypted wallet: +``` +$ ./bisq-cli --password=xyz setwalletpassword --wallet-password= \ + --new-wallet-password= +``` + +Unlock your password encrypted wallet for N seconds before performing sensitive wallet operations: +``` +$ ./bisq-cli --password=xyz unlockwallet --wallet-password= --timeout= +``` +You can override a `timeout` before it expires by calling `unlockwallet` again. + +Lock your wallet before the `unlockwallet` timeout expires: +``` +$ ./bisq-cli --password=xyz lockwallet +``` + +### Checking Balances + +Show full BSQ and BTC wallet balance information: +``` +$ ./bisq-cli --password=xyz --port=9998 getbalance +``` + +Show full BSQ wallet balance information: +``` +$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq +``` +_Note: The example above is asking for Bob’s balance (using port `9999`), not Alice’s balance._ + +Show Bob’s full BTC wallet balance information: +``` +$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=btc +``` + +### Funding a Bisq Wallet + +#### Receiving BTC + +To receive BTC from an external wallet, find an unused BTC address (with a zero balance) to receive the BTC. +``` +$ ./bisq-cli --password=xyz --port=9998 getfundingaddresses +``` +You can check a block explorer for the status of a transaction, or you can check your Bisq BTC wallet address directly: +``` +$ ./bisq-cli --password=xyz --port=9998 getaddressbalance --address= +``` + +#### Receiving BSQ +To receive BSQ from an external wallet, find an unused BSQ address: +``` +$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress +``` + +Give the public address to the sender. After the BSQ is sent, you can check block explorers for the status of +the transaction. There is no support (yet) to check the balance of an individual BSQ address in your wallet, +but you can check your BSQ wallet’s balance to determine if the new funds have arrived: +``` +$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq +``` + +### Sending BSQ and BTC to External Wallets + +Below are commands for sending BSQ and BTC to external wallets. + +Send BSQ: +``` +$ ./bisq-cli --password=xyz --port=9998 sendbsq --address= --amount= +``` +_Note: Sending BSQ to non-Bisq wallets is not supported and highly discouraged._ + +Send BSQ with a withdrawal transaction fee of 10 sats/byte: +``` +$ ./bisq-cli --password=xyz --port=9998 sendbsq --address= --amount= --tx-fee-rate=10 +``` + +Send BTC: +``` +$ ./bisq-cli --password=xyz --port=9998 sendbtc --address= --amount= +``` +Send BTC with a withdrawal transaction fee of 20 sats/byte: +``` +$ ./bisq-cli --password=xyz --port=9998 sendbtc --address= --amount= --tx-fee-rate=20 +``` + +### Withdrawal Transaction Fees + +If you have traded using the Bisq UI, you are probably aware of the default network bitcoin withdrawal transaction +fee and custom withdrawal transaction fee user preference in the UI’s setting view. The Api uses these same +withdrawal transaction fee rates, and affords a third – as mentioned in the previous section -- withdrawal +transaction fee option in the `sendbsq` and `sendbtc` commands. The `sendbsq` and `sendbtc` commands' +`--tx-fee-rate=` options override both the default network fee rate, and your custom transaction fee +setting for the execution of those commands. + +#### Using Default Network Transaction Fee + +If you have not set your custom withdrawal transaction fee setting, the default network transaction fee will be used +when withdrawing funds. In either case, you can check the current (default or custom) withdrawal transaction fee rate: +``` +$ ./bisq-cli --password=xyz gettxfeerate +``` +#### Setting Custom Transaction Fee Preference +To set a custom withdrawal transaction fee rate preference of 50 sats/byte: +``` +$ ./bisq-cli --password=xyz settxfeerate --tx-fee-rate=50 +``` + +#### Removing User’s Custom Transaction Fee Preference +To remove a custom withdrawal transaction fee rate preference, and revert to the network fee rate: +``` +$ ./bisq-cli --password=xyz unsettxfeerate +``` + +### Creating Test Payment Accounts + +Creating a payment account using the Api involves three steps: + +1. Find the payment-method-id for the payment account type you wish to create. For example, if you want to + create a face-to-face type payment account, find the face-to-face payment-method-id (`F2F`): + ``` + $ ./bisq-cli --password=xyz --port=9998 getpaymentmethods + ``` + +2. Use the payment-method-id `F2F` found in the `getpaymentmethods` command output to create a blank payment account + form: + ``` + $ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=F2F + ``` + This `getpaymentacctform` command generates a json file (form) for creating an `F2F` payment account, + prints the file’s contents, and tells you where it is. In this example, the sever created an `F2F` account + form named `f2f_1612381824625.json`. + + +3. Manually edit the json file, and use its path in the `createpaymentacct` command. + ``` + $ ./bisq-cli --password=xyz --port=9998 createpaymentacct \ + --payment-account-form=f2f_1612381824625.json + ``` + _Note: You can rename the file before passing it to the `createpaymentacct` command._ + + The server will create and save the new payment account from details defined in the json file then + return the new payment account to the CLI. The CLI will display the account ID with other details + in the console, but if you ever need to find a payment account ID, use the `getpaymentaccts` command: + ``` + $ ./bisq-cli --password=xyz --port=9998 getpaymentaccts + ``` + +### Creating Offers + +The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory, +and CLI `createoffer` command help gives you specific information about each option. +``` +$ ./bisq-cli --password=xyz --port=9998 createoffer --help +``` + +#### Examples + +The `trade-simulation.sh` script described above is an easy way to figure out how to use this command. +In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR, +and pay the Bisq maker fee in BSQ. Alice had already created an EUR face-to-face payment account with id +`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command: +``` +$ ./bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=BUY \ + --currency-code=EUR \ + --amount=0.125 \ + --fixed-price=30800 \ + --security-deposit=15.0 \ + --fee-currency=BSQ +``` + +If Alice was in Japan, and wanted to create an offer to sell 0.125 BTC at 0.5% above the current market JPY price, +putting up a 15% security deposit, the `createoffer` command to do that would be: +``` +$ ./bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=SELL \ + --currency-code=JPY \ + --amount=0.125 \ + --market-price-margin=0.5 \ + --security-deposit=15.0 \ + --fee-currency=BSQ +``` + +The `trade-simulation.sh` script options that would generate the previous `createoffer` example is: +``` +$ apitest/scripts/trade-simulation.sh -d sell -c jp -m 0.5 -a 0.125 +``` + +### Browsing Your Own Offers + +There are different commands to browse available offers you can take, and offers you created. + +To see all offers you created with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...): +``` +$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction= --currency-code= +``` + +To look at a specific offer you created: +``` +$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id= +``` + +### Browsing Available Offers + +To see all available offers you can take, with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...): +``` +$ ./bisq-cli --password=xyz --port=9998 getoffers --direction= --currency-code= +``` + +To look at a specific, available offer you could take: +``` +$ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id= +``` + +### Removing An Offer + +To cancel one of your offers: +``` +$ ./bisq-cli --password=xyz --port=9998 canceloffer --offer-id= +``` +The offer will be removed from other Bisq users' offer views, and paid transaction fees will be forfeited. + +### Editing an Existing Offer + +Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees +for the canceled offer will be forfeited. + +### Taking Offers + +Taking an available offer involves two CLI commands: `getoffers` and `takeoffer`. + +A CLI user browses available offers with the getoffers command. For example, the user browses SELL / EUR offers: +``` +$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=EUR +``` + +And takes one of the available offers with an EUR payment account ( id `fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e`) +with the `takeoffer` command: +``` +$ ./bisq-cli --password=xyz --port=9998 takeoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \ + --fee-currency=btc +``` +The taken offer will be used to create a trade contract. The next section describes how to use the Api to execute +the trade. + +### Completing Trade Protocol + +The first step in the Bisq trade protocol is completed when a `takeoffer` command successfully creates a new trade from +the taken offer. After the Bisq nodes prepare the trade, its status can be viewed with the `gettrade` command: +``` +$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id= +``` +The `trade-id` is the same as the taken `offer-id`, but when viewing and interacting with trades, it is referred to as +the `trade-id`. Note that the `trade-id` argument is a full `offer-id`, not a truncated `short-id` as displayed in the +Bisq UI. + +You can also view the entire trade contract in `json` format by using the `gettrade` command's `--show-contract=true` +option: +``` +$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id= --show-contract=true +``` + + +The `gettrade` command’s output shows the state of the trade from initial preparation through completion and closure. +Output columns include: +``` +Deposit Published YES if the taker fee tx deposit has been broadcast to the network. +Deposit Confirmed YES if the taker fee tx deposit has been confirmed by the network. +Fiat Sent YES if the buyer has sent a “payment started” message to seller. +Fiat Received YES if the seller has sent a “payment received” message to buyer. +Payout Published YES if the seller’s BTC payout tx has been broadcast to the network. +Withdrawn YES if the buyer’s BTC proceeds have been sent to an external wallet. +``` +Trade status information informs both sides of a trade which steps have been completed, and which step to perform next. +It should be frequently checked by both sides before proceeding to the next step of the protocol. + +_Note: There is some delay after a new trade is created due to the time it takes for a taker’s trade deposit fee +transaction to be published and confirmed on the bitcoin network. Both sides of the trade can check the `gettrade` +output's `Deposit Published` and `Deposit Confirmed` columns to find out when this early phase of the trade protocol is +complete._ + +Once the taker fee transaction has been confirmed, payment can be sent, payment receipt confirmed, and the trade +protocol completed. There are three CLI commands that must be performed in coordinated order by each side of the trade: +``` +confirmpaymentstarted Buyer sends seller a message confirming payment has been sent. +confirmpaymentreceived Seller sends buyer a message confirming payment has been received. +keepfunds Keep trade proceeds in their Bisq wallets. + OR +withdrawfunds Send trade proceeds to an external wallet. +``` +The last two mutually exclusive commands (`keepfunds` or `withdrawfunds`) may seem unnecessary, but they are critical +because they inform the Bisq node that a trade’s state can be set to `CLOSED`. Please close out your trades with one +or the other command. + +Each of the CLI commands above takes one argument: `--trade-id=`: +``` +$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id= +$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id= +$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id= +$ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id= --address= [--memo=<"memo">] +``` + +## Shutting Down Test Harness + +The test harness should cleanly shutdown all the background apps in proper order after entering ^C. + +Once shutdown, all Bisq and bitcoin-core data files are left in the state they were in at shutdown time, +so they and logs can be examined after a test run. All datafiles will be refreshed the next time the test harness +is started, so if you want to save datafiles and logs from a test run, copy them to a safe place first. +They can be found in `apitest/build/resources/main`. + diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md new file mode 100644 index 0000000000..ba9149e238 --- /dev/null +++ b/apitest/docs/build-run.md @@ -0,0 +1,90 @@ +# Build and Run API + +The Java based API runs on Linux and OSX. + +## Mainnet + +To build from the source, clone the github repository found at `https://github.com/bisq-network/bisq`, +and build with gradle: + + $ ./gradlew clean build + +To skip tests: + + $ ./gradlew clean build -x test + +To run the Bisq daemon: + + $ ./bisq-daemon --apiPassword= --apiPort= --appDataDir=$APPDIR` + +_Note: `$APPDIR` is empty or otherwise contains a Bisq wallet created by the daemon or the UI._ + +_Note: Never run the API daemon and Bisq UI on the same host, or you will corrupt your wallet. It will be possible +with specific command line options, i.e., unique appDatadir and ports, but this scenario has not been tested yet._ + +## Test Harness + +The API test harness uses the GNU Bourne-Again SHell `bash`, and is not supported on Windows. + +### Predefined DAO / Regtest Setup + +The API test harness depends on the contents of https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip. +The files contained in dao-setup.zip include a bitcoin-core wallet, a regtest genesis tx and chain of 111 blocks, plus +data directories for Bob and Alice Bisq instances. Bob & Alice wallets are pre-configured with 10 BTC each, and the +equivalent of 2.5 BTC in BSQ distributed among Bob & Alice's BSQ wallets. + +See https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md for details. + +### Install DAO / Regtest Setup Files + +Bisq's gradle build file defines a task for downloading dao-setup.zip and extracting its contents to the +`apitest/src/main/resources` folder, and the test harness will install a fresh set of data files to the +`apitest/build/resources/main` folder during a test case's scaffold setup phase -- normally a static `@BeforeAll` method. + +The dao-setup files can be downloaded during a normal build: + + $ ./gradlew clean build :apitest:installDaoSetup + +Or by running a single task: + + $ ./gradlew :apitest:installDaoSetup + +The `:apitest:installDaoSetup` task does not need to be run again until after the next time you run the gradle `clean` task. + +### Run API Tests + +The API test harness supports narrow & broad functional and full end to end test cases requiring +long setup and teardown times -- for example, to start a bitcoind instance, seednode, arbnode, plus Bob & Alice +Bisq instances, then shut everything down in proper order. For this reason, API test cases do not run during a normal +gradle build. + +To run API test cases, pass system property`-DrunApiTests=true`. + +To run all existing test cases: + + $ ./gradlew :apitest:test -DrunApiTests=true + +To run all test cases in a package: + + $ ./gradlew :apitest:test --tests "bisq.apitest.method.*" -DrunApiTests=true + +To run a single test case: + + $ ./gradlew :apitest:test --tests "bisq.apitest.scenario.WalletTest" -DrunApiTests=true + +To run test cases from Intellij, add two JVM arguments to your JUnit launchers: + + -DrunApiTests=true -Dlogback.configurationFile=apitest/build/resources/main/logback.xml + +The `-Dlogback.configurationFile` property will prevent `logback` from printing warnings about multiple `logback.xml` +files it will find in Bisq jars `cli.jar`, `daemon.jar`, and `seednode.jar`. + +### Gradle Test Reports + +To see detailed test results, logs, and full stack traces for test failures, open +`apitest/build/reports/tests/test/index.html` in a browser. + +### See also + + - [test-categories.md](test-categories.md) + diff --git a/apitest/docs/regtest-port-conflicts.md b/apitest/docs/regtest-port-conflicts.md new file mode 100644 index 0000000000..7ec3e6bf45 --- /dev/null +++ b/apitest/docs/regtest-port-conflicts.md @@ -0,0 +1,12 @@ +# Avoiding bitcoin-core regtest port conflicts + +Some developers may already be running a `bitcoind` or `bitcoin-qt` instance in regtest mode when they try to run API +test cases. If a `bitcoin-qt` instance is bound to the default regtest port 18444, `apitest` will not be able to start +its own bitcoind instances. + +Though it would be preferable for `apitest` to change the bind port for Bisq's `bitcoinj` module at runtime, this is +not currently possible because `bitcoinj` hardcodes the default regtest mode bind port in `RegTestParams`. + +To avoid the bind address:port conflict, pass a port option to your bitcoin-core instance: + + bitcoin-qt -regtest -port=20444 diff --git a/apitest/docs/test-categories.md b/apitest/docs/test-categories.md new file mode 100644 index 0000000000..ba1c095dc0 --- /dev/null +++ b/apitest/docs/test-categories.md @@ -0,0 +1,35 @@ +# API Test Categories + +This guide describes the categorization of tests. + +## Method Tests + +A `method` test is the `apitest` analog of a unit test. It tests a single API method such as `getbalance`, but is not +considered a unit test because the code execution path traverses so many layers: from `gRPC` client -> `gRPC` server +side service -> one or more Bisq `core` services, and back to the client. + +Method tests have direct access to `gRPC` client stubs, and test asserts are made directly on `gRPC` return values -- +Java Objects. + +All `method` tests are part of the `bisq.apitest.method` package. + +## Scenario Tests + +A `scenario` test is a narrow or broad functional test case covering a simple use case such as funding a wallet to a +complex series of trades. Generally, a scenario test case requires multiple `gRPC` method calls. + +Scenario tests have direct access to `gRPC` client stubs, and test asserts are made directly on `gRPC` return values -- +Java Objects. + +All `scenario` tests are part of the `bisq.apitest.scenario` package. + +## End to End Tests + +An end to end (`e2e`) test can cover a narrow or broad use case, and all client calls go through the `CLI` shell script +`bisq-cli`. End to end tests do not have access to `gRPC` client stubs, and test asserts are made on what the end +user sees on the console -- what`gRPC CLI` prints to `STDOUT`. + +As test coverage grows, stable scenario test cases should be migrated to `e2e` test cases. + +All `e2e` tests are part of the `bisq.apitest.e2e` package. + diff --git a/apitest/scripts/editf2faccountform.py b/apitest/scripts/editf2faccountform.py new file mode 100644 index 0000000000..f9849a75a3 --- /dev/null +++ b/apitest/scripts/editf2faccountform.py @@ -0,0 +1,28 @@ +import sys, os, json + +# Writes a Bisq json F2F payment account form for the given country_code to the current working directory. + +if len(sys.argv) < 2: + print("usage: editf2faccountform.py country_code") + exit(1) + +country_code = str(sys.argv[1]).upper() +acct_form = { + "_COMMENTS_": [ + "Do not manually edit the paymentMethodId field.", + "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." + ], + "paymentMethodId": "F2F", + "accountName": "Face to Face Payment Account", + "city": "Anytown", + "contact": "Me", + "country": country_code, + "extraInfo": "", + "salt": "" +} +target=os.path.dirname(os.path.realpath(__file__)) + '/' + 'f2f-acct.json' +with open (target, 'w') as outfile: + json.dump(acct_form, outfile, indent=2) + outfile.write('\n') + +exit(0) diff --git a/apitest/scripts/get-bisq-pid.sh b/apitest/scripts/get-bisq-pid.sh new file mode 100755 index 0000000000..450f1290c0 --- /dev/null +++ b/apitest/scripts/get-bisq-pid.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Find the pid of the java process by grepping for the mainClassName and appName, +# then print the 2nd column of the output to stdout. +# +# Doing this from Java is problematic, probably due to limitation of the +# apitest.linux.BashCommand implementation. + + +MAIN_CLASS_NAME=$1 +APP_NAME=$2 + +# TODO args validation + +ps aux | grep java | grep "${MAIN_CLASS_NAME}" | grep "${APP_NAME}" | awk '{print $2}' diff --git a/apitest/scripts/limit-order-simulation.sh b/apitest/scripts/limit-order-simulation.sh new file mode 100755 index 0000000000..db37d8ab94 --- /dev/null +++ b/apitest/scripts/limit-order-simulation.sh @@ -0,0 +1,159 @@ +#! /bin/bash + +# Demonstrates a way to create a limit order (offer) using the API CLI with a local regtest bitcoin node. +# +# A country code argument is used to create a country based face to face payment account for the simulated offer. +# +# Prerequisites: +# +# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, v0.21). +# +# - Bisq must be fully built with apitest dao setup files installed. +# Build command: `./gradlew clean build :apitest:installDaoSetup` +# +# - All supporting nodes must be run locally, in dev/dao/regtest mode: +# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon +# +# These should be run using the apitest harness. From the root project dir, run: +# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` +# +# - Only regtest btc can be bought or sold with the test payment account. +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# `$ apitest/scripts/limit-order-simulation.sh -l 40000 -d buy -c fr -m 3.00 -a 0.125` +# +# Script options: -l -d -c (-m || -f ) -a [-w ] +# +# Example: +# +# Create a sell/eur offer to sell 0.125 btc at a fixed-price of 38,000 euros, using a France face to face +# payment account, when the BTC market price rises to or above 40,000 EUR: +# +# `$ apitest/scripts/limit-order-simulation.sh -l 40000 -d sell -c fr -m 0.00 -a 0.125` + +APP_BASE_NAME=$(basename "$0") +APP_HOME=$(pwd -P) +APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" + +source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" +source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" + +checksetup +parselimitorderopts "$@" + +printdate "Started $APP_BASE_NAME with parameters:" +printscriptparams +printbreak + +editpaymentaccountform "$COUNTRY_CODE" +exitoncommandalert $? +cat "$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printbreak + +# Create F2F payment accounts for $COUNTRY_CODE, and get the $CURRENCY_CODE. +printdate "Creating Alice's face to face $COUNTRY_CODE payment account." +CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "ALICE CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +exitoncommandalert $? +echo "$CMD_OUTPUT" +ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +exitoncommandalert $? +CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +exitoncommandalert $? +printdate "ALICE F2F payment-account-id = $ALICE_ACCT_ID, currency-code = $CURRENCY_CODE." +printbreak + +printdate "Creating Bob's face to face $COUNTRY_CODE payment account." +CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "BOB CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +exitoncommandalert $? +echo "$CMD_OUTPUT" +BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +exitoncommandalert $? +CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +exitoncommandalert $? +printdate "BOB F2F payment-account-id = $BOB_ACCT_ID, currency-code = $CURRENCY_CODE." +printbreak + +# Bob & Alice now have matching payment accounts, now loop until the price limit is reached, then create an offer. +if [ "$DIRECTION" = "BUY" ] +then + printdate "Create a BUY / $CURRENCY_CODE offer when the market price falls to or below $LIMIT_PRICE $CURRENCY_CODE." +else + printdate "Create a SELL / $CURRENCY_CODE offer when the market price rises to or above $LIMIT_PRICE $CURRENCY_CODE." +fi + +DONE=0 +while : ; do + if [ "$DONE" -ne 0 ]; then + break + fi + + CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") + exitoncommandalert $? + printdate "Current Market Price: $CURRENT_PRICE" + + if [ "$DIRECTION" = "BUY" ] && [ "$CURRENT_PRICE" -le "$LIMIT_PRICE" ]; then + printdate "Limit price reached." + DONE=1 + break + fi + + if [ "$DIRECTION" = "SELL" ] && [ "$CURRENT_PRICE" -ge "$LIMIT_PRICE" ]; then + printdate "Limit price reached." + DONE=1 + break + fi + + sleep "$WAIT" +done + +printdate "ALICE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." +CMD="$CLI_BASE --port=$ALICE_PORT createoffer" +CMD+=" --payment-account=$ALICE_ACCT_ID" +CMD+=" --direction=$DIRECTION" +CMD+=" --currency-code=$CURRENCY_CODE" +CMD+=" --amount=$AMOUNT" +if [ -z "$MKT_PRICE_MARGIN" ]; then + CMD+=" --fixed-price=$FIXED_PRICE" +else + CMD+=" --market-price-margin=$MKT_PRICE_MARGIN" +fi +CMD+=" --security-deposit=50.0" +CMD+=" --fee-currency=BSQ" +printdate "ALICE CLI: $CMD" +OFFER_ID=$(createoffer "$CMD") +exitoncommandalert $? +printdate "ALICE: Created offer with id: $OFFER_ID." +printbreak +sleeptraced 3 + +# Show Alice's new offer. +printdate "ALICE: Looking at her new $DIRECTION $CURRENCY_CODE offer." +CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" +printdate "ALICE CLI: $CMD" +OFFER=$($CMD) +exitoncommandalert $? +echo "$OFFER" +printbreak +sleeptraced 4 + +# Generate some btc blocks. +printdate "Generating btc blocks after publishing Alice's offer." +genbtcblocks 3 3 +printbreak + +# Show Alice's offer in Bob's CLI. +printdate "BOB: Looking at $DIRECTION $CURRENCY_CODE offers." +CMD="$CLI_BASE --port=$BOB_PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" +printdate "BOB CLI: $CMD" +OFFERS=$($CMD) +exitoncommandalert $? +echo "$OFFERS" + +exit 0 diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh new file mode 100755 index 0000000000..8f2815d4d0 --- /dev/null +++ b/apitest/scripts/mainnet-test.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bats +# +# Smoke tests for bisq-cli running against a live bisq-daemon (on mainnet) +# +# Prerequisites: +# +# - bats-core 1.2.0+ must be installed (brew install bats-core on macOS) +# see https://github.com/bats-core/bats-core +# +# - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR +# is empty or otherwise contains an unencrypted wallet with a 0 BTC balance +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# bats apitest/scripts/mainnet-test.sh + +@test "test unsupported method error" { + run ./bisq-cli --password=xyz bogus + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 # printed only on test failure + [ "$output" = "Error: 'bogus' is not a supported method" ] +} + +@test "test unrecognized option error" { + run ./bisq-cli --bogus getversion + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: missing required 'password' option" ] +} + +@test "test missing required password option error" { + run ./bisq-cli getversion + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: missing required 'password' option" ] +} + +@test "test incorrect password error" { + run ./bisq-cli --password=bogus getversion + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: incorrect 'password' rpc header value" ] +} + +@test "test getversion call with quoted password" { + load 'version-parser' + run ./bisq-cli --password="xyz" getversion + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "$CURRENT_VERSION" ] +} + +@test "test getversion" { + # Wait 1 second before calling getversion again. + sleep 1 + load 'version-parser' + run ./bisq-cli --password=xyz getversion + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "$CURRENT_VERSION" ] +} + +@test "test setwalletpassword \"a b c\"" { + run ./bisq-cli --password=xyz setwalletpassword --wallet-password="a b c" + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet encrypted" ] + sleep 1 +} + +@test "test unlockwallet without password & timeout args" { + run ./bisq-cli --password=xyz unlockwallet + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no password specified" ] +} + +@test "test unlockwallet without timeout arg" { + run ./bisq-cli --password=xyz unlockwallet --wallet-password="a b c" + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no unlock timeout specified" ] +} + + +@test "test unlockwallet \"a b c\" 8" { + run ./bisq-cli --password=xyz unlockwallet --wallet-password="a b c" --timeout=8 + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet unlocked" ] +} + +@test "test getbalance while wallet unlocked for 8s" { + run ./bisq-cli --password=xyz getbalance + [ "$status" -eq 0 ] + sleep 8 +} + +@test "test unlockwallet \"a b c\" 6" { + run ./bisq-cli --password=xyz unlockwallet --wallet-password="a b c" --timeout=6 + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet unlocked" ] +} + +@test "test lockwallet before unlockwallet timeout=6s expires" { + run ./bisq-cli --password=xyz lockwallet + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet locked" ] +} + +@test "test setwalletpassword incorrect old pwd error" { + run ./bisq-cli --password=xyz setwalletpassword --wallet-password="z z z" --new-wallet-password="d e f" + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: incorrect old password" ] +} + +@test "test setwalletpassword oldpwd newpwd" { + # Wait 5 seconds before calling setwalletpassword again. + sleep 5 + run ./bisq-cli --password=xyz setwalletpassword --wallet-password="a b c" --new-wallet-password="d e f" + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet encrypted with new password" ] + sleep 1 +} + +@test "test getbalance wallet locked error" { + run ./bisq-cli --password=xyz getbalance + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: wallet is locked" ] +} + +@test "test removewalletpassword" { + run ./bisq-cli --password=xyz removewalletpassword --wallet-password="d e f" + [ "$status" -eq 0 ] + echo "actual output: $output" >&2 + [ "$output" = "wallet decrypted" ] + sleep 3 +} + +@test "test getbalance when wallet available & unlocked with 0 btc balance" { + run ./bisq-cli --password=xyz getbalance + [ "$status" -eq 0 ] +} + +@test "test getfundingaddresses" { + run ./bisq-cli --password=xyz getfundingaddresses + [ "$status" -eq 0 ] +} + +@test "test getunusedbsqaddress" { + run ./bisq-cli --password=xyz getunusedbsqaddress + [ "$status" -eq 0 ] +} + +@test "test getaddressbalance missing address argument" { + run ./bisq-cli --password=xyz getaddressbalance + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no address specified" ] +} + +@test "test getaddressbalance bogus address argument" { + # Wait 1 second before calling getaddressbalance again. + sleep 1 + run ./bisq-cli --password=xyz getaddressbalance --address=bogus + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: address bogus not found in wallet" ] +} + +@test "test getpaymentmethods" { + run ./bisq-cli --password=xyz getpaymentmethods + [ "$status" -eq 0 ] +} + +@test "test getpaymentaccts" { + run ./bisq-cli --password=xyz getpaymentaccts + [ "$status" -eq 0 ] +} + +@test "test getoffers missing direction argument" { + run ./bisq-cli --password=xyz getoffers + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no direction (buy|sell) specified" ] +} + +@test "test getoffers sell eur check return status" { + # Wait 1 second before calling getoffers again. + sleep 1 + run ./bisq-cli --password=xyz getoffers --direction=sell --currency-code=eur + [ "$status" -eq 0 ] +} + +@test "test getoffers buy eur check return status" { + # Wait 1 second before calling getoffers again. + sleep 1 + run ./bisq-cli --password=xyz getoffers --direction=buy --currency-code=eur + [ "$status" -eq 0 ] +} + +@test "test getoffers sell gbp check return status" { + # Wait 1 second before calling getoffers again. + sleep 1 + run ./bisq-cli --password=xyz getoffers --direction=sell --currency-code=gbp + [ "$status" -eq 0 ] +} + +@test "test help displayed on stderr if no options or arguments" { + run ./bisq-cli + [ "$status" -eq 1 ] + [ "${lines[0]}" = "Bisq RPC Client" ] + [ "${lines[1]}" = "Usage: bisq-cli [options] [params]" ] + # TODO add asserts after help text is modified for new endpoints +} + +@test "test --help option" { + run ./bisq-cli --help + [ "$status" -eq 0 ] + [ "${lines[0]}" = "Bisq RPC Client" ] + [ "${lines[1]}" = "Usage: bisq-cli [options] [params]" ] + # TODO add asserts after help text is modified for new endpoints +} + +@test "test takeoffer method --help" { + run ./bisq-cli --password=xyz takeoffer --help + [ "$status" -eq 0 ] + [ "${lines[0]}" = "takeoffer" ] +} diff --git a/apitest/scripts/rolling-offer-simulation.sh b/apitest/scripts/rolling-offer-simulation.sh new file mode 100755 index 0000000000..c269940e64 --- /dev/null +++ b/apitest/scripts/rolling-offer-simulation.sh @@ -0,0 +1,119 @@ +#! /bin/bash + +# Demonstrates a way to always keep one offer in the market, using the API CLI with a local regtest bitcoin node. +# Alice creates an offer, waits for Bob to take it, and completes the trade protocol with him. Then Alice +# creates a new offer... +# +# Stop the script by entering ^C. +# +# A country code argument is used to create a country based face to face payment account for the simulated offer. +# +# Prerequisites: +# +# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, v0.21). +# +# - Bisq must be fully built with apitest dao setup files installed. +# Build command: `./gradlew clean build :apitest:installDaoSetup` +# +# - All supporting nodes must be run locally, in dev/dao/regtest mode: +# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon +# +# These should be run using the apitest harness. From the root project dir, run: +# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` +# +# - Only regtest btc can be bought or sold with the test payment account. +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# `$ apitest/scripts/rolling-offer-simulation.sh -d buy -c us -m 2.00 -a 0.125` +# +# Script options: -d -c (-m || -f ) -a +# +# Example: +# +# Create a buy/usd offer to sell 0.1 btc at 2% above market price, using a US face to face payment account: +# +# `$ apitest/scripts/rolling-offer-simulation.sh -d sell -c us -m 2.00 -a 0.1` + + +APP_BASE_NAME=$(basename "$0") +APP_HOME=$(pwd -P) +APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" + +source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" +source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" + +checksetup +parseopts "$@" + +printdate "Started $APP_BASE_NAME with parameters:" +printscriptparams +printbreak + +registerdisputeagents + +showcreatepaymentacctsteps "Alice" "$ALICE_PORT" + +CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "ALICE CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE" +exitoncommandalert $? +printbreak + +printdate "Bob creates his F2F payment account." +CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "BOB CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE" +exitoncommandalert $? +printbreak + +while : ; do + printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." + CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") + exitoncommandalert $? + printdate "Current Market Price: $CURRENT_PRICE" + CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") + printdate "ALICE CLI: $CMD" + OFFER_ID=$(createoffer "$CMD") + exitoncommandalert $? + printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." + printbreak + sleeptraced 3 + + # Show Alice's new offer. + printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." + CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" + printdate "ALICE CLI: $CMD" + OFFER=$($CMD) + exitoncommandalert $? + echo "$OFFER" + printbreak + sleeptraced 3 + + # Generate some btc blocks. + printdate "Generating btc blocks after publishing Alice's offer." + genbtcblocks 3 2 + printbreak + + RANDOM_WAIT=$(echo $[$RANDOM % 10 + 1]) + printdate "Bob will take Alice's offer in $RANDOM_WAIT seconds..." + sleeptraced "$RANDOM_WAIT" + + executetrade + exitoncommandalert $? + printbreak +done + +exit 0 diff --git a/apitest/scripts/trade-simulation-env.sh b/apitest/scripts/trade-simulation-env.sh new file mode 100755 index 0000000000..45fc385e07 --- /dev/null +++ b/apitest/scripts/trade-simulation-env.sh @@ -0,0 +1,312 @@ +#! /bin/bash + +# This file must be sourced by the main driver. + +export CLI_BASE="./bisq-cli --password=xyz" +export ARBITRATOR_PORT=9997 +export ALICE_PORT=9998 +export BOB_PORT=9999 +export F2F_ACCT_FORM="f2f-acct.json" + +checkos() { + LINUX=FALSE + DARWIN=FALSE + UNAME=$(uname) + case "$UNAME" in + Linux* ) + export LINUX=TRUE + ;; + Darwin* ) + export DARWIN=TRUE + ;; + esac + if [[ "$LINUX" == "TRUE" ]]; then + printdate "Running on supported Linux OS." + elif [[ "$DARWIN" == "TRUE" ]]; then + printdate "Running on supported Mac OS." + else + printdate "Script cannot run on $OSTYPE OS, only Linux and OSX are supported." + exit 1 + fi +} + +checksetup() { + checkos + + apitestusage() { + echo "The apitest harness must be running a local bitcoin regtest node, a seednode, an arbitration node," + echo "Bob & Alice daemons, and bitcoin-core's bitcoin-cli must be in the system PATH." + echo "" + echo "From the project's root dir, start all supporting nodes from a terminal:" + echo "./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false" + exit 1; + } + printdate "Checking $APP_HOME for some expected directories and files." + if [ -d "$APP_HOME/apitest" ]; then + printdate "Subproject apitest exists."; + else + printdate "Error: Subproject apitest not found, maybe because you are not running the script from the project root dir." + exit 1 + fi + if [ -f "$APP_HOME/bisq-cli" ]; then + printdate "The bisq-cli script exists."; + else + printdate "Error: The bisq-cli script not found, maybe because you are not running the script from the project root dir." + exit 1 + fi + printdate "Checking to see local bitcoind is running, and bitcoin-cli is in PATH." + checkbitcoindrunning + checkbitcoincliinpath + printdate "Checking to see bisq servers are running." + checkseednoderunning + checkarbnoderunning + checkalicenoderunning + checkbobnoderunning +} + +parseopts() { + usage() { + echo "Usage: $0 [-d buy|sell] [-c ] [-f || -m ] [-a ]" 1>&2 + exit 1; + } + + local OPTIND o d c f m a + while getopts "d:c:f:m:a:" o; do + case "${o}" in + d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]') + ((d == "BUY" || d == "SELL")) || usage + export DIRECTION=${d} + ;; + c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]') + export COUNTRY_CODE=${c} + ;; + f) f=${OPTARG} + export FIXED_PRICE=${f} + ;; + m) m=${OPTARG} + export MKT_PRICE_MARGIN=${m} + ;; + a) a=${OPTARG} + export AMOUNT=${a} + ;; + *) usage ;; + esac + done + shift $((OPTIND-1)) + + if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then + usage + fi + + if [ -z "${f}" ] && [ -z "${m}" ]; then + usage + fi + + if [ -n "${f}" ] && [ -n "${m}" ]; then + printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both." + usage + fi + + if [ "$DIRECTION" = "SELL" ] + then + export BOB_ROLE="(taker/buyer)" + export ALICE_ROLE="(maker/seller)" + else + export BOB_ROLE="(taker/seller)" + export ALICE_ROLE="(maker/buyer)" + fi +} + + +parselimitorderopts() { + usage() { + echo "Usage: $0 [-l limit-price] [-d buy|sell] [-c ] [-f || -m ] [-a ] [-w ]" 1>&2 + exit 1; + } + + local OPTIND o l d c f m a w + while getopts "l:d:c:f:m:a:w:" o; do + case "${o}" in + l) l=${OPTARG} + export LIMIT_PRICE=${l} + ;; + d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]') + ((d == "BUY" || d == "SELL")) || usage + export DIRECTION=${d} + ;; + c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]') + export COUNTRY_CODE=${c} + ;; + f) f=${OPTARG} + export FIXED_PRICE=${f} + ;; + m) m=${OPTARG} + export MKT_PRICE_MARGIN=${m} + ;; + a) a=${OPTARG} + export AMOUNT=${a} + ;; + w) w=${OPTARG} + export WAIT=${w} + ;; + *) usage ;; + esac + done + shift $((OPTIND-1)) + + if [ -z "${l}" ]; then + usage + fi + + if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then + usage + fi + + if [ -z "${f}" ] && [ -z "${m}" ]; then + usage + fi + + if [ -n "${f}" ] && [ -n "${m}" ]; then + printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both." + usage + fi + + if [ -z "${w}" ]; then + WAIT=120 + elif [ "$w" -lt 20 ]; then + printdate "The -w option is too low, minimum allowed is 20s. Using default 120s." + WAIT=120 + fi +} + +checkbitcoindrunning() { + # There may be a '+' char in the path and we have to escape it for pgrep. + if [[ $APP_HOME == *"+"* ]]; then + ESCAPED_APP_HOME=$(escapepluschar "$APP_HOME") + else + ESCAPED_APP_HOME="$APP_HOME" + fi + if pgrep -f "bitcoind -datadir=$ESCAPED_APP_HOME/apitest/build/resources/main/Bitcoin-regtest" > /dev/null ; then + printdate "The regtest bitcoind node is running on host." + else + printdate "Error: regtest bitcoind node is not running on host, exiting." + apitestusage + fi +} + +checkbitcoincliinpath() { + if which bitcoin-cli > /dev/null ; then + printdate "The bitcoin-cli binary is in the system PATH." + else + printdate "Error: bitcoin-cli binary is not in the system PATH, exiting." + apitestusage + fi +} + +checkseednoderunning() { + if [[ "$LINUX" == "TRUE" ]]; then + if pgrep -f "bisq.seednode.SeedNodeMain" > /dev/null ; then + printdate "The seed node is running on host." + else + printdate "Error: seed node is not running on host, exiting." + apitestusage + fi + elif [[ "$DARWIN" == "TRUE" ]]; then + if ps -A | awk '/[S]eedNodeMain/ {print $1}' > /dev/null ; then + printdate "The seednode is running on host." + else + printdate "Error: seed node is not running on host, exiting." + apitestusage + fi + else + printdate "Error: seed node is not running on host, exiting." + apitestusage + fi +} + +checkarbnoderunning() { + if [[ "$LINUX" == "TRUE" ]]; then + if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao" > /dev/null ; then + printdate "The arbitration node is running on host." + else + printdate "Error: arbitration node is not running on host, exiting." + apitestusage + fi + elif [[ "$DARWIN" == "TRUE" ]]; then + if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao/ {print $1}' > /dev/null ; then + printdate "The arbitration node is running on host." + else + printdate "Error: arbitration node is not running on host, exiting." + apitestusage + fi + else + printdate "Error: arbitration node is not running on host, exiting." + apitestusage + fi +} + +checkalicenoderunning() { + if [[ "$LINUX" == "TRUE" ]]; then + if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then + printdate "Alice's node is running on host." + else + printdate "Error: Alice's node is not running on host, exiting." + apitestusage + fi + elif [[ "$DARWIN" == "TRUE" ]]; then + if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then + printdate "Alice's node node is running on host." + else + printdate "Error: Alice's node is not running on host, exiting." + apitestusage + fi + else + printdate "Error: Alice's node is not running on host, exiting." + apitestusage + fi +} + +checkbobnoderunning() { + if [[ "$LINUX" == "TRUE" ]]; then + if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then + printdate "Bob's node is running on host." + else + printdate "Error: Bob's node is not running on host, exiting." + apitestusage + fi + elif [[ "$DARWIN" == "TRUE" ]]; then + if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then + printdate "Bob's node node is running on host." + else + printdate "Error: Bob's node is not running on host, exiting." + apitestusage + fi + else + printdate "Error: Bob's node is not running on host, exiting." + apitestusage + fi +} + +printscriptparams() { + if [ -n "${LIMIT_PRICE+1}" ]; then + echo " LIMIT_PRICE = $LIMIT_PRICE" + fi + + echo " DIRECTION = $DIRECTION" + echo " COUNTRY_CODE = $COUNTRY_CODE" + echo " FIXED_PRICE = $FIXED_PRICE" + echo " MKT_PRICE_MARGIN = $MKT_PRICE_MARGIN" + echo " AMOUNT = $AMOUNT" + + if [ -n "${BOB_ROLE+1}" ]; then + echo " BOB_ROLE = $BOB_ROLE" + fi + + if [ -n "${ALICE_ROLE+1}" ]; then + echo " ALICE_ROLE = $ALICE_ROLE" + fi + + if [ -n "${WAIT+1}" ]; then + echo " WAIT = $WAIT" + fi +} diff --git a/apitest/scripts/trade-simulation-utils.sh b/apitest/scripts/trade-simulation-utils.sh new file mode 100755 index 0000000000..3ed7d3488b --- /dev/null +++ b/apitest/scripts/trade-simulation-utils.sh @@ -0,0 +1,571 @@ +#! /bin/bash + +# This file must be sourced by the main driver. + +source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" + +printdate() { + echo "[$(date)] $@" +} + +printbreak() { + echo "" + echo "" +} + +printcmd() { + echo -en "$@\n" +} + +sleeptraced() { + PERIOD="$1" + printdate "sleeping for $PERIOD" + sleep "$PERIOD" +} + +commandalert() { + # Used in a script function when it needs to fail early with an error message, & pass the error code to the caller. + # usage: commandalert <$?> + if [ "$1" -ne 0 ] + then + printdate "Error: $2" >&2 + exit "$1" + fi +} + +# TODO rename exitonalert ? +exitoncommandalert() { + # Used in a parent script when you need it to fail immediately, with no error message. + # usage: exitoncommandalert <$?> + if [ "$1" -ne 0 ] + then + exit "$1" + fi +} + +registerdisputeagents() { + # Silently register dev dispute agents. It's easy to forget. + REG_KEY="6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a" + CMD="$CLI_BASE --port=$ARBITRATOR_PORT registerdisputeagent --dispute-agent-type=mediator --registration-key=$REG_KEY" + SILENT=$($CMD) + commandalert $? "Could not register dev/test mediator." + CMD="$CLI_BASE --port=$ARBITRATOR_PORT registerdisputeagent --dispute-agent-type=refundagent --registration-key=$REG_KEY" + SILENT=$($CMD) + commandalert $? "Could not register dev/test refundagent." + # Do something with $SILENT to keep codacy happy. + echo "$SILENT" > /dev/null +} + +getbtcoreaddress() { + CMD="bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest -rpcpassword=apitest getnewaddress" + NEW_ADDRESS=$($CMD) + echo "$NEW_ADDRESS" +} + +genbtcblocks() { + NUM_BLOCKS="$1" + SECONDS_BETWEEN_BLOCKS="$2" + ADDR_PARAM="$(getbtcoreaddress)" + CMD_PREFIX="bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest -rpcpassword=apitest generatetoaddress 1" + # Print the generatetoaddress command with double quoted address param, to make it cut & pastable from the console. + printdate "$CMD_PREFIX \"$ADDR_PARAM\"" + # Now create the full generatetoaddress command to be run now. + CMD="$CMD_PREFIX $ADDR_PARAM" + for i in $(seq -f "%02g" 1 "$NUM_BLOCKS") + do + NEW_BLOCK_HASH=$(genbtcblock "$CMD") + printdate "Block Hash #$i:$NEW_BLOCK_HASH" + sleep "$SECONDS_BETWEEN_BLOCKS" + done +} + +genbtcblock() { + CMD="$1" + NEW_BLOCK_HASH=$($CMD | sed -n '2p') + echo "$NEW_BLOCK_HASH" +} + +escapepluschar() { + STRING="$1" + NEW_STRING=$(echo "${STRING//+/\\+}") + echo "$NEW_STRING" +} + +printbalances() { + PORT="$1" + printcmd "$CLI_BASE --port=$PORT getbalance" + $CLI_BASE --port="$PORT" getbalance +} + +getpaymentaccountmethods() { + CMD="$1" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not get payment method ids." + printdate "Payment Method IDs:" + echo "$CMD_OUTPUT" +} + +getpaymentaccountform() { + CMD="$1" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not get new payment account form." + echo "$CMD_OUTPUT" +} + +editpaymentaccountform() { + COUNTRY_CODE="$1" + CMD="python3 $APITEST_SCRIPTS_HOME/editf2faccountform.py $COUNTRY_CODE" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not edit payment account form." + printdate "Saved payment account form as $F2F_ACCT_FORM." +} + +getnewpaymentacctid() { + CREATE_PAYMENT_ACCT_OUTPUT="$1" + PAYMENT_ACCT_DETAIL=$(echo -e "$CREATE_PAYMENT_ACCT_OUTPUT" | sed -n '3p') + ACCT_ID=$(echo -e "$PAYMENT_ACCT_DETAIL" | awk '{print $NF}') + echo "$ACCT_ID" +} + +getnewpaymentacctcurrency() { + CREATE_PAYMENT_ACCT_OUTPUT="$1" + PAYMENT_ACCT_DETAIL=$(echo -e "$CREATE_PAYMENT_ACCT_OUTPUT" | sed -n '3p') + # This is brittle; it requires the account name field to have N words, + # e.g, "Face to Face Payment Account" as defined in editf2faccountform.py. + CURRENCY_CODE=$(echo -e "$PAYMENT_ACCT_DETAIL" | awk '{print $6}') + echo "$CURRENCY_CODE" +} + +createpaymentacct() { + CMD="$1" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not create new payment account." + echo "$CMD_OUTPUT" +} + +getpaymentaccounts() { + PORT="$1" + printcmd "$CLI_BASE --port=$PORT getpaymentaccts" + CMD="$CLI_BASE --port=$PORT getpaymentaccts" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not get payment accounts." + echo "$CMD_OUTPUT" +} + +showcreatepaymentacctsteps() { + USER="$1" + PORT="$2" + printdate "$USER looks for the ID of the face to face payment account method (Bob will use same payment method)." + CMD="$CLI_BASE --port=$PORT getpaymentmethods" + printdate "$USER CLI: $CMD" + PAYMENT_ACCT_METHODS=$(getpaymentaccountmethods "$CMD") + echo "$PAYMENT_ACCT_METHODS" + printbreak + + printdate "$USER uses the F2F payment method id to create a face to face payment account in country $COUNTRY_CODE." + CMD="$CLI_BASE --port=$PORT getpaymentacctform --payment-method-id=F2F" + printdate "$USER CLI: $CMD" + getpaymentaccountform "$CMD" + printbreak + + printdate "$USER edits the $COUNTRY_CODE payment account form, and (optionally) renames it as $F2F_ACCT_FORM" + editpaymentaccountform "$COUNTRY_CODE" + cat "$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" + + # Remove the autogenerated json template because we are going to use one created by a python script in the next step. + CMD="rm -v $APP_HOME/f2f_*.json" + DELETE_JSON_TEMPLATE=$($CMD) + printdate "$DELETE_JSON_TEMPLATE" + printbreak +} + +gencreateoffercommand() { + PORT="$1" + ACCT_ID="$2" + CMD="$CLI_BASE --port=$PORT createoffer" + CMD+=" --payment-account=$ACCT_ID" + CMD+=" --direction=$DIRECTION" + CMD+=" --currency-code=$CURRENCY_CODE" + CMD+=" --amount=$AMOUNT" + if [ -z "$MKT_PRICE_MARGIN" ]; then + CMD+=" --fixed-price=$FIXED_PRICE" + else + CMD+=" --market-price-margin=$MKT_PRICE_MARGIN" + fi + CMD+=" --security-deposit=15.0" + CMD+=" --fee-currency=BSQ" + echo "$CMD" +} + +createoffer() { + CREATE_OFFER_CMD="$1" + OFFER_DESC=$($CREATE_OFFER_CMD) + + # If the CLI command exited with an error, print the CLI error, and + # return from this function now, passing the error status code to the caller. + commandalert $? "Could not create offer." + + OFFER_DETAIL=$(echo -e "$OFFER_DESC" | sed -n '2p') + NEW_OFFER_ID=$(echo -e "$OFFER_DETAIL" | awk '{print $NF}') + echo "$NEW_OFFER_ID" +} + +getfirstofferid() { + PORT="$1" + CMD="$CLI_BASE --port=$PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not get current $DIRECTION / $CURRENCY_CODE offers." + FIRST_OFFER_DETAIL=$(echo -e "$CMD_OUTPUT" | sed -n '2p') + FIRST_OFFER_ID=$(echo -e "$FIRST_OFFER_DETAIL" | awk '{print $NF}') + commandalert $? "Could parse the offer-id from the first listed offer." + echo "$FIRST_OFFER_ID" +} + +gettrade() { + GET_TRADE_CMD="$1" + TRADE_DESC=$($GET_TRADE_CMD) + commandalert $? "Could not get trade." + echo "$TRADE_DESC" +} + +gettradedetail() { + TRADE_DESC="$1" + # Get 2nd line of gettrade cmd output, and squeeze multi space delimiters into one space. + TRADE_DETAIL=$(echo "$TRADE_DESC" | sed -n '2p' | tr -s ' ') + commandalert $? "Could not get trade detail (line 2 of gettrade output)." + echo "$TRADE_DETAIL" +} + +istradedepositpublished() { + TRADE_DETAIL="$1" + ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}') + commandalert $? "Could not parse istradedepositpublished from trade detail." + echo "$ANSWER" +} + +istradedepositconfirmed() { + TRADE_DETAIL="$1" + ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}') + commandalert $? "Could not parse istradedepositconfirmed from trade detail." + echo "$ANSWER" +} + +istradepaymentsent() { + TRADE_DETAIL="$1" + ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}') + commandalert $? "Could not parse istradepaymentsent from trade detail." + echo "$ANSWER" +} + +istradepaymentreceived() { + TRADE_DETAIL="$1" + ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}') + commandalert $? "Could not parse istradepaymentreceived from trade detail." + echo "$ANSWER" +} + +istradepayoutpublished() { + TRADE_DETAIL="$1" + ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}') + commandalert $? "Could not parse istradepayoutpublished from trade detail." + echo "$ANSWER" +} + +waitfortradedepositpublished() { + # Loops until Bob's trade deposit is published. (Bob is always the trade taker.) + OFFER_ID="$1" + DONE=0 + while : ; do + if [ "$DONE" -ne 0 ]; then + break + fi + + printdate "BOB $BOB_ROLE: Looking at his trade with id $OFFER_ID." + CMD="$CLI_BASE --port=$BOB_PORT gettrade --trade-id=$OFFER_ID" + printdate "BOB CLI: $CMD" + GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") + exitoncommandalert $? + echo "$GETTRADE_CMD_OUTPUT" + printbreak + + TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") + exitoncommandalert $? + + IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL") + exitoncommandalert $? + + printdate "BOB $BOB_ROLE: Has taker's trade deposit been published? $IS_TRADE_DEPOSIT_PUBLISHED" + if [ "$IS_TRADE_DEPOSIT_PUBLISHED" = "YES" ] + then + DONE=1 + else + RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) + sleeptraced "$RANDOM_WAIT" + fi + printbreak + done +} + +waitfortradedepositconfirmed() { + # Loops until Bob's trade deposit is confirmed. (Bob is always the trade taker.) + OFFER_ID="$1" + DONE=0 + while : ; do + if [ "$DONE" -ne 0 ]; then + break + fi + + printdate "BOB $BOB_ROLE: Looking at his trade with id $OFFER_ID." + CMD="$CLI_BASE --port=$BOB_PORT gettrade --trade-id=$OFFER_ID" + printdate "BOB CLI: $CMD" + GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") + exitoncommandalert $? + echo "$GETTRADE_CMD_OUTPUT" + printbreak + + TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") + exitoncommandalert $? + + IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL") + exitoncommandalert $? + printdate "BOB $BOB_ROLE: Has taker's trade deposit been confirmed? $IS_TRADE_DEPOSIT_CONFIRMED" + printbreak + + if [ "$IS_TRADE_DEPOSIT_CONFIRMED" = "YES" ] + then + DONE=1 + else + printdate "Generating btc block while Bob waits for trade deposit to be confirmed." + genbtcblocks 1 0 + + RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) + sleeptraced "$RANDOM_WAIT" + fi + done +} + +waitfortradepaymentsent() { + # Loops until buyer's trade payment has been sent. + PORT="$1" + SELLER="$2" + OFFER_ID="$3" + DONE=0 + while : ; do + if [ "$DONE" -ne 0 ]; then + break + fi + + printdate "$SELLER: Looking at trade with id $OFFER_ID." + CMD="$CLI_BASE --port=$PORT gettrade --trade-id=$OFFER_ID" + printdate "$SELLER CLI: $CMD" + GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") + exitoncommandalert $? + echo "$GETTRADE_CMD_OUTPUT" + printbreak + + TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") + exitoncommandalert $? + + IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL") + exitoncommandalert $? + printdate "$SELLER: Has buyer's fiat payment been initiated? $IS_TRADE_PAYMENT_SENT" + if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] + then + DONE=1 + else + RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) + sleeptraced "$RANDOM_WAIT" + fi + printbreak + done +} + +waitfortradepaymentreceived() { + # Loops until buyer's trade payment has been received. + PORT="$1" + SELLER="$2" + OFFER_ID="$3" + DONE=0 + while : ; do + if [ "$DONE" -ne 0 ]; then + break + fi + + printdate "$SELLER: Looking at trade with id $OFFER_ID." + CMD="$CLI_BASE --port=$PORT gettrade --trade-id=$OFFER_ID" + printdate "$SELLER CLI: $CMD" + GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") + exitoncommandalert $? + echo "$GETTRADE_CMD_OUTPUT" + printbreak + + TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") + exitoncommandalert $? + + # When the seller receives a 'payment sent' message, it is assumed funds (fiat) have already been deposited. + # In a real trade, there is usually a delay between receipt of a 'payment sent' message, and the funds deposit, + # but we do not need to simulate that in this regtest script. + IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL") + exitoncommandalert $? + printdate "$SELLER: Has buyer's payment been transferred to seller's fiat account? $IS_TRADE_PAYMENT_SENT" + if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] + then + DONE=1 + else + RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) + sleeptraced "$RANDOM_WAIT" + fi + printbreak + done +} + +delayconfirmpaymentstarted() { + # Confirm payment started after a random delay. This should be run in the background + # while the payee polls the trade status, waiting for the message before confirming + # payment has been received. + PAYER="$1" + PORT="$2" + OFFER_ID="$3" + RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) + printdate "$PAYER: Sending fiat payment sent message to seller in $RANDOM_WAIT seconds..." + sleeptraced "$RANDOM_WAIT" + CMD="$CLI_BASE --port=$PORT confirmpaymentstarted --trade-id=$OFFER_ID" + printdate "$PAYER_CLI: $CMD" + SENT_MSG=$($CMD) + commandalert $? "Could not send confirmpaymentstarted message." + # Print the confirmpaymentstarted command's console output. + printdate "$SENT_MSG" + printbreak +} + +delayconfirmpaymentreceived() { + # Confirm payment received after a random delay. This should be run in the background + # while the payer polls the trade status, waiting for the confirmation from the seller + # that funds have been received. + PAYEE="$1" + PORT="$2" + OFFER_ID="$3" + RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) + printdate "$PAYEE: Sending fiat payment sent message to seller in $RANDOM_WAIT seconds..." + sleeptraced "$RANDOM_WAIT" + CMD="$CLI_BASE --port=$PORT confirmpaymentreceived --trade-id=$OFFER_ID" + printdate "$PAYEE_CLI: $CMD" + RCVD_MSG=$($CMD) + commandalert $? "Could not send confirmpaymentstarted message." + # Print the confirmpaymentstarted command's console output. + printdate "$RCVD_MSG" + printbreak +} + +# This is a large function that should be broken up if it ever makes sense to not treat a trade +# execution simulation as an atomic operation. But we are not testing api methods here, just +# demonstrating how to use them to get through the trade protocol. It should work for any trade +# between Bob & Alice, as long as Alice is maker, Bob is taker, and the offer to be taken is the +# first displayed in Bob's getoffers command output. +executetrade() { + # Bob list available offers. + printdate "BOB $BOB_ROLE: Looking at $DIRECTION $CURRENCY_CODE offers." + CMD="$CLI_BASE --port=$BOB_PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" + printdate "BOB CLI: $CMD" + OFFERS=$($CMD) + exitoncommandalert $? + echo "$OFFERS" + printbreak + + OFFER_ID=$(getfirstofferid "$BOB_PORT") + exitoncommandalert $? + printdate "First offer found: $OFFER_ID" + + # Take Alice's offer. + CMD="$CLI_BASE --port=$BOB_PORT takeoffer --offer-id=$OFFER_ID --payment-account=$BOB_ACCT_ID --fee-currency=bsq" + printdate "BOB CLI: $CMD" + TRADE=$($CMD) + commandalert $? "Could not take offer." + # Print the takeoffer command's console output. + printdate "$TRADE" + printbreak + + waitfortradedepositpublished "$OFFER_ID" + waitfortradedepositconfirmed "$OFFER_ID" + + # Send payment sent and received messages. + if [ "$DIRECTION" = "BUY" ] + then + PAYER="ALICE $ALICE_ROLE" + PAYER_PORT=$ALICE_PORT + PAYER_CLI="ALICE CLI" + PAYEE="BOB $BOB_ROLE" + PAYEE_PORT=$BOB_PORT + PAYEE_CLI="BOB CLI" + else + PAYER="BOB $BOB_ROLE" + PAYER_PORT=$BOB_PORT + PAYER_CLI="BOB CLI" + PAYEE="ALICE $ALICE_ROLE" + PAYEE_PORT=$ALICE_PORT + PAYEE_CLI="ALICE CLI" + fi + + # Asynchronously send a confirm payment started message after a random delay. + delayconfirmpaymentstarted "$PAYER" "$PAYER_PORT" "$OFFER_ID" & + + if [ "$DIRECTION" = "BUY" ] + then + # Bob waits for payment, polling status in taker specific trade detail. + waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" + else + # Alice waits for payment, polling status in maker specific trade detail. + waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" + fi + + + # Asynchronously send a confirm payment received message after a random delay. + delayconfirmpaymentreceived "$PAYEE" "$PAYEE_PORT" "$OFFER_ID" & + + if [ "$DIRECTION" = "BUY" ] + then + # Alice waits for payment rcvd confirm from Bob, polling status in maker specific trade detail. + waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" + else + # Bob waits for payment rcvd confirm from Alice, polling status in taker specific trade detail. + waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" + fi + + # Generate some btc blocks + printdate "Generating btc blocks after fiat transfer." + genbtcblocks 2 2 + printbreak + + # Complete the trade on the seller side. + if [ "$DIRECTION" = "BUY" ] + then + printdate "BOB $BOB_ROLE: Closing trade by keeping funds in Bisq wallet." + CMD="$CLI_BASE --port=$BOB_PORT keepfunds --trade-id=$OFFER_ID" + printdate "BOB CLI: $CMD" + else + printdate "ALICE (taker): Closing trade by keeping funds in Bisq wallet." + CMD="$CLI_BASE --port=$ALICE_PORT keepfunds --trade-id=$OFFER_ID" + printdate "ALICE CLI: $CMD" + fi + KEEP_FUNDS_MSG=$($CMD) + commandalert $? "Could close trade with keepfunds command." + # Print the keepfunds command's console output. + printdate "$KEEP_FUNDS_MSG" + sleeptraced 3 + printbreak + + printdate "Trade $OFFER_ID complete." +} + +getcurrentprice() { + PORT="$1" + CURRENCY_CODE="$2" + CMD="$CLI_BASE --port=$PORT getbtcprice --currency-code=$CURRENCY_CODE" + CMD_OUTPUT=$($CMD) + commandalert $? "Could not get current market $CURRENCY_CODE price." + FLOOR=$(echo "$CMD_OUTPUT" | cut -d'.' -f 1) + commandalert $? "Could not get the floor of the current market $CURRENCY_CODE price." + INTEGER=$(echo "$FLOOR" | tr -cd '[[:digit:]]') + commandalert $? "Could not convert the current market $CURRENCY_CODE price string to an integer." + echo "$INTEGER" +} diff --git a/apitest/scripts/trade-simulation.sh b/apitest/scripts/trade-simulation.sh new file mode 100755 index 0000000000..5aa540bf71 --- /dev/null +++ b/apitest/scripts/trade-simulation.sh @@ -0,0 +1,126 @@ +#! /bin/bash + +# Runs fiat <-> btc trading scenarios using the API CLI with a local regtest bitcoin node. +# +# A country code argument is used to create a country based face to face payment account for the simulated +# trade, and the maker's face to face payment account's currency code is used when creating the offer. +# +# Prerequisites: +# +# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, or v0.21). +# +# - Bisq must be fully built with apitest dao setup files installed. +# Build command: `./gradlew clean build :apitest:installDaoSetup` +# +# - All supporting nodes must be run locally, in dev/dao/regtest mode: +# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon +# +# These should be run using the apitest harness. From the root project dir, run: +# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` +# +# - Only regtest btc can be bought or sold with the test payment account. +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# `$ apitest/scripts/trade-simulation.sh -d buy -c fr -m 3.00 -a 0.125` +# +# Script options: -d -c -m - f -a +# +# Examples: +# +# Create a buy/eur offer to buy 0.125 btc at a mkt-price-margin of 0%, using an Italy face to face payment account: +# +# `$ apitest/scripts/trade-simulation.sh -d buy -c it -m 0.00 -a 0.125` +# +# Create a sell/eur offer to sell 0.125 btc at a fixed-price of 38,000 euros, using a France face to face +# payment account: +# +# `$ apitest/scripts/trade-simulation.sh -d sell -c fr -f 38000 -a 0.125` + +export APP_BASE_NAME=$(basename "$0") +export APP_HOME=$(pwd -P) +export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" + +source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" +source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" + +checksetup +parseopts "$@" + +printdate "Started $APP_BASE_NAME with parameters:" +printscriptparams +printbreak + +registerdisputeagents + +# Demonstrate how to create a country based, face to face account. +showcreatepaymentacctsteps "Alice" "$ALICE_PORT" + +CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "ALICE CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE" +exitoncommandalert $? +printbreak + +printdate "Bob creates his F2F payment account." +CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" +printdate "BOB CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") +printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE" +exitoncommandalert $? +printbreak + +# Alice creates an offer. +printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." +CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") +exitoncommandalert $? +printdate "Current Market Price: $CURRENT_PRICE" +CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") +printdate "ALICE CLI: $CMD" +OFFER_ID=$(createoffer "$CMD") +exitoncommandalert $? +printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." +printbreak +sleeptraced 3 + +# Show Alice's new offer. +printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." +CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" +printdate "ALICE CLI: $CMD" +OFFER=$($CMD) +exitoncommandalert $? +echo "$OFFER" +printbreak +sleeptraced 3 + +# Generate some btc blocks. +printdate "Generating btc blocks after publishing Alice's offer." +genbtcblocks 3 1 +printbreak + +# Go through the trade protocol. +executetrade +exitoncommandalert $? +printbreak + +# Get balances after trade completion. +printdate "Bob & Alice's balances after trade:" +printdate "ALICE CLI:" +printbalances "$ALICE_PORT" +printbreak +printdate "BOB CLI:" +printbalances "$BOB_PORT" +printbreak + +exit 0 diff --git a/apitest/scripts/version-parser.bash b/apitest/scripts/version-parser.bash new file mode 100755 index 0000000000..ba7f2c2d0f --- /dev/null +++ b/apitest/scripts/version-parser.bash @@ -0,0 +1,5 @@ +#!/bin/bash + +# Bats helper script for parsing current version from Version.java. + +export CURRENT_VERSION=$(grep "String VERSION =" common/src/main/java/bisq/common/app/Version.java | sed 's/[^0-9.]*//g') diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java new file mode 100644 index 0000000000..20a40850ec --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.apitest; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.Scaffold.EXIT_FAILURE; +import static bisq.apitest.Scaffold.EXIT_SUCCESS; +import static java.lang.System.err; +import static java.lang.System.exit; + + + +import bisq.apitest.config.ApiTestConfig; + +/** + * ApiTestMain is a placeholder for the gradle build file, which requires a valid + * 'mainClassName' property in the :apitest subproject configuration. + * + * It does has some uses: + * + * It can be used to print test scaffolding options: bisq-apitest --help. + * + * It can be used to smoke test your bitcoind environment: bisq-apitest. + * + * It can be used to run the regtest/dao environment for release testing: + * bisq-test --shutdownAfterTests=false + * + * All method, scenario and end to end tests are found in the test sources folder. + * + * Requires bitcoind v0.19, v0.20, or v0.21. + */ +@Slf4j +public class ApiTestMain { + + public static void main(String[] args) { + new ApiTestMain().execute(args); + } + + public void execute(@SuppressWarnings("unused") String[] args) { + try { + Scaffold scaffold = new Scaffold(args).setUp(); + ApiTestConfig config = scaffold.config; + + if (config.skipTests) { + log.info("Skipping tests ..."); + } else { + new SmokeTestBitcoind(config).run(); + } + + if (config.shutdownAfterTests) { + scaffold.tearDown(); + exit(EXIT_SUCCESS); + } else { + log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ..."); + } + + } catch (Throwable ex) { + err.println("Fault: An unexpected error occurred. " + + "Please file a report at https://bisq.network/issues"); + ex.printStackTrace(err); + exit(EXIT_FAILURE); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java new file mode 100644 index 0000000000..d518c0bcb0 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -0,0 +1,469 @@ +/* + * 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 bisq.apitest; + +import bisq.common.config.BisqHelpFormatter; +import bisq.common.util.Utilities; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.ApiTestConfig.MEDIATOR; +import static bisq.apitest.config.ApiTestConfig.REFUND_AGENT; +import static bisq.apitest.config.BisqAppConfig.*; +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; +import static java.lang.String.format; +import static java.lang.System.exit; +import static java.lang.System.out; +import static java.net.InetAddress.getLoopbackAddress; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.linux.BashCommand; +import bisq.apitest.linux.BisqProcess; +import bisq.apitest.linux.BitcoinDaemon; +import bisq.apitest.linux.LinuxProcess; +import bisq.cli.GrpcClient; + +@Slf4j +public class Scaffold { + + public static final int EXIT_SUCCESS = 0; + public static final int EXIT_FAILURE = 1; + + public enum BitcoinCoreApp { + bitcoind + } + + public final ApiTestConfig config; + + @Nullable + private SetupTask bitcoindTask; + @Nullable + private Future bitcoindTaskFuture; + @Nullable + private SetupTask seedNodeTask; + @Nullable + private Future seedNodeTaskFuture; + @Nullable + private SetupTask arbNodeTask; + @Nullable + private Future arbNodeTaskFuture; + @Nullable + private SetupTask aliceNodeTask; + @Nullable + private Future aliceNodeTaskFuture; + @Nullable + private SetupTask bobNodeTask; + @Nullable + private Future bobNodeTaskFuture; + + private final ExecutorService executor; + + /** + * Constructor for passing comma delimited list of supporting apps to + * ApiTestConfig, e.g., "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon". + * + * @param supportingApps String + */ + public Scaffold(String supportingApps) { + this(new ApiTestConfig("--supportingApps", supportingApps)); + } + + /** + * Constructor for passing options accepted by ApiTestConfig. + * + * @param args String[] + */ + public Scaffold(String[] args) { + this(new ApiTestConfig(args)); + } + + /** + * Constructor for passing ApiTestConfig instance. + * + * @param config ApiTestConfig + */ + public Scaffold(ApiTestConfig config) { + verifyNotWindows(); + this.config = config; + this.executor = Executors.newFixedThreadPool(config.supportingApps.size()); + if (config.helpRequested) { + config.printHelp(out, + new BisqHelpFormatter( + "Bisq ApiTest", + "bisq-apitest", + "0.1.0")); + exit(EXIT_SUCCESS); + } + } + + + public Scaffold setUp() throws IOException, InterruptedException, ExecutionException { + installDaoSetupDirectories(); + + // Start each background process from an executor, then add a shutdown hook. + CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size()); + startBackgroundProcesses(executor, countdownLatch); + installShutdownHook(); + + // Wait for all submitted startup tasks to decrement the count of the latch. + Objects.requireNonNull(countdownLatch).await(); + + // Verify each startup task's future is done. + verifyStartupCompleted(); + + maybeRegisterDisputeAgents(); + return this; + } + + public void tearDown() { + if (!executor.isTerminated()) { + try { + log.info("Shutting down executor service ..."); + executor.shutdownNow(); + //noinspection ResultOfMethodCallIgnored + executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS); + + SetupTask[] orderedTasks = new SetupTask[]{ + bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; + Optional firstException = shutDownAll(orderedTasks); + + if (firstException.isPresent()) + throw new IllegalStateException( + "There were errors shutting down one or more background instances.", + firstException.get()); + else + log.info("Teardown complete"); + + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + + private Optional shutDownAll(SetupTask[] orderedTasks) { + Optional firstException = Optional.empty(); + for (SetupTask t : orderedTasks) { + if (t != null && t.getLinuxProcess() != null) { + try { + LinuxProcess p = t.getLinuxProcess(); + p.shutdown(); + MILLISECONDS.sleep(1000); + if (p.hasShutdownExceptions()) { + // We log shutdown exceptions, but do not throw any from here + // because all of the background instances must be shut down. + p.logExceptions(p.getShutdownExceptions(), log); + + // We cache only the 1st shutdown exception and move on to the + // next process to be shutdown. This cached exception will be the + // one thrown to the calling test case (the @AfterAll method). + if (!firstException.isPresent()) + firstException = Optional.of(p.getShutdownExceptions().get(0)); + } + } catch (InterruptedException ignored) { + // empty + } + } + } + return firstException; + } + + public void installDaoSetupDirectories() { + cleanDaoSetupDirectories(); + + String daoSetupDir = Paths.get(config.baseSrcResourcesDir, "dao-setup").toFile().getAbsolutePath(); + String buildDataDir = config.rootAppDataDir.getAbsolutePath(); + try { + if (!new File(daoSetupDir).exists()) + throw new FileNotFoundException( + format("Dao setup dir '%s' not found. Run gradle :apitest:installDaoSetup" + + " to download dao-setup.zip and extract contents to resources folder", + daoSetupDir)); + + BashCommand copyBitcoinRegtestDir = new BashCommand( + "cp -rf " + daoSetupDir + "/Bitcoin-regtest/regtest" + + " " + config.bitcoinDatadir); + if (copyBitcoinRegtestDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install bitcoin regtest dir"); + + String aliceDataDir = daoSetupDir + "/" + alicedaemon.appName; + BashCommand copyAliceDataDir = new BashCommand( + "cp -rf " + aliceDataDir + " " + config.rootAppDataDir); + if (copyAliceDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install alice data dir"); + + String bobDataDir = daoSetupDir + "/" + bobdaemon.appName; + BashCommand copyBobDataDir = new BashCommand( + "cp -rf " + bobDataDir + " " + config.rootAppDataDir); + if (copyBobDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install bob data dir"); + + log.info("Installed dao-setup files into {}", buildDataDir); + + if (!config.callRateMeteringConfigPath.isEmpty()) { + installCallRateMeteringConfiguration(aliceDataDir); + installCallRateMeteringConfiguration(bobDataDir); + } + + // Copy the blocknotify script from the src resources dir to the build + // resources dir. Users may want to edit comment out some lines when all + // of the default block notifcation ports being will not be used (to avoid + // seeing rpc notifcation warnings in log files). + installBitcoinBlocknotify(); + + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException("Could not install dao-setup files from " + daoSetupDir, ex); + } + } + + private void cleanDaoSetupDirectories() { + String buildDataDir = config.rootAppDataDir.getAbsolutePath(); + log.info("Cleaning dao-setup data in {}", buildDataDir); + + try { + BashCommand rmBobDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + bobdaemon.appName); + if (rmBobDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete bob data dir"); + + BashCommand rmAliceDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + alicedaemon.appName); + if (rmAliceDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete alice data dir"); + + BashCommand rmArbNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + arbdaemon.appName); + if (rmArbNodeDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete arbitrator data dir"); + + BashCommand rmSeedNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + seednode.appName); + if (rmSeedNodeDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete seednode data dir"); + + BashCommand rmBitcoinRegtestDir = new BashCommand("rm -rf " + config.bitcoinDatadir + "/regtest"); + if (rmBitcoinRegtestDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not clean bitcoind regtest dir"); + + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException("Could not clean dao-setup files from " + buildDataDir, ex); + } + } + + private void installBitcoinBlocknotify() { + // gradle is not working for this + try { + Path srcPath = Paths.get(config.baseSrcResourcesDir, "blocknotify"); + Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify"); + Files.copy(srcPath, destPath, REPLACE_EXISTING); + String chmod700Perms = "rwx------"; + Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); + log.info("Installed {} with perms {}.", destPath, chmod700Perms); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException { + File testRateMeteringFile = new File(config.callRateMeteringConfigPath); + if (!testRateMeteringFile.exists()) + throw new FileNotFoundException( + format("Call rate metering config file '%s' not found", config.callRateMeteringConfigPath)); + + BashCommand copyRateMeteringConfigFile = new BashCommand( + "cp -rf " + config.callRateMeteringConfigPath + " " + dataDir); + if (copyRateMeteringConfigFile.run().getExitStatus() != 0) + throw new IllegalStateException( + format("Could not install %s file in %s", + testRateMeteringFile.getAbsolutePath(), dataDir)); + + Path destPath = Paths.get(dataDir, testRateMeteringFile.getName()); + String chmod700Perms = "rwx------"; + Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); + log.info("Installed {} with perms {}.", destPath, chmod700Perms); + } + + private void installShutdownHook() { + // Background apps can be left running until the jvm is manually shutdown, + // so we add a shutdown hook for that use case. + Runtime.getRuntime().addShutdownHook(new Thread(this::tearDown)); + } + + // Starts bitcoind and bisq apps (seednode, arbnode, etc...) + private void startBackgroundProcesses(ExecutorService executor, + CountDownLatch countdownLatch) + throws InterruptedException, IOException { + + log.info("Starting supporting apps {}", config.supportingApps.toString()); + + if (config.hasSupportingApp(bitcoind.name())) { + BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); + bitcoinDaemon.verifyBitcoinPathsExist(true); + bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); + bitcoindTaskFuture = executor.submit(bitcoindTask); + MILLISECONDS.sleep(config.bisqAppInitTime); + + LinuxProcess bitcoindProcess = bitcoindTask.getLinuxProcess(); + if (bitcoindProcess.hasStartupExceptions()) { + bitcoindProcess.logExceptions(bitcoindProcess.getStartupExceptions(), log); + throw new IllegalStateException(bitcoindProcess.getStartupExceptions().get(0)); + } + + bitcoinDaemon.verifyBitcoindRunning(); + } + + // Start Bisq apps defined by the supportingApps option, in the in proper order. + + if (config.hasSupportingApp(seednode.name())) + startBisqApp(seednode, executor, countdownLatch); + + if (config.hasSupportingApp(arbdaemon.name())) + startBisqApp(arbdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(arbdesktop.name())) + startBisqApp(arbdesktop, executor, countdownLatch); + + if (config.hasSupportingApp(alicedaemon.name())) + startBisqApp(alicedaemon, executor, countdownLatch); + else if (config.hasSupportingApp(alicedesktop.name())) + startBisqApp(alicedesktop, executor, countdownLatch); + + if (config.hasSupportingApp(bobdaemon.name())) + startBisqApp(bobdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(bobdesktop.name())) + startBisqApp(bobdesktop, executor, countdownLatch); + } + + private void startBisqApp(BisqAppConfig bisqAppConfig, + ExecutorService executor, + CountDownLatch countdownLatch) + throws IOException, InterruptedException { + + BisqProcess bisqProcess = createBisqProcess(bisqAppConfig); + switch (bisqAppConfig) { + case seednode: + seedNodeTask = new SetupTask(bisqProcess, countdownLatch); + seedNodeTaskFuture = executor.submit(seedNodeTask); + break; + case arbdaemon: + case arbdesktop: + arbNodeTask = new SetupTask(bisqProcess, countdownLatch); + arbNodeTaskFuture = executor.submit(arbNodeTask); + break; + case alicedaemon: + case alicedesktop: + aliceNodeTask = new SetupTask(bisqProcess, countdownLatch); + aliceNodeTaskFuture = executor.submit(aliceNodeTask); + break; + case bobdaemon: + case bobdesktop: + bobNodeTask = new SetupTask(bisqProcess, countdownLatch); + bobNodeTaskFuture = executor.submit(bobNodeTask); + break; + default: + throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name()); + } + log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName); + MILLISECONDS.sleep(config.bisqAppInitTime); + if (bisqProcess.hasStartupExceptions()) { + bisqProcess.logExceptions(bisqProcess.getStartupExceptions(), log); + throw new IllegalStateException(bisqProcess.getStartupExceptions().get(0)); + } + } + + private BisqProcess createBisqProcess(BisqAppConfig bisqAppConfig) + throws IOException, InterruptedException { + BisqProcess bisqProcess = new BisqProcess(bisqAppConfig, config); + bisqProcess.verifyAppNotRunning(); + bisqProcess.verifyAppDataDirInstalled(); + return bisqProcess; + } + + private void verifyStartupCompleted() + throws ExecutionException, InterruptedException { + if (bitcoindTaskFuture != null) + verifyStartupCompleted(bitcoindTaskFuture); + + if (seedNodeTaskFuture != null) + verifyStartupCompleted(seedNodeTaskFuture); + + if (arbNodeTaskFuture != null) + verifyStartupCompleted(arbNodeTaskFuture); + + if (aliceNodeTaskFuture != null) + verifyStartupCompleted(aliceNodeTaskFuture); + + if (bobNodeTaskFuture != null) + verifyStartupCompleted(bobNodeTaskFuture); + } + + private void verifyStartupCompleted(Future futureStatus) + throws ExecutionException, InterruptedException { + for (int i = 0; i < 10; i++) { + if (futureStatus.isDone()) { + log.info("{} completed startup at {} {}", + futureStatus.get().getName(), + futureStatus.get().getStartTime().toLocalDate(), + futureStatus.get().getStartTime().toLocalTime()); + return; + } else { + // We are giving the thread more time to terminate after the countdown + // latch reached 0. If we are running only bitcoind, we need to be even + // more lenient. + SECONDS.sleep(config.supportingApps.size() == 1 ? 2 : 1); + } + } + throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); + } + + private void verifyNotWindows() { + if (Utilities.isWindows()) + throw new IllegalStateException("ApiTest not supported on Windows"); + } + + private void maybeRegisterDisputeAgents() { + if (config.hasSupportingApp(arbdaemon.name()) && config.registerDisputeAgents) { + log.info("Option --registerDisputeAgents=true, registering dispute agents in arbdaemon ..."); + GrpcClient arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(), + arbdaemon.apiPort, + config.apiPassword); + arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY); + arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java new file mode 100644 index 0000000000..7e519b65ce --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.apitest; + +import java.time.LocalDateTime; + +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.linux.LinuxProcess; + +@Slf4j +public class SetupTask implements Callable { + + private final LinuxProcess linuxProcess; + private final CountDownLatch countdownLatch; + + public SetupTask(LinuxProcess linuxProcess, CountDownLatch countdownLatch) { + this.linuxProcess = linuxProcess; + this.countdownLatch = countdownLatch; + } + + @Override + public Status call() throws Exception { + try { + linuxProcess.start(); // always runs in background + MILLISECONDS.sleep(1000); // give 1s for bg process to init + } catch (InterruptedException ex) { + throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); + } + Objects.requireNonNull(countdownLatch).countDown(); + return new Status(linuxProcess.getName(), LocalDateTime.now()); + } + + public LinuxProcess getLinuxProcess() { + return linuxProcess; + } + + public static class Status { + private final String name; + private final LocalDateTime startTime; + + public Status(String name, LocalDateTime startTime) { + super(); + this.name = name; + this.startTime = startTime; + } + + public String getName() { + return name; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + @Override + public String toString() { + return "SetupTask.Status [name=" + name + ", completionTime=" + startTime + "]"; + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java b/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java new file mode 100644 index 0000000000..c3c8a26bc2 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.apitest; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.linux.BashCommand; + +@Slf4j +class SmokeTestBashCommand { + + public SmokeTestBashCommand() { + } + + public void runSmokeTest() { + try { + BashCommand cmd = new BashCommand("ls -l").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("free -g").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("date").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("netstat -a | grep localhost").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java new file mode 100644 index 0000000000..92d1ba8051 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.apitest; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.linux.BitcoinCli; + +@Slf4j +class SmokeTestBitcoind { + + private final ApiTestConfig config; + + public SmokeTestBitcoind(ApiTestConfig config) { + this.config = config; + } + + public void run() throws IOException, InterruptedException { + runBitcoinGetWalletInfo(); // smoke test bitcoin-cli + String newBitcoinAddress = getNewAddress(); + generateToAddress(1, newBitcoinAddress); + } + + public void runBitcoinGetWalletInfo() throws IOException, InterruptedException { + // This might be good for a sanity check to make sure the regtest data was installed. + log.info("Smoke test bitcoin-cli getwalletinfo"); + BitcoinCli walletInfo = new BitcoinCli(config, "getwalletinfo").run(); + log.info("{}\n{}", walletInfo.getCommandWithOptions(), walletInfo.getOutput()); + log.info("balance str = {}", walletInfo.getOutputValueAsString("balance")); + log.info("balance dbl = {}", walletInfo.getOutputValueAsDouble("balance")); + log.info("keypoololdest long = {}", walletInfo.getOutputValueAsLong("keypoololdest")); + log.info("paytxfee dbl = {}", walletInfo.getOutputValueAsDouble("paytxfee")); + log.info("keypoolsize_hd_internal int = {}", walletInfo.getOutputValueAsInt("keypoolsize_hd_internal")); + log.info("private_keys_enabled bool = {}", walletInfo.getOutputValueAsBoolean("private_keys_enabled")); + log.info("hdseedid str = {}", walletInfo.getOutputValueAsString("hdseedid")); + } + + public String getNewAddress() throws IOException, InterruptedException { + BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); + log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutput()); + return newAddress.getOutput(); + } + + public void generateToAddress(int blocks, String address) throws IOException, InterruptedException { + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); + // Return value is an array of TxIDs. + log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray()); + } +} diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java new file mode 100644 index 0000000000..44897a2da0 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -0,0 +1,380 @@ +/* + * 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 bisq.apitest.config; + +import bisq.common.config.CompositeOptionSet; + +import joptsimple.AbstractOptionSpec; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.HelpFormatter; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.net.InetAddress; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.lang.System.getenv; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static joptsimple.internal.Strings.EMPTY; + +@Slf4j +public class ApiTestConfig { + + // Global constants + public static final String BSQ = "BSQ"; + public static final String BTC = "BTC"; + public static final String ARBITRATOR = "arbitrator"; + public static final String MEDIATOR = "mediator"; + public static final String REFUND_AGENT = "refundagent"; + + // Option name constants + static final String HELP = "help"; + static final String BASH_PATH = "bashPath"; + static final String BERKELEYDB_LIB_PATH = "berkeleyDbLibPath"; + static final String BITCOIN_PATH = "bitcoinPath"; + static final String BITCOIN_RPC_PORT = "bitcoinRpcPort"; + static final String BITCOIN_RPC_USER = "bitcoinRpcUser"; + static final String BITCOIN_RPC_PASSWORD = "bitcoinRpcPassword"; + static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost"; + static final String CONFIG_FILE = "configFile"; + static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; + static final String API_PASSWORD = "apiPassword"; + static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; + static final String BISQ_APP_INIT_TIME = "bisqAppInitTime"; + static final String SKIP_TESTS = "skipTests"; + static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; + static final String SUPPORTING_APPS = "supportingApps"; + static final String CALL_RATE_METERING_CONFIG_PATH = "callRateMeteringConfigPath"; + static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging"; + static final String REGISTER_DISPUTE_AGENTS = "registerDisputeAgents"; + + // Default values for certain options + static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; + + // Static fields that provide access to Config properties in locations where injecting + // a Config instance is not feasible. + public static String BASH_PATH_VALUE; + + public final File defaultConfigFile; + + // Options supported only at the command line, not within a config file. + public final boolean helpRequested; + public final File configFile; + + // Options supported at the command line and a config file. + public final File rootAppDataDir; + public final String bashPath; + public final String berkeleyDbLibPath; + public final String bitcoinPath; + public final String bitcoinRegtestHost; + public final int bitcoinRpcPort; + public final String bitcoinRpcUser; + public final String bitcoinRpcPassword; + // Daemon instances can use same gRPC password, but each needs a different apiPort. + public final String apiPassword; + public final boolean runSubprojectJars; + public final long bisqAppInitTime; + public final boolean skipTests; + public final boolean shutdownAfterTests; + public final List supportingApps; + public final String callRateMeteringConfigPath; + public final boolean enableBisqDebugging; + public final boolean registerDisputeAgents; + + // Immutable system configurations set in the constructor. + public final String bitcoinDatadir; + public final String userDir; + public final boolean isRunningTest; + public final String rootProjectDir; + public final String baseBuildResourcesDir; + public final String baseSrcResourcesDir; + + // The parser that will be used to parse both cmd line and config file options + private final OptionParser parser = new OptionParser(); + + public ApiTestConfig(String... args) { + this.userDir = getProperty("user.dir"); + // If running a @Test, the current working directory is the :apitest subproject + // folder. If running ApiTestMain, the current working directory is the + // bisq root project folder. + this.isRunningTest = Paths.get(userDir).getFileName().toString().equals("apitest"); + this.rootProjectDir = isRunningTest + ? Paths.get(userDir).getParent().toFile().getAbsolutePath() + : Paths.get(userDir).toFile().getAbsolutePath(); + this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main") + .toFile().getAbsolutePath(); + this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources") + .toFile().getAbsolutePath(); + + this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME); + this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath(); + + AbstractOptionSpec helpOpt = + parser.accepts(HELP, "Print this help text") + .forHelp(); + + ArgumentAcceptingOptionSpec configFileOpt = + parser.accepts(CONFIG_FILE, format("Specify configuration file. " + + "Relative paths will be prefixed by %s location.", userDir)) + .withRequiredArg() + .ofType(String.class) + .defaultsTo(DEFAULT_CONFIG_FILE_NAME); + + ArgumentAcceptingOptionSpec appDataDirOpt = + parser.accepts(ROOT_APP_DATA_DIR, "Application data directory") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File(baseBuildResourcesDir)); + + ArgumentAcceptingOptionSpec bashPathOpt = + parser.accepts(BASH_PATH, "Bash path") + .withRequiredArg() + .ofType(String.class) + .defaultsTo( + (getenv("SHELL") == null || !getenv("SHELL").contains("bash")) + ? "/bin/bash" + : getenv("SHELL")); + + ArgumentAcceptingOptionSpec berkeleyDbLibPathOpt = + parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path") + .withRequiredArg() + .ofType(String.class).defaultsTo(EMPTY); + + ArgumentAcceptingOptionSpec bitcoinPathOpt = + parser.accepts(BITCOIN_PATH, "Bitcoin path") + .withRequiredArg() + .ofType(String.class).defaultsTo("/usr/local/bin"); + + ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = + parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host") + .withRequiredArg() + .ofType(String.class).defaultsTo(InetAddress.getLoopbackAddress().getHostAddress()); + + ArgumentAcceptingOptionSpec bitcoinRpcPortOpt = + parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)") + .withRequiredArg() + .ofType(Integer.class).defaultsTo(19443); + + ArgumentAcceptingOptionSpec bitcoinRpcUserOpt = + parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user") + .withRequiredArg() + .ofType(String.class).defaultsTo("apitest"); + + ArgumentAcceptingOptionSpec bitcoinRpcPasswordOpt = + parser.accepts(BITCOIN_RPC_PASSWORD, "Bitcoin rpc password") + .withRequiredArg() + .ofType(String.class).defaultsTo("apitest"); + + ArgumentAcceptingOptionSpec apiPasswordOpt = + parser.accepts(API_PASSWORD, "gRPC API password") + .withRequiredArg() + .defaultsTo("xyz"); + + ArgumentAcceptingOptionSpec runSubprojectJarsOpt = + parser.accepts(RUN_SUBPROJECT_JARS, + "Run subproject build jars instead of full build jars") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = + parser.accepts(BISQ_APP_INIT_TIME, + "Amount of time (ms) to wait on a Bisq instance's initialization") + .withRequiredArg() + .ofType(Long.class) + .defaultsTo(5000L); + + ArgumentAcceptingOptionSpec skipTestsOpt = + parser.accepts(SKIP_TESTS, + "Start apps, but skip tests") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec shutdownAfterTestsOpt = + parser.accepts(SHUTDOWN_AFTER_TESTS, + "Terminate all processes after tests") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(true); + + ArgumentAcceptingOptionSpec supportingAppsOpt = + parser.accepts(SUPPORTING_APPS, + "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") + .withRequiredArg() + .ofType(String.class) + .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); + + ArgumentAcceptingOptionSpec callRateMeteringConfigPathOpt = + parser.accepts(CALL_RATE_METERING_CONFIG_PATH, + "Install a ratemeters.json file to configure call rate metering interceptors") + .withRequiredArg() + .defaultsTo(EMPTY); + + ArgumentAcceptingOptionSpec enableBisqDebuggingOpt = + parser.accepts(ENABLE_BISQ_DEBUGGING, + "Start Bisq apps with remote debug options") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec registerDisputeAgentsOpt = + parser.accepts(REGISTER_DISPUTE_AGENTS, + "Register dispute agents in arbitration daemon") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(true); + try { + CompositeOptionSet options = new CompositeOptionSet(); + + // Parse command line options + OptionSet cliOpts = parser.parse(args); + options.addOptionSet(cliOpts); + + // Parse config file specified at the command line only if it was specified as + // an absolute path. Otherwise, the config file will be processed later below. + File configFile = null; + OptionSpec[] disallowedOpts = new OptionSpec[]{helpOpt, configFileOpt}; + final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt); + boolean configFileHasBeenProcessed = false; + if (cliHasConfigFileOpt) { + configFile = new File(cliOpts.valueOf(configFileOpt)); + if (configFile.isAbsolute()) { + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + if (configFileOpts.isPresent()) { + options.addOptionSet(configFileOpts.get()); + configFileHasBeenProcessed = true; + } + } + } + + // If the config file has not yet been processed, either because a relative + // path was provided at the command line, or because no value was provided at + // the command line, attempt to process the file now, falling back to the + // default config file location if none was specified at the command line. + if (!configFileHasBeenProcessed) { + configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ? + absoluteConfigFile(userDir, configFile.getPath()) : + defaultConfigFile; + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + configFileOpts.ifPresent(options::addOptionSet); + } + + + // Assign all remaining properties, with command line options taking + // precedence over those provided in the config file (if any) + this.helpRequested = options.has(helpOpt); + this.configFile = configFile; + this.rootAppDataDir = options.valueOf(appDataDirOpt); + bashPath = options.valueOf(bashPathOpt); + this.berkeleyDbLibPath = options.valueOf(berkeleyDbLibPathOpt); + this.bitcoinPath = options.valueOf(bitcoinPathOpt); + this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt); + this.bitcoinRpcPort = options.valueOf(bitcoinRpcPortOpt); + this.bitcoinRpcUser = options.valueOf(bitcoinRpcUserOpt); + this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); + this.apiPassword = options.valueOf(apiPasswordOpt); + this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); + this.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt); + this.skipTests = options.valueOf(skipTestsOpt); + this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); + this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); + this.callRateMeteringConfigPath = options.valueOf(callRateMeteringConfigPathOpt); + this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt); + this.registerDisputeAgents = options.valueOf(registerDisputeAgentsOpt); + + // Assign values to special-case static fields. + BASH_PATH_VALUE = bashPath; + + } catch (OptionException ex) { + throw new IllegalStateException(format("Problem parsing option '%s': %s", + ex.options().get(0), + ex.getCause() != null ? + ex.getCause().getMessage() : + ex.getMessage())); + } + } + + public boolean hasSupportingApp(String... supportingApp) { + return stream(supportingApp).anyMatch(this.supportingApps::contains); + } + + public void printHelp(OutputStream sink, HelpFormatter formatter) { + try { + parser.formatHelpWith(formatter); + parser.printHelpOn(sink); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { + if (!configFile.exists() && !configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) + throw new IllegalStateException(format("The specified config file '%s' does not exist.", configFile)); + + Properties properties = getProperties(configFile); + List optionLines = new ArrayList<>(); + properties.forEach((k, v) -> { + optionLines.add("--" + k + "=" + v); // dashes expected by jopt parser below + }); + + OptionSet configFileOpts = parser.parse(optionLines.toArray(new String[0])); + for (OptionSpec disallowedOpt : disallowedOpts) + if (configFileOpts.has(disallowedOpt)) + throw new IllegalStateException( + format("The '%s' option is disallowed in config files", + disallowedOpt.options().get(0))); + + return Optional.of(configFileOpts); + } + + private Properties getProperties(File configFile) { + try { + Properties properties = new Properties(); + properties.load(new FileInputStream(configFile.getAbsolutePath())); + return properties; + } catch (IOException ex) { + throw new IllegalStateException( + format("Could not load properties from config file %s", + configFile.getAbsolutePath()), ex); + } + } + + private static File absoluteConfigFile(String parentDir, String relativeConfigFilePath) { + return new File(parentDir, relativeConfigFilePath); + } +} diff --git a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java new file mode 100644 index 0000000000..08a7531ca3 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java @@ -0,0 +1,133 @@ +/* + * 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 bisq.apitest.config; + +import bisq.seednode.SeedNodeMain; + +import bisq.desktop.app.BisqAppMain; + + + +import bisq.daemon.app.BisqDaemonMain; + +/** + Some non user configurable Bisq seednode, arb node, bob and alice daemon option values. + @see dev-setup.md + @see dao-setup.md + */ +public enum BisqAppConfig { + + seednode("bisq-BTC_REGTEST_Seed_2002", + "bisq-seednode", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + SeedNodeMain.class.getName(), + 2002, + 5120, + -1, + 49996), + arbdaemon("bisq-BTC_REGTEST_Arb_dao", + "bisq-daemon", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqDaemonMain.class.getName(), + 4444, + 5121, + 9997, + 49997), + arbdesktop("bisq-BTC_REGTEST_Arb_dao", + "bisq-desktop", + "-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqAppMain.class.getName(), + 4444, + 5121, + -1, + 49997), + alicedaemon("bisq-BTC_REGTEST_Alice_dao", + "bisq-daemon", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqDaemonMain.class.getName(), + 7777, + 5122, + 9998, + 49998), + alicedesktop("bisq-BTC_REGTEST_Alice_dao", + "bisq-desktop", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqAppMain.class.getName(), + 7777, + 5122, + -1, + 49998), + bobdaemon("bisq-BTC_REGTEST_Bob_dao", + "bisq-daemon", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqDaemonMain.class.getName(), + 8888, + 5123, + 9999, + 49999), + bobdesktop("bisq-BTC_REGTEST_Bob_dao", + "bisq-desktop", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", + BisqAppMain.class.getName(), + 8888, + 5123, + -1, + 49999); + + public final String appName; + public final String startupScript; + public final String javaOpts; + public final String mainClassName; + public final int nodePort; + public final int rpcBlockNotificationPort; + // Daemons can use a global gRPC password, but each needs a unique apiPort. + public final int apiPort; + public final int remoteDebugPort; + + BisqAppConfig(String appName, + String startupScript, + String javaOpts, + String mainClassName, + int nodePort, + int rpcBlockNotificationPort, + int apiPort, + int remoteDebugPort) { + this.appName = appName; + this.startupScript = startupScript; + this.javaOpts = javaOpts; + this.mainClassName = mainClassName; + this.nodePort = nodePort; + this.rpcBlockNotificationPort = rpcBlockNotificationPort; + this.apiPort = apiPort; + this.remoteDebugPort = remoteDebugPort; + } + + @Override + public String toString() { + return "BisqAppConfig{" + "\n" + + " appName='" + appName + '\'' + "\n" + + ", startupScript='" + startupScript + '\'' + "\n" + + ", javaOpts='" + javaOpts + '\'' + "\n" + + ", mainClassName='" + mainClassName + '\'' + "\n" + + ", nodePort=" + nodePort + "\n" + + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + + ", apiPort=" + apiPort + "\n" + + ", remoteDebugPort=" + remoteDebugPort + "\n" + + '}'; + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java new file mode 100644 index 0000000000..83ae6718ae --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -0,0 +1,129 @@ +/* + * 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 bisq.apitest.linux; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; +import static joptsimple.internal.Strings.EMPTY; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +abstract class AbstractLinuxProcess implements LinuxProcess { + + protected final String name; + protected final ApiTestConfig config; + + protected long pid; + + protected final List startupExceptions; + protected final List shutdownExceptions; + + public AbstractLinuxProcess(String name, ApiTestConfig config) { + this.name = name; + this.config = config; + this.startupExceptions = new ArrayList<>(); + this.shutdownExceptions = new ArrayList<>(); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean hasStartupExceptions() { + return !startupExceptions.isEmpty(); + } + + @Override + public boolean hasShutdownExceptions() { + return !shutdownExceptions.isEmpty(); + } + + @Override + public void logExceptions(List exceptions, org.slf4j.Logger log) { + for (Throwable t : exceptions) { + log.error("", t); + } + } + + @Override + public List getStartupExceptions() { + return startupExceptions; + } + + @Override + public List getShutdownExceptions() { + return shutdownExceptions; + } + + @SuppressWarnings("unused") + public void verifyBitcoinPathsExist() { + verifyBitcoinPathsExist(false); + } + + public void verifyBitcoinPathsExist(boolean verbose) { + if (verbose) + log.info(format("Checking bitcoind env...%n" + + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s", + "berkeleyDbLibPath", config.berkeleyDbLibPath, + "bitcoinPath", config.bitcoinPath, + "bitcoinDatadir", config.bitcoinDatadir, + "blocknotify", config.bitcoinDatadir + "/blocknotify")); + + if (!config.berkeleyDbLibPath.equals(EMPTY)) { + File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); + if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) + throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); + } + + File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile(); + if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) + throw new IllegalStateException(format("'%s' cannot be found or executed.%n" + + "A bitcoin-core v0.19, v0.20, or v0.21 installation is required," + + " and the 'bitcoinPath' must be configured in 'apitest.properties'", + bitcoindExecutable.getAbsolutePath())); + + File bitcoindDatadir = new File(config.bitcoinDatadir); + if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) + throw new IllegalStateException(bitcoindDatadir + " cannot be found or written to"); + + File blocknotify = new File(bitcoindDatadir, "blocknotify"); + if (!blocknotify.exists() || !blocknotify.canExecute()) + throw new IllegalStateException(blocknotify.getAbsolutePath() + " cannot be found or executed"); + } + + public void verifyBitcoindRunning() throws IOException, InterruptedException { + long bitcoindPid = BashCommand.getPid("bitcoind"); + if (bitcoindPid < 0 || !isAlive(bitcoindPid)) + throw new IllegalStateException("Bitcoind not running"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java new file mode 100644 index 0000000000..f40d9b06c9 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -0,0 +1,156 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static bisq.apitest.config.ApiTestConfig.BASH_PATH_VALUE; +import static java.lang.management.ManagementFactory.getRuntimeMXBean; + +@Slf4j +public class BashCommand { + + private int exitStatus = -1; + private String output; + private String error; + + private final String command; + private final int numResponseLines; + + public BashCommand(String command) { + this(command, 0); + } + + public BashCommand(String command, int numResponseLines) { + this.command = command; + this.numResponseLines = numResponseLines; // only want the top N lines of output + } + + public BashCommand run() throws IOException, InterruptedException { + SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); + exitStatus = commandExecutor.exec(); + processOutput(commandExecutor); + return this; + } + + public BashCommand runInBackground() throws IOException, InterruptedException { + SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); + exitStatus = commandExecutor.exec(false); + processOutput(commandExecutor); + return this; + } + + private void processOutput(SystemCommandExecutor commandExecutor) { + // Get the error status and stderr from system command. + StringBuilder stderr = commandExecutor.getStandardErrorFromCommand(); + if (stderr.length() > 0) + error = stderr.toString(); + + if (exitStatus != 0) + return; + + // Format and cache the stdout from system command. + StringBuilder stdout = commandExecutor.getStandardOutputFromCommand(); + String[] rawLines = stdout.toString().split("\n"); + StringBuilder truncatedLines = new StringBuilder(); + int limit = numResponseLines > 0 ? Math.min(numResponseLines, rawLines.length) : rawLines.length; + for (int i = 0; i < limit; i++) { + String line = rawLines[i].length() >= 220 ? rawLines[i].substring(0, 220) + " ..." : rawLines[i]; + truncatedLines.append(line).append((i < limit - 1) ? "\n" : ""); + } + output = truncatedLines.toString(); + } + + public String getCommand() { + return this.command; + } + + public int getExitStatus() { + return this.exitStatus; + } + + // TODO return Optional + public String getOutput() { + return this.output; + } + + // TODO return Optional + public String getError() { + return this.error; + } + + @NotNull + private List tokenizeSystemCommand() { + return new ArrayList<>() {{ + add(BASH_PATH_VALUE); + add("-c"); + add(command); + }}; + } + + @SuppressWarnings("unused") + // Convenience method for getting system load info. + public static String printSystemLoadString(Exception tracingException) throws IOException, InterruptedException { + StackTraceElement[] stackTraceElement = tracingException.getStackTrace(); + StringBuilder stackTraceBuilder = new StringBuilder(tracingException.getMessage()).append("\n"); + int traceLimit = Math.min(stackTraceElement.length, 4); + for (int i = 0; i < traceLimit; i++) { + stackTraceBuilder.append(stackTraceElement[i]).append("\n"); + } + stackTraceBuilder.append("..."); + log.info(stackTraceBuilder.toString()); + BashCommand cmd = new BashCommand("ps -aux --sort -rss --headers", 2).run(); + return cmd.getOutput() + "\n" + + "System load: Memory (MB): " + getUsedMemoryInMB() + " / No. of threads: " + Thread.activeCount() + + " JVM uptime (ms): " + getRuntimeMXBean().getUptime(); + } + + public static long getUsedMemoryInMB() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory() / 1024 / 1024; + long total = runtime.totalMemory() / 1024 / 1024; + return total - free; + } + + public static long getPid(String processName) throws IOException, InterruptedException { + String psCmd = "ps aux | pgrep " + processName + " | grep -v grep"; + String psCmdOutput = new BashCommand(psCmd).run().getOutput(); + if (psCmdOutput == null || psCmdOutput.isEmpty()) + return -1; + + return Long.parseLong(psCmdOutput); + } + + @SuppressWarnings("unused") + public static BashCommand grep(String processName) throws IOException, InterruptedException { + String c = "ps -aux | grep " + processName + " | grep -v grep"; + return new BashCommand(c).run(); + } + + public static boolean isAlive(long pid) throws IOException, InterruptedException { + String isAliveScript = "if ps -p " + pid + " > /dev/null; then echo true; else echo false; fi"; + return new BashCommand(isAliveScript).run().getOutput().equals("true"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqProcess.java b/apitest/src/main/java/bisq/apitest/linux/BisqProcess.java new file mode 100644 index 0000000000..f6c2e7f22b --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BisqProcess.java @@ -0,0 +1,266 @@ +/* + * 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 bisq.apitest.linux; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; +import bisq.daemon.app.BisqDaemonMain; + +/** + * Runs a regtest/dao Bisq application instance in the background. + */ +@Slf4j +public class BisqProcess extends AbstractLinuxProcess implements LinuxProcess { + + private final BisqAppConfig bisqAppConfig; + private final String baseCurrencyNetwork; + private final String genesisTxId; + private final int genesisBlockHeight; + private final String seedNodes; + private final boolean daoActivated; + private final boolean fullDaoNode; + private final boolean useLocalhostForP2P; + public final boolean useDevPrivilegeKeys; + private final String findBisqPidScript; + private final String debugOpts; + + public BisqProcess(BisqAppConfig bisqAppConfig, ApiTestConfig config) { + super(bisqAppConfig.appName, config); + this.bisqAppConfig = bisqAppConfig; + this.baseCurrencyNetwork = "BTC_REGTEST"; + this.genesisTxId = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf"; + this.genesisBlockHeight = 111; + this.seedNodes = "localhost:2002"; + this.daoActivated = true; + this.fullDaoNode = true; + this.useLocalhostForP2P = true; + this.useDevPrivilegeKeys = true; + this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest") + + "/scripts/get-bisq-pid.sh"; + this.debugOpts = config.enableBisqDebugging + ? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + bisqAppConfig.remoteDebugPort + : ""; + } + + @Override + public void start() { + try { + if (config.runSubprojectJars) + runJar(); // run subproject/build/lib/*.jar (not full build) + else + runStartupScript(); // run bisq-* script for end to end test (default) + } catch (Throwable t) { + startupExceptions.add(t); + } + } + + @Override + public long getPid() { + return this.pid; + } + + @Override + public void shutdown() { + try { + log.info("Shutting down {} ...", bisqAppConfig.appName); + if (!isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException(format("%s already shut down", bisqAppConfig.appName))); + return; + } + + String killCmd = "kill -15 " + pid; + if (new BashCommand(killCmd).run().getExitStatus() != 0) { + this.shutdownExceptions.add(new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName))); + return; + } + + // Be lenient about the time it takes for a java app to shut down. + for (int i = 0; i < 5; i++) { + if (!isAlive(pid)) { + log.info("{} stopped", bisqAppConfig.appName); + break; + } + MILLISECONDS.sleep(2500); + } + + if (isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName))); + } + + } catch (Exception e) { + this.shutdownExceptions.add(new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e)); + } + } + + public void verifyAppNotRunning() throws IOException, InterruptedException { + long pid = findBisqAppPid(); + if (pid >= 0) + throw new IllegalStateException(format("%s %s already running with pid %d", + bisqAppConfig.mainClassName, bisqAppConfig.appName, pid)); + } + + public void verifyAppDataDirInstalled() { + // If we're running an Alice or Bob daemon, make sure the dao-setup directory + // are installed. + switch (bisqAppConfig) { + case alicedaemon: + case alicedesktop: + case bobdaemon: + case bobdesktop: + File bisqDataDir = new File(config.rootAppDataDir, bisqAppConfig.appName); + if (!bisqDataDir.exists()) + throw new IllegalStateException(format("Application dataDir %s/%s not found", + config.rootAppDataDir, bisqAppConfig.appName)); + break; + default: + break; + } + } + + // This is the non-default way of running a Bisq app (--runSubprojectJars=true). + // It runs a java cmd, and does not depend on a full build. Bisq jars are loaded + // from the :subproject/build/libs directories. + private void runJar() throws IOException, InterruptedException { + String java = getJavaExecutable().getAbsolutePath(); + String classpath = System.getProperty("java.class.path"); + String bisqCmd = getJavaOptsSpec() + + " " + java + " -cp " + classpath + + " " + bisqAppConfig.mainClassName + + " " + String.join(" ", getOptsList()) + + " &"; // run in background without nohup + runBashCommand(bisqCmd); + } + + // This is the default way of running a Bisq app (--runSubprojectJars=false). + // It runs a bisq-* startup script, and depends on a full build. Bisq jars + // are loaded from the root project's lib directory. + private void runStartupScript() throws IOException, InterruptedException { + String startupScriptPath = config.rootProjectDir + + "/" + bisqAppConfig.startupScript; + String bisqCmd = getJavaOptsSpec() + + " " + startupScriptPath + + " " + String.join(" ", getOptsList()) + + " &"; // run in background without nohup + runBashCommand(bisqCmd); + } + + private void runBashCommand(String bisqCmd) throws IOException, InterruptedException { + String cmdDescription = config.runSubprojectJars + ? "java -> " + bisqAppConfig.mainClassName + " -> " + bisqAppConfig.appName + : bisqAppConfig.startupScript + " -> " + bisqAppConfig.appName; + BashCommand bashCommand = new BashCommand(bisqCmd); + log.info("Starting {} ...\n$ {}", cmdDescription, bashCommand.getCommand()); + bashCommand.runInBackground(); + + if (bashCommand.getExitStatus() != 0) + throw new IllegalStateException(format("Error starting BisqApp%n%s%nError: %s", + bisqAppConfig.appName, + bashCommand.getError())); + + // Sometimes it takes a little extra time to find the linux process id. + // Wait up to two seconds before giving up and throwing an Exception. + for (int i = 0; i < 4; i++) { + pid = findBisqAppPid(); + if (pid != -1) + break; + + MILLISECONDS.sleep(500L); + } + if (!isAlive(pid)) + throw new IllegalStateException(format("Error finding pid for %s", this.name)); + + log.info("{} running with pid {}", cmdDescription, pid); + log.info("Log {}", config.rootAppDataDir + "/" + bisqAppConfig.appName + "/bisq.log"); + } + + private long findBisqAppPid() throws IOException, InterruptedException { + // Find the pid of the java process by grepping for the mainClassName and appName. + String findPidCmd = findBisqPidScript + " " + bisqAppConfig.mainClassName + " " + bisqAppConfig.appName; + String psCmdOutput = new BashCommand(findPidCmd).run().getOutput(); + return (psCmdOutput == null || psCmdOutput.isEmpty()) ? -1 : Long.parseLong(psCmdOutput); + } + + private String getJavaOptsSpec() { + return "export JAVA_OPTS=\"" + bisqAppConfig.javaOpts + debugOpts + "\"; "; + } + + private List getOptsList() { + return new ArrayList<>() {{ + add("--appName=" + bisqAppConfig.appName); + add("--appDataDir=" + config.rootAppDataDir.getAbsolutePath() + "/" + bisqAppConfig.appName); + add("--nodePort=" + bisqAppConfig.nodePort); + add("--rpcBlockNotificationPort=" + bisqAppConfig.rpcBlockNotificationPort); + add("--rpcUser=" + config.bitcoinRpcUser); + add("--rpcPassword=" + config.bitcoinRpcPassword); + add("--rpcPort=" + config.bitcoinRpcPort); + add("--daoActivated=" + daoActivated); + add("--fullDaoNode=" + fullDaoNode); + add("--seedNodes=" + seedNodes); + add("--baseCurrencyNetwork=" + baseCurrencyNetwork); + add("--useDevPrivilegeKeys=" + useDevPrivilegeKeys); + add("--useLocalhostForP2P=" + useLocalhostForP2P); + switch (bisqAppConfig) { + case seednode: + break; // no extra opts needed for seed node + case arbdaemon: + case arbdesktop: + case alicedaemon: + case alicedesktop: + case bobdaemon: + case bobdesktop: + add("--genesisBlockHeight=" + genesisBlockHeight); + add("--genesisTxId=" + genesisTxId); + if (bisqAppConfig.mainClassName.equals(BisqDaemonMain.class.getName())) { + add("--apiPassword=" + config.apiPassword); + add("--apiPort=" + bisqAppConfig.apiPort); + } + break; + default: + throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name()); + } + }}; + } + + private File getJavaExecutable() { + File javaHome = Paths.get(System.getProperty("java.home")).toFile(); + if (!javaHome.exists()) + throw new IllegalStateException(format("$JAVA_HOME not found, cannot run %s", bisqAppConfig.mainClassName)); + + File javaExecutable = Paths.get(javaHome.getAbsolutePath(), "bin", "java").toFile(); + if (javaExecutable.exists() || javaExecutable.canExecute()) + return javaExecutable; + else + throw new IllegalStateException("$JAVA_HOME/bin/java not found or executable"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java new file mode 100644 index 0000000000..2367443b0c --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java @@ -0,0 +1,182 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +public class BitcoinCli extends AbstractLinuxProcess implements LinuxProcess { + + private final String command; + + private String commandWithOptions; + private String output; + private boolean error; + private String errorMessage; + + public BitcoinCli(ApiTestConfig config, String command) { + super("bitcoin-cli", config); + this.command = command; + this.error = false; + this.errorMessage = null; + } + + public BitcoinCli run() throws IOException, InterruptedException { + this.start(); + return this; + } + + public String getCommandWithOptions() { + return commandWithOptions; + } + + public String getOutput() { + if (isError()) + throw new IllegalStateException(output); + + // Some responses are not in json format, such as what is returned by + // 'getnewaddress'. The raw output string is the value. + + return output; + } + + public String[] getOutputValueAsStringArray() { + if (isError()) + throw new IllegalStateException(output); + + if (!output.startsWith("[") && !output.endsWith("]")) + throw new IllegalStateException(output + "\nis not a json array"); + + String[] lines = output.split("\n"); + String[] array = new String[lines.length - 2]; + for (int i = 1; i < lines.length - 1; i++) { + array[i - 1] = lines[i].replaceAll("[^a-zA-Z0-9.]", ""); + } + + return array; + } + + public String getOutputValueAsString(String key) { + if (isError()) + throw new IllegalStateException(output); + + // Some assumptions about bitcoin-cli json string parsing: + // Every multi valued, non-error bitcoin-cli response will be a json string. + // Every key/value in the json string will terminate with a newline. + // Most key/value lines in json strings have a ',' char in front of the newline. + // e.g., bitcoin-cli 'getwalletinfo' output: + // { + // "walletname": "", + // "walletversion": 159900, + // "balance": 527.49941568, + // "unconfirmed_balance": 0.00000000, + // "immature_balance": 5000.00058432, + // "txcount": 114, + // "keypoololdest": 1528018235, + // "keypoolsize": 1000, + // "keypoolsize_hd_internal": 1000, + // "paytxfee": 0.00000000, + // "hdseedid": "179b609a60c2769138844c3e36eb430fd758a9c6", + // "private_keys_enabled": true, + // "avoid_reuse": false, + // "scanning": false + // } + + int keyIdx = output.indexOf("\"" + key + "\":"); + int eolIdx = output.indexOf("\n", keyIdx); + String valueLine = output.substring(keyIdx, eolIdx); // "balance": 527.49941568, + String[] keyValue = valueLine.split(":"); + + // Remove all but alphanumeric chars and decimal points from the return value, + // including quotes around strings, and trailing commas. + // Adjustments will be necessary as we begin to work with more complex + // json values, such as arrays. + return keyValue[1].replaceAll("[^a-zA-Z0-9.]", ""); + } + + public boolean getOutputValueAsBoolean(String key) { + String valueStr = getOutputValueAsString(key); + return Boolean.parseBoolean(valueStr); + } + + + public int getOutputValueAsInt(String key) { + String valueStr = getOutputValueAsString(key); + return Integer.parseInt(valueStr); + } + + public double getOutputValueAsDouble(String key) { + String valueStr = getOutputValueAsString(key); + return Double.parseDouble(valueStr); + } + + public long getOutputValueAsLong(String key) { + String valueStr = getOutputValueAsString(key); + return Long.parseLong(valueStr); + } + + public boolean isError() { + return error; + } + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public void start() throws InterruptedException, IOException { + verifyBitcoinPathsExist(false); + verifyBitcoindRunning(); + commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest " + + " -rpcport=" + config.bitcoinRpcPort + + " -rpcuser=" + config.bitcoinRpcUser + + " -rpcpassword=" + config.bitcoinRpcPassword + + " " + command; + BashCommand bashCommand = new BashCommand(commandWithOptions).run(); + + error = bashCommand.getExitStatus() != 0; + if (error) { + errorMessage = bashCommand.getError(); + if (errorMessage == null || errorMessage.isEmpty()) + throw new IllegalStateException("bitcoin-cli returned an error without a message"); + + } else { + output = bashCommand.getOutput(); + } + } + + @Override + public long getPid() { + // We don't cache the pid. The bitcoin-cli will quickly return a + // response, including server error info if any. + throw new UnsupportedOperationException("getPid not supported"); + } + + @Override + public void shutdown() { + // We don't try to shutdown the bitcoin-cli. It will quickly return a + // response, including server error info if any. + throw new UnsupportedOperationException("shutdown not supported"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java new file mode 100644 index 0000000000..4deaa8863c --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -0,0 +1,117 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static joptsimple.internal.Strings.EMPTY; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess { + + public BitcoinDaemon(ApiTestConfig config) { + super("bitcoind", config); + } + + @Override + public void start() throws InterruptedException, IOException { + + // If the bitcoind binary is dynamically linked to berkeley db libs, export the + // configured berkeley-db lib path. If statically linked, the berkeley db lib + // path will not be exported. + String berkeleyDbLibPathExport = config.berkeleyDbLibPath.equals(EMPTY) ? EMPTY + : "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + "; "; + + String bitcoindCmd = berkeleyDbLibPathExport + + config.bitcoinPath + "/bitcoind" + + " -datadir=" + config.bitcoinDatadir + + " -daemon" + + " -regtest=1" + + " -server=1" + + " -txindex=1" + + " -peerbloomfilters=1" + + " -debug=net" + + " -fallbackfee=0.0002" + + " -rpcport=" + config.bitcoinRpcPort + + " -rpcuser=" + config.bitcoinRpcUser + + " -rpcpassword=" + config.bitcoinRpcPassword + + " -blocknotify=" + "\"" + config.bitcoinDatadir + "/blocknotify" + " %s\""; + + BashCommand cmd = new BashCommand(bitcoindCmd).run(); + log.info("Starting ...\n$ {}", cmd.getCommand()); + + if (cmd.getExitStatus() != 0) { + startupExceptions.add(new IllegalStateException( + format("Error starting bitcoind%nstatus: %d%nerror msg: %s", + cmd.getExitStatus(), cmd.getError()))); + return; + } + + pid = BashCommand.getPid("bitcoind"); + if (!isAlive(pid)) + throw new IllegalStateException("Error starting regtest bitcoind daemon:\n" + cmd.getCommand()); + + log.info("Running with pid {}", pid); + log.info("Log {}", config.bitcoinDatadir + "/regtest/debug.log"); + } + + @Override + public long getPid() { + return this.pid; + } + + @Override + public void shutdown() { + try { + log.info("Shutting down bitcoind daemon..."); + + if (!isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException("Bitcoind already shut down.")); + return; + } + + if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) { + this.shutdownExceptions.add(new IllegalStateException("Could not shut down bitcoind; probably already stopped.")); + return; + } + + MILLISECONDS.sleep(2500); // allow it time to shutdown + + if (isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException( + format("Could not kill bitcoind process with pid %d.", pid))); + return; + } + + log.info("Stopped"); + } catch (InterruptedException ignored) { + // empty + } catch (IOException e) { + this.shutdownExceptions.add(new IllegalStateException("Error shutting down bitcoind.", e)); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java new file mode 100644 index 0000000000..fff25c2976 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.IOException; + +import java.util.List; + +public interface LinuxProcess { + void start() throws InterruptedException, IOException; + + String getName(); + + long getPid(); + + boolean hasStartupExceptions(); + + boolean hasShutdownExceptions(); + + void logExceptions(List exceptions, org.slf4j.Logger log); + + List getStartupExceptions(); + + List getShutdownExceptions(); + + void shutdown(); +} diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java new file mode 100644 index 0000000000..28c0fde235 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.IOException; +import java.io.InputStream; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * This class can be used to execute a system command from a Java application. + * See the documentation for the public methods of this class for more + * information. + * + * Documentation for this class is available at this URL: + * + * http://devdaily.com/java/java-processbuilder-process-system-exec + * + * Copyright 2010 alvin j. alexander, devdaily.com. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program 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 Lesser Public License for more details. + * You should have received a copy of the GNU Lesser Public License + * along with this program. If not, see . + * + * Please ee the following page for the LGPL license: + * http://www.gnu.org/licenses/lgpl.txt + * + */ +@Slf4j +class SystemCommandExecutor { + private final List cmdOptions; + private ThreadedStreamHandler inputStreamHandler; + private ThreadedStreamHandler errorStreamHandler; + + public SystemCommandExecutor(final List cmdOptions) { + if (log.isDebugEnabled()) + log.debug("cmd options {}", cmdOptions.toString()); + + if (cmdOptions.isEmpty()) + throw new IllegalStateException("No command params specified."); + + if (cmdOptions.contains("sudo")) + throw new IllegalStateException("'sudo' commands are prohibited."); + + this.cmdOptions = cmdOptions; + } + + // Execute a system command and return its status code (0 or 1). + // The system command's output (stderr or stdout) can be accessed from accessors. + public int exec() throws IOException, InterruptedException { + return exec(true); + } + + // Execute a system command and return its status code (0 or 1). + // The system command's output (stderr or stdout) can be accessed from accessors + // if the waitOnErrStream flag is true, else the method will not wait on (join) + // the error stream handler thread. + public int exec(boolean waitOnErrStream) throws IOException, InterruptedException { + Process process = new ProcessBuilder(cmdOptions).start(); + + // I'm currently doing these on a separate line here in case i need to set them to null + // to get the threads to stop. + // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html + InputStream inputStream = process.getInputStream(); + InputStream errorStream = process.getErrorStream(); + + // These need to run as java threads to get the standard output and error from the command. + // the inputstream handler gets a reference to our stdOutput in case we need to write + // something to it. + inputStreamHandler = new ThreadedStreamHandler(inputStream); + errorStreamHandler = new ThreadedStreamHandler(errorStream); + + inputStreamHandler.start(); + errorStreamHandler.start(); + + int exitStatus = process.waitFor(); + + inputStreamHandler.interrupt(); + errorStreamHandler.interrupt(); + + inputStreamHandler.join(); + if (waitOnErrStream) + errorStreamHandler.join(); + + return exitStatus; + } + + // Get the standard error from an executed system command. + public StringBuilder getStandardErrorFromCommand() { + return errorStreamHandler.getOutputBuffer(); + } + + // Get the standard output from an executed system command. + public StringBuilder getStandardOutputFromCommand() { + return inputStreamHandler.getOutputBuffer(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java new file mode 100644 index 0000000000..540b136100 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java @@ -0,0 +1,91 @@ +/* + * 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 bisq.apitest.linux; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import lombok.extern.slf4j.Slf4j; + +/** + * This class is intended to be used with the SystemCommandExecutor + * class to let users execute system commands from Java applications. + * + * This class is based on work that was shared in a JavaWorld article + * named "When System.exec() won't". That article is available at this + * url: + * + * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html + * + * Documentation for this class is available at this URL: + * + * http://devdaily.com/java/java-processbuilder-process-system-exec + * + * + * Copyright 2010 alvin j. alexander, devdaily.com. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program 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 Lesser Public License for more details. + * You should have received a copy of the GNU Lesser Public License + * along with this program. If not, see . + * + * Please ee the following page for the LGPL license: + * http://www.gnu.org/licenses/lgpl.txt + * + */ +@Slf4j +class ThreadedStreamHandler extends Thread { + final InputStream inputStream; + final StringBuilder outputBuffer = new StringBuilder(); + + ThreadedStreamHandler(InputStream inputStream) { + this.inputStream = inputStream; + } + + public void run() { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) + outputBuffer.append(line).append("\n"); + + } catch (Throwable t) { + t.printStackTrace(); + } + } + + @SuppressWarnings("unused") + private void doSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { + // empty + } + } + + public StringBuilder getOutputBuffer() { + return outputBuffer; + } +} + diff --git a/apitest/src/main/resources/apitest.properties b/apitest/src/main/resources/apitest.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apitest/src/main/resources/blocknotify b/apitest/src/main/resources/blocknotify new file mode 100755 index 0000000000..210d23f17c --- /dev/null +++ b/apitest/src/main/resources/blocknotify @@ -0,0 +1,20 @@ +#!/bin/bash + +# Regtest ports start with 512* + +# To avoid pesky bitcoind io errors, do not specify ports Bisq is not listening to. + +# SeedNode listens on port 5120 +echo $1 | nc -w 1 127.0.0.1 5120 + +# Arb Node listens on port 5121 +echo $1 | nc -w 1 127.0.0.1 5121 + +# Alice Node listens on port 5122 +echo $1 | nc -w 1 127.0.0.1 5122 + +# Bob Node listens on port 5123 +echo $1 | nc -w 1 127.0.0.1 5123 + +# Some other node listens on port 5124, etc. +# echo $1 | nc -w 1 127.0.0.1 5124 diff --git a/apitest/src/main/resources/logback.xml b/apitest/src/main/resources/logback.xml new file mode 100644 index 0000000000..28279faa11 --- /dev/null +++ b/apitest/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java new file mode 100644 index 0000000000..1ecd7e53c3 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -0,0 +1,184 @@ +/* + * 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 bisq.apitest; + +import java.io.File; +import java.io.IOException; + +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.TestInfo; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod; +import static bisq.proto.grpc.GetVersionGrpc.getGetVersionMethod; +import static java.net.InetAddress.getLoopbackAddress; +import static java.util.Arrays.stream; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.method.BitcoinCliHelper; +import bisq.cli.GrpcClient; +import bisq.daemon.grpc.GrpcVersionService; +import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig; + +/** + * Base class for all test types: 'method', 'scenario' and 'e2e'. + *

+ * During scaffold setup, various combinations of bitcoind and bisq instances + * can be started in the background before test cases are run. Currently, this test + * harness supports only the "Bisq DAO development environment running against a + * local Bitcoin regtest network" as described in + * dev-setup.md + * and dao-setup.md. + *

+ * Those documents contain information about the configurations used by this test harness: + * bitcoin-core's bitcoin.conf and blocknotify values, bisq instance options, the DAO genesis + * transaction id, initial BSQ and BTC balances for Bob & Alice accounts, and Bob and + * Alice's default payment accounts. + *

+ * During a build, the + * dao-setup.zip + * file is downloaded and extracted if necessary. In each test case's @BeforeClass + * method, the DAO setup files are re-installed into the run time's data directories + * (each test case runs on a refreshed DAO/regtest environment setup). + *

+ * Initial Alice balances & accounts: 10.0 BTC, 1000000.00 BSQ, USD PerfectMoney dummy + *

+ * Initial Bob balances & accounts: 10.0 BTC, 1500000.00 BSQ, USD PerfectMoney dummy + */ +@Slf4j +public class ApiTestCase { + + protected static Scaffold scaffold; + protected static ApiTestConfig config; + protected static BitcoinCliHelper bitcoinCli; + + @Nullable + protected static GrpcClient arbClient; + @Nullable + protected static GrpcClient aliceClient; + @Nullable + protected static GrpcClient bobClient; + + public static void setUpScaffold(Enum... supportingApps) + throws InterruptedException, ExecutionException, IOException { + String[] params = new String[]{ + "--supportingApps", stream(supportingApps).map(Enum::name).collect(Collectors.joining(",")), + "--callRateMeteringConfigPath", defaultRateMeterInterceptorConfig().getAbsolutePath(), + "--enableBisqDebugging", "false" + }; + setUpScaffold(params); + } + + public static void setUpScaffold(String[] params) + throws InterruptedException, ExecutionException, IOException { + // Test cases needing to pass more than just an ApiTestConfig + // --supportingApps option will use this setup method, but the + // --supportingApps option will need to be passed too, with its comma + // delimited app list value, e.g., "bitcoind,seednode,arbdaemon". + scaffold = new Scaffold(params).setUp(); + config = scaffold.config; + bitcoinCli = new BitcoinCliHelper((config)); + createGrpcClients(); + } + + public static void tearDownScaffold() { + scaffold.tearDown(); + } + + protected static void createGrpcClients() { + if (config.supportingApps.contains(alicedaemon.name())) { + aliceClient = new GrpcClient(getLoopbackAddress().getHostAddress(), + alicedaemon.apiPort, + config.apiPassword); + } + if (config.supportingApps.contains(bobdaemon.name())) { + bobClient = new GrpcClient(getLoopbackAddress().getHostAddress(), + bobdaemon.apiPort, + config.apiPassword); + } + if (config.supportingApps.contains(arbdaemon.name())) { + arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(), + arbdaemon.apiPort, + config.apiPassword); + } + } + + protected static void genBtcBlocksThenWait(int numBlocks, long wait) { + bitcoinCli.generateBlocks(numBlocks); + sleep(wait); + } + + protected static void sleep(long ms) { + try { + MILLISECONDS.sleep(ms); + } catch (InterruptedException ignored) { + // empty + } + } + + protected final String testName(TestInfo testInfo) { + return testInfo.getTestMethod().isPresent() + ? testInfo.getTestMethod().get().getName() + : "unknown test name"; + } + + protected static File defaultRateMeterInterceptorConfig() { + GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder(); + builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), + getGetVersionMethod().getFullMethodName(), + 1, + SECONDS); + // Only GrpcVersionService is @VisibleForTesting, so we need to + // hardcode other grpcServiceClassName parameter values used in + // builder.addCallRateMeter(...). + builder.addCallRateMeter("GrpcDisputeAgentsService", + getRegisterDisputeAgentMethod().getFullMethodName(), + 10, // Same as default. + SECONDS); + // Define rate meters for non-existent method 'disabled', to override other grpc + // services' default rate meters -- defined in their rateMeteringInterceptor() + // methods. + String[] serviceClassNames = new String[]{ + "GrpcGetTradeStatisticsService", + "GrpcHelpService", + "GrpcOffersService", + "GrpcPaymentAccountsService", + "GrpcPriceService", + "GrpcTradesService", + "GrpcWalletsService" + }; + for (String service : serviceClassNames) { + builder.addCallRateMeter(service, "disabled", 1, MILLISECONDS); + } + File file = builder.build(); + file.deleteOnExit(); + return file; + } +} diff --git a/apitest/src/test/java/bisq/apitest/JUnitHelper.java b/apitest/src/test/java/bisq/apitest/JUnitHelper.java new file mode 100644 index 0000000000..8ea80ad0d5 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/JUnitHelper.java @@ -0,0 +1,58 @@ +package bisq.apitest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +import static java.lang.String.format; + +@Slf4j +public class JUnitHelper { + + private static boolean allPass; + + public static void runTests(Class... testClasses) { + JUnitCore jUnitCore = new JUnitCore(); + jUnitCore.addListener(new RunListener() { + public void testStarted(Description description) { + log.info("{}", description); + } + + public void testIgnored(Description description) { + log.info("Ignored {}", description); + } + + public void testFailure(Failure failure) { + log.error("Failed {}", failure.getTrace()); + } + }); + Result result = jUnitCore.run(testClasses); + printTestResults(result); + } + + public static boolean allTestsPassed() { + return allPass; + } + + private static void printTestResults(Result result) { + log.info("Total tests: {}, Failed: {}, Ignored: {}", + result.getRunCount(), + result.getFailureCount(), + result.getIgnoreCount()); + + if (result.wasSuccessful()) { + log.info("All {} tests passed", result.getRunCount()); + allPass = true; + } else if (result.getFailureCount() > 0) { + log.error("{} test(s) failed", result.getFailureCount()); + result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", + f.getDescription().getTestClass().getName(), + f.getDescription().getMethodName(), + f.getTrace()))); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java new file mode 100644 index 0000000000..6e5b52080f --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java @@ -0,0 +1,92 @@ +/* + * 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 bisq.apitest.method; + +import java.io.IOException; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.linux.BitcoinCli; + +public final class BitcoinCliHelper { + + private final ApiTestConfig config; + + public BitcoinCliHelper(ApiTestConfig config) { + this.config = config; + } + + // Convenience methods for making bitcoin-cli calls. + + public String getNewBtcAddress() { + try { + BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); + + if (newAddress.isError()) + fail(format("Could not generate new bitcoin address:%n%s", newAddress.getErrorMessage())); + + return newAddress.getOutput(); + } catch (IOException | InterruptedException ex) { + fail(ex); + return null; + } + } + + public String[] generateToAddress(int blocks, String address) { + try { + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); + + if (generateToAddress.isError()) + fail(format("Could not generate bitcoin block(s):%n%s", generateToAddress.getErrorMessage())); + + return generateToAddress.getOutputValueAsStringArray(); + } catch (IOException | InterruptedException ex) { + fail(ex); + return null; + } + } + + public void generateBlocks(int blocks) { + generateToAddress(blocks, getNewBtcAddress()); + } + + public String sendToAddress(String address, String amount) { + // sendtoaddress "address" amount \ + // ( "comment" "comment_to" subtractfeefromamount \ + // replaceable conf_target "estimate_mode" ) + // returns a transaction id + try { + String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", + address, amount); + BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); + + if (sendToAddress.isError()) + fail(format("Could not send BTC to address:%n%s", sendToAddress.getErrorMessage())); + + return sendToAddress.getOutput(); + } catch (IOException | InterruptedException ex) { + fail(ex); + return null; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/CallRateMeteringInterceptorTest.java b/apitest/src/test/java/bisq/apitest/method/CallRateMeteringInterceptorTest.java new file mode 100644 index 0000000000..f7a076a9f9 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/CallRateMeteringInterceptorTest.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.apitest.method; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CallRateMeteringInterceptorTest extends MethodTest { + + private static final GetVersionTest getVersionTest = new GetVersionTest(); + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + false, + bitcoind, alicedaemon); + } + + @BeforeEach + public void sleep200Milliseconds() { + sleep(200); + } + + @Test + @Order(1) + public void testGetVersionCall1IsAllowed() { + getVersionTest.testGetVersion(); + } + + @Test + @Order(2) + public void testGetVersionCall2ShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion); + assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded", + exception.getMessage()); + } + + @Test + @Order(3) + public void testGetVersionCall3ShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion); + assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded", + exception.getMessage()); + } + + @Test + @Order(4) + public void testGetVersionCall4IsAllowed() { + sleep(1100); // Let the server's rate meter reset the call count. + getVersionTest.testGetVersion(); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/GetMethodHelpTest.java b/apitest/src/test/java/bisq/apitest/method/GetMethodHelpTest.java new file mode 100644 index 0000000000..4e426be660 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/GetMethodHelpTest.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.apitest.method; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.cli.Method.createoffer; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class GetMethodHelpTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetCreateOfferHelp() { + var help = aliceClient.getMethodHelp(createoffer); + assertNotNull(help); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java new file mode 100644 index 0000000000..6b84b6ccd7 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.apitest.method; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.common.app.Version.VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class GetVersionTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetVersion() { + var version = aliceClient.getVersion(); + assertEquals(VERSION, version); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java new file mode 100644 index 0000000000..51308d8ccd --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -0,0 +1,149 @@ +/* + * 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 bisq.apitest.method; + +import bisq.core.api.model.PaymentAccountForm; +import bisq.core.payment.F2FAccount; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.util.Utilities; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.stream; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.ApiTestCase; +import bisq.cli.GrpcClient; + +public class MethodTest extends ApiTestCase { + + protected static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver(); + + private static final Function[], String> toNameList = (enums) -> + stream(enums).map(Enum::name).collect(Collectors.joining(",")); + + public static void startSupportingApps(File callRateMeteringConfigFile, + boolean generateBtcBlock, + Enum... supportingApps) { + startSupportingApps(callRateMeteringConfigFile, + generateBtcBlock, + false, + supportingApps); + } + + public static void startSupportingApps(File callRateMeteringConfigFile, + boolean generateBtcBlock, + boolean startSupportingAppsInDebugMode, + Enum... supportingApps) { + try { + setUpScaffold(new String[]{ + "--supportingApps", toNameList.apply(supportingApps), + "--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(), + "--enableBisqDebugging", startSupportingAppsInDebugMode ? "true" : "false" + }); + doPostStartup(generateBtcBlock); + } catch (Exception ex) { + fail(ex); + } + } + + public static void startSupportingApps(boolean generateBtcBlock, + Enum... supportingApps) { + startSupportingApps(generateBtcBlock, + false, + supportingApps); + } + + public static void startSupportingApps(boolean generateBtcBlock, + boolean startSupportingAppsInDebugMode, + Enum... supportingApps) { + try { + // Disable call rate metering where there is no callRateMeteringConfigFile. + File callRateMeteringConfigFile = defaultRateMeterInterceptorConfig(); + setUpScaffold(new String[]{ + "--supportingApps", toNameList.apply(supportingApps), + "--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(), + "--enableBisqDebugging", startSupportingAppsInDebugMode ? "true" : "false" + }); + doPostStartup(generateBtcBlock); + } catch (Exception ex) { + fail(ex); + } + } + + protected static void doPostStartup(boolean generateBtcBlock) { + // Generate 1 regtest block for alice's and/or bob's wallet to + // show 10 BTC balance, and allow time for daemons parse the new block. + if (generateBtcBlock) + genBtcBlocksThenWait(1, 1500); + } + + protected final File getPaymentAccountForm(GrpcClient grpcClient, String paymentMethodId) { + // We take seemingly unnecessary steps to get a File object, but the point is to + // test the API, and we do not directly ask bisq.core.api.model.PaymentAccountForm + // for an empty json form (file). + String jsonString = grpcClient.getPaymentAcctFormAsJson(paymentMethodId); + // Write the json string to a file here in the test case. + File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId); + try (PrintWriter out = new PrintWriter(jsonFile, UTF_8)) { + out.println(jsonString); + } catch (IOException ex) { + fail("Could not create tmp payment account form.", ex); + } + return jsonFile; + } + + + protected bisq.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient, + String countryCode) { + String f2fAccountJsonString = "{\n" + + " \"_COMMENTS_\": \"This is a dummy account.\",\n" + + " \"paymentMethodId\": \"F2F\",\n" + + " \"accountName\": \"Dummy " + countryCode.toUpperCase() + " F2F Account\",\n" + + " \"city\": \"Anytown\",\n" + + " \"contact\": \"Morse Code\",\n" + + " \"country\": \"" + countryCode.toUpperCase() + "\",\n" + + " \"extraInfo\": \"Salt Lick #213\"\n" + + "}\n"; + F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(grpcClient, f2fAccountJsonString); + return f2FAccount; + } + + protected final bisq.core.payment.PaymentAccount createPaymentAccount(GrpcClient grpcClient, String jsonString) { + // Normally, we do asserts on the protos from the gRPC service, but in this + // case we need a bisq.core.payment.PaymentAccount so it can be cast to its + // sub type. + var paymentAccount = grpcClient.createPaymentAccount(jsonString); + return bisq.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); + } + + // Static conveniences for test methods and test case fixture setups. + + protected static String encodeToHex(String s) { + return Utilities.bytesAsHexString(s.getBytes(UTF_8)); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java new file mode 100644 index 0000000000..b5011ffced --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.apitest.method; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.ApiTestConfig.ARBITRATOR; +import static bisq.apitest.config.ApiTestConfig.MEDIATOR; +import static bisq.apitest.config.ApiTestConfig.REFUND_AGENT; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +@SuppressWarnings("ResultOfMethodCallIgnored") +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class RegisterDisputeAgentsTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, seednode, arbdaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testRegisterArbitratorShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY)); + assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI", + exception.getMessage()); + } + + @Test + @Order(2) + public void testInvalidDisputeAgentTypeArgShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + arbClient.registerDisputeAgent("badagent", DEV_PRIVILEGE_PRIV_KEY)); + assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'", + exception.getMessage()); + } + + @Test + @Order(3) + public void testInvalidRegistrationKeyArgShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + arbClient.registerDisputeAgent(REFUND_AGENT, "invalid" + DEV_PRIVILEGE_PRIV_KEY)); + assertEquals("INVALID_ARGUMENT: invalid registration key", + exception.getMessage()); + } + + @Test + @Order(4) + public void testRegisterMediator() { + arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY); + } + + @Test + @Order(5) + public void testRegisterRefundAgent() { + arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java new file mode 100644 index 0000000000..f0e95dd25f --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -0,0 +1,96 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.core.monetary.Altcoin; + +import protobuf.PaymentAccount; + +import org.bitcoinj.utils.Fiat; + +import java.math.BigDecimal; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.common.util.MathUtils.roundDouble; +import static bisq.common.util.MathUtils.scaleDownByPowerOf10; +import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; +import static java.math.RoundingMode.HALF_UP; + + + +import bisq.apitest.method.MethodTest; + +@Slf4j +public abstract class AbstractOfferTest extends MethodTest { + + @Setter + protected static boolean isLongRunningTest; + + protected static PaymentAccount alicesBsqAcct; + protected static PaymentAccount bobsBsqAcct; + + @BeforeAll + public static void setUp() { + startSupportingApps(true, + false, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); + } + + + public static void createBsqPaymentAccounts() { + alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account", + BSQ, + aliceClient.getUnusedBsqAddress(), + false); + bobsBsqAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's BSQ Account", + BSQ, + bobClient.getUnusedBsqAddress(), + false); + } + + protected double getScaledOfferPrice(double offerPrice, String currencyCode) { + int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + return scaleDownByPowerOf10(offerPrice, precision); + } + + protected final double getPercentageDifference(double price1, double price2) { + return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5)) + .setScale(4, HALF_UP) + .doubleValue(); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java new file mode 100644 index 0000000000..fe21e4aa8f --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static protobuf.OfferPayload.Direction.BUY; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CancelOfferTest extends AbstractOfferTest { + + private static final String DIRECTION = BUY.name(); + private static final String CURRENCY_CODE = "cad"; + private static final int MAX_OFFERS = 3; + + private final Consumer createOfferToCancel = (paymentAccountId) -> { + aliceClient.createMarketBasedPricedOffer(DIRECTION, + CURRENCY_CODE, + 10000000L, + 10000000L, + 0.00, + getDefaultBuyerSecurityDepositAsPercent(), + paymentAccountId, + BSQ); + }; + + @Test + @Order(1) + public void testCancelOffer() { + PaymentAccount cadAccount = createDummyF2FAccount(aliceClient, "CA"); + + // Create some offers. + for (int i = 1; i <= MAX_OFFERS; i++) { + createOfferToCancel.accept(cadAccount.getId()); + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay. + sleep(2500); + } + + List offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); + assertEquals(MAX_OFFERS, offers.size()); + + // Cancel the offers, checking the open offer count after each offer removal. + for (int i = 1; i <= MAX_OFFERS; i++) { + aliceClient.cancelOffer(offers.remove(0).getId()); + offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); + assertEquals(MAX_OFFERS - i, offers.size()); + } + + sleep(1000); // wait for offer removal + + offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); + assertEquals(0, offers.size()); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java new file mode 100644 index 0000000000..ba4f8ce47b --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java @@ -0,0 +1,252 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CreateBSQOffersTest extends AbstractOfferTest { + + private static final String MAKER_FEE_CURRENCY_CODE = BSQ; + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + createBsqPaymentAccounts(); + } + + @Test + @Order(1) + public void testCreateBuy1BTCFor20KBSQOffer() { + // Remember alt coin trades are BTC trades. When placing an offer, you are + // offering to buy or sell BTC, not BSQ, XMR, etc. In this test case, + // Alice places an offer to BUY BTC with BSQ. + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + "0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("Sell BSQ (Buy BTC) OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(100_000_000L, newOffer.getAmount()); + assertEquals(100_000_000L, newOffer.getMinAmount()); + assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(100_000_000L, newOffer.getAmount()); + assertEquals(100_000_000L, newOffer.getMinAmount()); + assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(2) + public void testCreateSell1BTCFor20KBSQOffer() { + // Alice places an offer to SELL BTC for BSQ. + var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), + BSQ, + 100_000_000L, + 100_000_000L, + "0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("SELL 20K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(100_000_000L, newOffer.getAmount()); + assertEquals(100_000_000L, newOffer.getMinAmount()); + assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(100_000_000L, newOffer.getAmount()); + assertEquals(100_000_000L, newOffer.getMinAmount()); + assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(3) + public void testCreateBuyBTCWith1To2KBSQOffer() { + // Alice places an offer to BUY 0.05 - 0.10 BTC with BSQ. + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 10_000_000L, + 5_000_000L, + "0.00005", // FIXED PRICE IN BTC sats FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("BUY 1-2K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(10_000_000L, newOffer.getAmount()); + assertEquals(5_000_000L, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(10_000_000L, newOffer.getAmount()); + assertEquals(5_000_000L, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(4) + public void testCreateSellBTCFor5To10KBSQOffer() { + // Alice places an offer to SELL 0.25 - 0.50 BTC for BSQ. + var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), + BSQ, + 50_000_000L, + 25_000_000L, + "0.00005", // FIXED PRICE IN BTC sats FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("SELL 5-10K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(50_000_000L, newOffer.getAmount()); + assertEquals(25_000_000L, newOffer.getMinAmount()); + assertEquals(7_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(5_000, newOffer.getPrice()); + assertEquals(50_000_000L, newOffer.getAmount()); + assertEquals(25_000_000L, newOffer.getMinAmount()); + assertEquals(7_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(alicesBsqAcct.getId(), newOffer.getPaymentAccountId()); + assertEquals(BSQ, newOffer.getBaseCurrencyCode()); + assertEquals(BTC, newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(5) + public void testGetAllMyBsqOffers() { + List offers = aliceClient.getMyBsqOffersSortedByDate(); + log.info("ALL ALICE'S BSQ OFFERS:\n{}", formatOfferTable(offers, BSQ)); + assertEquals(4, offers.size()); + log.info("ALICE'S BALANCES\n{}", formatBalancesTbls(aliceClient.getBalances())); + } + + @Test + @Order(6) + public void testGetAvailableBsqOffers() { + List offers = bobClient.getBsqOffersSortedByDate(); + log.info("ALL BOB'S AVAILABLE BSQ OFFERS:\n{}", formatOfferTable(offers, BSQ)); + assertEquals(4, offers.size()); + log.info("BOB'S BALANCES\n{}", formatBalancesTbls(bobClient.getBalances())); + } + + private void genBtcBlockAndWaitForOfferPreparation() { + // Extra time is needed for the OfferUtils#isBsqForMakerFeeAvailable, which + // can sometimes return an incorrect false value if the BsqWallet's + // available confirmed balance is temporarily = zero during BSQ offer prep. + genBtcBlocksThenWait(1, 5000); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java new file mode 100644 index 0000000000..081c6feadc --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -0,0 +1,167 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.core.payment.PaymentAccount; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { + + private static final String MAKER_FEE_CURRENCY_CODE = BSQ; + + @Test + @Order(1) + public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { + PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU"); + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + "aud", + 10_000_000L, + 10_000_000L, + "36000", + getDefaultBuyerSecurityDepositAsPercent(), + audAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(360_000_000, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("AUD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(360_000_000, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("AUD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(2) + public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { + PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + "usd", + 10_000_000L, + 10_000_000L, + "30000.1234", + getDefaultBuyerSecurityDepositAsPercent(), + usdAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(300_001_234, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(300_001_234, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(3) + public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { + PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR"); + var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), + "eur", + 10_000_000L, + 5_000_000L, + "29500.1234", + getDefaultBuyerSecurityDepositAsPercent(), + eurAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(295_001_234, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(295_001_234, newOffer.getPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java new file mode 100644 index 0000000000..94c2519d91 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -0,0 +1,293 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.OfferInfo; + +import java.text.DecimalFormat; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.common.util.MathUtils.scaleDownByPowerOf10; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static java.lang.Math.abs; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { + + private static final DecimalFormat PCT_FORMAT = new DecimalFormat("##0.00"); + private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50% + private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01% + + private static final String MAKER_FEE_CURRENCY_CODE = BTC; + + @Test + @Order(1) + public void testCreateUSDBTCBuyOffer5PctPriceMargin() { + PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); + double priceMarginPctInput = 5.00; + var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + "usd", + 10_000_000L, + 10_000_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + usdAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); + } + + @Test + @Order(2) + public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { + PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ"); + double priceMarginPctInput = -2.00; + var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + "nzd", + 10_000_000L, + 10_000_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + nzdAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("NZD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(10_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("NZD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); + } + + @Test + @Order(3) + public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { + PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB"); + double priceMarginPctInput = -1.5; + var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), + "gbp", + 10_000_000L, + 5_000_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + gbpAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("GBP", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("GBP", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); + } + + @Test + @Order(4) + public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { + PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR"); + double priceMarginPctInput = 6.55; + var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), + "brl", + 10_000_000L, + 5_000_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + brlAccount.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("BRL", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + assertEquals(10_000_000, newOffer.getAmount()); + assertEquals(5_000_000, newOffer.getMinAmount()); + assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); + assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(BTC, newOffer.getBaseCurrencyCode()); + assertEquals("BRL", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); + + assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); + } + + private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { + assertTrue(() -> { + String counterCurrencyCode = offer.getCounterCurrencyCode(); + double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode); + double scaledOfferPrice = getScaledOfferPrice(offer.getPrice(), counterCurrencyCode); + double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2); + double actualDiffPct = offer.getDirection().equals(BUY.name()) + ? getPercentageDifference(scaledOfferPrice, mktPrice) + : getPercentageDifference(mktPrice, scaledOfferPrice); + double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct); + return isCalculatedPriceWithinErrorTolerance(pctDiffDelta, + expectedDiffPct, + actualDiffPct, + mktPrice, + scaledOfferPrice, + offer); + }); + } + + private boolean isCalculatedPriceWithinErrorTolerance(double delta, + double expectedDiffPct, + double actualDiffPct, + double mktPrice, + double scaledOfferPrice, + OfferInfo offer) { + if (abs(delta) > MKT_PRICE_MARGIN_ERROR_TOLERANCE) { + logCalculatedPricePoppedErrorTolerance(expectedDiffPct, + actualDiffPct, + mktPrice, + scaledOfferPrice); + log.error(offer.toString()); + return false; + } + + if (abs(delta) >= MKT_PRICE_MARGIN_WARNING_TOLERANCE) { + logCalculatedPricePoppedWarningTolerance(expectedDiffPct, + actualDiffPct, + mktPrice, + scaledOfferPrice); + log.warn(offer.toString()); + } + + return true; + } + + private void logCalculatedPricePoppedWarningTolerance(double expectedDiffPct, + double actualDiffPct, + double mktPrice, + double scaledOfferPrice) { + log.warn(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s," + + " not by %s%s, outside the %s%s warning tolerance," + + " but within the %s%s error tolerance.", + scaledOfferPrice, mktPrice, + PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%", + PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%", + PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_WARNING_TOLERANCE, 2)), "%", + PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%")); + } + + private void logCalculatedPricePoppedErrorTolerance(double expectedDiffPct, + double actualDiffPct, + double mktPrice, + double scaledOfferPrice) { + log.error(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s," + + " not by %s%s, outside the %s%s error tolerance.", + scaledOfferPrice, mktPrice, + PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%", + PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%", + PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%")); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java new file mode 100644 index 0000000000..33626ee6c3 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java @@ -0,0 +1,97 @@ +/* + * 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 bisq.apitest.method.offer; + +import bisq.core.payment.PaymentAccount; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static protobuf.OfferPayload.Direction.BUY; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ValidateCreateOfferTest extends AbstractOfferTest { + + @Test + @Order(1) + public void testAmtTooLargeShouldThrowException() { + PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); + @SuppressWarnings("ResultOfMethodCallIgnored") + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.createFixedPricedOffer(BUY.name(), + "usd", + 100000000000L, // exceeds amount limit + 100000000000L, + "10000.0000", + getDefaultBuyerSecurityDepositAsPercent(), + usdAccount.getId(), + BSQ)); + assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); + } + + @Test + @Order(2) + public void testNoMatchingEURPaymentAccountShouldThrowException() { + PaymentAccount chfAccount = createDummyF2FAccount(aliceClient, "ch"); + @SuppressWarnings("ResultOfMethodCallIgnored") + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.createFixedPricedOffer(BUY.name(), + "eur", + 10000000L, + 10000000L, + "40000.0000", + getDefaultBuyerSecurityDepositAsPercent(), + chfAccount.getId(), + BTC)); + String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); + assertEquals(expectedError, exception.getMessage()); + } + + @Test + @Order(2) + public void testNoMatchingCADPaymentAccountShouldThrowException() { + PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "au"); + @SuppressWarnings("ResultOfMethodCallIgnored") + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.createFixedPricedOffer(BUY.name(), + "cad", + 10000000L, + 10000000L, + "63000.0000", + getDefaultBuyerSecurityDepositAsPercent(), + audAccount.getId(), + BTC)); + 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/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java new file mode 100644 index 0000000000..433898731e --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -0,0 +1,205 @@ +package bisq.apitest.method.payment; + +import bisq.core.api.model.PaymentAccountForm; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonWriter; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; + +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + + + +import bisq.apitest.method.MethodTest; +import bisq.cli.GrpcClient; + +@Slf4j +public class AbstractPaymentAccountTest extends MethodTest { + + static final String PROPERTY_NAME_JSON_COMMENTS = "_COMMENTS_"; + static final List PROPERTY_VALUE_JSON_COMMENTS = new ArrayList<>() {{ + add("Do not manually edit the paymentMethodId field."); + add("Edit the salt field only if you are recreating a payment" + + " account on a new installation and wish to preserve the account age."); + }}; + + static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId"; + + static final String PROPERTY_NAME_ACCOUNT_ID = "accountId"; + static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName"; + static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr"; + static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType"; + static final String PROPERTY_NAME_ANSWER = "answer"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber"; + static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType"; + static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode"; + static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName"; + static final String PROPERTY_NAME_BANK_CODE = "bankCode"; + @SuppressWarnings("unused") + static final String PROPERTY_NAME_BANK_ID = "bankId"; + static final String PROPERTY_NAME_BANK_NAME = "bankName"; + static final String PROPERTY_NAME_BRANCH_ID = "branchId"; + static final String PROPERTY_NAME_BIC = "bic"; + static final String PROPERTY_NAME_COUNTRY = "country"; + static final String PROPERTY_NAME_CITY = "city"; + static final String PROPERTY_NAME_CONTACT = "contact"; + static final String PROPERTY_NAME_EMAIL = "email"; + static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr"; + static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo"; + static final String PROPERTY_NAME_HOLDER_EMAIL = "holderEmail"; + static final String PROPERTY_NAME_HOLDER_NAME = "holderName"; + static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId"; + static final String PROPERTY_NAME_IBAN = "iban"; + static final String PROPERTY_NAME_MOBILE_NR = "mobileNr"; + static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId"; + static final String PROPERTY_NAME_PAY_ID = "payid"; + static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress"; + static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId"; + static final String PROPERTY_NAME_QUESTION = "question"; + static final String PROPERTY_NAME_REQUIREMENTS = "requirements"; + static final String PROPERTY_NAME_SALT = "salt"; + static final String PROPERTY_NAME_SORT_CODE = "sortCode"; + static final String PROPERTY_NAME_STATE = "state"; + static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies"; + static final String PROPERTY_NAME_USERNAME = "userName"; + + static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .create(); + + static final Map COMPLETED_FORM_MAP = new HashMap<>(); + + // A payment account serializer / deserializer. + static final PaymentAccountForm PAYMENT_ACCOUNT_FORM = new PaymentAccountForm(); + + @BeforeEach + public void setup() { + Res.setup(); + } + + protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) { + // This would normally be done in @BeforeEach, but these test cases might be + // called from a single 'scenario' test case, and the @BeforeEach -> clear() + // would be skipped. + COMPLETED_FORM_MAP.clear(); + + File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId); + // A short cut over the API: + // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); + log.debug("{} Empty form saved to {}", + testName(testInfo), + PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + emptyForm.deleteOnExit(); + return emptyForm; + } + + protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) { + @SuppressWarnings("unchecked") + Map emptyForm = (Map) GSON.fromJson( + PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm), + Object.class); + assertNotNull(emptyForm); + assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); + assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID)); + assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME)); + for (String field : fields) { + if (field.equals("country")) + assertEquals("your two letter country code", emptyForm.get(field)); + else + assertEquals("your " + field.toLowerCase(), emptyForm.get(field)); + } + } + + protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) { + // All PaymentAccount subclasses have paymentMethodId and an accountName fields. + assertNotNull(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId()); + assertTrue(paymentAccount.getCreationDate().getTime() > 0); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName()); + } + + protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) { + assertNotNull(paymentAccount.getSingleTradeCurrency()); + assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode()); + } + + protected final void verifyAccountTradeCurrencies(List expectedTradeCurrencies, + PaymentAccount paymentAccount) { + assertNotNull(paymentAccount.getTradeCurrencies()); + assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray()); + } + + protected final void verifyUserPayloadHasPaymentAccountWithId(GrpcClient grpcClient, + String paymentAccountId) { + Optional paymentAccount = grpcClient.getPaymentAccounts() + .stream() + .filter(a -> a.getId().equals(paymentAccountId)) + .findFirst(); + assertTrue(paymentAccount.isPresent()); + } + + protected final String getCompletedFormAsJsonString() { + File completedForm = fillPaymentAccountForm(); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + log.debug("Completed form: {}", jsonString); + return jsonString; + } + + private File fillPaymentAccountForm() { + File tmpJsonForm = null; + try { + tmpJsonForm = File.createTempFile("temp_acct_form_", + ".json", + Paths.get(getProperty("java.io.tmpdir")).toFile()); + JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(tmpJsonForm), UTF_8)); + writer.beginObject(); + + writer.name(PROPERTY_NAME_JSON_COMMENTS); + writer.beginArray(); + for (String s : PROPERTY_VALUE_JSON_COMMENTS) { + writer.value(s); + } + writer.endArray(); + + for (Map.Entry entry : COMPLETED_FORM_MAP.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + writer.name(k); + writer.value(v.toString()); + } + writer.endObject(); + writer.close(); + } catch (IOException ex) { + log.error("", ex); + fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP)); + } + tmpJsonForm.deleteOnExit(); + return tmpJsonForm; + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java new file mode 100644 index 0000000000..3caef6ee39 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -0,0 +1,948 @@ +/* + * 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 bisq.apitest.method.payment; + +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AdvancedCashAccount; +import bisq.core.payment.AliPayAccount; +import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.CashDepositAccount; +import bisq.core.payment.ChaseQuickPayAccount; +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.FasterPaymentsAccount; +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.InteracETransferAccount; +import bisq.core.payment.JapanBankAccount; +import bisq.core.payment.MoneyBeamAccount; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.NationalBankAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PerfectMoneyAccount; +import bisq.core.payment.PopmoneyAccount; +import bisq.core.payment.PromptPayAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.SameBankAccount; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.SepaInstantAccount; +import bisq.core.payment.SpecificBanksAccount; +import bisq.core.payment.SwishAccount; +import bisq.core.payment.TransferwiseAccount; +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.UpholdAccount; +import bisq.core.payment.WeChatPayAccount; +import bisq.core.payment.WesternUnionAccount; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.CashDepositAccountPayload; +import bisq.core.payment.payload.SameBankAccountPayload; +import bisq.core.payment.payload.SpecificBanksAccountPayload; + +import io.grpc.StatusRuntimeException; + +import java.io.File; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.cli.TableFormat.formatPaymentAcctTbl; +import static bisq.core.locale.CurrencyUtil.*; +import static bisq.core.payment.payload.PaymentMethod.*; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"}) +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class CreatePaymentAccountTest extends AbstractPaymentAccountTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + public void testCreateAdvancedCashAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID); + verifyEmptyForm(emptyForm, + ADVANCED_CASH_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountTradeCurrencies(getAllAdvancedCashCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateAliPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, ALI_PAY_ID); + verifyEmptyForm(emptyForm, + ALI_PAY_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ALI_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Ali Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("CNY", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + print(paymentAccount); + } + + @Test + public void testCreateAustraliaPayidAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID); + verifyEmptyForm(emptyForm, + AUSTRALIA_PAYID_ID, + PROPERTY_NAME_BANK_ACCOUNT_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAY_ID, "123 456 789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("AUD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateCashDepositAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID); + verifyEmptyForm(emptyForm, + CASH_DEPOSIT_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_ID, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_EMAIL, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID, + PROPERTY_NAME_REQUIREMENTS); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements..."); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + + CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements()); + print(paymentAccount); + } + + @Test + public void testCreateBrazilNationalBankAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID); + verifyEmptyForm(emptyForm, + NATIONAL_BANK_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87"); + // No BankId is required for BR. + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Banco do Brasil Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("BRL", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + + BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + // When no BankId is required, getBankId() returns bankName. + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateChaseQuickPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID); + verifyEmptyForm(emptyForm, + CHASE_QUICK_PAY_ID, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_HOLDER_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + print(paymentAccount); + } + + @Test + public void testCreateClearXChangeAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID); + verifyEmptyForm(emptyForm, + CLEAR_X_CHANGE_ID, + PROPERTY_NAME_EMAIL_OR_MOBILE_NR, + PROPERTY_NAME_HOLDER_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CLEAR_X_CHANGE_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Zelle Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateF2FAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, F2F_ID); + verifyEmptyForm(emptyForm, + F2F_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_CITY, + PROPERTY_NAME_CONTACT, + PROPERTY_NAME_EXTRA_INFO); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("BRL", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo()); + print(paymentAccount); + } + + @Test + public void testCreateFasterPaymentsAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID); + verifyEmptyForm(emptyForm, + FASTER_PAYMENTS_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_SORT_CODE); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Faster Payments Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateHalCashAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID); + verifyEmptyForm(emptyForm, + HAL_CASH_ID, + PROPERTY_NAME_MOBILE_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); + print(paymentAccount); + } + + @Test + public void testCreateInteracETransferAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID); + verifyEmptyForm(emptyForm, + INTERAC_E_TRANSFER_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_QUESTION, + PROPERTY_NAME_ANSWER); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Interac Transfer Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("CAD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateJapanBankAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID); + verifyEmptyForm(emptyForm, + JAPAN_BANK_ID, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BANK_CODE, + PROPERTY_NAME_BANK_BRANCH_CODE, + PROPERTY_NAME_BANK_BRANCH_NAME, + PROPERTY_NAME_BANK_ACCOUNT_NAME, + PROPERTY_NAME_BANK_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_ACCOUNT_NUMBER); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("JPY", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber()); + print(paymentAccount); + } + + @Test + public void testCreateMoneyBeamAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID); + verifyEmptyForm(emptyForm, + MONEY_BEAM_ID, + PROPERTY_NAME_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Money Beam Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateMoneyGramAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID); + verifyEmptyForm(emptyForm, + MONEY_GRAM_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_STATE); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); + print(paymentAccount); + } + + @Test + public void testCreatePerfectMoneyAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID); + verifyEmptyForm(emptyForm, + PERFECT_MONEY_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Perfect Money Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreatePopmoneyAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, POPMONEY_ID); + verifyEmptyForm(emptyForm, + POPMONEY_ID, + PROPERTY_NAME_ACCOUNT_ID, + PROPERTY_NAME_HOLDER_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, POPMONEY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Pop Money Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "POPMONEY 0000 1111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + print(paymentAccount); + } + + @Test + public void testCreatePromptPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, PROMPT_PAY_ID); + verifyEmptyForm(emptyForm, + PROMPT_PAY_ID, + PROPERTY_NAME_PROMPT_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PROMPT_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Prompt Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Prompt Pay Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("THB", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateRevolutAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, REVOLUT_ID); + verifyEmptyForm(emptyForm, + REVOLUT_ID, + PROPERTY_NAME_USERNAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountTradeCurrencies(getAllRevolutCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName()); + print(paymentAccount); + } + + @Test + public void testCreateSameBankAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SAME_BANK_ID); + verifyEmptyForm(emptyForm, + SAME_BANK_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SAME_BANK_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Same Bank Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Same Bank Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + // The bankId == bankName because bank id is not required in the UK. + assertEquals(payload.getBankId(), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateSepaInstantAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SEPA_INSTANT_ID); + verifyEmptyForm(emptyForm, + SEPA_INSTANT_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_IBAN, + PROPERTY_NAME_BIC); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_INSTANT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa Instant"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); + // bankId == bic + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); + print(paymentAccount); + } + + @Test + public void testCreateSepaAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SEPA_ID); + verifyEmptyForm(emptyForm, + SEPA_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_IBAN, + PROPERTY_NAME_BIC); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Conta Sepa Salt")); + String jsonString = getCompletedFormAsJsonString(); + SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); + // bankId == bic + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateSpecificBanksAccount(TestInfo testInfo) { + // TODO Supporting set of accepted banks may require some refactoring + // of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e., + // public void setAcceptedBanks(String... bankNames) { ... } + File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID); + verifyEmptyForm(emptyForm, + SPECIFIC_BANKS_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + // The bankId == bankName because bank id is not required in the UK. + assertEquals(payload.getBankId(), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + print(paymentAccount); + } + + @Test + public void testCreateSwishAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SWISH_ID); + verifyEmptyForm(emptyForm, + SWISH_ID, + PROPERTY_NAME_MOBILE_NR, + PROPERTY_NAME_HOLDER_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Acct Holder"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swish Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("SEK", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateTransferwiseAccountWith1TradeCurrency(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); + verifyEmptyForm(emptyForm, + TRANSFERWISE_ID, + PROPERTY_NAME_EMAIL); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + assertEquals(1, paymentAccount.getTradeCurrencies().size()); + TradeCurrency expectedCurrency = getTradeCurrency("EUR").get(); + assertEquals(expectedCurrency, paymentAccount.getSelectedTradeCurrency()); + List expectedTradeCurrencies = singletonList(expectedCurrency); + verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + print(paymentAccount); + } + + @Test + public void testCreateTransferwiseAccountWith10TradeCurrencies(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); + verifyEmptyForm(emptyForm, + TRANSFERWISE_ID, + PROPERTY_NAME_EMAIL); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "ars, cad, hrk, czk, eur, hkd, idr, jpy, chf, nzd"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + assertEquals(10, paymentAccount.getTradeCurrencies().size()); + List expectedTradeCurrencies = new ArrayList<>() {{ + add(getTradeCurrency("ARS").get()); // 1st in list = selected ccy + add(getTradeCurrency("CAD").get()); + add(getTradeCurrency("HRK").get()); + add(getTradeCurrency("CZK").get()); + add(getTradeCurrency("EUR").get()); + add(getTradeCurrency("HKD").get()); + add(getTradeCurrency("IDR").get()); + add(getTradeCurrency("JPY").get()); + add(getTradeCurrency("CHF").get()); + add(getTradeCurrency("NZD").get()); + }}; + verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount); + TradeCurrency expectedSelectedCurrency = expectedTradeCurrencies.get(0); + assertEquals(expectedSelectedCurrency, paymentAccount.getSelectedTradeCurrency()); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + print(paymentAccount); + } + + @Test + public void testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); + verifyEmptyForm(emptyForm, + TRANSFERWISE_ID, + PROPERTY_NAME_EMAIL); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur, hkd, idr, jpy, chf, nzd, brl, gbp"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + createPaymentAccount(aliceClient, jsonString)); + assertEquals("INVALID_ARGUMENT: BRL is not a member of valid currencies list", + exception.getMessage()); + } + + @Test + public void testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); + verifyEmptyForm(emptyForm, + TRANSFERWISE_ID, + PROPERTY_NAME_EMAIL); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, ""); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + createPaymentAccount(aliceClient, jsonString)); + assertEquals("INVALID_ARGUMENT: no trade currencies defined for transferwise payment account", + exception.getMessage()); + } + + @Test + public void testCreateUpholdAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, UPHOLD_ID); + verifyEmptyForm(emptyForm, + UPHOLD_ID, + PROPERTY_NAME_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID); + verifyEmptyForm(emptyForm, + US_POSTAL_MONEY_ORDER_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_POSTAL_ADDRESS); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress()); + print(paymentAccount); + } + + @Test + public void testCreateWeChatPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID); + verifyEmptyForm(emptyForm, + WECHAT_PAY_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored WeChat Pay Acct Salt")); + String jsonString = getCompletedFormAsJsonString(); + WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("CNY", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); + print(paymentAccount); + } + + @Test + public void testCreateWesternUnionAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, WESTERN_UNION_ID); + verifyEmptyForm(emptyForm, + WESTERN_UNION_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_CITY, + PROPERTY_NAME_STATE, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_EMAIL); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WESTERN_UNION_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Western Union Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Fargo"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "North Dakota"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + print(paymentAccount); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + + private void print(PaymentAccount paymentAccount) { + if (log.isDebugEnabled()) { + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + log.debug("\n{}", formatPaymentAcctTbl(singletonList(paymentAccount.toProtoMessage()))); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java new file mode 100644 index 0000000000..6b0aae999b --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java @@ -0,0 +1,55 @@ +package bisq.apitest.method.payment; + +import protobuf.PaymentMethod; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class GetPaymentMethodsTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetPaymentMethods() { + List paymentMethodIds = aliceClient.getPaymentMethods() + .stream() + .map(PaymentMethod::getId) + .collect(Collectors.toList()); + assertTrue(paymentMethodIds.size() >= 20); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java new file mode 100644 index 0000000000..4c4a6b3453 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -0,0 +1,123 @@ +package bisq.apitest.method.trade; + +import bisq.proto.grpc.TradeInfo; + +import java.util.function.Supplier; + +import org.slf4j.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInfo; + +import static bisq.cli.CurrencyFormat.formatBsqAmount; +import static bisq.cli.TradeFormat.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.cli.GrpcClient; + +public class AbstractTradeTest extends AbstractOfferTest { + + public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus(); + + // A Trade ID cache for use in @Test sequences. + protected static String tradeId; + + protected final Supplier maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; + + @BeforeAll + public static void initStaticFixtures() { + EXPECTED_PROTOCOL_STATUS.init(); + } + + protected final TradeInfo takeAlicesOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode) { + return bobClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); + } + + @SuppressWarnings("unused") + protected final TradeInfo takeBobsOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode) { + return aliceClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); + } + + protected final void verifyExpectedProtocolStatus(TradeInfo trade) { + assertNotNull(trade); + assertEquals(EXPECTED_PROTOCOL_STATUS.state.name(), trade.getState()); + assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase()); + + if (!isLongRunningTest) + assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished()); + + assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositConfirmed()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatSent, trade.getIsFiatSent()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatReceived, trade.getIsFiatReceived()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isWithdrawn, trade.getIsWithdrawn()); + } + + protected final void sendBsqPayment(Logger log, + GrpcClient grpcClient, + TradeInfo trade) { + var contract = trade.getContract(); + String receiverAddress = contract.getIsBuyerMakerAndSellerTaker() + ? contract.getTakerPaymentAccountPayload().getAddress() + : contract.getMakerPaymentAccountPayload().getAddress(); + String sendBsqAmount = formatBsqAmount(trade.getOffer().getVolume()); + log.info("Sending {} BSQ to address {}", sendBsqAmount, receiverAddress); + grpcClient.sendBsq(receiverAddress, sendBsqAmount, ""); + } + + protected final void verifyBsqPaymentHasBeenReceived(Logger log, + GrpcClient grpcClient, + TradeInfo trade) { + var contract = trade.getContract(); + var bsqSats = trade.getOffer().getVolume(); + var receiveAmountAsString = formatBsqAmount(bsqSats); + var address = contract.getIsBuyerMakerAndSellerTaker() + ? contract.getTakerPaymentAccountPayload().getAddress() + : contract.getMakerPaymentAccountPayload().getAddress(); + boolean receivedBsqSatoshis = grpcClient.verifyBsqSentToAddress(address, receiveAmountAsString); + if (receivedBsqSatoshis) + log.info("Payment of {} BSQ was received to address {} for trade with id {}.", + receiveAmountAsString, + address, + trade.getTradeId()); + else + fail(String.format("Payment of %s BSQ was was not sent to address %s for trade with id %s.", + receiveAmountAsString, + address, + trade.getTradeId())); + } + + protected final void logTrade(Logger log, + TestInfo testInfo, + String description, + TradeInfo trade) { + logTrade(log, testInfo, description, trade, false); + } + + protected final void logTrade(Logger log, + TestInfo testInfo, + String description, + TradeInfo trade, + boolean force) { + if (force) + log.info(String.format("%s %s%n%s", + testName(testInfo), + description.toUpperCase(), + format(trade))); + else if (log.isDebugEnabled()) { + log.debug(String.format("%s %s%n%s", + testName(testInfo), + description.toUpperCase(), + format(trade))); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java new file mode 100644 index 0000000000..6365558594 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java @@ -0,0 +1,69 @@ +package bisq.apitest.method.trade; + +import bisq.core.trade.Trade; + +/** + * A test fixture encapsulating expected trade protocol status. + * Status flags should be cleared via init() before starting a new trade protocol. + */ +public class ExpectedProtocolStatus { + Trade.State state; + Trade.Phase phase; + boolean isDepositPublished; + boolean isDepositConfirmed; + boolean isFiatSent; + boolean isFiatReceived; + boolean isPayoutPublished; + boolean isWithdrawn; + + public ExpectedProtocolStatus setState(Trade.State state) { + this.state = state; + return this; + } + + public ExpectedProtocolStatus setPhase(Trade.Phase phase) { + this.phase = phase; + return this; + } + + public ExpectedProtocolStatus setDepositPublished(boolean depositPublished) { + isDepositPublished = depositPublished; + return this; + } + + public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) { + isDepositConfirmed = depositConfirmed; + return this; + } + + public ExpectedProtocolStatus setFiatSent(boolean fiatSent) { + isFiatSent = fiatSent; + return this; + } + + public ExpectedProtocolStatus setFiatReceived(boolean fiatReceived) { + isFiatReceived = fiatReceived; + return this; + } + + public ExpectedProtocolStatus setPayoutPublished(boolean payoutPublished) { + isPayoutPublished = payoutPublished; + return this; + } + + public ExpectedProtocolStatus setWithdrawn(boolean withdrawn) { + isWithdrawn = withdrawn; + return this; + } + + public void init() { + state = null; + phase = null; + isDepositPublished = false; + isDepositConfirmed = false; + isFiatSent = false; + isFiatReceived = false; + isPayoutPublished = false; + isWithdrawn = false; + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java new file mode 100644 index 0000000000..fc365931d5 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java @@ -0,0 +1,304 @@ +/* + * 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 bisq.apitest.method.trade; + +import bisq.proto.grpc.TradeInfo; + +import io.grpc.StatusRuntimeException; + +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OfferPayload.Direction.SELL; + + + +import bisq.apitest.method.offer.AbstractOfferTest; + +// https://github.com/ghubstan/bisq/blob/master/cli/src/main/java/bisq/cli/TradeFormat.java + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeBuyBSQOfferTest extends AbstractTradeTest { + + // Alice is maker / bsq buyer (btc seller), Bob is taker / bsq seller (btc buyer). + + // Maker and Taker fees are in BSQ. + private static final String TRADE_FEE_CURRENCY_CODE = BSQ; + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + createBsqPaymentAccounts(); + EXPECTED_PROTOCOL_STATUS.init(); + } + + @Test + @Order(1) + public void testTakeAlicesSellBTCForBSQOffer(final TestInfo testInfo) { + try { + // Alice is going to BUY BSQ, but the Offer direction = SELL because it is a + // BTC trade; Alice will SELL BTC for BSQ. Bob will send Alice BSQ. + // Confused me, but just need to remember there are only BTC offers. + var btcTradeDirection = SELL.name(); + var alicesOffer = aliceClient.createFixedPricedOffer(btcTradeDirection, + BSQ, + 15_000_000L, + 7_500_000L, + "0.000035", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + TRADE_FEE_CURRENCY_CODE); + log.info("ALICE'S BUY BSQ (SELL BTC) OFFER:\n{}", formatOfferTable(singletonList(alicesOffer), BSQ)); + genBtcBlocksThenWait(1, 5000); + var offerId = alicesOffer.getId(); + assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + var alicesBsqOffers = aliceClient.getMyCryptoCurrencyOffers(btcTradeDirection, BSQ); + assertEquals(1, alicesBsqOffers.size()); + + var trade = takeAlicesOffer(offerId, bobsBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + assertFalse(trade.getIsCurrencyForTakerFeeBtc()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 6000); + alicesBsqOffers = aliceClient.getMyBsqOffersSortedByDate(); + assertEquals(0, alicesBsqOffers.size()); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(trade.getTradeId()); + + if (!trade.getIsDepositConfirmed()) { + log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", + trade.getShortId(), + trade.getDepositTxId(), + i); + genBtcBlocksThenWait(1, 4000); + continue; + } else { + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositPublished(true) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after taking offer and deposit confirmed", trade); + break; + } + } + + genBtcBlocksThenWait(1, 2500); + + if (!trade.getIsDepositConfirmed()) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) + && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot send payment started msg yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + sleep(10_000); + trade = bobClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, could not send payment started msg.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + sendBsqPayment(log, bobClient, trade); + genBtcBlocksThenWait(1, 2500); + bobClient.confirmPaymentStarted(trade.getTradeId()); + sleep(6000); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = aliceClient.getTrade(tradeId); + + if (!trade.getIsFiatSent()) { + log.warn("Alice still waiting for trade {} SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", + trade.getShortId(), + i); + sleep(5000); + continue; + } else { + // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. + EXPECTED_PROTOCOL_STATUS.setState(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + break; + } + } + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { + try { + var trade = aliceClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + sleep(1000 * 10); + trade = aliceClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + sleep(2000); + verifyBsqPaymentHasBeenReceived(log, aliceClient, trade); + + aliceClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3000); + + trade = aliceClient.getTrade(tradeId); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testBobsKeepFunds(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1000); + + var trade = bobClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before keeping funds", trade); + + bobClient.keepFunds(tradeId); + genBtcBlocksThenWait(1, 1000); + + trade = bobClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after keeping funds", trade); + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true); + + var alicesBalances = aliceClient.getBalances(); + log.info("{} Alice's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.info("{} Bob's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(bobsBalances)); + + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java new file mode 100644 index 0000000000..93d9b1b9c8 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -0,0 +1,276 @@ +/* + * 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 bisq.apitest.method.trade; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.TradeInfo; + +import io.grpc.StatusRuntimeException; + +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.State.*; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OpenOffer.State.AVAILABLE; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeBuyBTCOfferTest extends AbstractTradeTest { + + // Alice is maker/buyer, Bob is taker/seller. + + // Maker and Taker fees are in BSQ. + private static final String TRADE_FEE_CURRENCY_CODE = BSQ; + + @Test + @Order(1) + public void testTakeAlicesBuyOffer(final TestInfo testInfo) { + try { + PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); + var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + "usd", + 12_500_000L, + 12_500_000L, // min-amount = amount + 0.00, + getDefaultBuyerSecurityDepositAsPercent(), + alicesUsdAccount.getId(), + TRADE_FEE_CURRENCY_CODE); + var offerId = alicesOffer.getId(); + assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay. + sleep(3000); // TODO loop instead of hard code wait time + var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd"); + assertEquals(1, alicesUsdOffers.size()); + + PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); + var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + assertFalse(trade.getIsCurrencyForTakerFeeBtc()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 4000); + alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd"); + assertEquals(0, alicesUsdOffers.size()); + + genBtcBlocksThenWait(1, 2500); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(trade.getTradeId()); + + if (!trade.getIsDepositConfirmed()) { + log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", + trade.getShortId(), + trade.getDepositTxId(), + i); + genBtcBlocksThenWait(1, 4000); + continue; + } else { + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositPublished(true) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true); + break; + } + } + + if (!trade.getIsDepositConfirmed()) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = aliceClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) + && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment started yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + // fail("Bad trade state and phase."); + sleep(1000 * 10); + trade = aliceClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, could not confirm payment started.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + aliceClient.confirmPaymentStarted(trade.getTradeId()); + sleep(6000); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = aliceClient.getTrade(tradeId); + + if (!trade.getIsFiatSent()) { + log.warn("Alice still waiting for trade {} BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", + trade.getShortId(), + i); + sleep(5000); + continue; + } else { + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade); + break; + } + } + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + // fail("Bad trade state and phase."); + sleep(1000 * 10); + trade = bobClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + bobClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3000); + + trade = bobClient.getTrade(tradeId); + // Note: offer.state == available + assertEquals(AVAILABLE.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testAlicesKeepFunds(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1000); + + var trade = aliceClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before keeping funds", trade); + + aliceClient.keepFunds(tradeId); + + genBtcBlocksThenWait(1, 1000); + + trade = aliceClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after keeping funds", trade); + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true); + + var alicesBalances = aliceClient.getBalances(); + log.info("{} Alice's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.info("{} Bob's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(bobsBalances)); + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java new file mode 100644 index 0000000000..786601e6fa --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java @@ -0,0 +1,309 @@ +/* + * 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 bisq.apitest.method.trade; + +import bisq.proto.grpc.TradeInfo; + +import io.grpc.StatusRuntimeException; + +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.Phase.WITHDRAWN; +import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.OfferPayload.Direction.BUY; + + + +import bisq.apitest.method.offer.AbstractOfferTest; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeSellBSQOfferTest extends AbstractTradeTest { + + // Alice is maker / bsq seller (btc buyer), Bob is taker / bsq buyer (btc seller). + + // Maker and Taker fees are in BTC. + private static final String TRADE_FEE_CURRENCY_CODE = BTC; + + private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + createBsqPaymentAccounts(); + EXPECTED_PROTOCOL_STATUS.init(); + } + + @Test + @Order(1) + public void testTakeAlicesBuyBTCForBSQOffer(final TestInfo testInfo) { + try { + // Alice is going to SELL BSQ, but the Offer direction = BUY because it is a + // BTC trade; Alice will BUY BTC for BSQ. Alice will send Bob BSQ. + // Confused me, but just need to remember there are only BTC offers. + var btcTradeDirection = BUY.name(); + var alicesOffer = aliceClient.createFixedPricedOffer(btcTradeDirection, + BSQ, + 15_000_000L, + 7_500_000L, + "0.000035", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + TRADE_FEE_CURRENCY_CODE); + log.info("ALICE'S SELL BSQ (BUY BTC) OFFER:\n{}", formatOfferTable(singletonList(alicesOffer), BSQ)); + genBtcBlocksThenWait(1, 4000); + var offerId = alicesOffer.getId(); + assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + var alicesBsqOffers = aliceClient.getMyCryptoCurrencyOffers(btcTradeDirection, BSQ); + assertEquals(1, alicesBsqOffers.size()); + + var trade = takeAlicesOffer(offerId, bobsBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + assertTrue(trade.getIsCurrencyForTakerFeeBtc()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 6000); + alicesBsqOffers = aliceClient.getMyBsqOffersSortedByDate(); + assertEquals(0, alicesBsqOffers.size()); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(trade.getTradeId()); + + if (!trade.getIsDepositConfirmed()) { + log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", + trade.getShortId(), + trade.getDepositTxId(), + i); + genBtcBlocksThenWait(1, 4000); + continue; + } else { + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositPublished(true) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after taking offer and deposit confirmed", trade); + break; + } + } + + genBtcBlocksThenWait(1, 2500); + + if (!trade.getIsDepositConfirmed()) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + logTrade(log, testInfo, "Alice's Maker/Seller View", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = aliceClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) + && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot send payment started msg yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + sleep(10_000); + trade = aliceClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, could not send payment started msg.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + sendBsqPayment(log, aliceClient, trade); + genBtcBlocksThenWait(1, 2500); + aliceClient.confirmPaymentStarted(trade.getTradeId()); + sleep(6000); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(tradeId); + + if (!trade.getIsFiatSent()) { + log.warn("Bob still waiting for trade {} SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", + trade.getShortId(), + i); + sleep(5000); + continue; + } else { + // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. + EXPECTED_PROTOCOL_STATUS.setState(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + break; + } + } + + logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + sleep(1000 * 10); + trade = bobClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + sleep(2000); + verifyBsqPaymentHasBeenReceived(log, bobClient, trade); + + bobClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3000); + + trade = bobClient.getTrade(tradeId); + // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + + logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId), true); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testAlicesBtcWithdrawalToExternalAddress(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1000); + + var trade = aliceClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before withdrawing BTC funds to external wallet", trade); + + String toAddress = bitcoinCli.getNewBtcAddress(); + aliceClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO); + + genBtcBlocksThenWait(1, 1000); + + trade = aliceClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED) + .setPhase(WITHDRAWN) + .setWithdrawn(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after withdrawing funds to external wallet", trade); + + + logTrade(log, testInfo, "Alice's Maker/Seller View (Done)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Done)", bobClient.getTrade(tradeId), true); + + var alicesBalances = aliceClient.getBalances(); + log.info("{} Alice's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.info("{} Bob's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(bobsBalances)); + + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java new file mode 100644 index 0000000000..ece3432123 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -0,0 +1,277 @@ +/* + * 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 bisq.apitest.method.trade; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.TradeInfo; + +import io.grpc.StatusRuntimeException; + +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.Phase.WITHDRAWN; +import static bisq.core.trade.Trade.State.*; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OfferPayload.Direction.SELL; +import static protobuf.OpenOffer.State.AVAILABLE; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeSellBTCOfferTest extends AbstractTradeTest { + + // Alice is maker/seller, Bob is taker/buyer. + + // Maker and Taker fees are in BTC. + private static final String TRADE_FEE_CURRENCY_CODE = BTC; + + private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; + + @Test + @Order(1) + public void testTakeAlicesSellOffer(final TestInfo testInfo) { + try { + PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); + var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), + "usd", + 12_500_000L, + 12_500_000L, // min-amount = amount + 0.00, + getDefaultBuyerSecurityDepositAsPercent(), + alicesUsdAccount.getId(), + TRADE_FEE_CURRENCY_CODE); + var offerId = alicesOffer.getId(); + assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay, but taking sell offers + // seems to require more time to prepare. + sleep(3000); // TODO loop instead of hard code wait time + var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), "usd"); + assertEquals(1, alicesUsdOffers.size()); + + PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); + var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + assertTrue(trade.getIsCurrencyForTakerFeeBtc()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 4000); + var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), "usd"); + assertEquals(0, takeableUsdOffers.size()); + + genBtcBlocksThenWait(1, 2500); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(trade.getTradeId()); + + if (!trade.getIsDepositConfirmed()) { + log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", + trade.getShortId(), + trade.getDepositTxId(), + i); + genBtcBlocksThenWait(1, 4000); + continue; + } else { + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositPublished(true) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true); + break; + } + } + + if (!trade.getIsDepositConfirmed()) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment started yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + // fail("Bad trade state and phase."); + sleep(1000 * 10); + trade = bobClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, could not confirm payment started.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + bobClient.confirmPaymentStarted(tradeId); + sleep(6000); + + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + trade = bobClient.getTrade(tradeId); + + if (!trade.getIsFiatSent()) { + log.warn("Bob still waiting for trade {} BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", + trade.getShortId(), + i); + sleep(5000); + continue; + } else { + // Note: offer.state == available + assertEquals(AVAILABLE.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade); + break; + } + } + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { + try { + var trade = aliceClient.getTrade(tradeId); + + Predicate tradeStateAndPhaseCorrect = (t) -> + t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + if (!tradeStateAndPhaseCorrect.test(trade)) { + log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", + trade.getShortId(), + trade.getState(), + trade.getPhase()); + // fail("Bad trade state and phase."); + sleep(1000 * 10); + trade = aliceClient.getTrade(tradeId); + continue; + } else { + break; + } + } + + if (!tradeStateAndPhaseCorrect.test(trade)) { + fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, could not confirm payment received.", + trade.getShortId(), + trade.getState(), + trade.getPhase())); + } + + aliceClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3000); + + trade = aliceClient.getTrade(tradeId); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1000); + + var trade = bobClient.getTrade(tradeId); + logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade); + + String toAddress = bitcoinCli.getNewBtcAddress(); + bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO); + + genBtcBlocksThenWait(1, 1000); + + trade = bobClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED) + .setPhase(WITHDRAWN) + .setWithdrawn(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after withdrawing BTC funds to external wallet", trade); + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true); + + var alicesBalances = aliceClient.getBalances(); + log.info("{} Alice's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.info("{} Bob's Current Balance:\n{}", + testName(testInfo), + formatBalancesTbls(bobsBalances)); + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java new file mode 100644 index 0000000000..6be9dd6654 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -0,0 +1,203 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BsqBalanceInfo; + +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.apitest.method.wallet.WalletTestUtil.ALICES_INITIAL_BSQ_BALANCES; +import static bisq.apitest.method.wallet.WalletTestUtil.BOBS_INITIAL_BSQ_BALANCES; +import static bisq.apitest.method.wallet.WalletTestUtil.bsqBalanceModel; +import static bisq.apitest.method.wallet.WalletTestUtil.verifyBsqBalances; +import static bisq.cli.TableFormat.formatBsqBalanceInfoTbl; +import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_MAINNET; +import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_REGTEST; +import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_TESTNET; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.method.MethodTest; +import bisq.cli.GrpcClient; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BsqWalletTest extends MethodTest { + + private static final String SEND_BSQ_AMOUNT = "25000.50"; + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testGetUnusedBsqAddress() { + var address = aliceClient.getUnusedBsqAddress(); + assertFalse(address.isEmpty()); + assertTrue(address.startsWith("B")); + + NetworkParameters networkParameters = LegacyAddress.getParametersFromAddress(address.substring(1)); + String addressNetwork = networkParameters.getPaymentProtocolId(); + assertNotEquals(PAYMENT_PROTOCOL_ID_MAINNET, addressNetwork); + // TODO Fix bug causing the regtest bsq address network to be evaluated as 'testnet' here. + assertTrue(addressNetwork.equals(PAYMENT_PROTOCOL_ID_TESTNET) + || addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST)); + } + + @Test + @Order(2) + public void testInitialBsqBalances(final TestInfo testInfo) { + BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances(); + log.debug("{} -> Alice's BSQ Initial Balances -> \n{}", + testName(testInfo), + formatBsqBalanceInfoTbl(alicesBsqBalances)); + verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances); + + BsqBalanceInfo bobsBsqBalances = bobClient.getBsqBalances(); + log.debug("{} -> Bob's BSQ Initial Balances -> \n{}", + testName(testInfo), + formatBsqBalanceInfoTbl(bobsBsqBalances)); + verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances); + } + + @Test + @Order(3) + public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) { + String bobsBsqAddress = bobClient.getUnusedBsqAddress(); + aliceClient.sendBsq(bobsBsqAddress, SEND_BSQ_AMOUNT, "100"); + sleep(2000); + + BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances(); + BsqBalanceInfo bobsBsqBalances = waitForNonZeroBsqUnverifiedBalance(bobClient); + + log.debug("BSQ Balances Before BTC Block Gen..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(bsqBalanceModel(150000000, + 2500050, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(bsqBalanceModel(97499950, + 97499950, + 97499950, + 0, + 0, + 0), + alicesBsqBalances); + } + + @Test + @Order(4) + public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) { + // There is a wallet persist delay; we have to + // wait for both wallets to be saved to disk. + genBtcBlocksThenWait(1, 4000); + + BsqBalanceInfo alicesBsqBalances = aliceClient.getBalances().getBsq(); + BsqBalanceInfo bobsBsqBalances = waitForBsqNewAvailableConfirmedBalance(bobClient, 150000000); + + log.debug("See Available Confirmed BSQ Balances..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(bsqBalanceModel(152500050, + 0, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(bsqBalanceModel(97499950, + 0, + 0, + 0, + 0, + 0), + alicesBsqBalances); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + + private BsqBalanceInfo waitForNonZeroBsqUnverifiedBalance(GrpcClient grpcClient) { + // A BSQ recipient needs to wait for her daemon to detect a new tx. + // Loop here until her unverifiedBalance != 0, or give up after 15 seconds. + // A slow test is preferred over a flaky test. + BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances(); + for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) { + sleep(1000); + bsqBalance = grpcClient.getBsqBalances(); + } + return bsqBalance; + } + + private BsqBalanceInfo waitForBsqNewAvailableConfirmedBalance(GrpcClient grpcClient, + long staleBalance) { + BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances(); + for (int numRequests = 1; + numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance; + numRequests++) { + sleep(1000); + bsqBalance = grpcClient.getBsqBalances(); + } + return bsqBalance; + } + + @SuppressWarnings("SameParameterValue") + private void printBobAndAliceBsqBalances(final TestInfo testInfo, + BsqBalanceInfo bobsBsqBalances, + BsqBalanceInfo alicesBsqBalances, + BisqAppConfig senderApp) { + log.debug("{} -> Bob's BSQ Balances After {} {} BSQ-> \n{}", + testName(testInfo), + senderApp.equals(bobdaemon) ? "Sending" : "Receiving", + SEND_BSQ_AMOUNT, + formatBsqBalanceInfoTbl(bobsBsqBalances)); + + log.debug("{} -> Alice's Balances After {} {} BSQ-> \n{}", + testName(testInfo), + senderApp.equals(alicedaemon) ? "Sending" : "Receiving", + SEND_BSQ_AMOUNT, + formatBsqBalanceInfoTbl(alicesBsqBalances)); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java new file mode 100644 index 0000000000..3d23196406 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java @@ -0,0 +1,94 @@ +package bisq.apitest.method.wallet; + +import bisq.core.api.model.TxFeeRateInfo; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BtcTxFeeRateTest extends MethodTest { + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon); + } + + @Test + @Order(1) + public void testGetTxFeeRate(final TestInfo testInfo) { + var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate()); + log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo); + + assertFalse(txFeeRateInfo.isUseCustomTxFeeRate()); + assertTrue(txFeeRateInfo.getFeeServiceRate() > 0); + } + + @Test + @Order(2) + public void testSetInvalidTxFeeRateShouldThrowException(final TestInfo testInfo) { + var currentTxFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate()); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.setTxFeeRate(10)); + String expectedExceptionMessage = + format("UNKNOWN: tx fee rate preference must be >= %d sats/byte", + currentTxFeeRateInfo.getMinFeeServiceRate()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + @Test + @Order(3) + public void testSetValidTxFeeRate(final TestInfo testInfo) { + var currentTxFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate()); + var customFeeRate = currentTxFeeRateInfo.getMinFeeServiceRate() + 5; + var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.setTxFeeRate(customFeeRate)); + log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo); + + assertTrue(txFeeRateInfo.isUseCustomTxFeeRate()); + assertEquals(customFeeRate, txFeeRateInfo.getCustomTxFeeRate()); + assertTrue(txFeeRateInfo.getFeeServiceRate() > 0); + } + + @Test + @Order(4) + public void testUnsetTxFeeRate(final TestInfo testInfo) { + var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.unsetTxFeeRate()); + log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo); + + assertFalse(txFeeRateInfo.isUseCustomTxFeeRate()); + assertTrue(txFeeRateInfo.getFeeServiceRate() > 0); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java new file mode 100644 index 0000000000..19d065c6cc --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -0,0 +1,141 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.TxInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.apitest.method.wallet.WalletTestUtil.INITIAL_BTC_BALANCES; +import static bisq.apitest.method.wallet.WalletTestUtil.verifyBtcBalances; +import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BtcWalletTest extends MethodTest { + + private static final String TX_MEMO = "tx memo"; + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testInitialBtcBalances(final TestInfo testInfo) { + // Bob & Alice's regtest Bisq wallets were initialized with 10 BTC. + + BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances(); + log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances)); + + BtcBalanceInfo bobsBalances = bobClient.getBtcBalances(); + log.debug("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances)); + + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance()); + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance()); + } + + @Test + @Order(2) + public void testFundAlicesBtcWallet(final TestInfo testInfo) { + String newAddress = aliceClient.getUnusedBtcAddress(); + bitcoinCli.sendToAddress(newAddress, "2.5"); + genBtcBlocksThenWait(1, 1000); + + BtcBalanceInfo btcBalanceInfo = aliceClient.getBtcBalances(); + // New balance is 12.5 BTC + assertEquals(1250000000, btcBalanceInfo.getAvailableBalance()); + + log.debug("{} -> Alice's Funded Address Balance -> \n{}", + testName(testInfo), + formatAddressBalanceTbl(singletonList(aliceClient.getAddressBalance(newAddress)))); + + // New balance is 12.5 BTC + btcBalanceInfo = aliceClient.getBtcBalances(); + bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = + bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000, + 0, + 1250000000, + 0); + verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo); + log.debug("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(btcBalanceInfo)); + } + + @Test + @Order(3) + public void testAliceSendBTCToBob(TestInfo testInfo) { + String bobsBtcAddress = bobClient.getUnusedBtcAddress(); + log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress); + + TxInfo txInfo = aliceClient.sendBtc(bobsBtcAddress, + "5.50", + "100", + TX_MEMO); + assertTrue(txInfo.getIsPending()); + + // Note that the memo is not set on the tx yet. + assertTrue(txInfo.getMemo().isEmpty()); + genBtcBlocksThenWait(1, 1000); + + // Fetch the tx and check for confirmation and memo. + txInfo = aliceClient.getTransaction(txInfo.getTxId()); + assertFalse(txInfo.getIsPending()); + assertEquals(TX_MEMO, txInfo.getMemo()); + + BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances(); + log.debug("{} Alice's BTC Balances:\n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(alicesBalances)); + bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = + bisq.core.api.model.BtcBalanceInfo.valueOf(700000000, + 0, + 700000000, + 0); + verifyBtcBalances(alicesExpectedBalances, alicesBalances); + + BtcBalanceInfo bobsBalances = bobClient.getBtcBalances(); + log.debug("{} Bob's BTC Balances:\n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(bobsBalances)); + // The sendbtc tx weight and size randomly varies between two distinct values + // (876 wu, 219 bytes, OR 880 wu, 220 bytes) from test run to test run, hence + // the assertion of an available balance range [1549978000, 1549978100]. + assertTrue(bobsBalances.getAvailableBalance() >= 1549978000); + assertTrue(bobsBalances.getAvailableBalance() <= 1549978100); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java new file mode 100644 index 0000000000..30de7f585b --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java @@ -0,0 +1,127 @@ +package bisq.apitest.method.wallet; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +@SuppressWarnings("ResultOfMethodCallIgnored") +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class WalletProtectionTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(alicedaemon); + MILLISECONDS.sleep(2000); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testSetWalletPassword() { + aliceClient.setWalletPassword("first-password"); + } + + @Test + @Order(2) + public void testGetBalanceOnEncryptedWalletShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(3) + public void testUnlockWalletFor4Seconds() { + aliceClient.unlockWallet("first-password", 4); + aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception + sleep(4500); // let unlock timeout expire + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(4) + public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { + aliceClient.unlockWallet("first-password", 3); + sleep(4000); // let unlock timeout expire + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(5) + public void testLockWalletBeforeUnlockTimeoutExpiry() { + aliceClient.unlockWallet("first-password", 60); + aliceClient.lockWallet(); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(6) + public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet()); + assertEquals("UNKNOWN: wallet is already locked", exception.getMessage()); + } + + @Test + @Order(7) + public void testUnlockWalletTimeoutOverride() { + aliceClient.unlockWallet("first-password", 2); + sleep(500); // override unlock timeout after 0.5s + aliceClient.unlockWallet("first-password", 6); + sleep(5000); + aliceClient.getBtcBalances(); // getbalance 5s after overriding timeout to 6s + } + + @Test + @Order(8) + public void testSetNewWalletPassword() { + aliceClient.setWalletPassword("first-password", "second-password"); + sleep(2500); // allow time for wallet save + aliceClient.unlockWallet("second-password", 2); + aliceClient.getBtcBalances(); + } + + @Test + @Order(9) + public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.setWalletPassword("bad old password", "irrelevant")); + assertEquals("UNKNOWN: incorrect old password", exception.getMessage()); + } + + @Test + @Order(10) + public void testRemoveNewWalletPassword() { + aliceClient.removeWalletPassword("second-password"); + aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java new file mode 100644 index 0000000000..85b9f04e84 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java @@ -0,0 +1,72 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; + +import lombok.extern.slf4j.Slf4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Slf4j +public class WalletTestUtil { + + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets + // are initialized with 10 BTC during the scaffolding setup. + public static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = + bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, + 0, + 1000000000, + 0); + + + // Alice's regtest BSQ wallet is initialized with 1,000,000 BSQ. + public static final bisq.core.api.model.BsqBalanceInfo ALICES_INITIAL_BSQ_BALANCES = + bsqBalanceModel(100000000, + 0, + 0, + 0, + 0, + 0); + + // Bob's regtest BSQ wallet is initialized with 1,500,000 BSQ. + public static final bisq.core.api.model.BsqBalanceInfo BOBS_INITIAL_BSQ_BALANCES = + bsqBalanceModel(150000000, + 0, + 0, + 0, + 0, + 0); + + @SuppressWarnings("SameParameterValue") + public static bisq.core.api.model.BsqBalanceInfo bsqBalanceModel(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + return bisq.core.api.model.BsqBalanceInfo.valueOf(availableConfirmedBalance, + unverifiedBalance, + unconfirmedChangeBalance, + lockedForVotingBalance, + lockupBondsBalance, + unlockingBondsBalance); + } + + public static void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected, + BsqBalanceInfo actual) { + assertEquals(expected.getAvailableConfirmedBalance(), actual.getAvailableConfirmedBalance()); + assertEquals(expected.getUnverifiedBalance(), actual.getUnverifiedBalance()); + assertEquals(expected.getUnconfirmedChangeBalance(), actual.getUnconfirmedChangeBalance()); + assertEquals(expected.getLockedForVotingBalance(), actual.getLockedForVotingBalance()); + assertEquals(expected.getLockupBondsBalance(), actual.getLockupBondsBalance()); + assertEquals(expected.getUnlockingBondsBalance(), actual.getUnlockingBondsBalance()); + } + + public static void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, + BtcBalanceInfo actual) { + assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); + assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); + assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); + assertEquals(expected.getLockedBalance(), actual.getLockedBalance()); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java new file mode 100644 index 0000000000..2ab8d7fdae --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledIf; + +import static java.lang.System.getenv; + + + +import bisq.apitest.method.trade.AbstractTradeTest; +import bisq.apitest.method.trade.TakeBuyBTCOfferTest; +import bisq.apitest.method.trade.TakeSellBTCOfferTest; + +@EnabledIf("envLongRunningTestEnabled") +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class LongRunningTradesTest extends AbstractTradeTest { + + @Test + @Order(1) + public void TradeLoop(final TestInfo testInfo) { + int numTrades = 0; + while (numTrades < 50) { + + log.info("*******************************************************************"); + log.info("Trade # {}", ++numTrades); + log.info("*******************************************************************"); + + EXPECTED_PROTOCOL_STATUS.init(); + testTakeBuyBTCOffer(testInfo); + + genBtcBlocksThenWait(1, 1000 * 15); + + log.info("*******************************************************************"); + log.info("Trade # {}", ++numTrades); + log.info("*******************************************************************"); + + EXPECTED_PROTOCOL_STATUS.init(); + testTakeSellBTCOffer(testInfo); + + genBtcBlocksThenWait(1, 1000 * 15); + } + } + + public void testTakeBuyBTCOffer(final TestInfo testInfo) { + TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); + setLongRunningTest(true); + test.testTakeAlicesBuyOffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesKeepFunds(testInfo); + } + + public void testTakeSellBTCOffer(final TestInfo testInfo) { + TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); + setLongRunningTest(true); + test.testTakeAlicesSellOffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsBtcWithdrawalToExternalAddress(testInfo); + } + + protected static boolean envLongRunningTestEnabled() { + String envName = "LONG_RUNNING_TRADES_TEST_ENABLED"; + String envX = getenv(envName); + if (envX != null) { + log.info("Enabled, found {}.", envName); + return true; + } else { + log.info("Skipped, no environment variable {} defined.", envName); + log.info("To enable on Mac OS or Linux:" + + "\tIf running in terminal, export LONG_RUNNING_TRADES_TEST_ENABLED=true in bash shell." + + "\tIf running in Intellij, set LONG_RUNNING_TRADES_TEST_ENABLED=true in launcher's Environment variables field."); + return false; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java new file mode 100644 index 0000000000..15c11e65b4 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.apitest.scenario; + + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.apitest.method.offer.CancelOfferTest; +import bisq.apitest.method.offer.CreateBSQOffersTest; +import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; +import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; +import bisq.apitest.method.offer.ValidateCreateOfferTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class OfferTest extends AbstractOfferTest { + + @Test + @Order(1) + public void testAmtTooLargeShouldThrowException() { + ValidateCreateOfferTest test = new ValidateCreateOfferTest(); + test.testAmtTooLargeShouldThrowException(); + test.testNoMatchingEURPaymentAccountShouldThrowException(); + test.testNoMatchingCADPaymentAccountShouldThrowException(); + } + + @Test + @Order(2) + public void testCancelOffer() { + CancelOfferTest test = new CancelOfferTest(); + test.testCancelOffer(); + } + + @Test + @Order(3) + public void testCreateOfferUsingFixedPrice() { + CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest(); + test.testCreateAUDBTCBuyOfferUsingFixedPrice16000(); + test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234(); + test.testCreateEURBTCSellOfferUsingFixedPrice95001234(); + } + + @Test + @Order(4) + public void testCreateOfferUsingMarketPriceMargin() { + CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest(); + test.testCreateUSDBTCBuyOffer5PctPriceMargin(); + test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin(); + test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin(); + test.testCreateBRLBTCSellOffer6Point55PctPriceMargin(); + } + + @Test + @Order(5) + public void testCreateBSQOffersTest() { + CreateBSQOffersTest test = new CreateBSQOffersTest(); + CreateBSQOffersTest.createBsqPaymentAccounts(); + test.testCreateBuy1BTCFor20KBSQOffer(); + test.testCreateSell1BTCFor20KBSQOffer(); + test.testCreateBuyBTCWith1To2KBSQOffer(); + test.testCreateSellBTCFor5To10KBSQOffer(); + test.testGetAllMyBsqOffers(); + test.testGetAvailableBsqOffers(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java new file mode 100644 index 0000000000..c3eb41343a --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -0,0 +1,90 @@ +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.payment.AbstractPaymentAccountTest; +import bisq.apitest.method.payment.CreatePaymentAccountTest; +import bisq.apitest.method.payment.GetPaymentMethodsTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PaymentAccountTest extends AbstractPaymentAccountTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, seednode, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetPaymentMethods() { + GetPaymentMethodsTest test = new GetPaymentMethodsTest(); + test.testGetPaymentMethods(); + } + + @Test + @Order(2) + public void testCreatePaymentAccount(TestInfo testInfo) { + CreatePaymentAccountTest test = new CreatePaymentAccountTest(); + + test.testCreateAdvancedCashAccount(testInfo); + test.testCreateAliPayAccount(testInfo); + test.testCreateAustraliaPayidAccount(testInfo); + test.testCreateCashDepositAccount(testInfo); + test.testCreateBrazilNationalBankAccount(testInfo); + test.testCreateChaseQuickPayAccount(testInfo); + test.testCreateClearXChangeAccount(testInfo); + test.testCreateF2FAccount(testInfo); + test.testCreateFasterPaymentsAccount(testInfo); + test.testCreateHalCashAccount(testInfo); + test.testCreateInteracETransferAccount(testInfo); + test.testCreateJapanBankAccount(testInfo); + test.testCreateMoneyBeamAccount(testInfo); + test.testCreateMoneyGramAccount(testInfo); + test.testCreatePerfectMoneyAccount(testInfo); + test.testCreatePopmoneyAccount(testInfo); + test.testCreatePromptPayAccount(testInfo); + test.testCreateRevolutAccount(testInfo); + test.testCreateSameBankAccount(testInfo); + test.testCreateSepaInstantAccount(testInfo); + test.testCreateSepaAccount(testInfo); + test.testCreateSpecificBanksAccount(testInfo); + test.testCreateSwishAccount(testInfo); + + // TransferwiseAccount is only PaymentAccount with a + // tradeCurrencies field in the json form. + test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo); + test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo); + test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo); + test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo); + + test.testCreateUpholdAccount(testInfo); + test.testCreateUSPostalMoneyOrderAccount(testInfo); + test.testCreateWeChatPayAccount(testInfo); + test.testCreateWesternUnionAccount(testInfo); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java b/apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java new file mode 100644 index 0000000000..fd18763880 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledIf; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.AbstractBotTest; +import bisq.apitest.scenario.bot.BotClient; +import bisq.apitest.scenario.bot.RobotBob; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; + +// The test case is enabled if AbstractBotTest#botScriptExists() returns true. +@EnabledIf("botScriptExists") +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ScriptedBotTest extends AbstractBotTest { + + private RobotBob robotBob; + + @BeforeAll + public static void startTestHarness() { + botScript = deserializeBotScript(); + + if (botScript.isUseTestHarness()) { + startSupportingApps(true, + true, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); + } else { + // We need just enough configurations to make sure Bob and testers use + // the right apiPassword, to create a bitcoin-cli helper, and RobotBob's + // gRPC stubs. But the user will have to register dispute agents before + // an offer can be taken. + config = new ApiTestConfig("--apiPassword", "xyz"); + bitcoinCli = new BitcoinCliHelper(config); + log.warn("Don't forget to register dispute agents before trying to trade with me."); + } + + botClient = new BotClient(bobClient); + } + + @BeforeEach + public void initRobotBob() { + try { + BashScriptGenerator bashScriptGenerator = getBashScriptGenerator(); + robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void runRobotBob() { + try { + + startShutdownTimer(); + robotBob.run(); + + } catch (ManualBotShutdownException ex) { + // This exception is thrown if a /tmp/bottest-shutdown file was found. + // You can also kill -15 + // of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #' + // + // This will cleanly shut everything down as well, but you will see a + // Process 'Gradle Test Executor #' finished with non-zero exit value 143 error, + // which you may think is a test failure. + log.warn("{} Shutting down test case before test completion;" + + " this is not a test failure.", + ex.getMessage()); + } catch (Throwable throwable) { + fail(throwable); + } + } + + @AfterAll + public static void tearDown() { + if (botScript.isUseTestHarness()) + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java new file mode 100644 index 0000000000..8aa9367511 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.apitest.scenario; + +import java.io.File; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.common.file.FileUtil.deleteFileIfExists; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.CallRateMeteringInterceptorTest; +import bisq.apitest.method.GetMethodHelpTest; +import bisq.apitest.method.GetVersionTest; +import bisq.apitest.method.MethodTest; +import bisq.apitest.method.RegisterDisputeAgentsTest; + + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class StartupTest extends MethodTest { + + private static File callRateMeteringConfigFile; + + @BeforeAll + public static void setUp() { + try { + callRateMeteringConfigFile = defaultRateMeterInterceptorConfig(); + startSupportingApps(callRateMeteringConfigFile, + false, + false, + bitcoind, seednode, arbdaemon, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testCallRateMeteringInterceptor() { + CallRateMeteringInterceptorTest test = new CallRateMeteringInterceptorTest(); + test.testGetVersionCall1IsAllowed(); + test.sleep200Milliseconds(); + test.testGetVersionCall2ShouldThrowException(); + test.sleep200Milliseconds(); + test.testGetVersionCall3ShouldThrowException(); + test.sleep200Milliseconds(); + test.testGetVersionCall4IsAllowed(); + sleep(1000); // Wait 1 second before calling getversion in next test. + } + + @Test + @Order(2) + public void testGetVersion() { + GetVersionTest test = new GetVersionTest(); + test.testGetVersion(); + } + + @Test + @Order(3) + public void testRegisterDisputeAgents() { + RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest(); + test.testRegisterArbitratorShouldThrowException(); + test.testInvalidDisputeAgentTypeArgShouldThrowException(); + test.testInvalidRegistrationKeyArgShouldThrowException(); + test.testRegisterMediator(); + test.testRegisterRefundAgent(); + } + + @Test + @Order(4) + public void testGetCreateOfferHelp() { + GetMethodHelpTest test = new GetMethodHelpTest(); + test.testGetCreateOfferHelp(); + } + + @AfterAll + public static void tearDown() { + try { + deleteFileIfExists(callRateMeteringConfigFile); + } catch (IOException ex) { + log.error(ex.getMessage()); + } + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java new file mode 100644 index 0000000000..f4e93ad35a --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + + + +import bisq.apitest.method.trade.AbstractTradeTest; +import bisq.apitest.method.trade.TakeBuyBSQOfferTest; +import bisq.apitest.method.trade.TakeBuyBTCOfferTest; +import bisq.apitest.method.trade.TakeSellBSQOfferTest; +import bisq.apitest.method.trade.TakeSellBTCOfferTest; + + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TradeTest extends AbstractTradeTest { + + @BeforeEach + public void init() { + EXPECTED_PROTOCOL_STATUS.init(); + } + + @Test + @Order(1) + public void testTakeBuyBTCOffer(final TestInfo testInfo) { + TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); + test.testTakeAlicesBuyOffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesKeepFunds(testInfo); + } + + @Test + @Order(2) + public void testTakeSellBTCOffer(final TestInfo testInfo) { + TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); + test.testTakeAlicesSellOffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsBtcWithdrawalToExternalAddress(testInfo); + } + + @Test + @Order(3) + public void testTakeBuyBSQOffer(final TestInfo testInfo) { + TakeBuyBSQOfferTest test = new TakeBuyBSQOfferTest(); + TakeBuyBSQOfferTest.createBsqPaymentAccounts(); + test.testTakeAlicesSellBTCForBSQOffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsKeepFunds(testInfo); + } + + @Test + @Order(4) + public void testTakeSellBSQOffer(final TestInfo testInfo) { + TakeSellBSQOfferTest test = new TakeSellBSQOfferTest(); + TakeSellBSQOfferTest.createBsqPaymentAccounts(); + test.testTakeAlicesBuyBTCForBSQOffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesBtcWithdrawalToExternalAddress(testInfo); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java new file mode 100644 index 0000000000..73a7e8ab16 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -0,0 +1,116 @@ +/* + * 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 bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; + + + +import bisq.apitest.method.MethodTest; +import bisq.apitest.method.wallet.BsqWalletTest; +import bisq.apitest.method.wallet.BtcTxFeeRateTest; +import bisq.apitest.method.wallet.BtcWalletTest; +import bisq.apitest.method.wallet.WalletProtectionTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class WalletTest extends MethodTest { + + // Batching all wallet tests in this test case reduces scaffold setup + // time. Here, we create a method WalletProtectionTest instance and run each + // test in declared order. + + @BeforeAll + public static void setUp() { + startSupportingApps(true, + false, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testBtcWalletFunding(final TestInfo testInfo) { + BtcWalletTest btcWalletTest = new BtcWalletTest(); + + btcWalletTest.testInitialBtcBalances(testInfo); + btcWalletTest.testFundAlicesBtcWallet(testInfo); + btcWalletTest.testAliceSendBTCToBob(testInfo); + } + + @Test + @Order(2) + public void testBsqWalletFunding(final TestInfo testInfo) { + BsqWalletTest bsqWalletTest = new BsqWalletTest(); + + bsqWalletTest.testGetUnusedBsqAddress(); + bsqWalletTest.testInitialBsqBalances(testInfo); + bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); + } + + @Test + @Order(3) + public void testWalletProtection() { + WalletProtectionTest walletProtectionTest = new WalletProtectionTest(); + + walletProtectionTest.testSetWalletPassword(); + walletProtectionTest.testGetBalanceOnEncryptedWalletShouldThrowException(); + walletProtectionTest.testUnlockWalletFor4Seconds(); + walletProtectionTest.testGetBalanceAfterUnlockTimeExpiryShouldThrowException(); + walletProtectionTest.testLockWalletBeforeUnlockTimeoutExpiry(); + walletProtectionTest.testLockWalletWhenWalletAlreadyLockedShouldThrowException(); + walletProtectionTest.testUnlockWalletTimeoutOverride(); + walletProtectionTest.testSetNewWalletPassword(); + walletProtectionTest.testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException(); + walletProtectionTest.testRemoveNewWalletPassword(); + } + + @Test + @Order(4) + public void testTxFeeRateMethods(final TestInfo testInfo) { + BtcTxFeeRateTest test = new BtcTxFeeRateTest(); + + test.testGetTxFeeRate(testInfo); + test.testSetInvalidTxFeeRateShouldThrowException(testInfo); + test.testSetValidTxFeeRate(testInfo); + test.testUnsetTxFeeRate(testInfo); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java b/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java new file mode 100644 index 0000000000..763dbac9e2 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java @@ -0,0 +1,110 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import bisq.core.locale.Country; + +import protobuf.PaymentAccount; + +import com.google.gson.GsonBuilder; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.locale.CountryUtil.findCountryByCode; +import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID; +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.file.Files.readAllBytes; + + + +import bisq.apitest.method.MethodTest; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.script.BotScript; + +@Slf4j +public abstract class AbstractBotTest extends MethodTest { + + protected static final String BOT_SCRIPT_NAME = "bot-script.json"; + protected static BotScript botScript; + protected static BotClient botClient; + + protected BashScriptGenerator getBashScriptGenerator() { + if (botScript.isUseTestHarness()) { + PaymentAccount alicesAccount = createAlicesPaymentAccount(); + botScript.setPaymentAccountIdForCliScripts(alicesAccount.getId()); + } + return new BashScriptGenerator(config.apiPassword, + botScript.getApiPortForCliScripts(), + botScript.getPaymentAccountIdForCliScripts(), + botScript.isPrintCliScripts()); + } + + private PaymentAccount createAlicesPaymentAccount() { + BotPaymentAccountGenerator accountGenerator = + new BotPaymentAccountGenerator(new BotClient(aliceClient)); + String paymentMethodId = botScript.getBotPaymentMethodId(); + if (paymentMethodId != null) { + if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) { + // Only Zelle test accts are supported now. + return accountGenerator.createZellePaymentAccount( + "Alice's Zelle Account", + "Alice"); + } else { + throw new UnsupportedOperationException( + format("This test harness bot does not work with %s payment accounts yet.", + getPaymentMethodById(paymentMethodId).getDisplayString())); + } + } else { + String countryCode = botScript.getCountryCode(); + Country country = findCountryByCode(countryCode).orElseThrow(() -> + new IllegalArgumentException(countryCode + " is not a valid iso country code.")); + return accountGenerator.createF2FPaymentAccount(country, + "Alice's " + country.name + " F2F Account"); + } + } + + protected static BotScript deserializeBotScript() { + try { + File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME); + String json = new String(readAllBytes(Paths.get(botScriptFile.getPath()))); + return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class); + } catch (IOException ex) { + throw new IllegalStateException("Error reading script bot file contents.", ex); + } + } + + @SuppressWarnings("unused") // This is used by the jupiter framework. + protected static boolean botScriptExists() { + File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME); + if (botScriptFile.exists()) { + botScriptFile.deleteOnExit(); + log.info("Enabled, found {}.", botScriptFile.getPath()); + return true; + } else { + log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator."); + return false; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java b/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java new file mode 100644 index 0000000000..2e8a248a4c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java @@ -0,0 +1,77 @@ +package bisq.apitest.scenario.bot; + +import bisq.core.locale.Country; + +import protobuf.PaymentAccount; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.locale.CountryUtil.findCountryByCode; +import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID; +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MINUTES; + + + +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.script.BotScript; + +@Slf4j +public +class Bot { + + static final String MAKE = "MAKE"; + static final String TAKE = "TAKE"; + + protected final BotClient botClient; + protected final BitcoinCliHelper bitcoinCli; + protected final BashScriptGenerator bashScriptGenerator; + protected final String[] actions; + protected final long protocolStepTimeLimitInMs; + protected final boolean stayAlive; + protected final boolean isUsingTestHarness; + protected final PaymentAccount paymentAccount; + + public Bot(BotClient botClient, + BotScript botScript, + BitcoinCliHelper bitcoinCli, + BashScriptGenerator bashScriptGenerator) { + this.botClient = botClient; + this.bitcoinCli = bitcoinCli; + this.bashScriptGenerator = bashScriptGenerator; + this.actions = botScript.getActions(); + this.protocolStepTimeLimitInMs = MINUTES.toMillis(botScript.getProtocolStepTimeLimitInMinutes()); + this.stayAlive = botScript.isStayAlive(); + this.isUsingTestHarness = botScript.isUseTestHarness(); + if (isUsingTestHarness) + this.paymentAccount = createBotPaymentAccount(botScript); + else + this.paymentAccount = botClient.getPaymentAccount(botScript.getPaymentAccountIdForBot()); + } + + private PaymentAccount createBotPaymentAccount(BotScript botScript) { + BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(botClient); + + String paymentMethodId = botScript.getBotPaymentMethodId(); + if (paymentMethodId != null) { + if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) { + return accountGenerator.createZellePaymentAccount("Bob's Zelle Account", + "Bob"); + } else { + throw new UnsupportedOperationException( + format("This bot test does not work with %s payment accounts yet.", + getPaymentMethodById(paymentMethodId).getDisplayString())); + } + } else { + Country country = findCountry(botScript.getCountryCode()); + return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account"); + } + } + + private Country findCountry(String countryCode) { + return findCountryByCode(countryCode).orElseThrow(() -> + new IllegalArgumentException(countryCode + " is not a valid iso country code.")); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java new file mode 100644 index 0000000000..c34dc14d28 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java @@ -0,0 +1,339 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.TradeInfo; + +import protobuf.PaymentAccount; + +import java.text.DecimalFormat; + +import java.util.List; +import java.util.function.BiPredicate; + +import lombok.extern.slf4j.Slf4j; + +import static org.apache.commons.lang3.StringUtils.capitalize; + + + +import bisq.cli.GrpcClient; + +/** + * Convenience GrpcClient wrapper for bots using gRPC services. + * + * TODO Consider if the duplication smell is bad enough to force a BotClient user + * to use the GrpcClient instead (and delete this class). But right now, I think it is + * OK because moving some of the non-gRPC related methods to GrpcClient is even smellier. + * + */ +@SuppressWarnings({"JavaDoc", "unused"}) +@Slf4j +public class BotClient { + + private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0"); + + private final GrpcClient grpcClient; + + public BotClient(GrpcClient grpcClient) { + this.grpcClient = grpcClient; + } + + /** + * Returns current BSQ and BTC balance information. + * @return BalancesInfo + */ + public BalancesInfo getBalance() { + return grpcClient.getBalances(); + } + + /** + * Return the most recent BTC market price for the given currencyCode. + * @param currencyCode + * @return double + */ + public double getCurrentBTCMarketPrice(String currencyCode) { + return grpcClient.getBtcPrice(currencyCode); + } + + /** + * Return the most recent BTC market price for the given currencyCode as an integer string. + * @param currencyCode + * @return String + */ + public String getCurrentBTCMarketPriceAsIntegerString(String currencyCode) { + return FIXED_PRICE_FMT.format(getCurrentBTCMarketPrice(currencyCode)); + } + + /** + * Return all BUY and SELL offers for the given currencyCode. + * @param currencyCode + * @return List + */ + public List getOffers(String currencyCode) { + var buyOffers = getBuyOffers(currencyCode); + if (buyOffers.size() > 0) { + return buyOffers; + } else { + return getSellOffers(currencyCode); + } + } + + /** + * Return BUY offers for the given currencyCode. + * @param currencyCode + * @return List + */ + public List getBuyOffers(String currencyCode) { + return grpcClient.getOffers("BUY", currencyCode); + } + + /** + * Return SELL offers for the given currencyCode. + * @param currencyCode + * @return List + */ + public List getSellOffers(String currencyCode) { + return grpcClient.getOffers("SELL", currencyCode); + } + + /** + * Create and return a new Offer using a market based price. + * @param paymentAccount + * @param direction + * @param currencyCode + * @param amountInSatoshis + * @param minAmountInSatoshis + * @param priceMarginAsPercent + * @param securityDepositAsPercent + * @param feeCurrency + * @return OfferInfo + */ + public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount, + String direction, + String currencyCode, + long amountInSatoshis, + long minAmountInSatoshis, + double priceMarginAsPercent, + double securityDepositAsPercent, + String feeCurrency) { + return grpcClient.createMarketBasedPricedOffer(direction, + currencyCode, + amountInSatoshis, + minAmountInSatoshis, + priceMarginAsPercent, + securityDepositAsPercent, + paymentAccount.getId(), + feeCurrency); + } + + /** + * Create and return a new Offer using a fixed price. + * @param paymentAccount + * @param direction + * @param currencyCode + * @param amountInSatoshis + * @param minAmountInSatoshis + * @param fixedOfferPriceAsString + * @param securityDepositAsPercent + * @param feeCurrency + * @return OfferInfo + */ + public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount, + String direction, + String currencyCode, + long amountInSatoshis, + long minAmountInSatoshis, + String fixedOfferPriceAsString, + double securityDepositAsPercent, + String feeCurrency) { + return grpcClient.createFixedPricedOffer(direction, + currencyCode, + amountInSatoshis, + minAmountInSatoshis, + fixedOfferPriceAsString, + securityDepositAsPercent, + paymentAccount.getId(), + feeCurrency); + } + + public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount, String feeCurrency) { + return grpcClient.takeOffer(offerId, paymentAccount.getId(), feeCurrency); + } + + /** + * Returns a persisted Trade with the given tradeId, or throws an exception. + * @param tradeId + * @return TradeInfo + */ + public TradeInfo getTrade(String tradeId) { + return grpcClient.getTrade(tradeId); + } + + /** + * Predicate returns true if the given exception indicates the trade with the given + * tradeId exists, but the trade's contract has not been fully prepared. + */ + public final BiPredicate tradeContractIsNotReady = (exception, tradeId) -> { + if (exception.getMessage().contains("no contract was found")) { + log.warn("Trade {} exists but is not fully prepared: {}.", + tradeId, + toCleanGrpcExceptionMessage(exception)); + return true; + } else { + return false; + } + }; + + /** + * Returns a trade's contract as a Json string, or null if the trade exists + * but the contract is not ready. + * @param tradeId + * @return String + */ + public String getTradeContract(String tradeId) { + try { + var trade = grpcClient.getTrade(tradeId); + return trade.getContractAsJson(); + } catch (Exception ex) { + if (tradeContractIsNotReady.test(ex, tradeId)) + return null; + else + throw ex; + } + } + + /** + * Returns true if the trade's taker deposit fee transaction has been published. + * @param tradeId a valid trade id + * @return boolean + */ + public boolean isTakerDepositFeeTxPublished(String tradeId) { + return grpcClient.getTrade(tradeId).getIsPayoutPublished(); + } + + /** + * Returns true if the trade's taker deposit fee transaction has been confirmed. + * @param tradeId a valid trade id + * @return boolean + */ + public boolean isTakerDepositFeeTxConfirmed(String tradeId) { + return grpcClient.getTrade(tradeId).getIsDepositConfirmed(); + } + + /** + * Returns true if the trade's 'start payment' message has been sent by the buyer. + * @param tradeId a valid trade id + * @return boolean + */ + public boolean isTradePaymentStartedSent(String tradeId) { + return grpcClient.getTrade(tradeId).getIsFiatSent(); + } + + /** + * Returns true if the trade's 'payment received' message has been sent by the seller. + * @param tradeId a valid trade id + * @return boolean + */ + public boolean isTradePaymentReceivedConfirmationSent(String tradeId) { + return grpcClient.getTrade(tradeId).getIsFiatReceived(); + } + + /** + * Returns true if the trade's payout transaction has been published. + * @param tradeId a valid trade id + * @return boolean + */ + public boolean isTradePayoutTxPublished(String tradeId) { + return grpcClient.getTrade(tradeId).getIsPayoutPublished(); + } + + /** + * Sends a 'confirm payment started message' for a trade with the given tradeId, + * or throws an exception. + * @param tradeId + */ + public void sendConfirmPaymentStartedMessage(String tradeId) { + grpcClient.confirmPaymentStarted(tradeId); + } + + /** + * Sends a 'confirm payment received message' for a trade with the given tradeId, + * or throws an exception. + * @param tradeId + */ + public void sendConfirmPaymentReceivedMessage(String tradeId) { + grpcClient.confirmPaymentReceived(tradeId); + } + + /** + * Sends a 'keep funds in wallet message' for a trade with the given tradeId, + * or throws an exception. + * @param tradeId + */ + public void sendKeepFundsMessage(String tradeId) { + grpcClient.keepFunds(tradeId); + } + + /** + * Create and save a new PaymentAccount with details in the given json. + * @param json + * @return PaymentAccount + */ + public PaymentAccount createNewPaymentAccount(String json) { + return grpcClient.createPaymentAccount(json); + } + + /** + * Returns a persisted PaymentAccount with the given paymentAccountId, or throws + * an exception. + * @param paymentAccountId The id of the PaymentAccount being looked up. + * @return PaymentAccount + */ + public PaymentAccount getPaymentAccount(String paymentAccountId) { + return grpcClient.getPaymentAccounts().stream() + .filter(a -> (a.getId().equals(paymentAccountId))) + .findFirst() + .orElseThrow(() -> + new PaymentAccountNotFoundException("Could not find a payment account with id " + + paymentAccountId + ".")); + } + + /** + * Returns a persisted PaymentAccount with the given accountName, or throws + * an exception. + * @param accountName + * @return PaymentAccount + */ + public PaymentAccount getPaymentAccountWithName(String accountName) { + var req = GetPaymentAccountsRequest.newBuilder().build(); + return grpcClient.getPaymentAccounts().stream() + .filter(a -> (a.getAccountName().equals(accountName))) + .findFirst() + .orElseThrow(() -> + new PaymentAccountNotFoundException("Could not find a payment account with name " + + accountName + ".")); + } + + public String toCleanGrpcExceptionMessage(Exception ex) { + return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", "")); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/BotPaymentAccountGenerator.java b/apitest/src/test/java/bisq/apitest/scenario/bot/BotPaymentAccountGenerator.java new file mode 100644 index 0000000000..e586c3236a --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/BotPaymentAccountGenerator.java @@ -0,0 +1,68 @@ +package bisq.apitest.scenario.bot; + +import bisq.core.api.model.PaymentAccountForm; +import bisq.core.locale.Country; + +import protobuf.PaymentAccount; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.File; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID; +import static bisq.core.payment.payload.PaymentMethod.F2F_ID; + +@Slf4j +public class BotPaymentAccountGenerator { + + private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); + + private final BotClient botClient; + + public BotPaymentAccountGenerator(BotClient botClient) { + this.botClient = botClient; + } + + public PaymentAccount createF2FPaymentAccount(Country country, String accountName) { + try { + return botClient.getPaymentAccountWithName(accountName); + } catch (PaymentAccountNotFoundException ignored) { + // Ignore not found exception, create a new account. + } + Map p = getPaymentAccountFormMap(F2F_ID); + p.put("accountName", accountName); + p.put("city", country.name + " City"); + p.put("country", country.code); + p.put("contact", "By Semaphore"); + p.put("extraInfo", ""); + // Convert the map back to a json string and create the payment account over gRPC. + return botClient.createNewPaymentAccount(gson.toJson(p)); + } + + public PaymentAccount createZellePaymentAccount(String accountName, String holderName) { + try { + return botClient.getPaymentAccountWithName(accountName); + } catch (PaymentAccountNotFoundException ignored) { + // Ignore not found exception, create a new account. + } + Map p = getPaymentAccountFormMap(CLEAR_X_CHANGE_ID); + p.put("accountName", accountName); + p.put("emailOrMobileNr", holderName + "@zelle.com"); + p.put("holderName", holderName); + return botClient.createNewPaymentAccount(gson.toJson(p)); + } + + private Map getPaymentAccountFormMap(String paymentMethodId) { + PaymentAccountForm paymentAccountForm = new PaymentAccountForm(); + File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId); + jsonFormTemplate.deleteOnExit(); + String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate); + //noinspection unchecked + return (Map) gson.fromJson(jsonString, Object.class); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/InvalidRandomOfferException.java b/apitest/src/test/java/bisq/apitest/scenario/bot/InvalidRandomOfferException.java new file mode 100644 index 0000000000..ccd1a2ebf1 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/InvalidRandomOfferException.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import bisq.common.BisqException; + +@SuppressWarnings("unused") +public class InvalidRandomOfferException extends BisqException { + public InvalidRandomOfferException(Throwable cause) { + super(cause); + } + + public InvalidRandomOfferException(String format, Object... args) { + super(format, args); + } + + public InvalidRandomOfferException(Throwable cause, String format, Object... args) { + super(cause, format, args); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/PaymentAccountNotFoundException.java b/apitest/src/test/java/bisq/apitest/scenario/bot/PaymentAccountNotFoundException.java new file mode 100644 index 0000000000..8578a38af7 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/PaymentAccountNotFoundException.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import bisq.common.BisqException; + +@SuppressWarnings("unused") +public class PaymentAccountNotFoundException extends BisqException { + public PaymentAccountNotFoundException(Throwable cause) { + super(cause); + } + + public PaymentAccountNotFoundException(String format, Object... args) { + super(format, args); + } + + public PaymentAccountNotFoundException(Throwable cause, String format, Object... args) { + super(cause, format, args); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java new file mode 100644 index 0000000000..1942f8ad07 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java @@ -0,0 +1,177 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import bisq.proto.grpc.OfferInfo; + +import protobuf.PaymentAccount; + +import java.security.SecureRandom; + +import java.text.DecimalFormat; + +import java.math.BigDecimal; + +import java.util.Objects; +import java.util.function.Supplier; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.CurrencyFormat.formatMarketPrice; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.common.util.MathUtils.scaleDownByPowerOf10; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.payment.payload.PaymentMethod.F2F_ID; +import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; + +@Slf4j +public class RandomOffer { + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0"); + + @SuppressWarnings("FieldCanBeLocal") + // If not an F2F account, keep amount <= 0.01 BTC to avoid hitting unsigned + // acct trading limit. + private final Supplier nextAmount = () -> + this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID) + ? (long) (10000000 + RANDOM.nextInt(2500000)) + : (long) (750000 + RANDOM.nextInt(250000)); + + @SuppressWarnings("FieldCanBeLocal") + private final Supplier nextMinAmount = () -> { + boolean useMinAmount = RANDOM.nextBoolean(); + if (useMinAmount) { + return this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID) + ? this.getAmount() - 5000000L + : this.getAmount() - 50000L; + } else { + return this.getAmount(); + } + }; + + @SuppressWarnings("FieldCanBeLocal") + private final Supplier nextPriceMargin = () -> { + boolean useZeroMargin = RANDOM.nextBoolean(); + if (useZeroMargin) { + return 0.00; + } else { + BigDecimal min = BigDecimal.valueOf(-5.0).setScale(2, HALF_UP); + BigDecimal max = BigDecimal.valueOf(5.0).setScale(2, HALF_UP); + BigDecimal randomBigDecimal = min.add(BigDecimal.valueOf(RANDOM.nextDouble()).multiply(max.subtract(min))); + return randomBigDecimal.setScale(2, HALF_UP).doubleValue(); + } + }; + + private final BotClient botClient; + @Getter + private final PaymentAccount paymentAccount; + @Getter + private final String direction; + @Getter + private final String currencyCode; + @Getter + private final long amount; + @Getter + private final long minAmount; + @Getter + private final boolean useMarketBasedPrice; + @Getter + private final double priceMargin; + @Getter + private final String feeCurrency; + + @Getter + private String fixedOfferPrice = "0"; + @Getter + private OfferInfo offer; + @Getter + private String id; + + public RandomOffer(BotClient botClient, PaymentAccount paymentAccount) { + this.botClient = botClient; + this.paymentAccount = paymentAccount; + this.direction = RANDOM.nextBoolean() ? "BUY" : "SELL"; + this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode(); + this.amount = nextAmount.get(); + this.minAmount = nextMinAmount.get(); + this.useMarketBasedPrice = RANDOM.nextBoolean(); + this.priceMargin = nextPriceMargin.get(); + this.feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC"; + } + + public RandomOffer create() throws InvalidRandomOfferException { + try { + printDescription(); + if (useMarketBasedPrice) { + this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount, + direction, + currencyCode, + amount, + minAmount, + priceMargin, + getDefaultBuyerSecurityDepositAsPercent(), + feeCurrency); + } else { + this.offer = botClient.createOfferAtFixedPrice(paymentAccount, + direction, + currencyCode, + amount, + minAmount, + fixedOfferPrice, + getDefaultBuyerSecurityDepositAsPercent(), + feeCurrency); + } + this.id = offer.getId(); + return this; + } catch (Exception ex) { + String error = format("Could not create valid %s offer for %s BTC: %s", + currencyCode, + formatSatoshis(amount), + ex.getMessage()); + throw new InvalidRandomOfferException(error, ex); + } + } + + private void printDescription() { + double currentMarketPrice = botClient.getCurrentBTCMarketPrice(currencyCode); + // Calculate a fixed price based on the random mkt price margin, even if we don't use it. + double differenceFromMarketPrice = currentMarketPrice * scaleDownByPowerOf10(priceMargin, 2); + double fixedOfferPriceAsDouble = direction.equals("BUY") + ? currentMarketPrice - differenceFromMarketPrice + : currentMarketPrice + differenceFromMarketPrice; + this.fixedOfferPrice = FIXED_PRICE_FMT.format(fixedOfferPriceAsDouble); + String description = format("Creating new %s %s / %s offer for amount = %s BTC, min-amount = %s BTC.", + useMarketBasedPrice ? "mkt-based-price" : "fixed-priced", + direction, + currencyCode, + formatSatoshis(amount), + formatSatoshis(minAmount)); + log.info(description); + if (useMarketBasedPrice) { + log.info("Offer Price Margin = {}%", priceMargin); + log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); + } else { + + log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode); + } + log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java b/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java new file mode 100644 index 0000000000..d81f385a2b --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java @@ -0,0 +1,141 @@ +/* + * 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 bisq.apitest.scenario.bot; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; +import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled; +import static bisq.cli.TableFormat.formatBalancesTbls; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.protocol.BotProtocol; +import bisq.apitest.scenario.bot.protocol.MakerBotProtocol; +import bisq.apitest.scenario.bot.protocol.TakerBotProtocol; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.script.BotScript; +import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; + +@Slf4j +public +class RobotBob extends Bot { + + @Getter + private int numTrades; + + public RobotBob(BotClient botClient, + BotScript botScript, + BitcoinCliHelper bitcoinCli, + BashScriptGenerator bashScriptGenerator) { + super(botClient, botScript, bitcoinCli, bashScriptGenerator); + } + + public void run() { + for (String action : actions) { + checkActionIsValid(action); + + BotProtocol botProtocol; + if (action.equalsIgnoreCase(MAKE)) { + botProtocol = new MakerBotProtocol(botClient, + paymentAccount, + protocolStepTimeLimitInMs, + bitcoinCli, + bashScriptGenerator); + } else { + botProtocol = new TakerBotProtocol(botClient, + paymentAccount, + protocolStepTimeLimitInMs, + bitcoinCli, + bashScriptGenerator); + } + + botProtocol.run(); + + if (!botProtocol.getCurrentProtocolStep().equals(DONE)) { + throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete."); + } + + log.info("Completed {} successful trade{}. Current Balance:\n{}", + ++numTrades, + numTrades == 1 ? "" : "s", + formatBalancesTbls(botClient.getBalance())); + + if (numTrades < actions.length) { + try { + SECONDS.sleep(20); + } catch (InterruptedException ignored) { + // empty + } + } + + } // end of actions loop + + if (stayAlive) + waitForManualShutdown(); + else + warnCLIUserBeforeShutdown(); + } + + private void checkActionIsValid(String action) { + if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE)) + throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'"); + } + + private void waitForManualShutdown() { + String harnessOrCase = isUsingTestHarness ? "harness" : "case"; + log.info("All script actions have been completed, but the test {} will stay alive" + + " until a /tmp/bottest-shutdown file is detected.", + harnessOrCase); + log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.", + harnessOrCase); + if (!isUsingTestHarness) { + log.warn("You will have to manually shutdown the bitcoind and Bisq nodes" + + " running outside of the test harness."); + } + try { + while (!isShutdownCalled()) { + SECONDS.sleep(10); + } + log.warn("Manual shutdown signal received."); + } catch (ManualBotShutdownException ex) { + log.warn(ex.getMessage()); + } catch (InterruptedException ignored) { + // empty + } + } + + private void warnCLIUserBeforeShutdown() { + if (isUsingTestHarness) { + long delayInSeconds = 30; + log.warn("All script actions have been completed. You have {} seconds to complete any" + + " remaining tasks before the test harness shuts down.", + delayInSeconds); + try { + SECONDS.sleep(delayInSeconds); + } catch (InterruptedException ignored) { + // empty + } + } else { + log.info("Shutting down test case"); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java new file mode 100644 index 0000000000..51d59e7537 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java @@ -0,0 +1,349 @@ +/* + * 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 bisq.apitest.scenario.bot.protocol; + + +import bisq.proto.grpc.TradeInfo; + +import protobuf.PaymentAccount; + +import java.security.SecureRandom; + +import java.io.File; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*; +import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.util.Arrays.stream; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.BotClient; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; +import bisq.cli.TradeFormat; + +@Slf4j +public abstract class BotProtocol { + + static final SecureRandom RANDOM = new SecureRandom(); + static final String BUY = "BUY"; + static final String SELL = "SELL"; + + protected final Supplier randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000)); + + protected final AtomicLong protocolStepStartTime = new AtomicLong(0); + protected final Consumer initProtocolStep = (step) -> { + currentProtocolStep = step; + printBotProtocolStep(); + protocolStepStartTime.set(currentTimeMillis()); + }; + + @Getter + protected ProtocolStep currentProtocolStep; + + @Getter // Functions within 'this' need the @Getter. + protected final BotClient botClient; + protected final PaymentAccount paymentAccount; + protected final String currencyCode; + protected final long protocolStepTimeLimitInMs; + protected final BitcoinCliHelper bitcoinCli; + @Getter + protected final BashScriptGenerator bashScriptGenerator; + + public BotProtocol(BotClient botClient, + PaymentAccount paymentAccount, + long protocolStepTimeLimitInMs, + BitcoinCliHelper bitcoinCli, + BashScriptGenerator bashScriptGenerator) { + this.botClient = botClient; + this.paymentAccount = paymentAccount; + this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode(); + this.protocolStepTimeLimitInMs = protocolStepTimeLimitInMs; + this.bitcoinCli = bitcoinCli; + this.bashScriptGenerator = bashScriptGenerator; + this.currentProtocolStep = START; + } + + public abstract void run(); + + protected boolean isWithinProtocolStepTimeLimit() { + return (currentTimeMillis() - protocolStepStartTime.get()) < protocolStepTimeLimitInMs; + } + + protected void checkIsStartStep() { + if (currentProtocolStep != START) { + throw new IllegalStateException("First bot protocol step must be " + START.name()); + } + } + + protected void printBotProtocolStep() { + log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.", + currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs)); + + if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) { + log.info("Generate a btc block to trigger taker's deposit fee tx confirmation."); + createGenerateBtcBlockScript(); + } + } + + protected final Function waitForTakerFeeTxConfirm = (trade) -> { + sleep(5000); + waitForTakerFeeTxPublished(trade.getTradeId()); + waitForTakerFeeTxConfirmed(trade.getTradeId()); + return trade; + }; + + protected final Function waitForPaymentStartedMessage = (trade) -> { + initProtocolStep.accept(WAIT_FOR_PAYMENT_STARTED_MESSAGE); + try { + createPaymentStartedScript(trade); + log.info(" Waiting for a 'payment started' message from buyer for trade with id {}.", trade.getTradeId()); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted before checking if 'payment started' message has been sent."); + try { + var t = this.getBotClient().getTrade(trade.getTradeId()); + if (t.getIsFiatSent()) { + log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t)); + return t; + } + } catch (Exception ex) { + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); + } + sleep(randomDelay.get()); + } // end while + + throw new IllegalStateException("Payment was never sent; we won't wait any longer."); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting payment sent message.", ex); + } + }; + + protected final Function sendPaymentStartedMessage = (trade) -> { + initProtocolStep.accept(SEND_PAYMENT_STARTED_MESSAGE); + checkIfShutdownCalled("Interrupted before sending 'payment started' message."); + this.getBotClient().sendConfirmPaymentStartedMessage(trade.getTradeId()); + return trade; + }; + + protected final Function waitForPaymentReceivedConfirmation = (trade) -> { + initProtocolStep.accept(WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE); + createPaymentReceivedScript(trade); + try { + log.info("Waiting for a 'payment received confirmation' message from seller for trade with id {}.", trade.getTradeId()); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted before checking if 'payment received confirmation' message has been sent."); + try { + var t = this.getBotClient().getTrade(trade.getTradeId()); + if (t.getIsFiatReceived()) { + log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t)); + return t; + } + } catch (Exception ex) { + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); + } + sleep(randomDelay.get()); + } // end while + + throw new IllegalStateException("Payment was never received; we won't wait any longer."); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting payment received confirmation message.", ex); + } + }; + + protected final Function sendPaymentReceivedMessage = (trade) -> { + initProtocolStep.accept(SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE); + checkIfShutdownCalled("Interrupted before sending 'payment received confirmation' message."); + this.getBotClient().sendConfirmPaymentReceivedMessage(trade.getTradeId()); + return trade; + }; + + protected final Function waitForPayoutTx = (trade) -> { + initProtocolStep.accept(WAIT_FOR_PAYOUT_TX); + try { + log.info("Waiting on the 'payout tx published confirmation' for trade with id {}.", trade.getTradeId()); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted before checking if payout tx has been published."); + try { + var t = this.getBotClient().getTrade(trade.getTradeId()); + if (t.getIsPayoutPublished()) { + log.info("Payout tx {} has been published for trade:\n{}", + t.getPayoutTxId(), + TradeFormat.format(t)); + return t; + } + } catch (Exception ex) { + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); + } + sleep(randomDelay.get()); + } // end while + + throw new IllegalStateException("Payout tx was never published; we won't wait any longer."); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting for published payout tx.", ex); + } + }; + + protected final Function keepFundsFromTrade = (trade) -> { + initProtocolStep.accept(KEEP_FUNDS); + var isBuy = trade.getOffer().getDirection().equalsIgnoreCase(BUY); + var isSell = trade.getOffer().getDirection().equalsIgnoreCase(SELL); + var cliUserIsSeller = (this instanceof MakerBotProtocol && isBuy) || (this instanceof TakerBotProtocol && isSell); + if (cliUserIsSeller) { + createKeepFundsScript(trade); + } else { + createGetBalanceScript(); + } + checkIfShutdownCalled("Interrupted before closing trade with 'keep funds' command."); + this.getBotClient().sendKeepFundsMessage(trade.getTradeId()); + return trade; + }; + + protected void createPaymentStartedScript(TradeInfo trade) { + File script = bashScriptGenerator.createPaymentStartedScript(trade); + printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message"); + } + + protected void createPaymentReceivedScript(TradeInfo trade) { + File script = bashScriptGenerator.createPaymentReceivedScript(trade); + printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message"); + } + + protected void createKeepFundsScript(TradeInfo trade) { + File script = bashScriptGenerator.createKeepFundsScript(trade); + printCliHintAndOrScript(script, "The manual CLI side can close the trade"); + } + + protected void createGetBalanceScript() { + File script = bashScriptGenerator.createGetBalanceScript(); + printCliHintAndOrScript(script, "The manual CLI side can view current balances"); + } + + protected void createGenerateBtcBlockScript() { + String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress(); + File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress); + printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block"); + } + + protected void printCliHintAndOrScript(File script, String hint) { + log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath()); + if (this.getBashScriptGenerator().isPrintCliScripts()) + this.getBashScriptGenerator().printCliScript(script, log); + + sleep(5000); // Allow 5s for CLI user to read the hint. + } + + protected void sleep(long ms) { + try { + MILLISECONDS.sleep(ms); + } catch (InterruptedException ignored) { + // empty + } + } + + private void waitForTakerFeeTxPublished(String tradeId) { + waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED); + } + + private void waitForTakerFeeTxConfirmed(String tradeId) { + waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED); + } + + private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) { + initProtocolStep.accept(depositTxProtocolStep); + validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED); + try { + log.info(waitingForDepositFeeTxMsg(tradeId)); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed."); + try { + var trade = this.getBotClient().getTrade(tradeId); + if (isDepositFeeTxStepComplete.test(trade)) + return; + else + sleep(randomDelay.get()); + } catch (Exception ex) { + if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId)) + sleep(randomDelay.get()); + else + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); + } + } // end while + throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getDepositTxId())); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex); + } + } + + private final Predicate isDepositFeeTxStepComplete = (trade) -> { + if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) { + log.info("Taker deposit fee tx {} has been published.", trade.getDepositTxId()); + return true; + } else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositConfirmed()) { + log.info("Taker deposit fee tx {} has been confirmed.", trade.getDepositTxId()); + return true; + } else { + return false; + } + }; + + private void validateCurrentProtocolStep(Enum... validBotSteps) { + for (Enum validBotStep : validBotSteps) { + if (currentProtocolStep.equals(validBotStep)) + return; + } + throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n" + + "Must be one of " + + stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(",")) + + "."); + } + + private String waitingForDepositFeeTxMsg(String tradeId) { + return format("Waiting for taker deposit fee tx for trade %s to be %s.", + tradeId, + currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed"); + } + + private String stoppedWaitingForDepositFeeTxMsg(String txId) { + return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.", + txId, + currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed"); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java new file mode 100644 index 0000000000..0ce26002ec --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java @@ -0,0 +1,114 @@ +package bisq.apitest.scenario.bot.protocol; + +import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.TradeInfo; + +import protobuf.PaymentAccount; + +import java.io.File; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER; +import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; +import static bisq.cli.TableFormat.formatOfferTable; +import static java.util.Collections.singletonList; + + + +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.BotClient; +import bisq.apitest.scenario.bot.RandomOffer; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; +import bisq.cli.TradeFormat; + +@Slf4j +public class MakerBotProtocol extends BotProtocol { + + public MakerBotProtocol(BotClient botClient, + PaymentAccount paymentAccount, + long protocolStepTimeLimitInMs, + BitcoinCliHelper bitcoinCli, + BashScriptGenerator bashScriptGenerator) { + super(botClient, + paymentAccount, + protocolStepTimeLimitInMs, + bitcoinCli, + bashScriptGenerator); + } + + @Override + public void run() { + checkIsStartStep(); + + Function, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm); + var trade = makeTrade.apply(randomOffer); + + var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY); + Function completeFiatTransaction = makerIsBuyer + ? sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation) + : waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage); + completeFiatTransaction.apply(trade); + + Function closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade); + closeTrade.apply(trade); + + currentProtocolStep = DONE; + } + + private final Supplier randomOffer = () -> { + checkIfShutdownCalled("Interrupted before creating random offer."); + OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer(); + log.info("Created random {} offer\n{}", currencyCode, formatOfferTable(singletonList(offer), currencyCode)); + return offer; + }; + + private final Function, TradeInfo> waitForNewTrade = (randomOffer) -> { + initProtocolStep.accept(WAIT_FOR_OFFER_TAKER); + OfferInfo offer = randomOffer.get(); + createTakeOfferCliScript(offer); + try { + log.info("Impatiently waiting for offer {} to be taken, repeatedly calling gettrade.", offer.getId()); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted while waiting for offer to be taken."); + try { + var trade = getNewTrade(offer.getId()); + if (trade.isPresent()) + return trade.get(); + else + sleep(randomDelay.get()); + } catch (Exception ex) { + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex); + } + } // end while + throw new IllegalStateException("Offer was never taken; we won't wait any longer."); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting for offer to be taken.", ex); + } + }; + + private Optional getNewTrade(String offerId) { + try { + var trade = botClient.getTrade(offerId); + log.info("Offer {} was taken, new trade:\n{}", offerId, TradeFormat.format(trade)); + return Optional.of(trade); + } catch (Exception ex) { + // Get trade will throw a non-fatal gRPC exception if not found. + log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex)); + return Optional.empty(); + } + } + + private void createTakeOfferCliScript(OfferInfo offer) { + File script = bashScriptGenerator.createTakeOfferScript(offer); + printCliHintAndOrScript(script, "The manual CLI side can take the offer"); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java new file mode 100644 index 0000000000..def2a0bb66 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java @@ -0,0 +1,17 @@ +package bisq.apitest.scenario.bot.protocol; + +public enum ProtocolStep { + START, + FIND_OFFER, + TAKE_OFFER, + WAIT_FOR_OFFER_TAKER, + WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, + WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED, + SEND_PAYMENT_STARTED_MESSAGE, + WAIT_FOR_PAYMENT_STARTED_MESSAGE, + SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, + WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, + WAIT_FOR_PAYOUT_TX, + KEEP_FUNDS, + DONE +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java new file mode 100644 index 0000000000..63b700824f --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java @@ -0,0 +1,136 @@ +package bisq.apitest.scenario.bot.protocol; + +import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.TradeInfo; + +import protobuf.PaymentAccount; + +import java.io.File; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.FIND_OFFER; +import static bisq.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER; +import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; +import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.core.payment.payload.PaymentMethod.F2F_ID; + + + +import bisq.apitest.method.BitcoinCliHelper; +import bisq.apitest.scenario.bot.BotClient; +import bisq.apitest.scenario.bot.script.BashScriptGenerator; +import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; + +@Slf4j +public class TakerBotProtocol extends BotProtocol { + + public TakerBotProtocol(BotClient botClient, + PaymentAccount paymentAccount, + long protocolStepTimeLimitInMs, + BitcoinCliHelper bitcoinCli, + BashScriptGenerator bashScriptGenerator) { + super(botClient, + paymentAccount, + protocolStepTimeLimitInMs, + bitcoinCli, + bashScriptGenerator); + } + + @Override + public void run() { + checkIsStartStep(); + + Function takeTrade = takeOffer.andThen(waitForTakerFeeTxConfirm); + var trade = takeTrade.apply(findOffer.get()); + + var takerIsSeller = trade.getOffer().getDirection().equalsIgnoreCase(BUY); + Function completeFiatTransaction = takerIsSeller + ? waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage) + : sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation); + completeFiatTransaction.apply(trade); + + Function closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade); + closeTrade.apply(trade); + + currentProtocolStep = DONE; + } + + private final Supplier> firstOffer = () -> { + var offers = botClient.getOffers(currencyCode); + if (offers.size() > 0) { + log.info("Offers found:\n{}", formatOfferTable(offers, currencyCode)); + OfferInfo offer = offers.get(0); + log.info("Will take first offer {}", offer.getId()); + return Optional.of(offer); + } else { + log.info("No buy or sell {} offers found.", currencyCode); + return Optional.empty(); + } + }; + + private final Supplier findOffer = () -> { + initProtocolStep.accept(FIND_OFFER); + createMakeOfferScript(); + try { + log.info("Impatiently waiting for at least one {} offer to be created, repeatedly calling getoffers.", currencyCode); + while (isWithinProtocolStepTimeLimit()) { + checkIfShutdownCalled("Interrupted while checking offers."); + try { + Optional offer = firstOffer.get(); + if (offer.isPresent()) + return offer.get(); + else + sleep(randomDelay.get()); + } catch (Exception ex) { + throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex); + } + } // end while + throw new IllegalStateException("Offer was never created; we won't wait any longer."); + } catch (ManualBotShutdownException ex) { + throw ex; // not an error, tells bot to shutdown + } catch (Exception ex) { + throw new IllegalStateException("Error while waiting for a new offer.", ex); + } + }; + + private final Function takeOffer = (offer) -> { + initProtocolStep.accept(TAKE_OFFER); + checkIfShutdownCalled("Interrupted before taking offer."); + String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC"; + return botClient.takeOffer(offer.getId(), paymentAccount, feeCurrency); + }; + + private void createMakeOfferScript() { + String direction = RANDOM.nextBoolean() ? "BUY" : "SELL"; + String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC"; + boolean createMarginPricedOffer = RANDOM.nextBoolean(); + // If not using an F2F account, don't go over possible 0.01 BTC + // limit if account is not signed. + String amount = paymentAccount.getPaymentMethod().getId().equals(F2F_ID) + ? "0.25" + : "0.01"; + File script; + if (createMarginPricedOffer) { + script = bashScriptGenerator.createMakeMarginPricedOfferScript(direction, + currencyCode, + amount, + "0.0", + "15.0", + feeCurrency); + } else { + script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction, + currencyCode, + amount, + botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode), + "15.0", + feeCurrency); + } + printCliHintAndOrScript(script, "The manual CLI side can create an offer"); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java new file mode 100644 index 0000000000..d41e8a1acd --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java @@ -0,0 +1,235 @@ +/* + * 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 bisq.apitest.scenario.bot.script; + +import bisq.common.file.FileUtil; + +import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.TradeInfo; + +import com.google.common.io.Files; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.io.FileWriteMode.APPEND; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readAllBytes; + +@Slf4j +@Getter +public class BashScriptGenerator { + + private final int apiPort; + private final String apiPassword; + private final String paymentAccountId; + private final String cliBase; + private final boolean printCliScripts; + + public BashScriptGenerator(String apiPassword, + int apiPort, + String paymentAccountId, + boolean printCliScripts) { + this.apiPassword = apiPassword; + this.apiPort = apiPort; + this.paymentAccountId = paymentAccountId; + this.printCliScripts = printCliScripts; + this.cliBase = format("./bisq-cli --password=%s --port=%d", apiPassword, apiPort); + } + + public File createMakeMarginPricedOfferScript(String direction, + String currencyCode, + String amount, + String marketPriceMargin, + String securityDeposit, + String feeCurrency) { + String makeOfferCmd = format("%s createoffer --payment-account=%s " + + " --direction=%s" + + " --currency-code=%s" + + " --amount=%s" + + " --market-price-margin=%s" + + " --security-deposit=%s" + + " --fee-currency=%s", + cliBase, + this.getPaymentAccountId(), + direction, + currencyCode, + amount, + marketPriceMargin, + securityDeposit, + feeCurrency); + String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", + cliBase, + direction, + currencyCode); + return createCliScript("createoffer.sh", + makeOfferCmd, + "sleep 2", + getOffersCmd); + } + + public File createMakeFixedPricedOfferScript(String direction, + String currencyCode, + String amount, + String fixedPrice, + String securityDeposit, + String feeCurrency) { + String makeOfferCmd = format("%s createoffer --payment-account=%s " + + " --direction=%s" + + " --currency-code=%s" + + " --amount=%s" + + " --fixed-price=%s" + + " --security-deposit=%s" + + " --fee-currency=%s", + cliBase, + this.getPaymentAccountId(), + direction, + currencyCode, + amount, + fixedPrice, + securityDeposit, + feeCurrency); + String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", + cliBase, + direction, + currencyCode); + return createCliScript("createoffer.sh", + makeOfferCmd, + "sleep 2", + getOffersCmd); + } + + public File createTakeOfferScript(OfferInfo offer) { + String getOffersCmd = format("%s getoffers --direction=%s --currency-code=%s", + cliBase, + offer.getDirection(), + offer.getCounterCurrencyCode()); + String takeOfferCmd = format("%s takeoffer --offer-id=%s --payment-account=%s --fee-currency=BSQ", + cliBase, + offer.getId(), + this.getPaymentAccountId()); + String getTradeCmd = format("%s gettrade --trade-id=%s", + cliBase, + offer.getId()); + return createCliScript("takeoffer.sh", + getOffersCmd, + takeOfferCmd, + "sleep 5", + getTradeCmd); + } + + public File createPaymentStartedScript(TradeInfo trade) { + String paymentStartedCmd = format("%s confirmpaymentstarted --trade-id=%s", + cliBase, + trade.getTradeId()); + String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); + return createCliScript("confirmpaymentstarted.sh", + paymentStartedCmd, + "sleep 2", + getTradeCmd); + } + + public File createPaymentReceivedScript(TradeInfo trade) { + String paymentStartedCmd = format("%s confirmpaymentreceived --trade-id=%s", + cliBase, + trade.getTradeId()); + String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); + return createCliScript("confirmpaymentreceived.sh", + paymentStartedCmd, + "sleep 2", + getTradeCmd); + } + + public File createKeepFundsScript(TradeInfo trade) { + String paymentStartedCmd = format("%s keepfunds --trade-id=%s", cliBase, trade.getTradeId()); + String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); + String getBalanceCmd = format("%s getbalance", cliBase); + return createCliScript("keepfunds.sh", + paymentStartedCmd, + "sleep 2", + getTradeCmd, + getBalanceCmd); + } + + public File createGetBalanceScript() { + String getBalanceCmd = format("%s getbalance", cliBase); + return createCliScript("getbalance.sh", getBalanceCmd); + } + + public File createGenerateBtcBlockScript(String address) { + String bitcoinCliCmd = format("bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest" + + " -rpcpassword=apitest generatetoaddress 1 \"%s\"", + address); + return createCliScript("genbtcblk.sh", + bitcoinCliCmd); + } + + public File createCliScript(String scriptName, String... commands) { + String filename = getProperty("java.io.tmpdir") + File.separator + scriptName; + File oldScript = new File(filename); + if (oldScript.exists()) { + try { + FileUtil.deleteFileIfExists(oldScript); + } catch (IOException ex) { + throw new IllegalStateException("Unable to delete old script.", ex); + } + } + File script = new File(filename); + try { + List lines = new ArrayList<>(); + lines.add("#!/bin/bash"); + lines.add("############################################################"); + lines.add("# This example CLI script may be overwritten during the test"); + lines.add("# run, and will be deleted when the test harness shuts down."); + lines.add("# Make a copy if you want to save it."); + lines.add("############################################################"); + lines.add("set -x"); + Collections.addAll(lines, commands); + Files.asCharSink(script, UTF_8, APPEND).writeLines(lines); + if (!script.setExecutable(true)) + throw new IllegalStateException("Unable to set script owner's execute permission."); + } catch (IOException ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } finally { + script.deleteOnExit(); + } + return script; + } + + public void printCliScript(File cliScript, + org.slf4j.Logger logger) { + try { + String contents = new String(readAllBytes(Paths.get(cliScript.getPath()))); + logger.info("CLI script {}:\n{}", cliScript.getAbsolutePath(), contents); + } catch (IOException ex) { + throw new IllegalStateException("Error reading CLI script contents.", ex); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScript.java b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScript.java new file mode 100644 index 0000000000..2caaed68ad --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScript.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.apitest.scenario.bot.script; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.annotation.Nullable; + +@Getter +@ToString +public +class BotScript { + + // Common, default is true. + private final boolean useTestHarness; + + // Used only with test harness. Mutually exclusive, but if both are not null, + // the botPaymentMethodId takes precedence over countryCode. + @Nullable + private final String botPaymentMethodId; + @Nullable + private final String countryCode; + + // Used only without test harness. + @Nullable + @Setter + private String paymentAccountIdForBot; + @Nullable + @Setter + private String paymentAccountIdForCliScripts; + + // Common, used with or without test harness. + private final int apiPortForCliScripts; + private final String[] actions; + private final long protocolStepTimeLimitInMinutes; + private final boolean printCliScripts; + private final boolean stayAlive; + + @SuppressWarnings("NullableProblems") + BotScript(boolean useTestHarness, + String botPaymentMethodId, + String countryCode, + String paymentAccountIdForBot, + String paymentAccountIdForCliScripts, + String[] actions, + int apiPortForCliScripts, + long protocolStepTimeLimitInMinutes, + boolean printCliScripts, + boolean stayAlive) { + this.useTestHarness = useTestHarness; + this.botPaymentMethodId = botPaymentMethodId; + this.countryCode = countryCode != null ? countryCode.toUpperCase() : null; + this.paymentAccountIdForBot = paymentAccountIdForBot; + this.paymentAccountIdForCliScripts = paymentAccountIdForCliScripts; + this.apiPortForCliScripts = apiPortForCliScripts; + this.actions = actions; + this.protocolStepTimeLimitInMinutes = protocolStepTimeLimitInMinutes; + this.printCliScripts = printCliScripts; + this.stayAlive = stayAlive; + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java new file mode 100644 index 0000000000..c81730c4c4 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java @@ -0,0 +1,247 @@ +/* + * 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 bisq.apitest.scenario.bot.script; + +import bisq.common.file.JsonFileManager; +import bisq.common.util.Utilities; + +import joptsimple.BuiltinHelpFormatter; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static java.lang.System.err; +import static java.lang.System.exit; +import static java.lang.System.getProperty; +import static java.lang.System.out; + +@Slf4j +public class BotScriptGenerator { + + private final boolean useTestHarness; + @Nullable + private final String countryCode; + @Nullable + private final String botPaymentMethodId; + @Nullable + private final String paymentAccountIdForBot; + @Nullable + private final String paymentAccountIdForCliScripts; + private final int apiPortForCliScripts; + private final String actions; + private final int protocolStepTimeLimitInMinutes; + private final boolean printCliScripts; + private final boolean stayAlive; + + public BotScriptGenerator(String[] args) { + OptionParser parser = new OptionParser(); + var helpOpt = parser.accepts("help", "Print this help text.") + .forHelp(); + OptionSpec useTestHarnessOpt = parser + .accepts("use-testharness", "Use the test harness, or manually start your own nodes.") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(true); + OptionSpec actionsOpt = parser + .accepts("actions", "A comma delimited list with no spaces, e.g., make,take,take,make,...") + .withRequiredArg(); + OptionSpec botPaymentMethodIdOpt = parser + .accepts("bot-payment-method", + "The bot's (Bob) payment method id. If using the test harness," + + " the id will be used to automatically create a payment account.") + .withRequiredArg(); + OptionSpec countryCodeOpt = parser + .accepts("country-code", + "The two letter country-code for an F2F payment account if using the test harness," + + " but the bot-payment-method option takes precedence.") + .withRequiredArg(); + OptionSpec apiPortForCliScriptsOpt = parser + .accepts("api-port-for-cli-scripts", + "The api port used in bot generated bash/cli scripts.") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); + OptionSpec paymentAccountIdForBotOpt = parser + .accepts("payment-account-for-bot", + "The bot side's payment account id, when the test harness is not used," + + " and Bob & Alice accounts are not automatically created.") + .withRequiredArg(); + OptionSpec paymentAccountIdForCliScriptsOpt = parser + .accepts("payment-account-for-cli-scripts", + "The other side's payment account id, used in generated bash/cli scripts when" + + " the test harness is not used, and Bob & Alice accounts are not automatically created.") + .withRequiredArg(); + OptionSpec protocolStepTimeLimitInMinutesOpt = parser + .accepts("step-time-limit", "Each protocol step's time limit in minutes") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(60); + OptionSpec printCliScriptsOpt = parser + .accepts("print-cli-scripts", "Print the generated CLI scripts from bot") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + OptionSpec stayAliveOpt = parser + .accepts("stay-alive", "Leave test harness nodes running after the last action.") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(true); + OptionSet options = parser.parse(args); + + if (options.has(helpOpt)) { + printHelp(parser, out); + exit(0); + } + + if (!options.has(actionsOpt)) { + printHelp(parser, err); + exit(1); + } + + this.useTestHarness = options.has(useTestHarnessOpt) ? options.valueOf(useTestHarnessOpt) : true; + this.actions = options.valueOf(actionsOpt); + this.apiPortForCliScripts = options.has(apiPortForCliScriptsOpt) ? options.valueOf(apiPortForCliScriptsOpt) : 9998; + this.botPaymentMethodId = options.has(botPaymentMethodIdOpt) ? options.valueOf(botPaymentMethodIdOpt) : null; + this.countryCode = options.has(countryCodeOpt) ? options.valueOf(countryCodeOpt) : null; + this.paymentAccountIdForBot = options.has(paymentAccountIdForBotOpt) ? options.valueOf(paymentAccountIdForBotOpt) : null; + this.paymentAccountIdForCliScripts = options.has(paymentAccountIdForCliScriptsOpt) ? options.valueOf(paymentAccountIdForCliScriptsOpt) : null; + this.protocolStepTimeLimitInMinutes = options.valueOf(protocolStepTimeLimitInMinutesOpt); + this.printCliScripts = options.valueOf(printCliScriptsOpt); + this.stayAlive = options.valueOf(stayAliveOpt); + + var noPaymentAccountCountryOrMethodForTestHarness = useTestHarness && + (!options.has(countryCodeOpt) && !options.has(botPaymentMethodIdOpt)); + if (noPaymentAccountCountryOrMethodForTestHarness) { + log.error("When running the test harness, payment accounts are automatically generated,"); + log.error("and you must provide one of the following options:"); + log.error(" \t\t(1) --bot-payment-method= OR"); + log.error(" \t\t(2) --country-code="); + log.error("If the bot-payment-method option is not present, the bot will create" + + " a country based F2F account using the country-code."); + log.error("If both are present, the bot-payment-method will take precedence. " + + "Currently, only the CLEAR_X_CHANGE_ID bot-payment-method is supported."); + printHelp(parser, err); + exit(1); + } + + var noPaymentAccountIdOrApiPortForCliScripts = !useTestHarness && + (!options.has(paymentAccountIdForCliScriptsOpt) || !options.has(paymentAccountIdForBotOpt)); + if (noPaymentAccountIdOrApiPortForCliScripts) { + log.error("If not running the test harness, payment accounts are not automatically generated,"); + log.error("and you must provide three options:"); + log.error(" \t\t(1) --api-port-for-cli-scripts="); + log.error(" \t\t(2) --payment-account-for-bot="); + log.error(" \t\t(3) --payment-account-for-cli-scripts="); + log.error("These will be used by the bot and in CLI scripts the bot will generate when creating an offer."); + printHelp(parser, err); + exit(1); + } + } + + private void printHelp(OptionParser parser, PrintStream stream) { + try { + String usage = "Examples\n--------\n" + + examplesUsingTestHarness() + + examplesNotUsingTestHarness(); + stream.println(); + parser.formatHelpWith(new HelpFormatter()); + parser.printHelpOn(stream); + stream.println(); + stream.println(usage); + stream.println(); + } catch (IOException ex) { + log.error("", ex); + } + } + + private String examplesUsingTestHarness() { + @SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder(); + builder.append("To generate a bot-script.json file that will start the test harness,"); + builder.append(" create F2F accounts for Bob and Alice,"); + builder.append(" and take an offer created by Alice's CLI:").append("\n"); + builder.append("\tUsage: BotScriptGenerator").append("\n"); + builder.append("\t\t").append("--use-testharness=true").append("\n"); + builder.append("\t\t").append("--country-code=").append("\n"); + builder.append("\t\t").append("--actions=take").append("\n"); + builder.append("\n"); + builder.append("To generate a bot-script.json file that will start the test harness,"); + builder.append(" create Zelle accounts for Bob and Alice,"); + builder.append(" and create an offer to be taken by Alice's CLI:").append("\n"); + builder.append("\tUsage: BotScriptGenerator").append("\n"); + builder.append("\t\t").append("--use-testharness=true").append("\n"); + builder.append("\t\t").append("--bot-payment-method=CLEAR_X_CHANGE").append("\n"); + builder.append("\t\t").append("--actions=make").append("\n"); + builder.append("\n"); + return builder.toString(); + } + + private String examplesNotUsingTestHarness() { + @SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder(); + builder.append("To generate a bot-script.json file that will not start the test harness,"); + builder.append(" but will create useful bash scripts for the CLI user,"); + builder.append(" and make two offers, then take two offers:").append("\n"); + builder.append("\tUsage: BotScriptGenerator").append("\n"); + builder.append("\t\t").append("--use-testharness=false").append("\n"); + builder.append("\t\t").append("--api-port-for-cli-scripts=").append("\n"); + builder.append("\t\t").append("--payment-account-for-bot=").append("\n"); + builder.append("\t\t").append("--payment-account-for-cli-scripts=").append("\n"); + builder.append("\t\t").append("--actions=make,make,take,take").append("\n"); + builder.append("\n"); + return builder.toString(); + } + + private String generateBotScriptTemplate() { + return Utilities.objectToJson(new BotScript( + useTestHarness, + botPaymentMethodId, + countryCode, + paymentAccountIdForBot, + paymentAccountIdForCliScripts, + actions.split("\\s*,\\s*").clone(), + apiPortForCliScripts, + protocolStepTimeLimitInMinutes, + printCliScripts, + stayAlive)); + } + + public static void main(String[] args) { + BotScriptGenerator generator = new BotScriptGenerator(args); + String json = generator.generateBotScriptTemplate(); + String destDir = getProperty("java.io.tmpdir"); + JsonFileManager jsonFileManager = new JsonFileManager(new File(destDir)); + jsonFileManager.writeToDisc(json, "bot-script"); + JsonFileManager.shutDownAllInstances(); + log.info("Saved {}/bot-script.json", destDir); + log.info("bot-script.json contents\n{}", json); + } + + // Makes a formatter with a given overall row width of 120 and column separator width of 2. + private static class HelpFormatter extends BuiltinHelpFormatter { + public HelpFormatter() { + super(120, 2); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualBotShutdownException.java b/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualBotShutdownException.java new file mode 100644 index 0000000000..8a0e68bad1 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualBotShutdownException.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.apitest.scenario.bot.shutdown; + +import bisq.common.BisqException; + +@SuppressWarnings("unused") +public class ManualBotShutdownException extends BisqException { + public ManualBotShutdownException(Throwable cause) { + super(cause); + } + + public ManualBotShutdownException(String format, Object... args) { + super(format, args); + } + + public ManualBotShutdownException(Throwable cause, String format, Object... args) { + super(cause, format, args); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualShutdown.java b/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualShutdown.java new file mode 100644 index 0000000000..fc680f1c81 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/shutdown/ManualShutdown.java @@ -0,0 +1,64 @@ +package bisq.apitest.scenario.bot.shutdown; + +import bisq.common.UserThread; + +import java.io.File; +import java.io.IOException; + +import java.util.concurrent.atomic.AtomicBoolean; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.file.FileUtil.deleteFileIfExists; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@Slf4j +public class ManualShutdown { + + public static final String SHUTDOWN_FILENAME = "/tmp/bottest-shutdown"; + + private static final AtomicBoolean SHUTDOWN_CALLED = new AtomicBoolean(false); + + /** + * Looks for a /tmp/bottest-shutdown file and throws a BotShutdownException if found. + * + * Running '$ touch /tmp/bottest-shutdown' could be used to trigger a scaffold teardown. + * + * This is much easier than manually shutdown down bisq apps & bitcoind. + */ + public static void startShutdownTimer() { + deleteStaleShutdownFile(); + + UserThread.runPeriodically(() -> { + File shutdownFile = new File(SHUTDOWN_FILENAME); + if (shutdownFile.exists()) { + log.warn("Caught manual shutdown signal: /tmp/bottest-shutdown file exists."); + try { + deleteFileIfExists(shutdownFile); + } catch (IOException ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + SHUTDOWN_CALLED.set(true); + } + }, 2000, MILLISECONDS); + } + + public static boolean isShutdownCalled() { + return SHUTDOWN_CALLED.get(); + } + + public static void checkIfShutdownCalled(String warning) throws ManualBotShutdownException { + if (isShutdownCalled()) + throw new ManualBotShutdownException(warning); + } + + private static void deleteStaleShutdownFile() { + try { + deleteFileIfExists(new File(SHUTDOWN_FILENAME)); + } catch (IOException ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + } +} diff --git a/assets/src/main/java/bisq/asset/AbstractAsset.java b/assets/src/main/java/bisq/asset/AbstractAsset.java new file mode 100644 index 0000000000..3cabaa2eaa --- /dev/null +++ b/assets/src/main/java/bisq/asset/AbstractAsset.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.asset; + +import static org.apache.commons.lang3.Validate.notBlank; +import static org.apache.commons.lang3.Validate.notNull; + +/** + * Abstract base class for {@link Asset} implementations. Most implementations should not + * extend this class directly, but should rather extend {@link Coin}, {@link Token} or one + * of their subtypes. + * + * @author Chris Beams + * @since 0.7.0 + */ +public abstract class AbstractAsset implements Asset { + + private final String name; + private final String tickerSymbol; + private final AddressValidator addressValidator; + + public AbstractAsset(String name, String tickerSymbol, AddressValidator addressValidator) { + this.name = notBlank(name); + this.tickerSymbol = notBlank(tickerSymbol); + this.addressValidator = notNull(addressValidator); + } + + @Override + public final String getName() { + return name; + } + + @Override + public final String getTickerSymbol() { + return tickerSymbol; + } + + @Override + public final AddressValidationResult validateAddress(String address) { + return addressValidator.validate(address); + } + + @Override + public String toString() { + return getClass().getName(); + } +} diff --git a/assets/src/main/java/bisq/asset/AddressValidationResult.java b/assets/src/main/java/bisq/asset/AddressValidationResult.java new file mode 100644 index 0000000000..7331695c02 --- /dev/null +++ b/assets/src/main/java/bisq/asset/AddressValidationResult.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.asset; + +/** + * Value object representing the result of validating an {@link Asset} address. Various + * factory methods are provided for typical use cases. + * + * @author Chris Beams + * @since 0.7.0 + * @see Asset#validateAddress(String) + */ +public class AddressValidationResult { + + private static final AddressValidationResult VALID_ADDRESS = new AddressValidationResult(true, "", ""); + + private final boolean isValid; + private final String message; + private final String i18nKey; + + private AddressValidationResult(boolean isValid, String message, String i18nKey) { + this.isValid = isValid; + this.message = message; + this.i18nKey = i18nKey; + } + + public boolean isValid() { + return isValid; + } + + public String getI18nKey() { + return i18nKey; + } + + public String getMessage() { + return message; + } + + public static AddressValidationResult validAddress() { + return VALID_ADDRESS; + } + + public static AddressValidationResult invalidAddress(Throwable cause) { + return invalidAddress(cause.getMessage()); + } + + public static AddressValidationResult invalidAddress(String cause) { + return invalidAddress(cause, "validation.altcoin.invalidAddress"); + } + + public static AddressValidationResult invalidAddress(String cause, String i18nKey) { + return new AddressValidationResult(false, cause, i18nKey); + } + + public static AddressValidationResult invalidStructure() { + return invalidAddress("", "validation.altcoin.wrongStructure"); + } +} diff --git a/assets/src/main/java/bisq/asset/AddressValidator.java b/assets/src/main/java/bisq/asset/AddressValidator.java new file mode 100644 index 0000000000..8cf656087a --- /dev/null +++ b/assets/src/main/java/bisq/asset/AddressValidator.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset; + +/** + * An {@link Asset} address validation function. + * + * @author Chris Beams + * @since 0.7.0 + */ +public interface AddressValidator { + + AddressValidationResult validate(String address); +} diff --git a/assets/src/main/java/bisq/asset/AltCoinAccountDisclaimer.java b/assets/src/main/java/bisq/asset/AltCoinAccountDisclaimer.java new file mode 100644 index 0000000000..9497df37d2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/AltCoinAccountDisclaimer.java @@ -0,0 +1,25 @@ +package bisq.asset; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When a new PaymentAccount is created for given asset, this annotation tells UI to show user a disclaimer message + * with requirements needed to be fulfilled when conducting trade given payment method. + * + * I.e. in case of Monero user must use official Monero GUI wallet or Monero CLI wallet with certain options enabled, + * user needs to keep tx private key, tx hash, recipient's address, etc. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AltCoinAccountDisclaimer { + + /** + * Translation key of the message to show, i.e. "account.altcoin.popup.xmr.msg" + * @return translation key + */ + String value(); + +} diff --git a/assets/src/main/java/bisq/asset/Asset.java b/assets/src/main/java/bisq/asset/Asset.java new file mode 100644 index 0000000000..464ffbe99f --- /dev/null +++ b/assets/src/main/java/bisq/asset/Asset.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset; + +/** + * Interface representing a given ("crypto") asset in its most abstract form, having a + * {@link #getName() name}, eg "Bitcoin", a {@link #getTickerSymbol() ticker symbol}, + * eg "BTC", and an address validation function. Together, these properties represent + * the minimum information and functionality required to register and trade an asset on + * the Bisq network. + *

+ * Implementations typically extend either the {@link Coin} or {@link Token} base + * classes, and must be registered in the {@code META-INF/services/bisq.asset.Asset} file + * in order to be available in the {@link AssetRegistry} at runtime. + * + * @author Chris Beams + * @since 0.7.0 + * @see AbstractAsset + * @see Coin + * @see Token + * @see Erc20Token + * @see AssetRegistry + */ +public interface Asset { + + String getName(); + + String getTickerSymbol(); + + AddressValidationResult validateAddress(String address); +} diff --git a/assets/src/main/java/bisq/asset/AssetRegistry.java b/assets/src/main/java/bisq/asset/AssetRegistry.java new file mode 100644 index 0000000000..c6a0c7b346 --- /dev/null +++ b/assets/src/main/java/bisq/asset/AssetRegistry.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Stream; + +/** + * Provides {@link Stream}-based access to {@link Asset} implementations registered in + * the {@code META-INF/services/bisq.asset.Asset} provider-configuration file. + * + * @author Chris Beams + * @since 0.7.0 + * @see ServiceLoader + */ +public class AssetRegistry { + + private static final List registeredAssets = new ArrayList<>(); + + static { + for (Asset asset : ServiceLoader.load(Asset.class)) { + registeredAssets.add(asset); + } + } + + public Stream stream() { + return registeredAssets.stream(); + } +} diff --git a/assets/src/main/java/bisq/asset/Base58AddressValidator.java b/assets/src/main/java/bisq/asset/Base58AddressValidator.java new file mode 100644 index 0000000000..f038a69423 --- /dev/null +++ b/assets/src/main/java/bisq/asset/Base58AddressValidator.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.asset; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; + +/** + * {@link AddressValidator} for Base58-encoded addresses. + * + * @author Chris Beams + * @since 0.7.0 + * @see org.bitcoinj.core.LegacyAddress#fromBase58(NetworkParameters, String) + */ +public class Base58AddressValidator implements AddressValidator { + + private final NetworkParameters networkParameters; + + public Base58AddressValidator() { + this(MainNetParams.get()); + } + + public Base58AddressValidator(NetworkParameters networkParameters) { + this.networkParameters = networkParameters; + } + + @Override + public AddressValidationResult validate(String address) { + try { + LegacyAddress.fromBase58(networkParameters, address); + } catch (AddressFormatException ex) { + return AddressValidationResult.invalidAddress(ex); + } + + return AddressValidationResult.validAddress(); + } +} diff --git a/assets/src/main/java/bisq/asset/BitcoinAddressValidator.java b/assets/src/main/java/bisq/asset/BitcoinAddressValidator.java new file mode 100644 index 0000000000..133d066d02 --- /dev/null +++ b/assets/src/main/java/bisq/asset/BitcoinAddressValidator.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.asset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; + +/** + * {@link AddressValidator} for Bitcoin addresses. + * + * @author Oscar Guindzberg + */ +public class BitcoinAddressValidator implements AddressValidator { + + private final NetworkParameters networkParameters; + + public BitcoinAddressValidator() { + this(MainNetParams.get()); + } + + public BitcoinAddressValidator(NetworkParameters networkParameters) { + this.networkParameters = networkParameters; + } + + @Override + public AddressValidationResult validate(String address) { + try { + Address.fromString(networkParameters, address); + } catch (AddressFormatException ex) { + return AddressValidationResult.invalidAddress(ex); + } + + return AddressValidationResult.validAddress(); + } +} diff --git a/assets/src/main/java/bisq/asset/Coin.java b/assets/src/main/java/bisq/asset/Coin.java new file mode 100644 index 0000000000..5eb596e471 --- /dev/null +++ b/assets/src/main/java/bisq/asset/Coin.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset; + +/** + * Abstract base class for {@link Asset}s with their own dedicated blockchain, such as + * {@link bisq.asset.coins.Bitcoin} itself or one of its many derivatives, competitors and + * alternatives, often called "altcoins", such as {@link bisq.asset.coins.Litecoin}, + * {@link bisq.asset.coins.Ether}, {@link bisq.asset.coins.Monero} and + * {@link bisq.asset.coins.Zcash}. + *

+ * In addition to the usual {@code Asset} properties, a {@code Coin} maintains information + * about which {@link Network} it may be used on. By default, coins are constructed with + * the assumption they are for use on that coin's "main network", or "main blockchain", + * i.e. that they are "real" coins for use in a production environment. In testing + * scenarios, however, a coin may be constructed for use only on "testnet" or "regtest" + * networks. + * + * @author Chris Beams + * @since 0.7.0 + */ +public abstract class Coin extends AbstractAsset { + + public enum Network { MAINNET, TESTNET, REGTEST } + + private final Network network; + + public Coin(String name, String tickerSymbol, AddressValidator addressValidator) { + this(name, tickerSymbol, addressValidator, Network.MAINNET); + } + + public Coin(String name, String tickerSymbol, AddressValidator addressValidator, Network network) { + super(name, tickerSymbol, addressValidator); + this.network = network; + } + + public Network getNetwork() { + return network; + } +} diff --git a/assets/src/main/java/bisq/asset/CryptoNoteAddressValidator.java b/assets/src/main/java/bisq/asset/CryptoNoteAddressValidator.java new file mode 100644 index 0000000000..295c1e0af8 --- /dev/null +++ b/assets/src/main/java/bisq/asset/CryptoNoteAddressValidator.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.asset; + +/** + * {@link AddressValidator} for Base58-encoded Cryptonote addresses. + * + * @author Xiphon + */ +public class CryptoNoteAddressValidator implements AddressValidator { + + private final long[] validPrefixes; + private final boolean validateChecksum; + + public CryptoNoteAddressValidator(boolean validateChecksum, long... validPrefixes) { + this.validPrefixes = validPrefixes; + this.validateChecksum = validateChecksum; + } + + public CryptoNoteAddressValidator(long... validPrefixes) { + this(true, validPrefixes); + } + + @Override + public AddressValidationResult validate(String address) { + try { + long prefix = CryptoNoteUtils.MoneroBase58.decodeAddress(address, this.validateChecksum); + for (long validPrefix : this.validPrefixes) { + if (prefix == validPrefix) { + return AddressValidationResult.validAddress(); + } + } + return AddressValidationResult.invalidAddress(String.format("invalid address prefix %x", prefix)); + } catch (Exception e) { + return AddressValidationResult.invalidStructure(); + } + } +} diff --git a/assets/src/main/java/bisq/asset/CryptoNoteUtils.java b/assets/src/main/java/bisq/asset/CryptoNoteUtils.java new file mode 100644 index 0000000000..d5150353c6 --- /dev/null +++ b/assets/src/main/java/bisq/asset/CryptoNoteUtils.java @@ -0,0 +1,269 @@ +/* + * 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 bisq.asset; + +import org.bitcoinj.core.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import java.math.BigInteger; + +import java.util.Arrays; +import java.util.Map; + +public class CryptoNoteUtils { + public static String getRawSpendKeyAndViewKey(String address) throws CryptoNoteUtils.CryptoNoteException { + try { + // See https://monerodocs.org/public-address/standard-address/ + byte[] decoded = CryptoNoteUtils.MoneroBase58.decode(address); + // Standard addresses are of length 69 and addresses with integrated payment ID of length 77. + + if (decoded.length <= 65) { + throw new CryptoNoteUtils.CryptoNoteException("The address we received is too short. address=" + address); + } + + // If the length is not as expected but still can be truncated we log an error and continue. + if (decoded.length != 69 && decoded.length != 77) { + System.out.println("The address we received is not in the expected format. address=" + address); + } + + // We remove the network type byte, the checksum (4 bytes) and optionally the payment ID (8 bytes if present) + // So extract the 64 bytes after the first byte, which are the 32 byte public spend key + the 32 byte public view key + byte[] slice = Arrays.copyOfRange(decoded, 1, 65); + return Utils.HEX.encode(slice); + } catch (CryptoNoteUtils.CryptoNoteException e) { + throw new CryptoNoteUtils.CryptoNoteException(e); + } + } + + public static class CryptoNoteException extends Exception { + CryptoNoteException(String msg) { + super(msg); + } + + public CryptoNoteException(CryptoNoteException exception) { + super(exception); + } + } + + static class Keccak { + private static final int BLOCK_SIZE = 136; + private static final int LONGS_PER_BLOCK = BLOCK_SIZE / 8; + private static final int KECCAK_ROUNDS = 24; + private static final long[] KECCAKF_RNDC = { + 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808aL, + 0x8000000080008000L, 0x000000000000808bL, 0x0000000080000001L, + 0x8000000080008081L, 0x8000000000008009L, 0x000000000000008aL, + 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL, + 0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L, + 0x8000000000008003L, 0x8000000000008002L, 0x8000000000000080L, + 0x000000000000800aL, 0x800000008000000aL, 0x8000000080008081L, + 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L + }; + private static final int[] KECCAKF_ROTC = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 + }; + private static final int[] KECCAKF_PILN = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 + }; + + private static long rotateLeft(long value, int shift) { + return (value << shift) | (value >>> (64 - shift)); + } + + private static void keccakf(long[] st, int rounds) { + long[] bc = new long[5]; + + for (int round = 0; round < rounds; ++round) { + for (int i = 0; i < 5; ++i) { + bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; + } + + for (int i = 0; i < 5; i++) { + long t = bc[(i + 4) % 5] ^ rotateLeft(bc[(i + 1) % 5], 1); + for (int j = 0; j < 25; j += 5) { + st[j + i] ^= t; + } + } + + long t = st[1]; + for (int i = 0; i < 24; ++i) { + int j = KECCAKF_PILN[i]; + bc[0] = st[j]; + st[j] = rotateLeft(t, KECCAKF_ROTC[i]); + t = bc[0]; + } + + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; i++) { + bc[i] = st[j + i]; + } + for (int i = 0; i < 5; i++) { + st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + } + + st[0] ^= KECCAKF_RNDC[round]; + } + } + + static ByteBuffer keccak1600(ByteBuffer input) { + input.order(ByteOrder.LITTLE_ENDIAN); + + int fullBlocks = input.remaining() / BLOCK_SIZE; + long[] st = new long[25]; + for (int block = 0; block < fullBlocks; ++block) { + for (int index = 0; index < LONGS_PER_BLOCK; ++index) { + st[index] ^= input.getLong(); + } + keccakf(st, KECCAK_ROUNDS); + } + + ByteBuffer lastBlock = ByteBuffer.allocate(144).order(ByteOrder.LITTLE_ENDIAN); + lastBlock.put(input); + lastBlock.put((byte) 1); + int paddingOffset = BLOCK_SIZE - 1; + lastBlock.put(paddingOffset, (byte) (lastBlock.get(paddingOffset) | 0x80)); + lastBlock.rewind(); + + for (int index = 0; index < LONGS_PER_BLOCK; ++index) { + st[index] ^= lastBlock.getLong(); + } + + keccakf(st, KECCAK_ROUNDS); + + ByteBuffer result = ByteBuffer.allocate(32); + result.slice().order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(st, 0, 4); + return result; + } + } + + static class MoneroBase58 { + + private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length()); + private static final int FULL_DECODED_BLOCK_SIZE = 8; + private static final int FULL_ENCODED_BLOCK_SIZE = 11; + private static final BigInteger UINT64_MAX = new BigInteger("18446744073709551615"); + private static final Map DECODED_CHUNK_LENGTH = Map.of(2, 1, + 3, 2, + 5, 3, + 6, 4, + 7, 5, + 9, 6, + 10, 7, + 11, 8); + + private static void decodeChunk(String input, + int inputOffset, + int inputLength, + byte[] decoded, + int decodedOffset, + int decodedLength) throws CryptoNoteException { + + BigInteger result = BigInteger.ZERO; + + BigInteger order = BigInteger.ONE; + for (int index = inputOffset + inputLength; index != inputOffset; order = order.multiply(ALPHABET_SIZE)) { + char character = input.charAt(--index); + int digit = ALPHABET.indexOf(character); + if (digit == -1) { + throw new CryptoNoteException("invalid character " + character); + } + result = result.add(order.multiply(BigInteger.valueOf(digit))); + if (result.compareTo(UINT64_MAX) > 0) { + throw new CryptoNoteException("64-bit unsigned integer overflow " + result.toString()); + } + } + + BigInteger maxCapacity = BigInteger.ONE.shiftLeft(8 * decodedLength); + if (result.compareTo(maxCapacity) >= 0) { + throw new CryptoNoteException("capacity overflow " + result.toString()); + } + + for (int index = decodedOffset + decodedLength; index != decodedOffset; result = result.shiftRight(8)) { + decoded[--index] = result.byteValue(); + } + } + + public static byte[] decode(String input) throws CryptoNoteException { + if (input.length() == 0) { + return new byte[0]; + } + + int chunks = input.length() / FULL_ENCODED_BLOCK_SIZE; + int lastEncodedSize = input.length() % FULL_ENCODED_BLOCK_SIZE; + int lastChunkSize = lastEncodedSize > 0 ? DECODED_CHUNK_LENGTH.get(lastEncodedSize) : 0; + + byte[] result = new byte[chunks * FULL_DECODED_BLOCK_SIZE + lastChunkSize]; + int inputOffset = 0; + int resultOffset = 0; + for (int chunk = 0; chunk < chunks; ++chunk, + inputOffset += FULL_ENCODED_BLOCK_SIZE, + resultOffset += FULL_DECODED_BLOCK_SIZE) { + decodeChunk(input, inputOffset, FULL_ENCODED_BLOCK_SIZE, result, resultOffset, FULL_DECODED_BLOCK_SIZE); + } + if (lastChunkSize > 0) { + decodeChunk(input, inputOffset, lastEncodedSize, result, resultOffset, lastChunkSize); + } + + return result; + } + + private static long readVarInt(ByteBuffer buffer) { + long result = 0; + for (int shift = 0; ; shift += 7) { + byte current = buffer.get(); + result += (current & 0x7fL) << shift; + if ((current & 0x80L) == 0) { + break; + } + } + return result; + } + + static long decodeAddress(String address, boolean validateChecksum) throws CryptoNoteException { + byte[] decoded = decode(address); + + int checksumSize = 4; + if (decoded.length < checksumSize) { + throw new CryptoNoteException("invalid length"); + } + + ByteBuffer decodedAddress = ByteBuffer.wrap(decoded, 0, decoded.length - checksumSize); + + long prefix = readVarInt(decodedAddress.slice()); + if (!validateChecksum) { + return prefix; + } + + ByteBuffer fastHash = Keccak.keccak1600(decodedAddress.slice()); + int checksum = fastHash.getInt(); + int expected = ByteBuffer.wrap(decoded, decoded.length - checksumSize, checksumSize).getInt(); + if (checksum != expected) { + throw new CryptoNoteException(String.format("invalid checksum %08X, expected %08X", checksum, expected)); + } + + return prefix; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/Erc20Token.java b/assets/src/main/java/bisq/asset/Erc20Token.java new file mode 100644 index 0000000000..33213a6553 --- /dev/null +++ b/assets/src/main/java/bisq/asset/Erc20Token.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.asset; + +/** + * Abstract base class for Ethereum-based {@link Token}s that implement the + * ERC-20 Token + * Standard. + * + * @author Chris Beams + * @since 0.7.0 + */ +public abstract class Erc20Token extends Token { + + public Erc20Token(String name, String tickerSymbol) { + super(name, tickerSymbol, new EtherAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/EtherAddressValidator.java b/assets/src/main/java/bisq/asset/EtherAddressValidator.java new file mode 100644 index 0000000000..4ed2cee492 --- /dev/null +++ b/assets/src/main/java/bisq/asset/EtherAddressValidator.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.asset; + +/** + * Validates an Ethereum address using the regular expression on record in the + * + * ethereum/web3.js project. Note that this implementation is widely used, not just + * for actual {@link bisq.asset.coins.Ether} address validation, but also for + * {@link Erc20Token} implementations and other Ethereum-based {@link Asset} + * implementations. + * + * @author Chris Beams + * @since 0.7.0 + */ +public class EtherAddressValidator extends RegexAddressValidator { + + public EtherAddressValidator() { + super("^(0x)?[0-9a-fA-F]{40}$"); + } + + public EtherAddressValidator(String errorMessageI18nKey) { + super("^(0x)?[0-9a-fA-F]{40}$", errorMessageI18nKey); + } +} diff --git a/assets/src/main/java/bisq/asset/GrinAddressValidator.java b/assets/src/main/java/bisq/asset/GrinAddressValidator.java new file mode 100644 index 0000000000..f397bca0ba --- /dev/null +++ b/assets/src/main/java/bisq/asset/GrinAddressValidator.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.asset; + +/** + * We only support the grinbox format as it is currently the only tool which offers a validation options of sender. + * Beside that is the IP:port format very insecure with MITM attacks. + * + * Here is the information from a conversation with the Grinbox developer regarding the Grinbox address format. + * + A Grinbox address is of the format: grinbox://@domain.com:port where everything besides is optional. + If no domain is specified, the default relay grinbox.io will be used. + + The is a base58check encoded value (like in Bitcoin). For Grin mainnet, the first 2 bytes will be [1, 11] and + the following 33 bytes should be a valid secp256k1 compressed public key. + + Some examples of valid addresses are: + + gVvRNiuopubvxPrs1BzJdQjVdFAxmkLzMqiVJzUZ7ubznhdtNTGB + gVvUcSafSTD3YTSqgNf9ojEYWkz3zMZNfsjdpdb9en5mxc6gmja6 + gVvk7rLBg3r3qoWYL3VsREnBbooT7nynxx5HtDvUWCJUaNCnddvY + grinbox://gVtWzX5NTLCBkyNV19QVdnLXue13heAVRD36sfkGD6xpqy7k7e4a + gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com + grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com:1220 + + Some examples of invalid addresses are: + + gVuBJDKcWkhueMfBLAbFwV4ax55YXPeinWXdRME1Zi3eiC6sFNye (invalid checksum) + geWGCMQjxZMHG3EtTaRbR7rH9rE4DsmLfpm1iiZEa7HFKjjkgpf2 (wrong version bytes) + gVvddC2jYAfxTxnikcbTEQKLjhJZpqpBg39tXkwAKnD2Pys2mWiK (invalid public key) + + We only add the basic validation without checksum, version byte and pubkey validation as that would require much more + effort. Any Grin developer is welcome to add that though! + + */ +public class GrinAddressValidator implements AddressValidator { + // A Grin Wallet URL (address is not the correct term) can be in the form IP:port or a grinbox format. + // The grinbox has the format grinbox://@domain.com:port where everything beside the key is optional. + + + // Regex for IP validation borrowed from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses + private static final String PORT = "((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"; + private static final String DOMAIN = "[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}$"; + private static final String KEY = "[a-km-zA-HJ-NP-Z1-9]{52}$"; + + public GrinAddressValidator() { + } + + @Override + public AddressValidationResult validate(String address) { + if (address == null || address.length() == 0) + return AddressValidationResult.invalidAddress("Address may not be empty (only Grinbox format is supported)"); + + // We only support grinbox address + String key; + String domain = null; + String port = null; + address = address.replace("grinbox://", ""); + if (address.contains("@")) { + String[] keyAndDomain = address.split("@"); + key = keyAndDomain[0]; + if (keyAndDomain.length > 1) { + domain = keyAndDomain[1]; + if (domain.contains(":")) { + String[] domainAndPort = domain.split(":"); + domain = domainAndPort[0]; + if (domainAndPort.length > 1) + port = domainAndPort[1]; + } + } + } else { + key = address; + } + + if (!key.matches("^" + KEY)) + return AddressValidationResult.invalidAddress("Invalid key (only Grinbox format is supported)"); + + if (domain != null && !domain.matches("^" + DOMAIN)) + return AddressValidationResult.invalidAddress("Invalid domain (only Grinbox format is supported)"); + + if (port != null && !port.matches("^" + PORT)) + return AddressValidationResult.invalidAddress("Invalid port (only Grinbox format is supported)"); + + return AddressValidationResult.validAddress(); + + } +} diff --git a/assets/src/main/java/bisq/asset/I18n.java b/assets/src/main/java/bisq/asset/I18n.java new file mode 100644 index 0000000000..86bcdecc44 --- /dev/null +++ b/assets/src/main/java/bisq/asset/I18n.java @@ -0,0 +1,8 @@ +package bisq.asset; + +import java.util.ResourceBundle; + +public class I18n { + + public static ResourceBundle DISPLAY_STRINGS = ResourceBundle.getBundle("i18n.displayStrings-assets"); +} diff --git a/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java new file mode 100644 index 0000000000..9ae5cf41d4 --- /dev/null +++ b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java @@ -0,0 +1,9 @@ +package bisq.asset; + +public class LiquidBitcoinAddressValidator extends RegexAddressValidator { + static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$"; + + public LiquidBitcoinAddressValidator() { + super(REGEX, "validation.altcoin.liquidBitcoin.invalidAddress"); + } +} diff --git a/assets/src/main/java/bisq/asset/NetworkParametersAdapter.java b/assets/src/main/java/bisq/asset/NetworkParametersAdapter.java new file mode 100644 index 0000000000..184b7dbf55 --- /dev/null +++ b/assets/src/main/java/bisq/asset/NetworkParametersAdapter.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.asset; + +import org.bitcoinj.core.BitcoinSerializer; +import org.bitcoinj.core.Block; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.utils.MonetaryFormat; + +/** + * Convenient abstract {@link NetworkParameters} base class providing no-op + * implementations of all methods that are not required for address validation + * purposes. + * + * @author Chris Beams + * @since 0.7.0 + */ +public abstract class NetworkParametersAdapter extends NetworkParameters { + + @Override + public String getPaymentProtocolId() { + return PAYMENT_PROTOCOL_ID_MAINNET; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) + throws VerificationException { + } + + @Override + public Coin getMaxMoney() { + return null; + } + + @Override + public Coin getMinNonDustOutput() { + return null; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return null; + } + + @Override + public String getUriScheme() { + return null; + } + + @Override + public boolean hasMaxMoney() { + return false; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return null; + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 0; + } +} diff --git a/assets/src/main/java/bisq/asset/PrintTool.java b/assets/src/main/java/bisq/asset/PrintTool.java new file mode 100644 index 0000000000..ba3cbf4f84 --- /dev/null +++ b/assets/src/main/java/bisq/asset/PrintTool.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.asset; + +import java.util.Comparator; + +public class PrintTool { + + public static void main(String[] args) { + // Prints out all coins in the format used in the FAQ webpage. + // Run that and copy paste the result to the FAQ webpage at new releases. + StringBuilder sb = new StringBuilder(); + new AssetRegistry().stream() + .sorted(Comparator.comparing(o -> o.getName().toLowerCase())) + .filter(e -> !e.getTickerSymbol().equals("BSQ")) // BSQ is not out yet... + .filter(e -> !e.getTickerSymbol().equals("BTC")) + .map(e -> new Pair(e.getName(), e.getTickerSymbol())) // We want to get rid of duplicated entries for regtest/testnet... + .distinct() + .forEach(e -> sb.append("

  • “") + .append(e.right) + .append("”, “") + .append(e.left) + .append("”
  • ") + .append("\n")); + System.out.println(sb.toString()); + } + + private static class Pair { + final String left; + final String right; + + Pair(String left, String right) { + this.left = left; + this.right = right; + } + } +} diff --git a/assets/src/main/java/bisq/asset/RegexAddressValidator.java b/assets/src/main/java/bisq/asset/RegexAddressValidator.java new file mode 100644 index 0000000000..c262cc6da9 --- /dev/null +++ b/assets/src/main/java/bisq/asset/RegexAddressValidator.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset; + +/** + * Validates an {@link Asset} address against a given regular expression. + * + * @author Chris Beams + * @since 0.7.0 + */ +public class RegexAddressValidator implements AddressValidator { + + private final String regex; + private final String errorMessageI18nKey; + + public RegexAddressValidator(String regex) { + this(regex, null); + } + + public RegexAddressValidator(String regex, String errorMessageI18nKey) { + this.regex = regex; + this.errorMessageI18nKey = errorMessageI18nKey; + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches(regex)) + if (errorMessageI18nKey == null) return AddressValidationResult.invalidStructure(); + else return AddressValidationResult.invalidAddress("", errorMessageI18nKey); + + return AddressValidationResult.validAddress(); + } +} diff --git a/assets/src/main/java/bisq/asset/Token.java b/assets/src/main/java/bisq/asset/Token.java new file mode 100644 index 0000000000..2c162e52d2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/Token.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.asset; + +/** + * Abstract base class for {@link Asset}s that do not have their own dedicated blockchain, + * but are rather based on or derived from another blockchain. Contrast with {@link Coin}. + * Note that this is essentially a "marker" base class in the sense that it (currently) + * exposes no additional information or functionality beyond that found in + * {@link AbstractAsset}, but it is nevertheless useful in distinguishing between major + * different {@code Asset} types. + * + * @author Chris Beams + * @since 0.7.0 + * @see Erc20Token + */ +public abstract class Token extends AbstractAsset { + + public Token(String name, String tickerSymbol, AddressValidator addressValidator) { + super(name, tickerSymbol, addressValidator); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Actinium.java b/assets/src/main/java/bisq/asset/coins/Actinium.java new file mode 100644 index 0000000000..304625b424 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Actinium.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Actinium extends Coin { + + public Actinium() { + super("Actinium", "ACM", new Base58AddressValidator(new ActiniumParams())); + } + + + public static class ActiniumParams extends NetworkParametersAdapter { + + public ActiniumParams() { + addressHeader = 53; + p2shHeader = 55; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Adeptio.java b/assets/src/main/java/bisq/asset/coins/Adeptio.java new file mode 100644 index 0000000000..b23ed19ec5 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Adeptio.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Adeptio extends Coin { + + public Adeptio() { + super("Adeptio", "ADE", new AdeptioAddressValidator()); + } + + + public static class AdeptioAddressValidator extends Base58AddressValidator { + + public AdeptioAddressValidator() { + super(new AdeptioParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[A][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class AdeptioParams extends NetworkParametersAdapter { + + public AdeptioParams() { + addressHeader = 23; + p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Aeon.java b/assets/src/main/java/bisq/asset/coins/Aeon.java new file mode 100644 index 0000000000..1af6aa838d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Aeon.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +public class Aeon extends Coin { + + public Aeon() { + super("Aeon", "AEON", new CryptoNoteAddressValidator(0xB2, 0x06B8)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Amitycoin.java b/assets/src/main/java/bisq/asset/coins/Amitycoin.java new file mode 100644 index 0000000000..8265df5d98 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Amitycoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Amitycoin extends Coin { + + public Amitycoin() { + super("Amitycoin", "AMIT", new RegexAddressValidator("^amit[1-9A-Za-z^OIl]{94}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Animecoin.java b/assets/src/main/java/bisq/asset/coins/Animecoin.java new file mode 100644 index 0000000000..e274e2bf69 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Animecoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Animecoin extends Coin { + public Animecoin() { + super("Animecoin", "ANI", new Base58AddressValidator(new AnimecoinMainNetParams())); + } + + public static class AnimecoinMainNetParams extends NetworkParametersAdapter { + public AnimecoinMainNetParams() { + this.addressHeader = 23; + this.p2shHeader = 9; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Arqma.java b/assets/src/main/java/bisq/asset/coins/Arqma.java new file mode 100644 index 0000000000..472db1c0b6 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Arqma.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.arq.msg") +public class Arqma extends Coin { + + public Arqma() { + super("Arqma", "ARQ", new CryptoNoteAddressValidator(0x2cca, 0x6847)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Askcoin.java b/assets/src/main/java/bisq/asset/coins/Askcoin.java new file mode 100644 index 0000000000..39d4ddf4c4 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Askcoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Askcoin extends Coin { + + public Askcoin() { + super("Askcoin", "ASK", new RegexAddressValidator("^[1-9][0-9]{0,11}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Australiacash.java b/assets/src/main/java/bisq/asset/coins/Australiacash.java new file mode 100644 index 0000000000..86e8a02de8 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Australiacash.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Australiacash extends Coin { + public Australiacash() { + super("Australiacash", "AUS", new Base58AddressValidator(new AustraliacashParams())); + } + public static class AustraliacashParams extends NetworkParametersAdapter { + + public AustraliacashParams() { + addressHeader = 23; + p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/BSQ.java b/assets/src/main/java/bisq/asset/coins/BSQ.java new file mode 100644 index 0000000000..f769a582e3 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/BSQ.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; + +public class BSQ extends Coin { + + public BSQ(Network network, NetworkParameters networkParameters) { + super("BSQ", "BSQ", new BSQAddressValidator(networkParameters), network); + } + + + public static class Mainnet extends BSQ { + + public Mainnet() { + super(Network.MAINNET, MainNetParams.get()); + } + } + + + public static class Testnet extends BSQ { + + public Testnet() { + super(Network.TESTNET, TestNet3Params.get()); + } + } + + + public static class Regtest extends BSQ { + + public Regtest() { + super(Network.REGTEST, RegTestParams.get()); + } + } + + + public static class BSQAddressValidator extends Base58AddressValidator { + + public BSQAddressValidator(NetworkParameters networkParameters) { + super(networkParameters); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.startsWith("B")) + return AddressValidationResult.invalidAddress("BSQ address must start with 'B'"); + + String addressAsBtc = address.substring(1, address.length()); + + return super.validate(addressAsBtc); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Beam.java b/assets/src/main/java/bisq/asset/coins/Beam.java new file mode 100644 index 0000000000..99fa273e02 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Beam.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +/** + * Here is info from a Beam developer regarding validation. + * + * Well, unfortunately the range length is quite large. The BbsChannel is 64 bit = 8 bytes, the pubkey is 32 bytes. + * So, the length may be up to 80 chars. The minimum length "theoretically" can also drop to a small length, if the + * channel==0, and the pubkey starts with many zeroes (very unlikely, but possible). So, besides being up to 80 chars + * lower-case hex there's not much can be tested. A more robust test would also check if the pubkey is indeed valid, + * but it's a more complex test, requiring cryptographic code. + * + */ +@AltCoinAccountDisclaimer("account.altcoin.popup.beam.msg") +public class Beam extends Coin { + public Beam() { + super("Beam", "BEAM", new RegexAddressValidator("^([0-9a-f]{1,80})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/BitDaric.java b/assets/src/main/java/bisq/asset/coins/BitDaric.java new file mode 100644 index 0000000000..7871fbbbe8 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/BitDaric.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class BitDaric extends Coin { + + public BitDaric() { + super("BitDaric", "DARX", new RegexAddressValidator("^[R][a-km-zA-HJ-NP-Z1-9]{25,34}$")); + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/Bitcoin.java b/assets/src/main/java/bisq/asset/coins/Bitcoin.java new file mode 100644 index 0000000000..74b9986966 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Bitcoin.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.BitcoinAddressValidator; +import bisq.asset.Coin; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; + +public abstract class Bitcoin extends Coin { + + public Bitcoin(Network network, NetworkParameters networkParameters) { + super("Bitcoin", "BTC", new BitcoinAddressValidator(networkParameters), network); + } + + + public static class Mainnet extends Bitcoin { + + public Mainnet() { + super(Network.MAINNET, MainNetParams.get()); + } + } + + + public static class Testnet extends Bitcoin { + + public Testnet() { + super(Network.TESTNET, TestNet3Params.get()); + } + } + + + public static class Regtest extends Bitcoin { + + public Regtest() { + super(Network.REGTEST, RegTestParams.get()); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/BitcoinRhodium.java b/assets/src/main/java/bisq/asset/coins/BitcoinRhodium.java new file mode 100644 index 0000000000..46f22bbe94 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/BitcoinRhodium.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class BitcoinRhodium extends Coin { + + public BitcoinRhodium() { + super("Bitcoin Rhodium", "XRC", new Base58AddressValidator(new BitcoinRhodiumParams())); + } + + public static class BitcoinRhodiumParams extends NetworkParametersAdapter { + + public BitcoinRhodiumParams() { + addressHeader = 61; + p2shHeader = 123; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/Bitmark.java b/assets/src/main/java/bisq/asset/coins/Bitmark.java new file mode 100644 index 0000000000..22650b001d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Bitmark.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Bitmark extends Coin { + + public static class BitmarkParams extends NetworkParametersAdapter { + public BitmarkParams() { + addressHeader = 85; + p2shHeader = 5; + } + } + + public Bitmark() { + super("Bitmark", "BTM", new Base58AddressValidator(new BitmarkParams())); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Bitzec.java b/assets/src/main/java/bisq/asset/coins/Bitzec.java new file mode 100644 index 0000000000..df1207315d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Bitzec.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Bitzec extends Coin { + + public Bitzec() { + super("Bitzec", "BZC", new RegexAddressValidator("^t.*", "validation.altcoin.zAddressesNotSupported")); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Blur.java b/assets/src/main/java/bisq/asset/coins/Blur.java new file mode 100644 index 0000000000..99fb0422af --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Blur.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.blur.msg") +public class Blur extends Coin { + + public Blur() { + super("Blur", "BLUR", new CryptoNoteAddressValidator(0x1e4d, 0x2195)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/BurntBlackCoin.java b/assets/src/main/java/bisq/asset/coins/BurntBlackCoin.java new file mode 100644 index 0000000000..c825d9fd82 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/BurntBlackCoin.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.blk-burnt.msg") +public class BurntBlackCoin extends Coin { + public static final short PAYLOAD_LIMIT = 15000; + + public BurntBlackCoin() { + super("Burnt BlackCoin", + "BLK-BURNT", + new RegexAddressValidator(String.format("(?:[0-9a-z]{2}?){1,%d}+", 2 * PAYLOAD_LIMIT))); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/CRowdCLassic.java b/assets/src/main/java/bisq/asset/coins/CRowdCLassic.java new file mode 100644 index 0000000000..db156cabef --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/CRowdCLassic.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class CRowdCLassic extends Coin { + + public CRowdCLassic() { + super("CRowdCLassic", "CRCL", new RegexAddressValidator("^[C][a-zA-Z0-9]{33}$")); + } +} + \ No newline at end of file diff --git a/assets/src/main/java/bisq/asset/coins/CTSCoin.java b/assets/src/main/java/bisq/asset/coins/CTSCoin.java new file mode 100644 index 0000000000..41f8f4328e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/CTSCoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class CTSCoin extends Coin { + public CTSCoin() { + super("CTSCoin", "CTSC", new Base58AddressValidator(new CtscMainNetParams())); + } + + public static class CtscMainNetParams extends NetworkParametersAdapter { + public CtscMainNetParams() { + this.addressHeader = 66; + this.p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Cash2.java b/assets/src/main/java/bisq/asset/coins/Cash2.java new file mode 100644 index 0000000000..3c7ad2718f --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Cash2.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.cash2.msg") +public class Cash2 extends Coin { + + public Cash2() { + super("Cash2", "CASH2", new CryptoNoteAddressValidator(false, 0x6)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Chaucha.java b/assets/src/main/java/bisq/asset/coins/Chaucha.java new file mode 100644 index 0000000000..327eb5b41e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Chaucha.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Chaucha extends Coin { + + public Chaucha() { + super("Chaucha", "CHA", new Base58AddressValidator(new ChauchaParams())); + } + + public static class ChauchaParams extends NetworkParametersAdapter { + + public ChauchaParams() { + addressHeader = 88; + p2shHeader = 50; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/CloakCoin.java b/assets/src/main/java/bisq/asset/coins/CloakCoin.java new file mode 100644 index 0000000000..19b5d7727e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/CloakCoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class CloakCoin extends Coin { + + public CloakCoin() { + super("CloakCoin", "CLOAK", new RegexAddressValidator("^[B|C][a-km-zA-HJ-NP-Z1-9]{33}|^smY[a-km-zA-HJ-NP-Z1-9]{99}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Counterparty.java b/assets/src/main/java/bisq/asset/coins/Counterparty.java new file mode 100644 index 0000000000..daea00e87c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Counterparty.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +public class Counterparty extends Coin { + + public Counterparty() { + super("Counterparty", "XCP", new XcpAddressValidator()); + } + + public static class XcpAddressValidator extends RegexAddressValidator { + + public XcpAddressValidator() { + super("^[1][a-zA-Z0-9]{33}", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.XCP")); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Credits.java b/assets/src/main/java/bisq/asset/coins/Credits.java new file mode 100644 index 0000000000..7b11cf199e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Credits.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Credits extends Coin { + + public Credits() { + super("Credits", "CRDS", new CreditsAddressValidator()); + } + + + public static class CreditsAddressValidator extends Base58AddressValidator { + + public CreditsAddressValidator() { + super(new CreditsParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[C][a-km-zA-HJ-NP-Z1-9]{25,34}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class CreditsParams extends NetworkParametersAdapter { + + public CreditsParams() { + addressHeader = 28; + p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Croat.java b/assets/src/main/java/bisq/asset/coins/Croat.java new file mode 100644 index 0000000000..c4e99877f4 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Croat.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Croat extends Coin { + + public Croat() { + super("Croat", "CROAT", new RegexAddressValidator("^C[1-9A-HJ-NP-Za-km-z]{94}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/DSTRA.java b/assets/src/main/java/bisq/asset/coins/DSTRA.java new file mode 100644 index 0000000000..0516f62782 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/DSTRA.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class DSTRA extends Coin { + + public DSTRA() { + super("DSTRA", "DST", new DSTRAAddressValidator()); + } + + + public static class DSTRAAddressValidator extends Base58AddressValidator { + + public DSTRAAddressValidator() { + super(new DSTRAParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[D][a-km-zA-HJ-NP-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class DSTRAParams extends NetworkParametersAdapter { + + public DSTRAParams() { + addressHeader = 30; + p2shHeader = 33; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/DarkPay.java b/assets/src/main/java/bisq/asset/coins/DarkPay.java new file mode 100644 index 0000000000..449bd46935 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/DarkPay.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class DarkPay extends Coin { + public DarkPay() { + super("DarkPay", "D4RK", new Base58AddressValidator(new DarkPayMainNetParams())); + } + + public static class DarkPayMainNetParams extends NetworkParametersAdapter { + public DarkPayMainNetParams() { + this.addressHeader = 31; + this.p2shHeader = 60; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Dash.java b/assets/src/main/java/bisq/asset/coins/Dash.java new file mode 100644 index 0000000000..2cde18acd7 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Dash.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Dash extends Coin { + public Dash() { + super("Dash", "DASH", new Base58AddressValidator(new DashMainNetParams()), Network.MAINNET); + } + + public static class DashMainNetParams extends NetworkParametersAdapter { + public DashMainNetParams() { + this.addressHeader = 76; + this.p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Decred.java b/assets/src/main/java/bisq/asset/coins/Decred.java new file mode 100644 index 0000000000..551937f298 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Decred.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +public class Decred extends Coin { + + public Decred() { + super("Decred", "DCR", new DcrAddressValidator()); + } + + public static class DcrAddressValidator extends RegexAddressValidator { + + public DcrAddressValidator() { + super("^[Dk|Ds|De|DS|Dc|Pm][a-zA-Z0-9]{24,34}", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.DCR")); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/DeepOnion.java b/assets/src/main/java/bisq/asset/coins/DeepOnion.java new file mode 100644 index 0000000000..dc452b343b --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/DeepOnion.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class DeepOnion extends Coin { + public DeepOnion() { + super("DeepOnion", "ONION", new DeepOnionAddressValidator()); + } + + public static class DeepOnionAddressValidator extends Base58AddressValidator { + + public DeepOnionAddressValidator() { + super(new DeepOnionParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[D][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + public static class DeepOnionParams extends NetworkParametersAdapter { + + public DeepOnionParams() { + super(); + addressHeader = 31; + p2shHeader = 78; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Dextro.java b/assets/src/main/java/bisq/asset/coins/Dextro.java new file mode 100644 index 0000000000..202db914a0 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Dextro.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Dextro extends Coin { + + public Dextro() { + super("Dextro", "DXO", new Base58AddressValidator(new DextroParams())); + } + + + public static class DextroAddressValidator extends Base58AddressValidator { + + public DextroAddressValidator() { + super(new DextroParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[D][a-km-zA-HJ-NP-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class DextroParams extends NetworkParametersAdapter { + + public DextroParams() { + super(); + addressHeader = 30; + p2shHeader = 90; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Dogecoin.java b/assets/src/main/java/bisq/asset/coins/Dogecoin.java new file mode 100644 index 0000000000..7318b7e04a --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Dogecoin.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Dogecoin extends Coin { + + public Dogecoin() { + super("Dogecoin", "DOGE", new Base58AddressValidator(new DogecoinMainNetParams()), Network.MAINNET); + } + + public static class DogecoinMainNetParams extends NetworkParametersAdapter { + public DogecoinMainNetParams() { + this.addressHeader = 30; + this.p2shHeader = 22; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Doichain.java b/assets/src/main/java/bisq/asset/coins/Doichain.java new file mode 100644 index 0000000000..46c254548f --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Doichain.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Doichain extends Coin { + + public Doichain() { + super("Doichain", "DOI", new Base58AddressValidator(new DoichainParams())); + } + + public static class DoichainParams extends NetworkParametersAdapter { + public DoichainParams() { + addressHeader = 52; + p2shHeader = 13; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Donu.java b/assets/src/main/java/bisq/asset/coins/Donu.java new file mode 100644 index 0000000000..f1e4bc2804 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Donu.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Donu extends Coin { + + public Donu() { + super("Donu", "DONU", new DonuAddressValidator()); + } + + + public static class DonuAddressValidator extends Base58AddressValidator { + + public DonuAddressValidator() { + super(new DonuParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[N][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class DonuParams extends NetworkParametersAdapter { + + public DonuParams() { + addressHeader = 53; + p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Dragonglass.java b/assets/src/main/java/bisq/asset/coins/Dragonglass.java new file mode 100644 index 0000000000..68bd41df2c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Dragonglass.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.drgl.msg") +public class Dragonglass extends Coin { + + public Dragonglass() { + super("Dragonglass", "DRGL", new RegexAddressValidator("^(dRGL)[1-9A-HJ-NP-Za-km-z]{94}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Emercoin.java b/assets/src/main/java/bisq/asset/coins/Emercoin.java new file mode 100644 index 0000000000..6a52f3b4a5 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Emercoin.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Emercoin extends Coin { + + public Emercoin() { + super("Emercoin", "EMC", new Base58AddressValidator(new EmercoinMainNetParams()), Network.MAINNET); + } + + public static class EmercoinMainNetParams extends NetworkParametersAdapter { + public EmercoinMainNetParams() { + this.addressHeader = 33; + this.p2shHeader = 92; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Ergo.java b/assets/src/main/java/bisq/asset/coins/Ergo.java new file mode 100644 index 0000000000..fd9ae801cd --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Ergo.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.AddressValidator; +import bisq.asset.Coin; + +import java.util.Arrays; + +import org.bitcoinj.core.Base58; +import org.bitcoinj.core.AddressFormatException; + +public class Ergo extends Coin { + + public Ergo() { + super("Ergo", "ERG", new ErgoAddressValidator()); + } + + public static class ErgoAddressValidator implements AddressValidator { + + @Override + public AddressValidationResult validate(String address) { + try { + byte[] decoded = Base58.decode(address); + if (decoded.length < 4) { + return AddressValidationResult.invalidAddress("Input too short: " + decoded.length); + } + if (decoded[0] != 1 && decoded[0] != 2 && decoded[0] != 3) { + return AddressValidationResult.invalidAddress("Invalid prefix"); + } + } catch (AddressFormatException e) { + return AddressValidationResult.invalidAddress(e); + } + return AddressValidationResult.validAddress(); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Ether.java b/assets/src/main/java/bisq/asset/coins/Ether.java new file mode 100644 index 0000000000..bb534c3421 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Ether.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.EtherAddressValidator; + +public class Ether extends Coin { + + public Ether() { + super("Ether", "ETH", new EtherAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/EtherClassic.java b/assets/src/main/java/bisq/asset/coins/EtherClassic.java new file mode 100644 index 0000000000..7b13a91559 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/EtherClassic.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.EtherAddressValidator; +import bisq.asset.I18n; + +public class EtherClassic extends Coin { + + public EtherClassic() { + super("Ether Classic", "ETC", new EtherAddressValidator(I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.ETC"))); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Faircoin.java b/assets/src/main/java/bisq/asset/coins/Faircoin.java new file mode 100644 index 0000000000..ffec962a18 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Faircoin.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Faircoin extends Coin { + + public Faircoin() { + super("Faircoin", "FAIR", new Base58AddressValidator(new Faircoin.FaircoinParams())); + } + + public static class FaircoinParams extends NetworkParametersAdapter { + + public FaircoinParams() { + addressHeader = 95; + p2shHeader = 36; + } + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/FourtyTwo.java b/assets/src/main/java/bisq/asset/coins/FourtyTwo.java new file mode 100644 index 0000000000..4e3c16d6a0 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/FourtyTwo.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +public class FourtyTwo extends Coin { + + public FourtyTwo() { + super("FourtyTwo", "FRTY", new CryptoNoteAddressValidator(0x1cbd67, 0x13271817)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Fujicoin.java b/assets/src/main/java/bisq/asset/coins/Fujicoin.java new file mode 100644 index 0000000000..63a415f6a8 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Fujicoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Fujicoin extends Coin { + public Fujicoin() { + super("Fujicoin", "FJC", new Base58AddressValidator(new FujicoinParams())); + } + public static class FujicoinParams extends NetworkParametersAdapter { + + public FujicoinParams() { + addressHeader = 36; + p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Galilel.java b/assets/src/main/java/bisq/asset/coins/Galilel.java new file mode 100644 index 0000000000..7834b08c81 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Galilel.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Galilel extends Coin { + public Galilel() { + super("Galilel", "GALI", new Base58AddressValidator(new GalilelMainNetParams())); + } + + public static class GalilelMainNetParams extends NetworkParametersAdapter { + public GalilelMainNetParams() { + this.addressHeader = 68; + this.p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/GambleCoin.java b/assets/src/main/java/bisq/asset/coins/GambleCoin.java new file mode 100644 index 0000000000..2b1f16da06 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/GambleCoin.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class GambleCoin extends Coin { + + public GambleCoin() { + super("GambleCoin", "GMCN", new GambleCoinAddressValidator()); + } + + + public static class GambleCoinAddressValidator extends Base58AddressValidator { + + public GambleCoinAddressValidator() { + super(new GambleCoinParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[C][a-km-zA-HJ-NP-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class GambleCoinParams extends NetworkParametersAdapter { + + public GambleCoinParams() { + super(); + addressHeader = 28; + p2shHeader = 18; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Genesis.java b/assets/src/main/java/bisq/asset/coins/Genesis.java new file mode 100644 index 0000000000..821c5b677a --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Genesis.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.*; + +public class Genesis extends Coin { + + public Genesis() { + super("Genesis", "GENX", new GenesisAddressValidator()); + } + + public static class GenesisAddressValidator extends Base58AddressValidator { + + public GenesisAddressValidator() { + super(new GenesisParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (address.startsWith("S")) { + return super.validate(address); + }else if (address.startsWith("genx")){ + return AddressValidationResult.invalidAddress("Bech32 GENX addresses are not supported on bisq"); + }else if (address.startsWith("C")){ + return AddressValidationResult.invalidAddress("Legacy GENX addresses are not supported on bisq"); + } + return AddressValidationResult.invalidStructure(); + } + } + + public static class GenesisParams extends NetworkParametersAdapter { + + public GenesisParams() { + addressHeader = 28; + p2shHeader = 63; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/Grin.java b/assets/src/main/java/bisq/asset/coins/Grin.java new file mode 100644 index 0000000000..7812aacc29 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Grin.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.GrinAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.grin.msg") +public class Grin extends Coin { + + public Grin() { + super("Grin", "GRIN", new GrinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Hatch.java b/assets/src/main/java/bisq/asset/coins/Hatch.java new file mode 100644 index 0000000000..8136878908 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Hatch.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Hatch extends Coin { + public Hatch() { + super("Hatch", "HATCH", new Base58AddressValidator(new HatchMainNetParams()), Network.MAINNET); + } + + public static class HatchMainNetParams extends NetworkParametersAdapter { + public HatchMainNetParams() { + this.addressHeader = 76; + this.p2shHeader = 16; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Helium.java b/assets/src/main/java/bisq/asset/coins/Helium.java new file mode 100644 index 0000000000..84a64d3a95 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Helium.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Helium extends Coin { + + public Helium() { + super("Helium", "HLM", new Base58AddressValidator(new HeliumParams())); + } + + public static class HeliumParams extends NetworkParametersAdapter { + + public HeliumParams() { + addressHeader = 63; + p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Horizen.java b/assets/src/main/java/bisq/asset/coins/Horizen.java new file mode 100644 index 0000000000..24e9fe24aa --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Horizen.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.AddressValidator; +import bisq.asset.Coin; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Base58; + +public class Horizen extends Coin { + + public Horizen() { + super("Horizen", "ZEN", new HorizenAddressValidator()); + } + + + public static class HorizenAddressValidator implements AddressValidator { + + @Override + public AddressValidationResult validate(String address) { + byte[] byteAddress; + try { + // Get the non Base58 form of the address and the bytecode of the first two bytes + byteAddress = Base58.decodeChecked(address); + } catch (AddressFormatException e) { + // Unhandled Exception (probably a checksum error) + return AddressValidationResult.invalidAddress(e); + } + int version0 = byteAddress[0] & 0xFF; + int version1 = byteAddress[1] & 0xFF; + + // We only support public ("zn" (0x20,0x89), "t1" (0x1C,0xB8)) + // and multisig ("zs" (0x20,0x96), "t3" (0x1C,0xBD)) addresses + + // Fail for private addresses + if (version0 == 0x16 && version1 == 0x9A) + // Address starts with "zc" + return AddressValidationResult.invalidAddress("", "validation.altcoin.zAddressesNotSupported"); + + if (version0 == 0x1C && (version1 == 0xB8 || version1 == 0xBD)) + // "t1" or "t3" address + return AddressValidationResult.validAddress(); + + if (version0 == 0x20 && (version1 == 0x89 || version1 == 0x96)) + // "zn" or "zs" address + return AddressValidationResult.validAddress(); + + // Unknown Type + return AddressValidationResult.invalidStructure(); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/IdaPay.java b/assets/src/main/java/bisq/asset/coins/IdaPay.java new file mode 100644 index 0000000000..e62094cd2b --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/IdaPay.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class IdaPay extends Coin { + + public IdaPay() { + super("IdaPay", "IDA", new IdaPayAddressValidator()); + } + + + public static class IdaPayAddressValidator extends Base58AddressValidator { + + public IdaPayAddressValidator() { + super(new IdaPayParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[CD][a-km-zA-HJ-NP-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class IdaPayParams extends NetworkParametersAdapter { + + public IdaPayParams() { + super(); + addressHeader = 29; + p2shHeader = 36; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Iridium.java b/assets/src/main/java/bisq/asset/coins/Iridium.java new file mode 100644 index 0000000000..f1263e81d5 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Iridium.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.asset.coins; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Iridium extends Coin { + public Iridium() { + super("Iridium", "IRD", new RegexAddressValidator("^ir[1-9A-Za-z^OIl]{95}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Kekcoin.java b/assets/src/main/java/bisq/asset/coins/Kekcoin.java new file mode 100644 index 0000000000..29df724275 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Kekcoin.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Kekcoin extends Coin { + + public Kekcoin() { + super("Kekcoin", "KEK", new Base58AddressValidator(new KekcoinParams())); + } + + + public static class KekcoinParams extends NetworkParametersAdapter { + + public KekcoinParams() { + super(); + addressHeader = 45; + p2shHeader = 88; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/KnowYourDeveloper.java b/assets/src/main/java/bisq/asset/coins/KnowYourDeveloper.java new file mode 100644 index 0000000000..687af6c11b --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/KnowYourDeveloper.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class KnowYourDeveloper extends Coin { + public KnowYourDeveloper() { + super("Know Your Developer", "KYDC", new Base58AddressValidator(new KydMainNetParams())); + } + + public static class KydMainNetParams extends NetworkParametersAdapter { + public KydMainNetParams() { + this.addressHeader = 78; + this.p2shHeader = 85; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Kore.java b/assets/src/main/java/bisq/asset/coins/Kore.java new file mode 100644 index 0000000000..560671a57f --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Kore.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Kore extends Coin { + public Kore() { + super("Kore", "KORE", new Base58AddressValidator(new KoreMainNetParams())); + } + + public static class KoreMainNetParams extends NetworkParametersAdapter { + public KoreMainNetParams() { + this.addressHeader = 45; + this.p2shHeader = 85; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Krypton.java b/assets/src/main/java/bisq/asset/coins/Krypton.java new file mode 100644 index 0000000000..1e8e077037 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Krypton.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Krypton extends Coin { + + public Krypton() { + super("Krypton", "ZOD", new RegexAddressValidator("^QQQ[1-9A-Za-z^OIl]{95}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/LBRYCredits.java b/assets/src/main/java/bisq/asset/coins/LBRYCredits.java new file mode 100644 index 0000000000..d445b69a42 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/LBRYCredits.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class LBRYCredits extends Coin { + + public LBRYCredits() { + super("LBRY Credits", "LBC", new Base58AddressValidator(new LBRYCreditsMainNetParams()), Network.MAINNET); + } + + public static class LBRYCreditsMainNetParams extends NetworkParametersAdapter { + public LBRYCreditsMainNetParams() { + this.addressHeader = 0x55; + this.p2shHeader = 0x7a; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java new file mode 100644 index 0000000000..a84659db90 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.LiquidBitcoinAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.liquidbitcoin.msg") +public class LiquidBitcoin extends Coin { + + public LiquidBitcoin() { + super("Liquid Bitcoin", "L-BTC", new LiquidBitcoinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Litecoin.java b/assets/src/main/java/bisq/asset/coins/Litecoin.java new file mode 100644 index 0000000000..34e7ef8671 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Litecoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Litecoin extends Coin { + public Litecoin() { + super("Litecoin", "LTC", new Base58AddressValidator(new LitecoinMainNetParams()), Network.MAINNET); + } + + public static class LitecoinMainNetParams extends NetworkParametersAdapter { + public LitecoinMainNetParams() { + this.addressHeader = 48; + this.p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/LitecoinPlus.java b/assets/src/main/java/bisq/asset/coins/LitecoinPlus.java new file mode 100644 index 0000000000..91aef5ccc2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/LitecoinPlus.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class LitecoinPlus extends Coin { + + public LitecoinPlus() { + super("LitecoinPlus", "LCP", new Base58AddressValidator(new LitecoinPlusMainNetParams())); + } + + public static class LitecoinPlusMainNetParams extends NetworkParametersAdapter { + public LitecoinPlusMainNetParams() { + this.addressHeader = 75; + this.p2shHeader = 8; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/LitecoinZ.java b/assets/src/main/java/bisq/asset/coins/LitecoinZ.java new file mode 100644 index 0000000000..2d6bc0a28e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/LitecoinZ.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class LitecoinZ extends Coin { + + public LitecoinZ() { + super("LitecoinZ", "LTZ", new RegexAddressValidator("^L.*", "validation.altcoin.ltz.zAddressesNotSupported")); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Lytix.java b/assets/src/main/java/bisq/asset/coins/Lytix.java new file mode 100644 index 0000000000..28eccfaac1 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Lytix.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Lytix extends Coin { + + public Lytix() { + super("Lytix", "LYTX", new Base58AddressValidator(new LytixParams())); + } + + public static class LytixParams extends NetworkParametersAdapter { + + public LytixParams() { + addressHeader = 19; + p2shHeader = 11; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Masari.java b/assets/src/main/java/bisq/asset/coins/Masari.java new file mode 100644 index 0000000000..0d05761027 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Masari.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.msr.msg") +public class Masari extends Coin { + + public Masari() { + super("Masari", "MSR", new CryptoNoteAddressValidator(28, 52)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Mask.java b/assets/src/main/java/bisq/asset/coins/Mask.java new file mode 100644 index 0000000000..1ab50ba4b3 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Mask.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +public class Mask extends Coin { + public Mask() { + super("Mask", "MASK", new CryptoNoteAddressValidator(123, 206)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Mile.java b/assets/src/main/java/bisq/asset/coins/Mile.java new file mode 100644 index 0000000000..639016749d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Mile.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.AddressValidator; +import bisq.asset.Coin; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Base58; + +import java.util.Arrays; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +public class Mile extends Coin { + + public Mile() { + super("Mile", "MILE", new MileAddressValidator()); + } + + + /** + * Mile address - base58(32 bytes of public key + 4 bytes of crc32) + */ + public static class MileAddressValidator implements AddressValidator { + public MileAddressValidator() { + } + + @Override + public AddressValidationResult validate(String address) { + byte[] decoded; + + try { + decoded = Base58.decode(address); + } catch (AddressFormatException e) { + return AddressValidationResult.invalidAddress(e.getMessage()); + } + if (decoded.length != 32 + 4) + return AddressValidationResult.invalidAddress("Invalid address"); + + byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4); + byte[] addrChecksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length); + + Checksum checksum = new CRC32(); + checksum.update(data, 0, data.length); + long checksumValue = checksum.getValue(); + + if ((byte)(checksumValue & 0xff) != addrChecksum[0] || + (byte)((checksumValue >> 8) & 0xff) != addrChecksum[1] || + (byte)((checksumValue >> 16) & 0xff) != addrChecksum[2] || + (byte)((checksumValue >> 24) & 0xff) != addrChecksum[3]) + { + return AddressValidationResult.invalidAddress("Invalid address checksum"); + } + + return AddressValidationResult.validAddress(); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/MirQuiX.java b/assets/src/main/java/bisq/asset/coins/MirQuiX.java new file mode 100644 index 0000000000..142daa46ca --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/MirQuiX.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class MirQuiX extends Coin { + + public MirQuiX() { + super("MirQuiX", "MQX", new RegexAddressValidator("^[M][a-km-zA-HJ-NP-Z1-9]{33}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/MoX.java b/assets/src/main/java/bisq/asset/coins/MoX.java new file mode 100644 index 0000000000..d67dbefa71 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/MoX.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class MoX extends Coin { + + public MoX() { + super("MoX", "MOX", new RegexAddressValidator("^X[1-9A-Za-z^OIl]{96}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/MobitGlobal.java b/assets/src/main/java/bisq/asset/coins/MobitGlobal.java new file mode 100644 index 0000000000..310126bb45 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/MobitGlobal.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class MobitGlobal extends Coin { + + public MobitGlobal() { + super("MobitGlobal", "MBGL", new MobitGlobalAddressValidator()); + } + + + public static class MobitGlobalAddressValidator extends Base58AddressValidator { + + public MobitGlobalAddressValidator() { + super(new MobitGlobalParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[M][a-zA-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class MobitGlobalParams extends NetworkParametersAdapter { + + public MobitGlobalParams() { + addressHeader = 50; + p2shHeader = 110; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Monero.java b/assets/src/main/java/bisq/asset/coins/Monero.java new file mode 100644 index 0000000000..91f22d4cb6 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Monero.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.xmr.msg") +public class Monero extends Coin { + + public Monero() { + super("Monero", "XMR", new CryptoNoteAddressValidator(18, 19, 42)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/MonetaryUnit.java b/assets/src/main/java/bisq/asset/coins/MonetaryUnit.java new file mode 100644 index 0000000000..782faf646d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/MonetaryUnit.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class MonetaryUnit extends Coin { + + public MonetaryUnit() { + super("MonetaryUnit", "MUE", new MonetaryUnitAddressValidator()); + } + + + public static class MonetaryUnitAddressValidator extends Base58AddressValidator { + + public MonetaryUnitAddressValidator() { + super(new MonetaryUnitParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[7][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class MonetaryUnitParams extends NetworkParametersAdapter { + + public MonetaryUnitParams() { + addressHeader = 16; + p2shHeader = 76; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Myce.java b/assets/src/main/java/bisq/asset/coins/Myce.java new file mode 100644 index 0000000000..c0fbdd6e56 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Myce.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Myce extends Coin { + + public Myce() { + super("Myce", "YCE", new Base58AddressValidator(new MyceParams())); + } + + public static class MyceParams extends NetworkParametersAdapter { + public MyceParams() { + addressHeader = 50; + p2shHeader = 85; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Namecoin.java b/assets/src/main/java/bisq/asset/coins/Namecoin.java new file mode 100644 index 0000000000..02bec802ba --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Namecoin.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +public class Namecoin extends Coin { + + public Namecoin() { + super("Namecoin", "NMC", new NmcAddressValidator()); + } + + public static class NmcAddressValidator extends RegexAddressValidator { + + public NmcAddressValidator() { + super("^[NM][a-zA-Z0-9]{33}$", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.NMC")); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Navcoin.java b/assets/src/main/java/bisq/asset/coins/Navcoin.java new file mode 100644 index 0000000000..a1d35f70aa --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Navcoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Navcoin extends Coin { + public Navcoin() { + super("Navcoin", "NAV", new Base58AddressValidator(new NavcoinParams())); + } + + public static class NavcoinParams extends NetworkParametersAdapter { + public NavcoinParams() { + this.addressHeader = 53; + this.p2shHeader = 85; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Ndau.java b/assets/src/main/java/bisq/asset/coins/Ndau.java new file mode 100644 index 0000000000..2cb6e8ac30 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Ndau.java @@ -0,0 +1,37 @@ +/* + * 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 . + */ + +/* + * Copyright © 2019 Oneiro NA, Inc. + */ + +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + + +public class Ndau extends Coin { + + public Ndau() { + // note: ndau addresses contain an internal checksum which was deemed too complicated to include here. + // this regex performs superficial validation, but there is a large space of addresses marked valid + // by this regex which are not in fact valid ndau addresses. For actual ndau address validation, + // use the Address class in github.com/oneiro-ndev/ndauj (java) or github.com/oneiro-ndev/ndaumath/pkg/address (go). + super("ndau", "XND", new RegexAddressValidator("nd[anexbm][abcdefghijkmnpqrstuvwxyz23456789]{45}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Noir.java b/assets/src/main/java/bisq/asset/coins/Noir.java new file mode 100644 index 0000000000..9a84a0526f --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Noir.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Noir extends Coin { + public Noir() { + super("Noir", "NOR", new RegexAddressValidator("^[Z][_A-z0-9]*([_A-z0-9])*$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/NoteBlockchain.java b/assets/src/main/java/bisq/asset/coins/NoteBlockchain.java new file mode 100644 index 0000000000..ceb5ed8d6d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/NoteBlockchain.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class NoteBlockchain extends Coin { + public NoteBlockchain() { + super("NoteBlockchain", "NTBC", new RegexAddressValidator("^[N][a-km-zA-HJ-NP-Z1-9]{26,33}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/PENG.java b/assets/src/main/java/bisq/asset/coins/PENG.java new file mode 100644 index 0000000000..2445038a10 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/PENG.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class PENG extends Coin { + + public PENG() { + super("PENG Coin", "PENG", new RegexAddressValidator("^[P][a-km-zA-HJ-NP-Z1-9]{33}$")); + } +} \ No newline at end of file diff --git a/assets/src/main/java/bisq/asset/coins/PIVX.java b/assets/src/main/java/bisq/asset/coins/PIVX.java new file mode 100644 index 0000000000..d8251e6c9e --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/PIVX.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class PIVX extends Coin { + + public PIVX() { + super("PIVX", "PIVX", new PIVXAddressValidator()); + } + + + public static class PIVXAddressValidator extends Base58AddressValidator { + + public PIVXAddressValidator() { + super(new PIVXParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[D][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class PIVXParams extends NetworkParametersAdapter { + + public PIVXParams() { + addressHeader = 30; + p2shHeader = 13; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/PZDC.java b/assets/src/main/java/bisq/asset/coins/PZDC.java new file mode 100644 index 0000000000..94453e3f7d --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/PZDC.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class PZDC extends Coin { + + public PZDC() { + super("PZDC", "PZDC", new PZDCAddressValidator()); + } + + + public static class PZDCAddressValidator extends Base58AddressValidator { + + public PZDCAddressValidator() { + super(new PZDCParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[P][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class PZDCParams extends NetworkParametersAdapter { + + public PZDCParams() { + addressHeader = 55; + p2shHeader = 13; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/ParsiCoin.java b/assets/src/main/java/bisq/asset/coins/ParsiCoin.java new file mode 100644 index 0000000000..cceee9f200 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/ParsiCoin.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.pars.msg") +public class ParsiCoin extends Coin { + + public ParsiCoin() { + super("ParsiCoin", "PARS", new CryptoNoteAddressValidator(false, 0x90004)); + } +} \ No newline at end of file diff --git a/assets/src/main/java/bisq/asset/coins/Particl.java b/assets/src/main/java/bisq/asset/coins/Particl.java new file mode 100644 index 0000000000..96f12b5e52 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Particl.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; +import bisq.asset.AddressValidationResult; + + +public class Particl extends Coin { + public Particl() { + super("Particl", "PART", new ParticlMainNetAddressValidator()); + } + + public static class ParticlMainNetParams extends NetworkParametersAdapter { + public ParticlMainNetParams() { + this.addressHeader = 56; + this.p2shHeader = 60; + } + } + public static class ParticlMainNetAddressValidator extends Base58AddressValidator { + + public ParticlMainNetAddressValidator() { + super(new ParticlMainNetParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[RP][a-km-zA-HJ-NP-Z1-9]{25,34}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Persona.java b/assets/src/main/java/bisq/asset/coins/Persona.java new file mode 100644 index 0000000000..1d449a0b60 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Persona.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Persona extends Coin { + + public Persona() { + super("Persona", "PRSN", new RegexAddressValidator("^[P][a-km-zA-HJ-NP-Z1-9]{33}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Pinkcoin.java b/assets/src/main/java/bisq/asset/coins/Pinkcoin.java new file mode 100644 index 0000000000..63544fd2de --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Pinkcoin.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Pinkcoin extends Coin { + + public Pinkcoin() { + super("Pinkcoin", "PINK", new Base58AddressValidator(new PinkcoinParams())); + } + + + public static class PinkcoinParams extends NetworkParametersAdapter { + + public PinkcoinParams() { + addressHeader = 3; + p2shHeader = 28; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Plenteum.java b/assets/src/main/java/bisq/asset/coins/Plenteum.java new file mode 100644 index 0000000000..4e99108383 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Plenteum.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Plenteum extends Coin { + + public Plenteum() { + super("Plenteum", "PLE", new RegexAddressValidator("^PLe[1-9A-Za-z^OIl]{95}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/QMCoin.java b/assets/src/main/java/bisq/asset/coins/QMCoin.java new file mode 100644 index 0000000000..b162a615cf --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/QMCoin.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class QMCoin extends Coin { + + public QMCoin() { + super("QMCoin", "QMCoin", new QMCoinAddressValidator()); + } + + + public static class QMCoinAddressValidator extends Base58AddressValidator { + + public QMCoinAddressValidator() { + super(new QMCoinParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[Q][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class QMCoinParams extends NetworkParametersAdapter { + + public QMCoinParams() { + addressHeader = 58; + p2shHeader = 120; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Qbase.java b/assets/src/main/java/bisq/asset/coins/Qbase.java new file mode 100644 index 0000000000..1cc57e540c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Qbase.java @@ -0,0 +1,37 @@ +package bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Qbase extends Coin { + public Qbase() { + super("Qbase", "QBS", new Qbase.QbaseAddressValidator()); + } + + + public static class QbaseAddressValidator extends Base58AddressValidator { + + public QbaseAddressValidator() { + super(new Qbase.QbaseParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[B][a-km-zA-HJ-NP-Z1-9]{25,34}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class QbaseParams extends NetworkParametersAdapter { + + public QbaseParams() { + addressHeader = 25; + p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Qwertycoin.java b/assets/src/main/java/bisq/asset/coins/Qwertycoin.java new file mode 100644 index 0000000000..58d0849f29 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Qwertycoin.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.qwertycoin.msg") +public class Qwertycoin extends Coin { + + public Qwertycoin() { + super("Qwertycoin", "QWC", new CryptoNoteAddressValidator(false, 0x14820c)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Radium.java b/assets/src/main/java/bisq/asset/coins/Radium.java new file mode 100644 index 0000000000..9fa42ac775 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Radium.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Radium extends Coin { + + public Radium() { + super("Radium", "RADS", new Base58AddressValidator(new RadiumParams())); + } + + + public static class RadiumParams extends NetworkParametersAdapter { + + public RadiumParams() { + super(); + addressHeader = 76; + p2shHeader = 58; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Remix.java b/assets/src/main/java/bisq/asset/coins/Remix.java new file mode 100644 index 0000000000..6ed1b334c2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Remix.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Remix extends Coin { + + public Remix() { + super("Remix", "RMX", new RegexAddressValidator("^((REMXi|SubRM)[1-9A-HJ-NP-Za-km-z]{94})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Ryo.java b/assets/src/main/java/bisq/asset/coins/Ryo.java new file mode 100644 index 0000000000..2d719cf251 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Ryo.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Ryo extends Coin { + + public Ryo() { + super("Ryo", "RYO", new RegexAddressValidator("^((RYoL|RYoS)[1-9A-HJ-NP-Za-km-z]{95}|(RYoK)[1-9A-HJ-NP-Za-km-z]{51})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/SUB1X.java b/assets/src/main/java/bisq/asset/coins/SUB1X.java new file mode 100644 index 0000000000..70052cbde2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/SUB1X.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class SUB1X extends Coin { + + public SUB1X() { + super("SUB1X", "SUB1X", new SUB1XAddressValidator()); + } + + + public static class SUB1XAddressValidator extends Base58AddressValidator { + + public SUB1XAddressValidator() { + super(new SUB1XParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[Z][a-km-zA-HJ-NP-Z1-9]{24,33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class SUB1XParams extends NetworkParametersAdapter { + + public SUB1XParams() { + addressHeader = 80; + p2shHeader = 13; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/SiaPrimeCoin.java b/assets/src/main/java/bisq/asset/coins/SiaPrimeCoin.java new file mode 100644 index 0000000000..02296f5bb7 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/SiaPrimeCoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class SiaPrimeCoin extends Coin { + + public SiaPrimeCoin() { + super("SiaPrimeCoin", "SCP", new RegexAddressValidator("^([0-9a-z]{76})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Siafund.java b/assets/src/main/java/bisq/asset/coins/Siafund.java new file mode 100644 index 0000000000..12b9008dcf --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Siafund.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +public class Siafund extends Coin { + + public Siafund() { + super("Siafund", "SF", new SfAddressValidator()); + } + + public static class SfAddressValidator extends RegexAddressValidator { + + public SfAddressValidator() { + super("^[0-9a-fA-F]{76}$", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.XCP")); + } + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/SixEleven.java b/assets/src/main/java/bisq/asset/coins/SixEleven.java new file mode 100644 index 0000000000..7e8d71df0c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/SixEleven.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class SixEleven extends Coin { + + public SixEleven() { + super("SixEleven", "SIL", new SixElevenAddressValidator()); + } + + public static class SixElevenAddressValidator extends Base58AddressValidator { + + public SixElevenAddressValidator() { + super(new SixEleven.SixElevenChainParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[MN][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + public static class SixElevenChainParams extends NetworkParametersAdapter { + public SixElevenChainParams() { + addressHeader = 52; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Solo.java b/assets/src/main/java/bisq/asset/coins/Solo.java new file mode 100644 index 0000000000..02d2beb948 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Solo.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.CryptoNoteAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.solo.msg") +public class Solo extends Coin { + + public Solo() { + super("Solo", "XSL", new CryptoNoteAddressValidator(13975, 23578)); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/SpaceCash.java b/assets/src/main/java/bisq/asset/coins/SpaceCash.java new file mode 100644 index 0000000000..46e8064477 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/SpaceCash.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class SpaceCash extends Coin { + + public SpaceCash() { + super("SpaceCash", "SPACE", new RegexAddressValidator("^([0-9a-z]{76})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Spectrecoin.java b/assets/src/main/java/bisq/asset/coins/Spectrecoin.java new file mode 100644 index 0000000000..1890b69cdd --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Spectrecoin.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Spectrecoin extends Coin { + + public Spectrecoin() { + super("Spectrecoin", "XSPEC", new Base58AddressValidator(new SpectrecoinParams())); + } + + + public static class SpectrecoinParams extends NetworkParametersAdapter { + + public SpectrecoinParams() { + addressHeader = 63; + p2shHeader = 136; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Starwels.java b/assets/src/main/java/bisq/asset/coins/Starwels.java new file mode 100644 index 0000000000..4b86038624 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Starwels.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; + +public class Starwels extends Coin { + + public Starwels() { + super("Starwels", "USDH", new Base58AddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/TEO.java b/assets/src/main/java/bisq/asset/coins/TEO.java new file mode 100644 index 0000000000..e36581e2da --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TEO.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class TEO extends Coin { + + public TEO() { + super("Trust Ether reOrigin", "TEO", new RegexAddressValidator("^0x[0-9a-fA-F]{40}$")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/TurtleCoin.java b/assets/src/main/java/bisq/asset/coins/TurtleCoin.java new file mode 100644 index 0000000000..ac9095c1f5 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TurtleCoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class TurtleCoin extends Coin { + + public TurtleCoin() { + super("TurtleCoin", "TRTL", new RegexAddressValidator("^TRTL[1-9A-Za-z^OIl]{95}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/UnitedCommunityCoin.java b/assets/src/main/java/bisq/asset/coins/UnitedCommunityCoin.java new file mode 100644 index 0000000000..e9a3263823 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/UnitedCommunityCoin.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class UnitedCommunityCoin extends Coin { + + public UnitedCommunityCoin() { + super("UnitedCommunityCoin", "UCC", new UnitedCommunityCoinAddressValidator()); + } + + + public static class UnitedCommunityCoinAddressValidator extends Base58AddressValidator { + + public UnitedCommunityCoinAddressValidator() { + super(new UnitedCommunityCoinParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (!address.matches("^[U][a-km-zA-HJ-NP-Z1-9]{33}$")) + return AddressValidationResult.invalidStructure(); + + return super.validate(address); + } + } + + + public static class UnitedCommunityCoinParams extends NetworkParametersAdapter { + + public UnitedCommunityCoinParams() { + super(); + addressHeader = 68; + p2shHeader = 18; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Unobtanium.java b/assets/src/main/java/bisq/asset/coins/Unobtanium.java new file mode 100644 index 0000000000..dd9b16055b --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Unobtanium.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +public class Unobtanium extends Coin { + + public Unobtanium() { + super("Unobtanium", "UNO", new UnoAddressValidator()); + } + + public static class UnoAddressValidator extends RegexAddressValidator { + + public UnoAddressValidator() { + super("^[u]?[a-zA-Z0-9]{33}", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.UNO")); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/VARIUS.java b/assets/src/main/java/bisq/asset/coins/VARIUS.java new file mode 100644 index 0000000000..abb3e00ce1 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/VARIUS.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class VARIUS extends Coin { + + public VARIUS() { + super("VARIUS Coin", "VARIUS", new RegexAddressValidator("^[V][a-km-zA-HJ-NP-Z1-9]{33}$")); + } +} \ No newline at end of file diff --git a/assets/src/main/java/bisq/asset/coins/Veil.java b/assets/src/main/java/bisq/asset/coins/Veil.java new file mode 100644 index 0000000000..c4caecc71c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Veil.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.*; + +public class Veil extends Coin { + + public Veil() { + super("Veil", "VEIL", new VeilAddressValidator()); + } + + public static class VeilAddressValidator extends Base58AddressValidator { + + public VeilAddressValidator() { + super(new VeilParams()); + } + + @Override + public AddressValidationResult validate(String address) { + if (address.startsWith("V")) { + return super.validate(address); + }else if (address.startsWith("bv")){ + // TODO: Add bech32 support + return AddressValidationResult.invalidAddress("Bech32 addresses not supported on bisq"); + } + return AddressValidationResult.invalidStructure(); + } + } + + public static class VeilParams extends NetworkParametersAdapter { + + public VeilParams() { + addressHeader = 70; + p2shHeader = 5; + } + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/Vertcoin.java b/assets/src/main/java/bisq/asset/coins/Vertcoin.java new file mode 100644 index 0000000000..bb78ff3742 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Vertcoin.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; +import bisq.asset.NetworkParametersAdapter; + +public class Vertcoin extends Coin { + public Vertcoin() { + super("Vertcoin", "VTC", new Base58AddressValidator(new VertcoinMainNetParams())); + } + + public static class VertcoinMainNetParams extends NetworkParametersAdapter { + public VertcoinMainNetParams() { + this.addressHeader = 71; + this.p2shHeader = 5; + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/WORX.java b/assets/src/main/java/bisq/asset/coins/WORX.java new file mode 100644 index 0000000000..837b3fe544 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/WORX.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class WORX extends Coin { + + public WORX() { + super("WORX Coin", "WORX", new RegexAddressValidator("^[W][a-km-zA-HJ-NP-Z1-9]{33}$")); + } +} \ No newline at end of file diff --git a/assets/src/main/java/bisq/asset/coins/Webchain.java b/assets/src/main/java/bisq/asset/coins/Webchain.java new file mode 100644 index 0000000000..38c90b2362 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Webchain.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class Webchain extends Coin { + + public Webchain() { + super("Webchain", "WEB", new RegexAddressValidator("^0x[0-9a-fA-F]{40}$")); + } +} + diff --git a/assets/src/main/java/bisq/asset/coins/WrkzCoin.java b/assets/src/main/java/bisq/asset/coins/WrkzCoin.java new file mode 100644 index 0000000000..69d20360b6 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/WrkzCoin.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class WrkzCoin extends Coin { + + public WrkzCoin() { + super("WrkzCoin", "WRKZ", new RegexAddressValidator("^Wrkz[1-9A-Za-z^OIl]{94}")); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/XDR.java b/assets/src/main/java/bisq/asset/coins/XDR.java new file mode 100644 index 0000000000..ea236b29e2 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/XDR.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; + +public class XDR extends Coin { + + public XDR() { + super("XDR", "XDR0", new Mile.MileAddressValidator()); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Zcash.java b/assets/src/main/java/bisq/asset/coins/Zcash.java new file mode 100644 index 0000000000..e6e576707c --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Zcash.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.ZEC.msg") +public class Zcash extends Coin { + + public Zcash() { + super("Zcash", "ZEC", new RegexAddressValidator("^t.*", "validation.altcoin.zAddressesNotSupported")); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Zcoin.java b/assets/src/main/java/bisq/asset/coins/Zcoin.java new file mode 100644 index 0000000000..5981ad5061 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Zcoin.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.I18n; +import bisq.asset.RegexAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.XZC.msg") +public class Zcoin extends Coin { + + public Zcoin() { + super("Zcoin", "XZC", new XzcAddressValidator()); + } + + public static class XzcAddressValidator extends RegexAddressValidator { + + public XzcAddressValidator() { + super("^a?[a-zA-Z0-9]{33}", I18n.DISPLAY_STRINGS.getString("account.altcoin.popup.validation.XZC")); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/ZelCash.java b/assets/src/main/java/bisq/asset/coins/ZelCash.java new file mode 100644 index 0000000000..b257bbdf11 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/ZelCash.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class ZelCash extends Coin { + + public ZelCash() { + super("ZelCash", "ZEL", new RegexAddressValidator("^t.*", "validation.altcoin.zAddressesNotSupported")); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/Zero.java b/assets/src/main/java/bisq/asset/coins/Zero.java new file mode 100644 index 0000000000..ea0fbc83cd --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/Zero.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AddressValidationResult; +import bisq.asset.AddressValidator; +import bisq.asset.Coin; + +public class Zero extends Coin { + + public Zero() { + super("Zero", "ZER", new ZeroAddressValidator()); + } + + + public static class ZeroAddressValidator implements AddressValidator { + + @Override + public AddressValidationResult validate(String address) { + // We only support t addresses (transparent transactions) + if (!address.startsWith("t1")) + return AddressValidationResult.invalidAddress("", "validation.altcoin.zAddressesNotSupported"); + + return AddressValidationResult.validAddress(); + } + } +} diff --git a/assets/src/main/java/bisq/asset/coins/ZeroClassic.java b/assets/src/main/java/bisq/asset/coins/ZeroClassic.java new file mode 100644 index 0000000000..a593b9bc70 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/ZeroClassic.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +public class ZeroClassic extends Coin { + + public ZeroClassic() { + super("ZeroClassic", "ZERC", new RegexAddressValidator("^t.*", "validation.altcoin.zAddressesNotSupported")); + } + +} diff --git a/assets/src/main/java/bisq/asset/coins/uPlexa.java b/assets/src/main/java/bisq/asset/coins/uPlexa.java new file mode 100644 index 0000000000..fbaaf7d534 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/uPlexa.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Coin; +import bisq.asset.RegexAddressValidator; + +@AltCoinAccountDisclaimer("account.altcoin.popup.upx.msg") +public class uPlexa extends Coin { + + public uPlexa() { + super("uPlexa", "UPX", new RegexAddressValidator("^((UPX)[1-9A-Za-z^OIl]{95}|(UPi)[1-9A-Za-z^OIl]{106}|(UmV|UmW)[1-9A-Za-z^OIl]{94})$")); + } +} diff --git a/assets/src/main/java/bisq/asset/package-info.java b/assets/src/main/java/bisq/asset/package-info.java new file mode 100644 index 0000000000..b466aedf31 --- /dev/null +++ b/assets/src/main/java/bisq/asset/package-info.java @@ -0,0 +1,37 @@ +/* + * 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 . + */ + +/** + * Bisq's family of abstractions representing different ("crypto") + * {@link bisq.asset.Asset} types such as {@link bisq.asset.Coin}, + * {@link bisq.asset.Token} and {@link bisq.asset.Erc20Token}, as well as concrete + * implementations of each, such as {@link bisq.asset.coins.Bitcoin} itself, altcoins like + * {@link bisq.asset.coins.Litecoin} and {@link bisq.asset.coins.Ether} and tokens like + * {@link bisq.asset.tokens.DaiStablecoin}. + *

    + * The purpose of this package is to provide everything necessary for registering + * ("listing") new assets and managing / accessing those assets within, e.g. the Bisq + * Desktop UI. + *

    + * Note that everything within this package is intentionally designed to be simple and + * low-level with no dependencies on any other Bisq packages or components. + * + * @author Chris Beams + * @since 0.7.0 + */ + +package bisq.asset; diff --git a/assets/src/main/java/bisq/asset/tokens/AugmintEuro.java b/assets/src/main/java/bisq/asset/tokens/AugmintEuro.java new file mode 100644 index 0000000000..32ac1d581e --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/AugmintEuro.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class AugmintEuro extends Erc20Token { + + public AugmintEuro() { + super("Augmint Euro", "AEUR"); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/DaiStablecoin.java b/assets/src/main/java/bisq/asset/tokens/DaiStablecoin.java new file mode 100644 index 0000000000..f0f673c527 --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/DaiStablecoin.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class DaiStablecoin extends Erc20Token { + + public DaiStablecoin() { + super("Dai Stablecoin", "DAI"); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/EtherStone.java b/assets/src/main/java/bisq/asset/tokens/EtherStone.java new file mode 100644 index 0000000000..cebe9f8171 --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/EtherStone.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class EtherStone extends Erc20Token { + + public EtherStone() { + super("EtherStone", "ETHS"); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/TrueUSD.java b/assets/src/main/java/bisq/asset/tokens/TrueUSD.java new file mode 100644 index 0000000000..0ca465fe8a --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/TrueUSD.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class TrueUSD extends Erc20Token { + + public TrueUSD() { + super("TrueUSD", "TUSD"); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/USDCoin.java b/assets/src/main/java/bisq/asset/tokens/USDCoin.java new file mode 100644 index 0000000000..91d5a926e3 --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/USDCoin.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class USDCoin extends Erc20Token { + + public USDCoin() { + super("USD Coin", "USDC"); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/VectorspaceAI.java b/assets/src/main/java/bisq/asset/tokens/VectorspaceAI.java new file mode 100644 index 0000000000..c080f848c6 --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/VectorspaceAI.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class VectorspaceAI extends Erc20Token { + + public VectorspaceAI() { + super("VectorspaceAI", "VXV"); + } +} diff --git a/assets/src/main/resources/META-INF/services/bisq.asset.Asset b/assets/src/main/resources/META-INF/services/bisq.asset.Asset new file mode 100644 index 0000000000..80a6168463 --- /dev/null +++ b/assets/src/main/resources/META-INF/services/bisq.asset.Asset @@ -0,0 +1,128 @@ +# All assets available for trading on the Bisq network. +# Contents are sorted according to the output of `sort --ignore-case --dictionary-order`. +# See bisq.asset.Asset and bisq.asset.AssetRegistry for further details. +# See https://bisq.network/list-asset for complete instructions. +bisq.asset.coins.Actinium +bisq.asset.coins.Adeptio +bisq.asset.coins.Aeon +bisq.asset.coins.Amitycoin +bisq.asset.coins.Animecoin +bisq.asset.coins.Arqma +bisq.asset.coins.Askcoin +bisq.asset.coins.Australiacash +bisq.asset.coins.Beam +bisq.asset.coins.Bitcoin$Mainnet +bisq.asset.coins.Bitcoin$Regtest +bisq.asset.coins.BitcoinRhodium +bisq.asset.coins.Bitcoin$Testnet +bisq.asset.coins.BitDaric +bisq.asset.coins.Bitmark +bisq.asset.coins.Bitzec +bisq.asset.coins.Blur +bisq.asset.coins.BSQ$Mainnet +bisq.asset.coins.BSQ$Regtest +bisq.asset.coins.BSQ$Testnet +bisq.asset.coins.BurntBlackCoin +bisq.asset.coins.Cash2 +bisq.asset.coins.Chaucha +bisq.asset.coins.CloakCoin +bisq.asset.coins.Counterparty +bisq.asset.coins.Credits +bisq.asset.coins.Croat +bisq.asset.coins.CRowdCLassic +bisq.asset.coins.CTSCoin +bisq.asset.coins.DarkPay +bisq.asset.coins.Dash +bisq.asset.coins.Decred +bisq.asset.coins.DeepOnion +bisq.asset.coins.Dextro +bisq.asset.coins.Dogecoin +bisq.asset.coins.Doichain +bisq.asset.coins.Donu +bisq.asset.coins.Dragonglass +bisq.asset.coins.DSTRA +bisq.asset.coins.Emercoin +bisq.asset.coins.Ergo +bisq.asset.coins.Ether +bisq.asset.coins.EtherClassic +bisq.asset.coins.Faircoin +bisq.asset.coins.FourtyTwo +bisq.asset.coins.Fujicoin +bisq.asset.coins.Galilel +bisq.asset.coins.GambleCoin +bisq.asset.coins.Genesis +bisq.asset.coins.Grin +bisq.asset.coins.Hatch +bisq.asset.coins.Helium +bisq.asset.coins.Horizen +bisq.asset.coins.IdaPay +bisq.asset.coins.Iridium +bisq.asset.coins.Kekcoin +bisq.asset.coins.KnowYourDeveloper +bisq.asset.coins.Kore +bisq.asset.coins.Krypton +bisq.asset.coins.LBRYCredits +bisq.asset.coins.LiquidBitcoin +bisq.asset.coins.Litecoin +bisq.asset.coins.LitecoinPlus +bisq.asset.coins.LitecoinZ +bisq.asset.coins.Lytix +bisq.asset.coins.Masari +bisq.asset.coins.Mask +bisq.asset.coins.Mile +bisq.asset.coins.MirQuiX +bisq.asset.coins.MobitGlobal +bisq.asset.coins.Monero +bisq.asset.coins.MonetaryUnit +bisq.asset.coins.MoX +bisq.asset.coins.Myce +bisq.asset.coins.Namecoin +bisq.asset.coins.Navcoin +bisq.asset.coins.Ndau +bisq.asset.coins.Noir +bisq.asset.coins.NoteBlockchain +bisq.asset.coins.ParsiCoin +bisq.asset.coins.Particl +bisq.asset.coins.PENG +bisq.asset.coins.Persona +bisq.asset.coins.Pinkcoin +bisq.asset.coins.PIVX +bisq.asset.coins.Plenteum +bisq.asset.coins.PZDC +bisq.asset.coins.Qbase +bisq.asset.coins.QMCoin +bisq.asset.coins.Qwertycoin +bisq.asset.coins.Radium +bisq.asset.coins.Remix +bisq.asset.coins.Ryo +bisq.asset.coins.Siafund +bisq.asset.coins.SiaPrimeCoin +bisq.asset.coins.SixEleven +bisq.asset.coins.Solo +bisq.asset.coins.SpaceCash +bisq.asset.coins.Spectrecoin +bisq.asset.coins.Starwels +bisq.asset.coins.SUB1X +bisq.asset.coins.TEO +bisq.asset.coins.TurtleCoin +bisq.asset.coins.UnitedCommunityCoin +bisq.asset.coins.Unobtanium +bisq.asset.coins.uPlexa +bisq.asset.coins.VARIUS +bisq.asset.coins.Veil +bisq.asset.coins.Vertcoin +bisq.asset.coins.Webchain +bisq.asset.coins.WORX +bisq.asset.coins.WrkzCoin +bisq.asset.coins.XDR +bisq.asset.coins.Zcash +bisq.asset.coins.Zcoin +bisq.asset.coins.ZelCash +bisq.asset.coins.Zero +bisq.asset.coins.ZeroClassic +bisq.asset.tokens.AugmintEuro +bisq.asset.tokens.DaiStablecoin +bisq.asset.tokens.EtherStone +bisq.asset.tokens.TrueUSD +bisq.asset.tokens.USDCoin +bisq.asset.tokens.VectorspaceAI diff --git a/assets/src/main/resources/i18n/displayStrings-assets.properties b/assets/src/main/resources/i18n/displayStrings-assets.properties new file mode 100644 index 0000000000..beabf782fb --- /dev/null +++ b/assets/src/main/resources/i18n/displayStrings-assets.properties @@ -0,0 +1,28 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + +account.altcoin.popup.validation.XCP=XCP address must start with '1' and must have 34 characters. +account.altcoin.popup.validation.DCR=DCR address must start with 'Dk' or 'Ds' or 'De' or 'DS' or 'Dc' or 'Pm' and must have 34 characters. +account.altcoin.popup.validation.ETC=ETC address must start with '0x' and made up of letters A to F and numbers which are 40 characters long. +account.altcoin.popup.validation.NMC=NMC address must start with 'N' or 'M' and must be 34 characters long. +account.altcoin.popup.validation.SF= Siafund address must be made up of letters A to F and numbers which are 76 characters long. +account.altcoin.popup.validation.UNO=UNO address must start with 'u' and must have 34 characters. +account.altcoin.popup.validation.XZC=XZC address must start with 'a' and must have 34 characters. \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/AbstractAssetTest.java b/assets/src/test/java/bisq/asset/AbstractAssetTest.java new file mode 100644 index 0000000000..6cf73a99b7 --- /dev/null +++ b/assets/src/test/java/bisq/asset/AbstractAssetTest.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.asset; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Abstract base class for all {@link Asset} unit tests. Subclasses must implement the + * {@link #testValidAddresses()} and {@link #testInvalidAddresses()} methods, and are + * expected to use the convenient {@link #assertValidAddress(String)} and + * {@link #assertInvalidAddress(String)} assertions when doing so. + *

    + * Blank / empty addresses are tested automatically by this base class and are always + * considered invalid. + *

    + * This base class also serves as a kind of integration test for {@link AssetRegistry}, in + * that all assets tested through subclasses are tested to make sure they are also + * properly registered and available there. + * + * @author Chris Beams + * @author Bernard Labno + * @since 0.7.0 + */ +public abstract class AbstractAssetTest { + + private final AssetRegistry assetRegistry = new AssetRegistry(); + + protected final Asset asset; + + public AbstractAssetTest(Asset asset) { + this.asset = asset; + } + + @Test + public void testPresenceInAssetRegistry() { + assertThat(asset + " is not registered in META-INF/services/" + Asset.class.getName(), + assetRegistry.stream().anyMatch(this::hasSameTickerSymbol), is(true)); + } + + @Test + public void testBlank() { + assertInvalidAddress(""); + } + + @Test + public abstract void testValidAddresses(); + + @Test + public abstract void testInvalidAddresses(); + + protected void assertValidAddress(String address) { + AddressValidationResult result = asset.validateAddress(address); + assertThat(result.getMessage(), result.isValid(), is(true)); + } + + protected void assertInvalidAddress(String address) { + assertThat(asset.validateAddress(address).isValid(), is(false)); + } + + private boolean hasSameTickerSymbol(Asset asset) { + return this.asset.getTickerSymbol().equals(asset.getTickerSymbol()); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ActiniumTest.java b/assets/src/test/java/bisq/asset/coins/ActiniumTest.java new file mode 100644 index 0000000000..e24f235bca --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ActiniumTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ActiniumTest extends AbstractAssetTest { + + public ActiniumTest() { + super(new Actinium()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("NLzB9iUGJ8GaKSn9GfVKfd55QVRdNdz9FK"); + assertValidAddress("NSz7PKmo1sLQYtFuZjTQ1zZXhPQtHLScKT"); + assertValidAddress("NTFtsh4Ff2ijPNsnQAUf5fKTp7DJaGxSZK"); + assertValidAddress("PLRiNpnTzWqufAoRFN1u9zBstHqjyM2qgB"); + assertValidAddress("PMFpWHR2AbBwaR4G2rA5nWB1F7cbZWua5Z"); + assertValidAddress("P9XE6tupGocWnsNgoUxRPzASYAPVAyu2T8"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("MgTFtsh4Ff2ijPNsnQAUf5fKTp7DJaGxSZK"); + assertInvalidAddress("F9z7PKmo1sLQYtFuZjTQ1zZXhPQtHLScKT"); + assertInvalidAddress("16Ftsh4Ff2ijPNsnQAUf5fKTp7DJaGxSZK"); + assertInvalidAddress("Z6Ftsh7LfGijPVzmQAUf5fKTp7DJaGxSZK"); + assertInvalidAddress("G5Fmxy4Ff2ijLjsnQAUf5fKTp7DJaGxACV"); + assertInvalidAddress("D4Hmqy4Ff2ijXYsnQAUf5fKTp7DJaGxBhJ"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/AdeptioTest.java b/assets/src/test/java/bisq/asset/coins/AdeptioTest.java new file mode 100644 index 0000000000..4efa16b6ee --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AdeptioTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AdeptioTest extends AbstractAssetTest { + + public AdeptioTest() { + super(new Adeptio()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("AP7rSyQMZRek9HGy9QB1bpung69xViesN7"); + assertValidAddress("AWVXtnMo4pS2vBSNrBPLVvMgYvJGD6gSXk"); + assertValidAddress("AHq8sM8DEeFoZXeDkaimfCLtnMuuSWXFE7"); + assertValidAddress("ANG52tPNJuVknLQiLUdzVFoZ3vyo8UzkDL"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("aP7rSyQMZRek9HGy9QB1bpung69xViesN7"); + assertInvalidAddress("DAeiBSH4nudXgoxS4kY6uhTPobc7AlrWDA"); + assertInvalidAddress("BGhVYBXk511m8TPvQA6YokzxdpdhRE3sG6"); + assertInvalidAddress("AZt2Kuy9cWFbTc888HNphppkuCTNyqu5PY"); + assertInvalidAddress("AbosH98t3TRKzyNb8pPQV9boupVcBAX6of"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBIDnG6Rc"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/AeonTest.java b/assets/src/test/java/bisq/asset/coins/AeonTest.java new file mode 100644 index 0000000000..17c5911955 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AeonTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + + import bisq.asset.AbstractAssetTest; + import org.junit.Test; + + public class AeonTest extends AbstractAssetTest { + + public AeonTest() { + super(new Aeon()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("WmsSXcudnpRFjXr5qZzEY5AF64J6CpFKHYXJS92rF9WjHVjQvJxrmSGNQnSfwwJtGGeUMKvLYn5nz2yL9f6M4FN51Z5r8zt4C"); + assertValidAddress("XnY88EywrSDKiQkeeoq261dShCcz1vEDwgk3Wxz77AWf9JBBtDRMTD9Fe3BMFAVyMPY1sP44ovKKpi4UrAR26o661aAcATQ1k"); + assertValidAddress("Wmu42kYBnVJgDhBUPEtK5dicGPEtQLDUVWTHW74GYvTv1Zrki2DWqJuWKcWV4GVcqnEMgb1ZiufinCi7WXaGAmiM2Bugn9yTx"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("WmsSXcudnpRFjXr5qZzEY5AF64J6CpFKHYXJS92rF9WjHVjQvJxrmSGNQnSfwwJtGGeUMKvLYn5nz2yL9f6M4FN51Z5r8zt4"); + assertInvalidAddress("XnY88EywrSDKiQkeeoq261dShCcz1vEDwgk3Wxz77AWf9JBBtDRMTD9Fe3BMFAVyMPY1sP44ovKKpi4UrAR26o661aAcATQ1kZz"); + assertInvalidAddress("XaY88EywrSDKiQkeeoq261dShCcz1vEDwgk3Wxz77AWf9JBBtDRMTD9Fe3BMFAVyMPY1sP44ovKKpi4UrAR26o661aAcATQ1k"); + assertInvalidAddress("Wmu42kYBnVJgDhBUPEtK5dicGPEtQLDUVWTHW74GYv#vZrki2DWqJuWKcWV4GVcqnEMgb1ZiufinCi7WXaGAmiM2Bugn9yTx"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/AmitycoinTest.java b/assets/src/test/java/bisq/asset/coins/AmitycoinTest.java new file mode 100644 index 0000000000..ae9135b981 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AmitycoinTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AmitycoinTest extends AbstractAssetTest { + + public AmitycoinTest() { + super(new Amitycoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("amitMgDfvfUZ2CP1g1SEJQSN4n7qK4d45hqXSDtiFMwE5uo7DnSihknJzcEG9WtFc26fnhDHK6ydjBDKe6wjCoGt4RiP18a5Zb"); + assertValidAddress("amitUnFFwApLG9btiPWRgTjRCQUj9kZjQJ8kH3ZraSsCU4yzX4AzgaoP8jkgXhp5c5jQT3idFJChAPYzA2EydJ5A4bShqrEixa"); + assertValidAddress("amitAcVJTUZKJtYYsosMXJBQeEbt3ZV9qSvoQ1EqkvA45MRUaYWECYNKyRZ82BvLM9MPD2Gpud3DbGzGsStKnZ9x5yKVPVGJUa"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("amitAcVJTUZKJtYYsosMXJBQeEbt3ZV9qSvoQ1EqkvA45MRUaYWECYNKyRZ82BvLM9MPD2Gpud3DbGzGsStKnZ9"); + assertInvalidAddress("amitAcVJTUZKJtYYsosMXJBQeEbt3ZV9qSvoQ1EqkvA45MRUaYWECYNKyRZ82BvLM9MPD2Gpud3DbGzGsStKnZ9x5yKVPVGJUaljashfeafh"); + assertInvalidAddress(""); + assertInvalidAddress("amitAcVJTUZKJtYYsosMXJBQeEbt3ZV9qSvoQ1EqkvA45MRUaYWECY#RoPOWRwpsx1F"); + assertInvalidAddress("amitAcVJTUZKJtYYsosMXJByRZ82BvLM9MPD2Gpud3DbGzGsStKnZ9x5yKVPVGJUaJbc2q4C4fWN$C4fWNLoDLDvADvpjNYdt3sdRB434UidKXimQQn"); + assertInvalidAddress("dsfkjasd56yaSDdguaw"); + assertInvalidAddress("KEKlulzlksadfwe"); + assertInvalidAddress("HOleeSheetdsdjqwqwpoo3"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/AnimecoinTest.java b/assets/src/test/java/bisq/asset/coins/AnimecoinTest.java new file mode 100644 index 0000000000..7538294578 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AnimecoinTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AnimecoinTest extends AbstractAssetTest { + + public AnimecoinTest() { + super(new Animecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("Aa6TDuudiNh7DRzs11wEzZWiw9QBZY3Qw1"); + assertValidAddress("AdsdUhnPsJwg5NvAuyxs4EsaE2GoSHohoq"); + assertValidAddress("4s2peLxJJ2atz1tnAKpFshnVPKTmR312fr"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("aa6TDuudiNh7DRzs11wEzZWiw9QBZY3Qw1"); + assertInvalidAddress("3s2peLxJJ2atz1tnAKpFshnVPKTmR312fr"); + assertInvalidAddress("ANNPzjj2ZYEhpyJ6p6sWeH1JXbkCSmNSd#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ArqmaTest.java b/assets/src/test/java/bisq/asset/coins/ArqmaTest.java new file mode 100644 index 0000000000..3c5a5a9842 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ArqmaTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + + import bisq.asset.AbstractAssetTest; + import org.junit.Test; + + public class ArqmaTest extends AbstractAssetTest { + + public ArqmaTest() { + super(new Arqma()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("ar3ZLUTSac5DhxhyLJB11gcXWLYPKJchg7c8hoaKmqchC9TtHEdXzxGgt2vzCLUYwtSvkJQTXNCjzCR7KZiFUySV138PEopVC"); + assertValidAddress("aRS3V2hXuVAGAb5XWcDvN7McsSyqrEZ3XWyfMdEDCqioWNmVUuoKyNxDo7rwPCg55Ugb6KHXLN7hLZEGcnZzbm8M7uJ9YdVpeN"); + assertValidAddress("ar3mXR6SQeC3P9Dmq2LGsAeq5eDvjiNnYaywtqdNzixe6xLr38DiNVaaRKMkAQkR3NV3TuVAwAwEGH3QDgXJF3th1RwxABa9a"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("ar3ZLUTSac5DhxhyLJB11gcXWLYPKJchg7c8hoaKmqchC9TtHEdXzxGgt2vzCLUYwtSvkJQTXNCjzCR7KZiFUySV138PEopV"); + assertInvalidAddress("aRS3V2hXuVAGAb5XWcDvN7McsSyqrEZ3XWyfMdEDCqioWNmVUuoKyNxDo7rwPCg55Ugb6KHXLN7hLZEGcnZzbm8M7uJ9YdVpeNZz"); + assertInvalidAddress("aRV3V2hXuVAGAb5XWcDvN7McsSyqrEZ3XWyfMdEDCqioWNmVUuoKyNxDo7rwPCg55Ugb6KHXLN7hLZEGcnZzbm8M7uJ9YdVpeN"); + assertInvalidAddress("ar3mXR6SQeC3P9Dmq2LGsAeq5eDvjiNnYaywtqdNzi#exLr38DiNVaaRKMkAQkR3NV3TuVAwAwEGH3QDgXJF3th1RwxABa9a"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/AskcoinTest.java b/assets/src/test/java/bisq/asset/coins/AskcoinTest.java new file mode 100644 index 0000000000..f1f5b587cc --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AskcoinTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AskcoinTest extends AbstractAssetTest { + + public AskcoinTest() { + super(new Askcoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("1"); + assertValidAddress("123"); + assertValidAddress("876982302333"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0"); + assertInvalidAddress("038292"); + assertInvalidAddress(""); + assertInvalidAddress("000232320382"); + assertInvalidAddress("1298934567890"); + assertInvalidAddress("123abc5ab"); + assertInvalidAddress("null"); + assertInvalidAddress("xidjfwi23ii0"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/AustraliacashTest.java b/assets/src/test/java/bisq/asset/coins/AustraliacashTest.java new file mode 100644 index 0000000000..3e2ef3c19a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/AustraliacashTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AustraliacashTest extends AbstractAssetTest { + + public AustraliacashTest() { + super(new Australiacash()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("AYf2TGCoQ15HatyE99R3q9jVcXHLx1zRWW"); + assertValidAddress("Aahw1A79we2jUbTaamP5YALh21GSxiWTZa"); + assertValidAddress("ALp3R9W3QsCdqaNNcULySXN31dYvfvDkRU"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1ALp3R9W3QsCdqaNNcULySXN31dYvfvDkRU"); + assertInvalidAddress("ALp3R9W3QsCdrqaNNcULySXN31dYvfvDkRU"); + assertInvalidAddress("ALp3R9W3QsCdqaNNcULySXN31dYvfvDkRU#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BSQTest.java b/assets/src/test/java/bisq/asset/coins/BSQTest.java new file mode 100644 index 0000000000..9bb9764b8a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BSQTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BSQTest extends AbstractAssetTest { + + public BSQTest() { + super(new BSQ.Mainnet()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("B17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertValidAddress("B3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); + assertValidAddress("B1111111111111111111114oLvT2"); + assertValidAddress("B1BitcoinEaterAddressDontSendf59kuE"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("B17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("B17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); + assertInvalidAddress("B17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BeamTest.java b/assets/src/test/java/bisq/asset/coins/BeamTest.java new file mode 100644 index 0000000000..83181e5ebe --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BeamTest.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BeamTest extends AbstractAssetTest { + + public BeamTest() { + super(new Beam()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("4a0e54b24d5fdf06891a8eaa57b4b3ac16731e932a64da8ec768083495d624f1"); + assertValidAddress("c7776e6d3fd3d9cc66f9e61b943e6d99473b16418ee93f3d5f6b70824cdb7f0a9"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("114a0e54b24d5fdf06891a8eaa57b4b3ac16731e932a64da8ec768083495d624f1111111111111111"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BitDaricTest.java b/assets/src/test/java/bisq/asset/coins/BitDaricTest.java new file mode 100644 index 0000000000..a21f1d8afc --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BitDaricTest.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.asset.coins; +import bisq.asset.AbstractAssetTest; +import org.junit.Test; +public class BitDaricTest extends AbstractAssetTest { + public BitDaricTest() { + super(new BitDaric()); + } + @Test + public void testValidAddresses() { + assertValidAddress("RKWuQUtmV3em1MyB7QKdshgDEAwKQXuifa"); + assertValidAddress("RG9YuDw7fa21a8h4E3Z2z2tgHrFNN27NnG"); + } + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BitcoinRhodiumTest.java b/assets/src/test/java/bisq/asset/coins/BitcoinRhodiumTest.java new file mode 100644 index 0000000000..3b9bacc886 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BitcoinRhodiumTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BitcoinRhodiumTest extends AbstractAssetTest { + + public BitcoinRhodiumTest() { + super(new BitcoinRhodium()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("RiMBe4uDXPzTxgKUEwqQobp2o7dqBDYM6S"); + assertValidAddress("RqvpFWRTSKo2QEMH89rNhs3C7CCmRRYKmg"); + assertValidAddress("Rhxz2uF9HaE2ync4eDetjkdhkS5qMXMQzz"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("Rhxz2uF9HaE2ync4eDetjkdhkS5qMXMQvdvdfbFzz"); + assertInvalidAddress("fqvpFWRTSKo2QEMH89rNhs3C7CCmRRYKmg"); + assertInvalidAddress("1HQQgsvLTgN9xD9hNmAgAreakzDsxUSLSH#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BitcoinTest.java b/assets/src/test/java/bisq/asset/coins/BitcoinTest.java new file mode 100644 index 0000000000..bad08da824 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BitcoinTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BitcoinTest extends AbstractAssetTest { + + public BitcoinTest() { + super(new Bitcoin.Mainnet()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); + assertValidAddress("1111111111111111111114oLvT2"); + assertValidAddress("1BitcoinEaterAddressDontSendf59kuE"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BitmarkTest.java b/assets/src/test/java/bisq/asset/coins/BitmarkTest.java new file mode 100644 index 0000000000..284baecec6 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BitmarkTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BitmarkTest extends AbstractAssetTest { + + public BitmarkTest() { + super(new Bitmark()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("bMigVohTEiA3gxhFWpDJBrZ14j2RnDkWCs"); + assertValidAddress("bKMivcHXMNfs3P3AaTtyhDiZ7s8Nw3ek6L"); + assertValidAddress("bXUYGzbV8v6pLZtkYDL3feyrRFFnc37e3H"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("bMigVohTEiA3gxhFWpDJBrZ14j2RnDkWCt"); + assertInvalidAddress("F9z7PKmo1sLQYtFuZjTQ1zZXhPQtHLScKT"); + assertInvalidAddress("16Ftsh4Ff2ijPNsnQAUf5fKTp7DJaGxSZK"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BitzecTest.java b/assets/src/test/java/bisq/asset/coins/BitzecTest.java new file mode 100644 index 0000000000..dcc3ac36c7 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BitzecTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class BitzecTest extends AbstractAssetTest { + + public BitzecTest() { + super(new Bitzec()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("t1K6LGT7z2uNTLxag6eK6XwGNpdkHbncBaK"); + assertValidAddress("t1ZjdqCGEkqL9nZ8fk9R6KA7bqNvXaVLUpF"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BlurTest.java b/assets/src/test/java/bisq/asset/coins/BlurTest.java new file mode 100644 index 0000000000..4cf93f3569 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BlurTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + + import bisq.asset.AbstractAssetTest; + import org.junit.Test; + + public class BlurTest extends AbstractAssetTest { + + public BlurTest() { + super(new Blur()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("bL3W1g1d12sbxQDTQ6q8bgU2bBp2rkfFFKfNvQuUQTHqgQHRaxKTHqK5Nqdm53BU3ibPnsqbdYAnnJMyqJ6FfN9m3CSZSNqDE"); + assertValidAddress("bL2zBGUBDkQdyYasdoAdvQCxWLa9Mk5Q1PW8Zk7S38vx9xu7T7NMPPWNfieXqUyswo544ZSB3C1n9jLMfsUvR6p91rnrSdx9h"); + assertValidAddress("Ry49oErHtqyHucxADDT2DfEJ9pRv2ciSpKV9XseCuWmx1PK1CZi4gbPKxhWBdtvLJNNc94c4yDutmZrD3WrsHPYV1nvE9X4Cc"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("bl4E2BCFY31DPLjeqF6Gu7TEUM5v2JwpmudFX64AubQtFDYEPBvgvQPzidaawDhjAmHeZSw92wEBnUfdfY5144Sad2ZCknZzC"); + assertInvalidAddress("Ry49oErHtqyHucxADDT2DfEJ9pRv2ciSpKV9XseCuWmx1PK1CZi4gbPKxhWBdtvLJNNc94c4yDutmZrD3WrsHPYV1nvE9X40"); + assertInvalidAddress("bLNHRh8pFh5Y14bhBVAoD4cvqHyoPsQJqB3dr49zoF6bNDFrts96tuuj#RoUKWRwpTHmYt4Kf78FES7LCXAXKXFf6bMsx1sdgz"); + assertInvalidAddress("82zBGUBDkQdyYasdoAdvQCxWLa9Mk5Q1PW#8Zk7S38vx9xu7T7NMPPWNfieXqUyswo544ZSB3C1n9jLMfsUvR6p91rnrSdxwd"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/BurntBlackCoinTest.java b/assets/src/test/java/bisq/asset/coins/BurntBlackCoinTest.java new file mode 100644 index 0000000000..941ea5f585 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/BurntBlackCoinTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import java.util.Collections; +import org.junit.Test; + +public class BurntBlackCoinTest extends AbstractAssetTest { + + public BurntBlackCoinTest() { + super(new BurntBlackCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("4b"); + assertValidAddress("536865206d616b657320796f75206275726e207769746820612077617665206f66206865722068616e64"); + String longAddress = String.join("", Collections.nCopies(2 * BurntBlackCoin.PAYLOAD_LIMIT, "af")); + assertValidAddress(longAddress); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("AF"); + assertInvalidAddress("afa"); + assertInvalidAddress("B4Wa1C8zFgkSY4daLg8jWnxuKpw7UmWFoo"); + String tooLongAddress = String.join("", Collections.nCopies(2 * BurntBlackCoin.PAYLOAD_LIMIT + 1, "af")); + assertInvalidAddress(tooLongAddress); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/CRowdCLassicTest.java b/assets/src/test/java/bisq/asset/coins/CRowdCLassicTest.java new file mode 100644 index 0000000000..ca834002ff --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CRowdCLassicTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class CRowdCLassicTest extends AbstractAssetTest { + + public CRowdCLassicTest() { + super(new CRowdCLassic()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("CfvddKQHdd975N5XQgmpVGTuK9mumvDBQo"); + assertValidAddress("CU7pAhQjw2mjgQEAkxpsvAmeLU4Gs7ogQb"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0xmnuL9poRmnuLd55bzKe7t48xtYv2bRES"); + assertInvalidAddress("cvaAgcLKrno2AC7kYhHVDC"); + assertInvalidAddress("19p49poRmnuLdnu55bzKe7t48xtYv2bRES"); + assertInvalidAddress("csabbfjqwr12fbdf2gvffbdb12vdssdcaa"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/CTSCoinTest.java b/assets/src/test/java/bisq/asset/coins/CTSCoinTest.java new file mode 100644 index 0000000000..adbe76f7da --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CTSCoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class CTSCoinTest extends AbstractAssetTest { + + public CTSCoinTest() { + super(new CTSCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("Ti6S7JhtxKjSytZDmyMV4pVNVAPeiVsnpT"); + assertValidAddress("TwzRDeNSPcJvquuGu7WxxH3RhXBR1VPYHZ"); + assertValidAddress("TgYGQJd5TEzDRkyXt1tCvUnrbWBu38C8YK"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("ti6S7JhtxKjSytZDmyMV4pVNVAPeiVsnpT"); + assertInvalidAddress("2i6S7JhtxKjSytZDmyMV4pVNVAPeiVsnpT"); + assertInvalidAddress("Ti6S7JhtxKjSytZDmyMV4pVNVAPeiVsnp#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/Cash2Test.java b/assets/src/test/java/bisq/asset/coins/Cash2Test.java new file mode 100644 index 0000000000..2cbfd40373 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/Cash2Test.java @@ -0,0 +1,108 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class Cash2Test extends AbstractAssetTest { + + public Cash2Test() { + super(new Cash2()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("21mCcygjJivUzYW4TMTeHbEfv3Fq9NnMGEeChYcVNa3cUcRrVvy3K6mD6oMZ99fv6DQq53hbWB8Td36oVbipR7Yk6bovyL7"); + assertValidAddress("22nnHUyz7DScf3QLL27NsuAxFiuVnfDWUaDF38a35aa3CEPp2zDgcEGLfBkCdu2ohzKt7mVNfNa2NDNZHSoFWa5j3kY9os6"); + assertValidAddress("232Vo5FGYWRHhKmJ3Vz8CRCTy25RJyLJQ8wQo8mUDtZJiGLqQqPgzPJSivKR1ux9GNizSSRh6ACMR74i4qREuQrRK6KF3XH"); + assertValidAddress("247r4orbN2jcXtnSSEBxxdJgRNxexjHZRUiE3aPU7ZL4AoRF6eVh9eY3GQTi2rNuw4PSbZzttoaJPWfJnbUnJ4ZSL8tYppR"); + assertValidAddress("25vJ3RnYBveVqavx1eZshSYc5Rn9YaFWcJ2q2WouT17DMouujdDiAT3MnE7C49hdmF84zbv1TG8mTNcchuTx6L2sBXkjFgw"); + assertValidAddress("26UfQDRNs5X7FFoTrfZXFv5Phze2aotTkGPB5iwDpaM3iXQrhX87e5eUGRbiSzVbb53yuA1jpHG5xiieVkYkrhnSBCJCWHU"); + assertValidAddress("27yDdygjMcLHPRh6iiEQDqZcb6dXYWDpiRhUYZbo3ztGTtEKG72FH7mUtRevHn4rdCX51MHLJMc2afycbSrouoXoGJkA8KE"); + assertValidAddress("28t4qvTKmt34kscL3raEx9EBFjBF9t4JadFpL7vq4GsTj4PSt1mEXW36ENBZgJfW3FRJoBGP47yhj7S9CRSCXEPdVrTBG4m"); + assertValidAddress("295wF4wHgFMGsP67t3te2e2ihruA1V5Bu9KBtrVrMRky9Wwt1mZhwFANpUTCiwuxHAV7cnWhx4y9bMN4esfZXAFJ59YrG9U"); + assertValidAddress("2AZqafQ7tXmgui7ReiGdqsCqKnWVPC4uJ4RDag7pspk5jCA5dQ7ysoNeMGTQss8D4jQhp2ySvvD7XZ8JeNNgHTgULErC5BA"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("09s5NiYva6XS9bhhVc6jKYgXsH9wuHhZhWsqyAoPoWPUEiwEo9AZCDNbksvknyvZk73zwhHWSiXdgcDGLyhk5teEM7yxTgk"); + assertInvalidAddress("15a2NPZy7Xe2WZt3nKMZBsBpgNdevnKfy6PLk99eCjYw5fWQ5nu4uM6LerRBvVKTJpdGp2acZ25X3QDPHc7ocTxz1WfN2Za"); + assertInvalidAddress("34B8imA1UH29uR6PHiGpcz9MYdnL3rku27gGeLosn5XuSedFC7jPBmzaB9DoxmDA5VUZa5hPv6PSe3tJH2MJhBktMEJXaZ8"); + assertInvalidAddress("45Ghz2JRTTrLh8Z4bm6QhzZxVbq7LPiKbgjjhHPNDvVXZAJLop91zRu9A7wJMjyrU89uF7QpZB5kHhniuGZ88MJv7jRZXNi"); + assertInvalidAddress("58FFmFEGcS52mTWmhAskaKSSiX1BnHo8YcDjuhPdYBpWT9Q6ZCDz54k6cs3jPF2nk6desb1T6vRfHLfthiNf561qPct2SY1"); + assertInvalidAddress("67rMF5ve4nt2wTHYJ1pZ6j3o2YP5KDBnE7GDxnr6bpem9WcqeHzw9yKWXvtxYdpDXCBbLiX9nm97r4aEtnXq8YNb9WPn15f"); + assertInvalidAddress("798Qr9sWTprQ2sH2y5PGpfV3RAnFxUsJYY2a2VA9GjZ3MiyScD8VEh8ifWk4toYRCcbLZmRJw2dSsJBJAJ1Ava8WBzW7J12"); + assertInvalidAddress("85CQSLDNNKR4HGHwhtsxhm8jheYEvk6ngf44AhqCRWDV2XsaTHr6ittDuyfCjinAP1SzBqnVJfqNhYGDJLzxq4Y7FBVofXV"); + assertInvalidAddress("9AeKW87bkao59oadmTXGf8Jv7sMYByPrKahRbnmZEmGzRgoxGRbWqmmXuPDW6jPJSUAdpZRZn6E5B9935LtWD5gHAPpZQAh"); + assertInvalidAddress("AATHHjFhvpWXksjxJri6yaRkjTAGML2wQ7B2srLFSFXCfQy4C4UdLx5gMLBaxtfvjLe54ZfdSyRDyH94gH9Z17WpSeoBnG6"); + assertInvalidAddress("B1NiHMasw7bQsTyGLYGWh3RyUtvfJtzPyKj7NoGxMr9nJwZ4Un7vzM69EV2xpduUYEf3YMFPF58QvBmttLrUoJYDJzdVWXY"); + assertInvalidAddress("C4ak4b51DGLhGm9sPCXzHe88r9J6bWbJo5LzG4jBVjfBKhqmiQseAUcPkeSwNeNZWtVxSHuTNAk8tRZCbpZcY1rZGvGrEqZ"); + assertInvalidAddress("D8NnxEjgt5LcKLHEydcB7eUhQ1RUMSaHwN6f4w7rYcNH35ti897AAbtAk5Yon72oUee5t45ByaM651ytVVYhDtAFGfuhPBL"); + assertInvalidAddress("E3LDMVt5dzdW9NfnUT7cmUBWjeTuKiYD6Uuq6LB1ETVaJLFwrEZekxLAtLhbcSwPQg2kPeTYG4gZJMK5qmSqTUoDKUvaR7N"); + assertInvalidAddress("F3Jj7vhhZSrWRJirVT3tVSQroPzFdFxB2ChN5276kXEP93We6KJ532ZMQAj136yrrG4exJYtcYVfNZQNGNnV2rkh2bwetrP"); + assertInvalidAddress("G4Lo9KzkK9LUpKL1n6StdrYG2oBKuTqaUEyHekA9jxy7T3VZj4og91CKtfBbuiPVgzYYhL7vpDsk4fDFpyrQ8k18LovPrw7"); + assertInvalidAddress("H1Sv8KX7bnxEXmo6fh4x2y9pfiftbxnsx1QM6bhBjWgwaEqNBeyPdqi1mQKB6JSZqU1u4oEQcsUFY8yyTeP9Jh3s2jZ6qVc"); + assertInvalidAddress("I5wP79cd9oz4Pw2Gdj5Youi5J2Jft1QRwBdtRdx7bFe782Gxacw9hCpQ3jbyqs5U18EWiuzpJaAgiGrwD3aC17He7vWRxQM"); + assertInvalidAddress("J7icdECeckdDAofi9aBLwMZDvLnABZiT6XHFTu115E2pe3GqqBkFPL9dtbRUMrL5ZicQ6JMs2VamZNYyCWNt9jkEJvE4Fir"); + assertInvalidAddress("K7vZECGxgqcgtLTcbyVH9d4z68Z3SYgJFD3jiyrimiiR87qqVQUXdoTciVr6JQCMSY3qNxk1SDqVFAmv7dFbhWA6AJBtQq1"); + assertInvalidAddress("L5zCC9MAJVqXEogZ8YZFAHFN3Pp9J47vi3bXKV3tvP69FoSNzpsCgBHATYpJY7Aho958RyvbwxMDYFNqK5UiFe6WKy8xxQC"); + assertInvalidAddress("M6kpYUXiyT7TBraQqtbxuZRtAzb9Px8vz7FWtBtM2UwqP68jYGoKENeDs8u6SGK7msXevM97AaB3ZYM9pk55uXSf9dbu8BD"); + assertInvalidAddress("N8FMot8AeSzBNeNGzAwXb8eN3rNmb8neUWWL7epoVE6mSJdQ87p1byWcs6NTLQkJUkgUx7t51WhfKSQcJFrZq9EBPcWsQmP"); + assertInvalidAddress("O4c4hCkAWf7RbVXsiZu4v9S9BQ5KdWZWreykUpHdMAFxYerg4xsg4KPCoR5bq9z9ahMKb3sHuHW627zCdcBRPLe8Ft2qvDp"); + assertInvalidAddress("P6PhpdMqmcgAwV8iRjcoboFprEHgrPgYEYLzjaeU5HvRJAXu1yzWM1Q3D9zxvju6bwEv54Sccdp2S33HPx1s86uGSsZ9K8Q"); + assertInvalidAddress("Q8DP7okc13sjL7Hens8obs3mgpKyXzteccUHpuRhDyRsfxyFx8KNeBdGUNHYxWcc4pVk4gdUQq9hiZhw5K8m4pxC5rtRXo1"); + assertInvalidAddress("R9xFn1dKj6Rdsp6ZwCkpfDC2n98HHLa6M7DmV47qkYhm9vbeer1bbPoJopG4DYhspgNmTpbwe2nco3o7AsaMrT4D6MQaVxg"); + assertInvalidAddress("S89QzLjkGJiFaYdb83wrQEEtMNBizmaiz3kHYUmwZURm5mbDcGLEnBpGrqSuJVxQjHE4cpiqFF1A6Gk2ZB2uwDUb1nms8Dg"); + assertInvalidAddress("T7iiGq9NExdRwgUV6WcT58DPWA8SL8VyDdBEqY6vG8mwHedpWzU1e1Z7k5wc8DL7nyWfCYFsZKn2KcP5DXwYcvwRB9Xy4zG"); + assertInvalidAddress("U2Ec4PmgrMuVahrYGAkS5jhisb1w8b63ra33t5eoY2e5V9syeyhqU8xUqSLe22WrxMYk4gve6isGb7EpU7RLrRGzANDVUtz"); + assertInvalidAddress("V4U3d86FzU3bKvgjNNs4fuGDNRYQFoLx3XAKKtq4SiwbPLwmTpo9P2jMbwvby4YZmPCkTu6RgAdv1XispG1qtnT64Vbkn6W"); + assertInvalidAddress("W1JeT4tzGAw4b9EiLsH1pC3dWLzTSGGaaVAGux1z8PuqCH8ziEmEMDZeEhcnxgjz8n27bS7oUvb1UcUYbYTHt5jaKwicC1x"); + assertInvalidAddress("X5EAA2wJRhC3QomYEgu6NMg7AW77LfjcxhhK7YHT94XnRwGsEWmNW71Ct3UiNLbs3ab1xiWVfu4ymF3enZ19hgfM93Xp2dz"); + assertInvalidAddress("Y3YWhEp25gw1gpVtdWxdyd9Fgj5quPGuy6hC9h8knpC58YPtB4TxpZEfhb9cRgXezxCXnq1GTmQ49TCQ3CsJ8gFW3q4WN95"); + assertInvalidAddress("Z6q1WzvdPpZhiCEqzugGSPM5b39S6cdn64DtF6FcvtFg4d9hNaxXevvbkEYM8GwEH4ihW98pd6SVkR8meDu21oDbPgncEz9"); + assertInvalidAddress("a6GQ2ZBomH5WojBELhHSmQXmgNDecv65ebzhhMMkmxZVhiJxRixh29ZJ7oQK4Yg8FF4qjYMv1ykA4JA7n6KoWy18DJPaUVs"); + assertInvalidAddress("b4jmgQskiHLW8PtdX3X662eMLN2hnAnTzJsu4PLbggomVuhVFuGaUWEDntQSzpcKXgReLZAeYGEaYWPpFsTgmwEpLhEfN4m"); + assertInvalidAddress("c9Nc2eUQXBnR2PpcRrKqbA2A4DSEeLDt7LhYy4aKmvCr6JCRGtXZxrePTJpbMC83MyR8W2KCCfVwfhygpLa437DBC2NQpYH"); + assertInvalidAddress("dA5bw23DkMoXpYXbX8aPncW8ZgHKyPLngJkJS7tGPegjZycvquRKktFFf46n3VFxfrLT2UnrCkBE4LrqbWnDfRUW6Nfut6g"); + assertInvalidAddress("e49CqpKzaDG8yV6rNRuQ2VVrZi8FCTVJ4b2C5AsVkqNtGFGKCBAjTeDFhWkGJvLfFTNDUkxsgQ9bP41Uhx5G3gdCB4k6oVJ"); + assertInvalidAddress("f6x8ajQFtUReSoNYvCc7RjXpzZRngNaUU6xTAc6YvFsw8uTzwL4WDuM71cuBAW5oNAQrBjmPQmBZV8DYav6bAEVQQR7mLh2"); + assertInvalidAddress("g1Pdnp4qJ4qYPu11EEBeUhTU7Bb3b7k1Rg8hAxAqJf2XDncBqrePPrNGsgqaLeRdpBFfsZRCHTabpHvQWMzUsiLNJGFvGnR"); + assertInvalidAddress("h3DzqGR97GwWe7G3YC7D4mgCA7b68zMQmAq4r559JtcAjbG1GeXvjedTnHys9aS9vL9iG8djvVeZMdVi4S8Kc6oX2Zovt8r"); + assertInvalidAddress("iALCRLmNc8WSzbyjri4dgw9iaL6a2Uf3QYsUwsXseJfb7FCQnUgGja2ATbYpLREQTxc5VLvExQpwqFsmvhdvpx5VEVAgmyJ"); + assertInvalidAddress("j3bKBtupDeZcauKPMvzBLWHnHFn5q6ZVxDCZdvmvXJ7sgT5cQKQ8iJgARzJQ1SpzSKLwb6Kx8EVebbv4d9Y4o8AcKSQgWyx"); + assertInvalidAddress("k1u7EQDv25yGG67XtWkCSDYsbSpHFwTKESVpAxM8XSPqFrYDWPQV8KWReVmPR4piLtByikCpTgvWW5tXaxrfSuRXKo8DwWh"); + assertInvalidAddress("l6kW6xriMLxTTJBHdErywLZHQp6HchZwmZLXM4GyTfV3GFsZ7rF9JzrLx7ykNe75bqjii4b3kVX7SU98AEZmfyExNQFnZTm"); + assertInvalidAddress("m5hY6yRHGDKbjYbZiveah4DntcKbGAaqnLR5QWnF8hFP7NDouKXcFPKfWX7q8jaXdKKTJsi3Aec36Lbz4rapaxB7PKtM4mw"); + assertInvalidAddress("n9kXzy9NJhS5txFKQzh88XFFx7vi8XoLGc1UMXp7uFdK5NoaABPvemWRGm4zb6VNqgh786Q9ch1muayCQnszr8YzRYrxwWE"); + assertInvalidAddress("o7bZkG5wPryY1Ld3TPhcjECcDFeP63neFD5cA1iXC8ZPJtBsAwXFFsdbyR9eV7oKp8bB26TKWyjU86ibaJVg9RtoR5Hr18N"); + assertInvalidAddress("p37iEL6AWJaZm357fSqgQPQLzFigpuzTT9wTmUaGM7VfEzf9qmcZjP76y8bnN51bjqBKsMYPF7XLxA87vMhGAiAe5jdBhtX"); + assertInvalidAddress("q6TkiMru8ELSdXeX1DWquJ9WH7yxjFC3QdGovpUZYG7v2nCDhCxyDdz8CcqpRaqzf55xg8suNHbVAgNaUVCeCv1yETsaFJq"); + assertInvalidAddress("r9HCvadnFDSjDUAqhzorvaZmPeRqKgGG5GUnr3ph2zQteQ3TgGEvq6oiK2H967UUoUJKCH9yXmfuX3oDiyHNm2xaP7ZcPqa"); + assertInvalidAddress("s7XmwW4Q2hdN4VqEcES8KnXGTaE7Dq2w4gsjzDfBAQKWZ3S45DGBSpwb32o7Y1STZvQDtxPKVXtgbScjWizvpbC8SNYcabo"); + assertInvalidAddress("t5ZmMnkKCU8h44srsPYr5JVkvDhHBZG44QQYE2gEbUhTN4orqDgiKnMTyQYPj3XC1M5bST42rNUfR9LuWpGvkJmoCZaFvYJ"); + assertInvalidAddress("uARGu2zWWtu9k9pb1TK6zmS1ASnNVX1Kj9H3nRq6MGGhLcNshUHML4gid6grswb3aaVq47t5qqre41isaExKFDGRD9LCUHG"); + assertInvalidAddress("v1tYtfLchdzUrJow8RqMbGcH1ZAtWzSUmJHaQnrG1EypRkktGDo3tqXdk5yby64rgJjBgmMjJJ6NkRWsukHi9RTQ3XQBbqB"); + assertInvalidAddress("w4Q6hafxurmZWPSHpmp8R2GSxYiAw5DJQ3fcS9irCF3zKRnYarXLxfQPd5t24rEyYRCDAUSV1DL7CTY5guRE6dk4FgXrw5H"); + assertInvalidAddress("x7o5bwgPr1JMLMZQX8RoDT6JYfD7GwFanQa3QcMsUNbaj3siUp5i5okZxF237s5MhjWmWxWyVDxNvTq1c9MXSnQJHpNKq4d"); + assertInvalidAddress("yAENYbDcrf49FUHHDdSbtCF4EEG6LxxjvE7CsusYoi6bQUoUCfqH9yqjbuTP8i8e5vPYzWqLj1VpdMSZ4DQdhhUB8MtdvcL"); + assertInvalidAddress("z2kefw5QxZ9CmzdPCnFD766oaJ1yU1NXr5WwD2xZTpTFAGL8HRjzUzmXkdo2fqiiZyiTVAYMfxMtfJvo5QLEkHUnJEPmps4"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ChauchaTest.java b/assets/src/test/java/bisq/asset/coins/ChauchaTest.java new file mode 100644 index 0000000000..7b77435373 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ChauchaTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ChauchaTest extends AbstractAssetTest { + + public ChauchaTest() { + super(new Chaucha()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("cTC7AodMWM4fXsG1TDu4JLn2qKQoMg4F9N"); + assertValidAddress("caWnffHrx8wkQqcSVJ7wpRvN1E7Ztz7kPP"); + assertValidAddress("ciWwaG4trw1vQZSL4F4phQqznK4NgZURdQ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1cTC7AodMWM4fXsG1TDu4JLn2qKQoMg4F9N"); + assertInvalidAddress("cTC7AodMWM4fXsG1TDu4JLn2qKQoMg4F9XN"); + assertInvalidAddress("cTC7AodMWM4fXsG1TDu4JLn2qKQoMg4F9N#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/CloakCoinTest.java b/assets/src/test/java/bisq/asset/coins/CloakCoinTest.java new file mode 100644 index 0000000000..4f6b3f7cf3 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CloakCoinTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class CloakCoinTest extends AbstractAssetTest { + + public CloakCoinTest() { + super(new CloakCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("C3MwbThsvquwA4Yg6recThXpAhR2hvRKws"); + assertValidAddress("B6MwbThsvquwA4Yg6recThXpAhR2hvKRsz"); + assertValidAddress("BCA31xPpijxiCuTQeYMpMTQsTH1m2jTg5t"); + assertValidAddress("smYmLVV33zExmaFyVp3AUjU3fJMK5E93kwzDfMnPLnEBQ7BoHZkSQhCP92hZz7Hm24yavCceNeQm8RHekqdvrhFe8gX7EdXNwnhQgQ"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1sA31xPpijxiCuTQeYMpMTQsTH1m2jTgtS"); + assertInvalidAddress("BsA31xPpijxiCuTQeYMpMTQsTH1m2jTgtSd"); + assertInvalidAddress("bech3ThsvquwA4Yg6recThXpAhR2hvRKws"); + assertInvalidAddress("smYmLYcVVzExmaFyVp3AUjU3fJMK5E93kwzDfMnPLnEBQ7BoHZkSQhCP92hZz7Hm24yavCceNeQm8RHekqdv"); + assertInvalidAddress("C3MwbThsvquwA4Yg6recThXpAhR2hvRKw"); + assertInvalidAddress(" B6MwbThsvquwA4Yg6recThXpAhR2hvKRsz"); + assertInvalidAddress("B6MwbThsvquwA4Yg6recThXpAhR2hvKRsz "); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/CounterpartyTest.java b/assets/src/test/java/bisq/asset/coins/CounterpartyTest.java new file mode 100644 index 0000000000..c56f08090d --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CounterpartyTest.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class CounterpartyTest extends AbstractAssetTest { + + public CounterpartyTest() { + super(new Counterparty()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); + assertValidAddress("1KBbojKRf1YnJKp1YK5eEz9TWlS4pFEbwS"); + assertValidAddress("1AtLN6BMlW0Rwj800LNcBBR2o0k0sYVuIN"); + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + assertInvalidAddress("122FRU9f3fx7Hty641DRK6S3sbf3"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/CreditsTest.java b/assets/src/test/java/bisq/asset/coins/CreditsTest.java new file mode 100644 index 0000000000..7a69999978 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CreditsTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class CreditsTest extends AbstractAssetTest { + + public CreditsTest() { + super(new Credits()); + } + + + @Test + public void testValidAddresses() { + assertValidAddress("CfXBhPhSxx1wqxGQCryfgn6iU1M1XFUuCo"); + assertValidAddress("CMde7YERCFWkCL2W5i8uyJmnpCVj8Chhww"); + assertValidAddress("CcbqU3MLZuGAED2CuhUkquyJxKaSJqv6Vb"); + assertValidAddress("CKaig5pznaUgiLqe6WkoCNGagNMhNLtqhK"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1fXBhPhSxx1wqxGQCryfgn6iU1M1XFUuCo32"); + assertInvalidAddress("CMde7YERCFWkCL2W5i8uyJmnpCVj8Chh"); + assertInvalidAddress("CcbqU3MLZuGAED2CuhUkquyJxKaSJqv6V6#"); + assertInvalidAddress("bKaig5pznaUgiLqe6WkoCNGagNMhNLtqhKkggg"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/CroatTest.java b/assets/src/test/java/bisq/asset/coins/CroatTest.java new file mode 100644 index 0000000000..8df3488670 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/CroatTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class CroatTest extends AbstractAssetTest { + + public CroatTest() { + super(new Croat()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("CsZ46x2mzB3GhjrC2Lt7oZ4Efmj8USUjVM7Bdz8B8EF6bQwN84NzSti7RwLZcFoZG5NR1iaiZY8GP2KwumVc1jGzHLvBzAv"); + assertValidAddress("CjxZDcoWCsx1wmYkmJcFpSTgqpjoFGRW9dQT8JqgwvkBaU6Q3X4MJ4QjVkNUM7GHp6NjYaTrKeH4bSRTK3mCYsHf2818vzv"); + assertValidAddress("CoCJje3bcEH2dkvb5suRy2ZiBtPBeBqWaY9sbMLEtqEvDn969eDx1zqV4FP8erJSJFK5Br6GheGnJJG7BDtG9XFbFcMkUJU"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("ZsZ46x2mzB3GhjrC2Lt7oZ4Efmj8USUjVM7Bdz8B8EF6bQwN84NzSti7RwLZcFoZG5NR1iaiZY8GP2KwumVc1jGzHLvBzAv"); + assertInvalidAddress(""); + assertInvalidAddress("CjxZDcoWCsx1wmYkmJcFpSTgqpjoFGRW9dQT8JqgwvkBaU6Q3X4MJ4QjV#NUM7GHp6NjYaTrKeH4bSRTK3mCYsHf2818vzv"); + assertInvalidAddress("CoCJje3bcEH2dkvb5suRy2ZiBtPBeBqWaY9sbMLEtqEvDn969eDx1zqV4FP8erJSJFK5Br6GheGnJJG7BDtG9XFbFcMkUJUuuuuuuuu"); + assertInvalidAddress("CsZ46x2mzB3GhjrC2Lt7oZ4Efmj8USUjVM7Bdz8B8EF6bQwN84NzSti7RwLZcFoZG5NR1iaiZY8GP2KwumVc1jGzHLvBzAv11111111"); + assertInvalidAddress("CjxZDcoWCsx1wmYkmJcFpSTgqpjoFGRW9dQT8JqgwvkBaU6Q3X4MJ4QjVkNUM7GHp6NjYaTrKeH4bSRTK3m"); + assertInvalidAddress("CjxZDcoWCsx1wmYkmJcFpSTgqpjoFGRW9dQT8JqgwvkBaU6Q3X4MJ4QjVkNUM7GHp6NjYaTrKeH4bSRTK3mCYsHf2818vzv$%"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DSTRATest.java b/assets/src/test/java/bisq/asset/coins/DSTRATest.java new file mode 100644 index 0000000000..d371044cfa --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DSTRATest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DSTRATest extends AbstractAssetTest { + + public DSTRATest() { + super(new DSTRA()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd7"); + assertValidAddress("DQcAKx5bFoeRwAEHE4EHQykyq8u2M1pwFa"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("DGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd77"); + assertInvalidAddress("DGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd"); + assertInvalidAddress("dGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd7"); + assertInvalidAddress("FGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd7"); + assertInvalidAddress("fGiwGS8n3tJZuKxUdWF6MyTYvv6xgDcyd7"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DarkPayTest.java b/assets/src/test/java/bisq/asset/coins/DarkPayTest.java new file mode 100644 index 0000000000..caa947b23c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DarkPayTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DarkPayTest extends AbstractAssetTest { + + public DarkPayTest() { + super(new DarkPay()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DXSi43hpVRjy1yGF3Vh3nnCK6ydwyWxVAD"); + assertValidAddress("DmHHAyocykozeW8fwJxPbn1o83dT4fDtoR"); + assertValidAddress("RSBxWDDMNxCKtnHvqf8Dsif5sm52ik36rW"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("DXSi43hpVRjy1yGF3Vh3nnCK6ydwyWxVAd"); + assertInvalidAddress("DmHHAyocykozeW888888fwJxPbn1o83dT4fDtoR"); + assertInvalidAddress("RSBxWDDMNxCKtnHvqf8Dsif5sm52ik35rW#"); + assertInvalidAddress("3GyEtTwXhxbjBtmAR3CtzeayAyshtvd44P"); + assertInvalidAddress("1CnXYrivw7pJy3asKftp41wRPgBggF9fBw"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DashTest.java b/assets/src/test/java/bisq/asset/coins/DashTest.java new file mode 100644 index 0000000000..ff3cdff2ed --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DashTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DashTest extends AbstractAssetTest { + + public DashTest() { + super(new Dash()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("XjNms118hx6dGyBqsrVMTbzMUmxDVijk7Y"); + assertValidAddress("XjNPzWfzGiY1jHUmwn9JDSVMsTs6EtZQMc"); + assertValidAddress("XnaJzoAKTNa67Fpt1tLxD5bFMcyN4tCvTT"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1XnaJzoAKTNa67Fpt1tLxD5bFMcyN4tCvTT"); + assertInvalidAddress("XnaJzoAKTNa67Fpt1tLxD5bFMcyN4tCvTTd"); + assertInvalidAddress("XnaJzoAKTNa67Fpt1tLxD5bFMcyN4tCvTT#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DecredTest.java b/assets/src/test/java/bisq/asset/coins/DecredTest.java new file mode 100644 index 0000000000..2f5a476a8f --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DecredTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class DecredTest extends AbstractAssetTest { + + public DecredTest() { + super(new Decred()); + } + + @Override + public void testValidAddresses() { + // TODO Auto-generated method stub + assertValidAddress("Dcur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx"); + assertValidAddress("Dsur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx"); + assertValidAddress("Deur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx"); + } + + @Override + public void testInvalidAddresses() { + // TODO Auto-generated method stub + assertInvalidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + assertInvalidAddress("a1HwTdCmQV3NspP2QqCGpehoFpi8NY4Zg3"); + assertInvalidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DeepOnionTest.java b/assets/src/test/java/bisq/asset/coins/DeepOnionTest.java new file mode 100644 index 0000000000..dd21153b36 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DeepOnionTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DeepOnionTest extends AbstractAssetTest { + + public DeepOnionTest() { + super(new DeepOnion()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DYQLyJ1CcxJyRBujtKdv2JDkQEkEkAzNNA"); + assertValidAddress("DW7YKfPgm7fdTNGyyaSVfQhY7ccBoeVK5D"); + assertValidAddress("DsA31xPpijxiCuTQeYMpMTQsTH1m2jTgtS"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1sA31xPpijxiCuTQeYMpMTQsTH1m2jTgtS"); + assertInvalidAddress("DsA31xPpijxiCuTQeYMpMTQsTH1m2jTgtSd"); + assertInvalidAddress("DsA31xPpijxiCuTQeYMpMTQsTH1m2jTgt#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DextroTest.java b/assets/src/test/java/bisq/asset/coins/DextroTest.java new file mode 100644 index 0000000000..d85d5751d0 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DextroTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DextroTest extends AbstractAssetTest { + + public DextroTest() { + super(new Dextro()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DP9LSAMzxNAuSei1GH3pppMjDqBhNrSGov"); + assertValidAddress("D8HwxDXPJhrSYonPF7YbCGENkM88cAYKb5"); + assertValidAddress("DLhJt6UfwMtWLGMH3ADzjqaLaGG6Bz96Bz"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("DP9LSAMzxNAuSei1GH3pppMjDqBhNrSG0v"); + assertInvalidAddress("DP9LSAMzxNAuSei1GH3pppMjDqBhNrSGovx"); + assertInvalidAddress("DP9LSAMzxNAuSei1GH3pppMjDqBhNrSG#v"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DogecoinTest.java b/assets/src/test/java/bisq/asset/coins/DogecoinTest.java new file mode 100644 index 0000000000..9351a87724 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DogecoinTest.java @@ -0,0 +1,43 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DogecoinTest extends AbstractAssetTest { + + public DogecoinTest() { + super(new Dogecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DEa7damK8MsbdCJztidBasZKVsDLJifWfE"); + assertValidAddress("DNkkfdUvkCDiywYE98MTVp9nQJTgeZAiFr"); + assertValidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); + assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxgs"); + assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DoichainTest.java b/assets/src/test/java/bisq/asset/coins/DoichainTest.java new file mode 100644 index 0000000000..5465a3415c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DoichainTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import org.junit.Test; +import bisq.asset.AbstractAssetTest; + +public class DoichainTest extends AbstractAssetTest { + + public DoichainTest() { + super(new Doichain()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("NGHV9LstnZfrkGx5QJmYhEepbzc66W7LN5"); + assertValidAddress("N4jeY9YhU49qHN5wUv7HBxeVZrFg32XFy7"); + assertValidAddress("6a6xk7Ff6XbgrNWhSwn7nM394KZJNt7JuV"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("NGHV9LstnZfrkGx5QJmYhEepbzc66W7LN5x"); + assertInvalidAddress("16iWWt1uoG8Dct56Cq6eKHFxvGSDha46Lo"); + assertInvalidAddress("38BFQkc9CdyJUxQK8PhebnDcA1tRRwLDW4"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DonuTest.java b/assets/src/test/java/bisq/asset/coins/DonuTest.java new file mode 100644 index 0000000000..04aba7be7d --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DonuTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DonuTest extends AbstractAssetTest { + + public DonuTest() { + super(new Donu()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("NS5cGWdERahJ11pn12GoV5Jb7nsLzdr3kP"); + assertValidAddress("NU7nCzyQiAtTxzXLnDsJu4NhwQqrnPyJZj"); + assertValidAddress("NeeAy35aQirpmTARHEXpP8uTmpPCcSD9Qn"); + assertValidAddress("NScgetCW5bqDTVWFH3EYNMtTo5RcvDxD6B"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("NScgetCW5bqDTVWFH3EYNMtTo5Rc#DxD6B"); + assertInvalidAddress("NeeAy35a0irpmTARHEXpP8uTmpPCcSD9Qn"); + assertInvalidAddress("neeAy35aQirpmTARHEXpP8uTmpPCcSD9Qn"); + assertInvalidAddress("NScgetCWRcvDxD6B"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/DragonglassTest.java b/assets/src/test/java/bisq/asset/coins/DragonglassTest.java new file mode 100644 index 0000000000..51e4974f90 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/DragonglassTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DragonglassTest extends AbstractAssetTest { + + public DragonglassTest() { + super(new Dragonglass()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("dRGLhxvCtLk1vfSD3WmFzyTN5ph2gZYvkZfxvLSrcdry95x4PPJrCKBTKDEFZYTw4bCGqoiaUWxNd8B41vqXaTY72Vi2XcvikX"); + assertValidAddress("dRGLjS5v91tDd4GDZeahUj95nkXSNQs5DMY1YStLN2hSNWD67iZh7ED7oDw841Kx6oUYouZaXmBNFcqSptNZ4dL94CbZbF53jt"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("dRGLjS5v91tDd4GDZeahUj95nkXSNQs5DMY1YStLN2hSNWD67iZh7ED7oDw841Kx6oUYouZaXmBNFcqSptNZ4dL94CbZbF53j"); + assertInvalidAddress("dRGLjS5v91tDd4GDZeahUj95nkXSNQs5DMY1YStLN2hSNWD67iZh7ED7oDw841Ko6oUYouZaXmBNFcqSptNZ4dL94oUCifk4048"); + assertInvalidAddress("DRGLhxvCtLk1vfSD3WmFzyTN5ph2gZYvkZfxvLSrcdry95x4PPJrCKBTKDEFZYTw4bCGqoiaUWxNd8B41vqXaTY72Vi2XcvikX"); + assertInvalidAddress("drglhxvCtLk1vfSD3WmFzyTN5ph2gZYvkZfxvLSrcdry95x4PPJrCKBTKDEFZYTw4bCGqoiaUWxNd8B41vqXaTY72Vi2XcvikX"); + assertInvalidAddress("dRgLjS5v91tDd4GDZeahUj95nkXSNQs5DMY1YStLN2hSNWD67iZh7ED7oDw841Kx6oUYouZaXmBNFcqSptNZ4dL94CbZbF53jt"); + assertInvalidAddress("dRGlhxvCtLk1vfSD3WmFzyTN5ph2gZYvkZfxvLSrcdry95x4PPJrCKBTKDEFZYTw4bCGqoiaUWxNd8B41vqXaTY72Vi2XcvikX"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/EmercoinTest.java b/assets/src/test/java/bisq/asset/coins/EmercoinTest.java new file mode 100644 index 0000000000..45aef5c76d --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/EmercoinTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class EmercoinTest extends AbstractAssetTest { + + public EmercoinTest() { + super(new Emercoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("EedXjU95QcVHLEFAs5EKNT9UWqAWXTyuhD"); // Regular p2pkh address + assertValidAddress("EHNiED27Un5yKHHsGFDsQipCH4TdsTo5xb"); // Regular p2pkh address + assertValidAddress("eMERCoinFunera1AddressXXXXXXYDAYke"); // Dummy p2sh address + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("19rem1SSWTphjsFLmcNEAvnfHaBFuDMMae"); // Valid BTC + assertInvalidAddress("EedXjU95QcVHLEFAs5EKNT9UWqAWXTyuhE"); // Invalid EMC address + assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); // Valid DOGE + + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ErgoTest.java b/assets/src/test/java/bisq/asset/coins/ErgoTest.java new file mode 100644 index 0000000000..213769cd81 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ErgoTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ErgoTest extends AbstractAssetTest { + + public ErgoTest() { + super(new Ergo()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("9fRAWhdxEsTcdb8PhGNrZfwqa65zfkuYHAMmkQLcic1gdLSV5vA"); + assertValidAddress("25qGdVWg2yyYho8uC1pLtc7KxFn4nEEAwD"); + assertValidAddress("23NL9a8ngN28ovtLiKLgHexcdTKBbUMLhH"); + assertValidAddress("7bwdkU5V8"); + assertValidAddress("BxKBaHkvrTvLZrDcZjcsxsF7aSsrN73ijeFZXtbj4CXZHHcvBtqSxQ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("9fRAWhdxEsTcdb8PhGNrZfwqa65zfkuYHAMmkQLcic1gdLSV5vAaa"); + assertInvalidAddress("25qGdVWg2yyYho8uC1pLtc7KxFn4nEEAwDaa"); + assertInvalidAddress("23NL9a8ngN28ovtLiKLgHexcdTKBbUMLhHaa"); + assertInvalidAddress("7bwdkU5V8aa"); + assertInvalidAddress("BxKBaHkvrTvLZrDcZjcsxsF7aSsrN73ijeFZXtbj4CXZHHcvBtqSxQ#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/EtherClassicTest.java b/assets/src/test/java/bisq/asset/coins/EtherClassicTest.java new file mode 100644 index 0000000000..0aadbd7800 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/EtherClassicTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class EtherClassicTest extends AbstractAssetTest { + + public EtherClassicTest() { + super(new EtherClassic()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("0x353c13b940aa5eed75aa97d477954289e7880bb8"); + assertValidAddress("0x9f5304DA62A5408416Ea58A17a92611019bD5ce3"); + assertValidAddress("0x180826b05452ce96E157F0708c43381Fee64a6B8"); + + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + assertInvalidAddress("N22FRU9f3fx7Hty641D5cg95kRK6S3sbf3"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/FaircoinTest.java b/assets/src/test/java/bisq/asset/coins/FaircoinTest.java new file mode 100644 index 0000000000..4f4b85186f --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/FaircoinTest.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class FaircoinTest extends AbstractAssetTest { + + public FaircoinTest() { + super(new Faircoin()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("fLsJC1Njap5NxSArYr5wCJbKBbTQfWikY6"); + assertValidAddress("FZHzHraqjty2Co7TinwcsBtPKoz5ANvgRd"); + assertValidAddress("fHbXBBBjU1xxEVmWEtAEwXnoBDxxsxfvxg"); + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("FLsJC1Njap5NxSArYr5wCJbKBbTQfWikY6"); + assertInvalidAddress("fZHzHraqjty2Co7TinwcsBtPKoz5ANvgRd"); + assertInvalidAddress("1HbXBBBjU1xxEVmWEtAEwXnoBDxxsxfvxg"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/FourtyTwoTest.java b/assets/src/test/java/bisq/asset/coins/FourtyTwoTest.java new file mode 100644 index 0000000000..76442ad0f8 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/FourtyTwoTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class FourtyTwoTest extends AbstractAssetTest { + + public FourtyTwoTest() { + super(new FourtyTwo()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("foUrDvc6vtJYMvqpx4oydJjL445udJ83M8rAqpkF8hEcbyLCp5MhvLaLGXtVYkqVXDG8YEpGBU7F241FtWXVCFEK7EMgnjrsM8"); + assertValidAddress("foUrFDEDkMGjV4HJzgYqSHhPTFaHfcpLM4WGZjYQZyrcCgyZs32QweCZEysK8eNxgsWdXv3YBP8QWDDWBAPu55eJ6gLf2TubwG"); + assertValidAddress("SNakeyQFcEacGHFaCgj4VpdfM3VTsFDygNHswx3CtKpn8uD1DmrbFwfM11cSyv3CZrNNWh4AALYuGS4U4pxYPHTiBn2DUJASoQw4B"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("fUrDvc6vtJYMvqpx4oydJjL445udJ83M8rAqpkF8hEcbyLCp5MhvLaLGXtVYkqVXDG8YEpGBU7F241FtWXVCFEK7EMgnjrsM8"); + assertInvalidAddress("UrFDEDkMGjV4HJzgYqSHhPTFaHfcpLM4WGZjYQZyrcCgyZs32QweCZEysK8eNxgsWdXv3YBP8QWDDWBAPu55eJ6gLf2TubwG"); + assertInvalidAddress("keyQFcEacGHFaCgj4VpdfM3VTsFDygNHswx3CtKpn8uD1DmrbFwfM11cSyv3CZrNNWh4AALYuGS4U4pxYPHTiBn2DUJASoQw4B!"); + assertInvalidAddress("akeyQFcEacGHFaCgj4VpdfM3VTsFDygNHswx3CtKpn8uD1DmrbFwfM11cSyv3CZrNNWh4AALYuGS4U4pxYPHTiBn2DUJASoQw4B"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/FujicoinTest.java b/assets/src/test/java/bisq/asset/coins/FujicoinTest.java new file mode 100644 index 0000000000..83b3e4ab59 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/FujicoinTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class FujicoinTest extends AbstractAssetTest { + + public FujicoinTest() { + super(new Fujicoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("FpEbvwxhmer2zSvqh61JtLiffu8Tk2abdo"); + assertValidAddress("7gcLWi78MFJ9akMzTAiug3uArvPch5LB6q"); + assertValidAddress("FrjN1LLWJj1DWVooBCdybBvmaEAqxMuuq8"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("FpEbvwxhmer2zSvqh61JtLiffu8Tk2abda"); + assertInvalidAddress("7gcLWi78MFJ9akMzTAiug3uArvPch5LB6a"); + assertInvalidAddress("fc1q3s2fc2xqgush29urtfdj0vhcj96h8424zyl6wa"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/GalilelTest.java b/assets/src/test/java/bisq/asset/coins/GalilelTest.java new file mode 100644 index 0000000000..70726fcf7c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/GalilelTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class GalilelTest extends AbstractAssetTest { + + public GalilelTest() { + super(new Galilel()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("UVwXGh5B1NZbYdgWThqf2cLdkEupVXEVNi"); + assertValidAddress("UbNJbC1hZgBH5tQ4HyrrQMEPswKxwwfziw"); + assertValidAddress("UgqDDV8aekEXFP7BWLmTNpSQfk7uVk1jCF"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1UgqDDV8aekEXFP7BWLmTNpSQfk7uVk1jCF"); + assertInvalidAddress("UgqDDV8aekEXFP7BWLmTNpSQfk7uVk1jCFd"); + assertInvalidAddress("UgqDDV8aekEXFP7BWLmTNpSQfk7uVk1jCF#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/GambleCoinTest.java b/assets/src/test/java/bisq/asset/coins/GambleCoinTest.java new file mode 100644 index 0000000000..64e5d46330 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/GambleCoinTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class GambleCoinTest extends AbstractAssetTest { + + public GambleCoinTest() { + super(new GambleCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("CKWCoP2Cog4gU3ARzNqGEqwDxNZNVEpPJP"); + assertValidAddress("CJmvkF84bW93o5E7RFe4VzWMSt4WcKo1nv"); + assertValidAddress("Caz2He7kZ8ir52CgAmQywCjm5hRjo3gLwT"); + assertValidAddress("CM2fRpzpxqyRvaWxtptEmRzpGCFE1qCA3V"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("CKWCoP2C0g4gU3ARzNqGEqwDxNZNVEpPJP"); + assertInvalidAddress("CJmvkF84bW93o5E7RFe4VzWMSt4WcKo1nvx"); + assertInvalidAddress("Caz2He7kZ8ir52CgAmQywC#m5hRjo3gLwT"); + assertInvalidAddress("DM2fRpzpxqyRvaWxtptEmRzpGCFE1qCA3V"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/GenesisTest.java b/assets/src/test/java/bisq/asset/coins/GenesisTest.java new file mode 100644 index 0000000000..94dd45c82a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/GenesisTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class GenesisTest extends AbstractAssetTest { + + public GenesisTest() { + super(new Genesis()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("STE5agX1VkUKZRTHBFufkQu6JtNP1QYJcd"); // Standard SegWit + assertValidAddress("SNMcFfcFkes6bWR5dviWQQAL4SYQg8T4Vu"); // Standard SegWit + assertValidAddress("SfMmJJdg8uDHK6ajurBNksry7zu3KHdbPv"); // Standard SegWit + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("genx1q5dlyjsktuztnwzs85as7vslqfddcmenhfc0ehl"); // Bech32 + assertInvalidAddress("genx1qxc0hp76tx9hse2evt8dx2k686nx42ljel5nenr"); // Bech32 + assertInvalidAddress("CT747k1CThgCxk4xRPQeJP6pyKiTfzRM1R"); // valid but unsupported legacy + assertInvalidAddress("CbGwkAWfLXjU2esjomFzJfKAFdUiKQjJUd"); // valid but unsupported legacy + assertInvalidAddress("0213ba949e295aabda252662ffed7c4c0906"); // random garbage + assertInvalidAddress("BwyzAAjVwV2mhR2WQ8SfXhHyUDoy4VL16zBc"); // random garbage + assertInvalidAddress("EpGQR83U34JRszcGENjegZLCoDLTwG6YWLBN7jVC"); // random garbage + assertInvalidAddress("Xp3Gv2JiP487Z8SULctveCKNM1ffpz5b3n"); // random garbage + } +} diff --git a/assets/src/test/java/bisq/asset/coins/GrinTest.java b/assets/src/test/java/bisq/asset/coins/GrinTest.java new file mode 100644 index 0000000000..83b602607a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/GrinTest.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class GrinTest extends AbstractAssetTest { + + public GrinTest() { + super(new Grin()); + } + + @Test + public void testValidAddresses() { + // grinbox + assertValidAddress("gVvk7rLBg3r3qoWYL3VsREnBbooT7nynxx5HtDvUWCJUaNCnddvY"); + assertValidAddress("grinbox://gVtWzX5NTLCBkyNV19QVdnLXue13heAVRD36sfkGD6xpqy7k7e4a"); + assertValidAddress("gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com"); + assertValidAddress("gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com:1220"); + assertValidAddress("grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com"); + assertValidAddress("grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com:1220"); + } + + @Test + public void testInvalidAddresses() { + // valid IP:port addresses but not supported in Bisq + assertInvalidAddress("0.0.0.0:8080"); + assertInvalidAddress("173.194.34.134:8080"); + assertInvalidAddress("127.0.0.1:8080"); + assertInvalidAddress("192.168.0.1:8080"); + assertInvalidAddress("18.101.25.153:8080"); + assertInvalidAddress("173.194.34.134:1"); + assertInvalidAddress("173.194.34.134:11"); + assertInvalidAddress("173.194.34.134:1111"); + assertInvalidAddress("173.194.34.134:65535"); + + // invalid IP:port addresses + assertInvalidAddress("google.com"); + assertInvalidAddress("100.100.100.100"); + assertInvalidAddress(".100.100.100.100:1222"); + assertInvalidAddress("100..100.100.100:1222."); + assertInvalidAddress("100.100.100.100.:1222"); + assertInvalidAddress("999.999.999.999:1222"); + assertInvalidAddress("256.256.256.256:1222"); + assertInvalidAddress("256.100.100.100.100:1222"); + assertInvalidAddress("123.123.123:1222"); + assertInvalidAddress("http://123.123.123:1222"); + assertInvalidAddress("1000.2.3.4:1222"); + assertInvalidAddress("999.2.3.4:1222"); + // too large port + assertInvalidAddress("173.194.34.134:65536"); + + assertInvalidAddress("gVvk7rLBg3r3qoWYL3VsREnBbooT7nynxx5HtDvUWCJUaNCnddvY1111"); + assertInvalidAddress("grinbox:/gVtWzX5NTLCBkyNV19QVdnLXue13heAVRD36sfkGD6xpqy7k7e4a"); + assertInvalidAddress("gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com."); + assertInvalidAddress("gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com:1220a"); + assertInvalidAddress("grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsAsomerelay.com"); + assertInvalidAddress("grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com1220"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/HatchTest.java b/assets/src/test/java/bisq/asset/coins/HatchTest.java new file mode 100644 index 0000000000..634bae0e02 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/HatchTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class HatchTest extends AbstractAssetTest { + + public HatchTest() { + super(new Hatch()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("XgUfhrcfKWTVprA1GGhTggAA3VVQy1xqNp"); + assertValidAddress("Xo88XjP8RD2w3k7Fd16UT62y3oNcjbv4bz"); + assertValidAddress("XrA7ZGDLQkiLwUsfKT6y6tLrYjsvRLrZQG"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1XrA7ZGDLQkiLwUsfKT6y6tLrYjsvRLrZQG"); + assertInvalidAddress("XrA7ZGDLQkiLwUsfKT6y6tLrYjsvRLrZQGd"); + assertInvalidAddress("XrA7ZGDLQkiLwUsfKT6y6tLrYjsvRLrZQG#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/HeliumTest.java b/assets/src/test/java/bisq/asset/coins/HeliumTest.java new file mode 100644 index 0000000000..0fa127d382 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/HeliumTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class HeliumTest extends AbstractAssetTest { + + public HeliumTest() { + super(new Helium()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("SPSXRJSwzGKxSiYXePf1vnkk4v9WKVLhZp"); + assertValidAddress("SbzXDLmMfWDJZ1wEikUVAMbAzM2UnaSt4g"); + assertValidAddress("Sd14293Zhxxur2Pim7NkjxPGVaJTjGR5qY"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1PSXRJSwzGKxSiYXePf1vnkk4v9WKVLhZp"); + assertInvalidAddress("SPSXRJSwzGKxSiYXePf1vnkk4v9WKVLhZpp"); + assertInvalidAddress("SPSSPSSPSGKxSiYXePf1vnkk4v9WKVLhZp"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/HorizenTest.java b/assets/src/test/java/bisq/asset/coins/HorizenTest.java new file mode 100644 index 0000000000..f79ae0e8cb --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/HorizenTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class HorizenTest extends AbstractAssetTest { + + public HorizenTest() { + super(new Horizen()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("znk62Ey7ptTyHgYLaLDTEwhLF6uN1DXTBfa"); + assertValidAddress("znTqzi5rTXf6KJnX5tLaC5CMGHfeWJwy1c7"); + assertValidAddress("t1V9h2P9n4sYg629Xn4jVDPySJJxGmPb1HK"); + assertValidAddress("t3Ut4KUq2ZSMTPNE67pBU5LqYCi2q36KpXQ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("zcKffBrza1cirFY47aKvXiV411NZMscf7zUY5bD1HwvkoQvKHgpxLYUHtMCLqBAeif1VwHmMjrMAKNrdCknCVqCzRNizHUq"); + assertInvalidAddress("AFTqzi5rTXf6KJnX5tLaC5CMGHfeWJwy1c7"); + assertInvalidAddress("zig-zag"); + assertInvalidAddress("0123456789"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/IdaPayTest.java b/assets/src/test/java/bisq/asset/coins/IdaPayTest.java new file mode 100644 index 0000000000..558786834e --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/IdaPayTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class IdaPayTest extends AbstractAssetTest { + + public IdaPayTest() { + super(new IdaPay()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("Cj6A8JJvovgSTiMc4r6PaJPrfwQnwnHDpg"); + assertValidAddress("D4SEkXMAcxRBu2Gc1KpgcGunAu5rWttjRy"); + assertValidAddress("CopBThXxkziyQEG6WxEfx36Ty46DygzHTW"); + assertValidAddress("D3bEgYWDS7fxfu9y1zTSrcdP681w3MKw6W"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("Cj6A8JJv0vgSTiMc4r6PaJPrfwQnwnHDpg"); + assertInvalidAddress("D4SEkXMAcxxRBu2Gc1KpgcGunAu5rWttjRy"); + assertInvalidAddress("CopBThXxkziyQEG6WxEfx36Ty4#DygzHTW"); + assertInvalidAddress("13bEgYWDS7fxfu9y1zTSrcdP681w3MKw6W"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/IridiumTest.java b/assets/src/test/java/bisq/asset/coins/IridiumTest.java new file mode 100644 index 0000000000..c9572aaaac --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/IridiumTest.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.asset.coins; +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class IridiumTest extends AbstractAssetTest { + public IridiumTest() { + super(new Iridium()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("ir2oHYW7MbBQuMzTELg5o6FRqXNwWCU1wNzFsJG3VUCT9qMwayNsdwaQ85NHC3vLFSQ1eWtAPsYpvV4tXpnXKM9M377BW5KQ4"); + assertValidAddress("ir2PK6y3hjq9wLqdTQnPQ2FXhCJqJ1pKXNXezZUqeUWbTb3T74Xqiy1Yqwtkgri934C1E9Ba2quJDDh75nxDqEQj1K8i9DQXf"); + assertValidAddress("ir3steHWr1FRbtpjWWCAaxhzNggzJK6tqBy3qFw32YGV4CJdRsgYrpLifA7ivGdgZGNRKbRtYUp9GKvxnFSRFWTt2XuWunRYb"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("ir2oHYW7MbBQuMzTELg5o6FRqXNwWCU1wNzFsJG3VUCT9qMwayNsdwaQ85NHC3vLFSQ1eWtAPsYpvV4tXpnXKM9M377BW5KQ4t"); + assertInvalidAddress("ir2PK6y3hjq9wLqdTQnPQ2FXhCJqJ1pKXNXezZUqeUWb#Tb3T74Xqiy1Yqwtkgri934C1E9Ba2quJDDh75nxDqEQj1K8i9DQXf"); + assertInvalidAddress(""); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/KekcoinTest.java b/assets/src/test/java/bisq/asset/coins/KekcoinTest.java new file mode 100644 index 0000000000..30e7dcc8b2 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/KekcoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class KekcoinTest extends AbstractAssetTest { + + public KekcoinTest() { + super(new Kekcoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("KHWHFVU5ZMUfkiYEMMuXRDv1LjD2j1HJ2H"); + assertValidAddress("KSXQWsaKC9qL2e2RoeXNXY4FgQC6qUBpjD"); + assertValidAddress("KNVy3X1iuiF7Gz9a4fSYLF3RehN2yGkFvP"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("1K5B7SDcuZvd2oUTaW9d62gwqsZkteXqA4"); + assertInvalidAddress("1GckU1XSCknLBcTGnayBVRjNsDjxqopNav"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/KnowYourDeveloperTest.java b/assets/src/test/java/bisq/asset/coins/KnowYourDeveloperTest.java new file mode 100644 index 0000000000..c270224583 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/KnowYourDeveloperTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class KnowYourDeveloperTest extends AbstractAssetTest { + + public KnowYourDeveloperTest() { + super(new KnowYourDeveloper()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("Yezk3yX7A8sMsgiLN1DKBzhBNuosZLxyxv"); + assertValidAddress("YY9YLd5RzEVZZjkm2fsaWmQ2QP9aHcnCu9"); + assertValidAddress("YeJowNuWXx2ZVthswT5cLMQtMapfr7L9ch"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("yezk3yX7A8sMsgiLN1DKBzhBNuosZLxyxv"); + assertInvalidAddress("yY9YLd5RzEVZZjkm2fsaWmQ2QP9aHcnCu9"); + assertInvalidAddress("yeJowNuWXx2ZVthswT5cLMQtMapfr7L9ch"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/KoreTest.java b/assets/src/test/java/bisq/asset/coins/KoreTest.java new file mode 100644 index 0000000000..699d80a599 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/KoreTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class KoreTest extends AbstractAssetTest { + + public KoreTest() { + super(new Kore()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("KViqqCDcdZn3DKJWGvmdUtmoDsxuGswzwU"); + assertValidAddress("KNnThWKeyJ5ibYL3JhuBacyjJxKXs2cXgv"); + assertValidAddress("bGcebbVyKD4PEBHeKRGX7cTydu1xRm63r4"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("KmVwB5dxph84tb15XqRUtfX1MfmP8WpWWW"); + assertInvalidAddress("Kt85555555555555c1QcQYE318zXqZUnjUB6fwjTrf1Xkb"); + assertInvalidAddress("33ny4vAPJHFu5Nic7uMHQrvCACYTKPFJ6r#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/KryptonTest.java b/assets/src/test/java/bisq/asset/coins/KryptonTest.java new file mode 100644 index 0000000000..1ac55127a7 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/KryptonTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class KryptonTest extends AbstractAssetTest { + + public KryptonTest() { + super(new Krypton()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("QQQ1LgQ1m8vX5tGrBZ2miS7A54Fmj5Qbij4UXT8nD4aqF75b1cpAauxVkjYaefcztV62UrDT1K9WHDeQWu4vpVXU2wezpshvex"); + assertValidAddress("QQQ1G56SKneSK1833tKjLH7E4ZgFwnqhqUb1HMHgYbnhaST56mukM1296jiYjTyTdMWnvH5FpWNAJWaQqwyPJHUR8qXRKBJy9o"); + assertValidAddress("QQQ1Bg61uUZhsNaTmUSZNcFgX2bk9wnAoYg9DSYZidDMJt7wVyccvMy8J7zRBoV5iT1pbraFUDWPQWWdXGPPws2P2ZGe8UzsaJ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("QQQ1Bg61uUZhsNaTmUSZNcFgX2bk9wnAoYg9DSYZidDMJt7wVyccvMy8J7zRBoV5iT1pbraFUDWPQWWdXGPPws2P2ZGe8"); + assertInvalidAddress("11QQQ1Bg61uUZhsNaTmUSZNcFgX2bk9wnAoYg9DSYZidDMJt7wVyccvMy8J7zRBoV5iT1pbraFUDWPQWWdXGPPws2P2ZGe8UzsaJ"); + assertInvalidAddress(""); + assertInvalidAddress("#RoUKWRwpsx1F"); + assertInvalidAddress("YQQ1G56SKneSK1833tKjLH7E4ZgFwnqhqUb1HMHgYbnhaST56mukM1296jiYjTyTdMWnvH5FpWNAJWaQqwyPJHUR8qXRKBJy9o"); + assertInvalidAddress("3jyRo3rcp9fjdfjdSGpx"); + assertInvalidAddress("QQQ1G56SKneSK1833tKjLH7E4ZgFwnqhqUb1HMHgYbnhaST56mukM1296jiYjTyTdMWnvH5FpWNAJWaQqwyPJHUR8qXRKBJy9#"); + assertInvalidAddress("ZOD1Bg61uUZhsNaTmUSZNcFgX2bk9wnAoYg9DSYZidDMJt7wVyccvMy8J7zRBoV5iT1pbraFUDWPQWWdXGPPws2P2ZGe8UzsaJ"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/LBRYCreditsTest.java b/assets/src/test/java/bisq/asset/coins/LBRYCreditsTest.java new file mode 100644 index 0000000000..79d014651b --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LBRYCreditsTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class LBRYCreditsTest extends AbstractAssetTest { + + public LBRYCreditsTest() { + super(new LBRYCredits()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("bYqg2q19uWmp3waRwptzj6o8e9viHgcA9z"); + assertValidAddress("bZEnLbYb3D29Sbo8QJdiQ2PQ3En6em31gt"); + assertValidAddress("rQ26jd9mqdfPizHZUdyMjUPgK6rRANPjne"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("Don'tBeSilly"); + assertInvalidAddress("_rQ26jd9mqdfPizHZUdyMjUPgK6rRANPjne"); + assertInvalidAddress("mzYvN2WuVLyp6RZE94rzzvZwBDfCdCse6i"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); + assertInvalidAddress("bYqg2q19uWmp3waRwptzj6o8e9viHgcA9a"); + assertInvalidAddress("bYqg2q19uWmp3waRwptzj6o8e9viHgcA9za"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/LiquidBitcoinTest.java b/assets/src/test/java/bisq/asset/coins/LiquidBitcoinTest.java new file mode 100644 index 0000000000..c4aea30995 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LiquidBitcoinTest.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class LiquidBitcoinTest extends AbstractAssetTest { + + + public LiquidBitcoinTest() { + super(new LiquidBitcoin()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("VJL6mu5gqT4pRzpd28Y6aXg9murwJpd25EBwMtrnCN82n6z6i5kMLKnN63nyaCgRuJWZu4EFFV7gp9Yb"); + assertValidAddress("Gq3AeVacy6EUWSJKsV4NScyYKvnM6Gf8We"); + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("lq1qqgu6g99aa4y7fly26gwj3k69t2kgx8eshn8gqacsul9yhpcgtvweyzuqt6cn3fjawvwaluq6ls6t9qnvg4jgwffyycwmgqh0h"); //no native segwit address support + assertInvalidAddress("lq1qqgu6g99aa4y7fly26gwj3k69t2kgx8eshn8gqacsul9yhpcgtvweyzuqt6cn3fjawvwaluq6ls6t9qnvg4jgwffyycwmgqsdf"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/LitecoinPlusTest.java b/assets/src/test/java/bisq/asset/coins/LitecoinPlusTest.java new file mode 100644 index 0000000000..c3a8b169d4 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LitecoinPlusTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class LitecoinPlusTest extends AbstractAssetTest { + + public LitecoinPlusTest() { + super(new LitecoinPlus()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("XGnikpGiuDTaxq9vPfDF9m9VfTpv4SnNN5"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("LgfapHEPhZbdRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW#"); + } +} + diff --git a/assets/src/test/java/bisq/asset/coins/LitecoinTest.java b/assets/src/test/java/bisq/asset/coins/LitecoinTest.java new file mode 100644 index 0000000000..f1fd9b4028 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LitecoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class LitecoinTest extends AbstractAssetTest { + + public LitecoinTest() { + super(new Litecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("Lg3PX8wRWmApFCoCMAsPF5P9dPHYQHEWKW"); + assertValidAddress("LTuoeY6RBHV3n3cfhXVVTbJbxzxnXs9ofm"); + assertValidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("LgfapHEPhZbdRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/LitecoinZTest.java b/assets/src/test/java/bisq/asset/coins/LitecoinZTest.java new file mode 100644 index 0000000000..e8f2e63ed0 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LitecoinZTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class LitecoinZTest extends AbstractAssetTest { + + public LitecoinZTest() { + super(new LitecoinZ()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("L17opZaVcRK4h9MV4KhkCmzUBa56BxSRb8A"); + assertValidAddress("L1EjNbAPVtg8jE9EyvbsA7epibZ9j8bdYmV"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/LytixTest.java b/assets/src/test/java/bisq/asset/coins/LytixTest.java new file mode 100644 index 0000000000..1274793802 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/LytixTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class LytixTest extends AbstractAssetTest { + + public LytixTest() { + super(new Lytix()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("8hTBZgtuexiUVMxCPcrPgMem7jurB2YJds"); + assertValidAddress("8hqgpDE5uSuyRrDMXo1w3y59SCxfv8sSsf"); + assertValidAddress("8wtCT2JHu4wd4JqCwxnWFQXhmggnwdjpSn"); + assertValidAddress("8pYVReruVqYdp6LRhsy63nuVgsg9Rx7FJT"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("6pYVReruVqYdp6LRhsy63nuVgsg9Rx7FJT"); + assertInvalidAddress("8mgfRRiHVxf4JZH3pvffuY6NrKhffh13Q"); + assertInvalidAddress("8j75cPWABNXdZ62u6ZfF4tDQ1tVdvJx2oh7"); + assertInvalidAddress("FryiHzNPFatNV15hTurq9iFWeHTrQhUhG6"); + assertInvalidAddress("8ryiHzNPFatNV15hTurq9iFWefffQhUhG6"); + assertInvalidAddress("8ryigz2PFatNV15hTurq9iFWeHTrQhUhG1"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MasariTest.java b/assets/src/test/java/bisq/asset/coins/MasariTest.java new file mode 100644 index 0000000000..0552df6285 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MasariTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + + import bisq.asset.AbstractAssetTest; + import org.junit.Test; + + public class MasariTest extends AbstractAssetTest { + + public MasariTest() { + super(new Masari()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("5n9Y2vwnf8oKBhHxRAyjS9aS9j5hTPjtS8RKzMbD3tP95yxkQWbUHkFhLs2UsjgNxj28W6YzNL9WFeY91xPGFXAaUwyVm1h"); + assertValidAddress("9n1AVze3gmj3ZpEz5Xju92FRiqtmcnQhhXJK7yx9D9qrHRvjZftndVci8HCYFttFeD7ftAMUqUGxG8iA4Sn2eVz45R2NUJj"); + assertValidAddress("5iB4LfuyvA5HSJP5A1xUKGb8pw5NkywxSeRZPxzy1U7kT3wBmypemQUUzTiCwjy6PTSrJpAvxiNDSUEjNryt17C8RvPdEg3"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("5hJpeWa9aogfpY5Su8YmeYaeuD7pyQvSZURcNx26QskbSk9UdZ6cR4HR4YsdWRiBJfCZKLHRTfj7ojGUJ7N5j5hg4pGGCE"); + assertInvalidAddress("5kYyn6K8hRWg16nztTuvaZ6Jg3ytH84gjbUoEKjbMU4u659PKLpKuLWVSujFwJ1Qp3ZUxhcFHBXMQDmeAz46By3FRRkdaug2"); + assertInvalidAddress("4okMfbVrFXE4nF9dRKnVLiJi2xiMDDuSk6MJexpBaNgsLutSaBN7euR8TCf4Z1dqmG85GdQHrzSpYgX8Lf2VJnkaAk9MtQV"); + assertInvalidAddress("5jrE2mwcHkvZq9rQcvX1GCELnwAF6wwmJ4rhVdDP6y#326Gp6KSNbeWWb1sD2dmDZvczHFs8LGM1UjTQfQjjAu6S4eXGC5h"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MaskTest.java b/assets/src/test/java/bisq/asset/coins/MaskTest.java new file mode 100644 index 0000000000..66203c5644 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MaskTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class MaskTest extends AbstractAssetTest { + + public MaskTest() { + super(new Mask()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("MbxYjp38aUXBuESwsFv8YmRvbQvMhNyJygU6ViLCjM4sUNqFjsHQim9dvzp9p8BVTjdsRkVNrC1Zy3NJRb18hav3CPe5eWn"); + assertValidAddress("MeGcanFnSr4bJFuNoHogCBdDCsqDrNu5njPc1Yh1DfsTUTL5dLbbtE119f4vztxXu6fFCKWRmpqjABdDyGrzMDkhTC38WwS"); + assertValidAddress("bTWEbW8kKVrZkDwyPs5t7BZXotMNyz5UY2QDJ6MjKT7ihA8kNKhoHDqPUiUB7jPxNpXLFkJsgL6TA1fo7yAzVUdm1hTopCocf"); + assertValidAddress("bTXejHgtfTLWzhyz9fHHBDKTWrsM8MKnebZCKeue8mbDWaKRhnQ8VisGRXUgTvUhsDiwX6PxeP5A22DFf5UVEk431Vjt8m3GM"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("MsopefFnSr4bJFuNoHogCBdDCsqDrNu5Pc1Yh1DfsTUTL5dLbbtE119f4vztxXu6fFCKWRmpqjABdDyGrzMDkhTC38gWw"); + assertInvalidAddress("MeGcanuyt4bJFuNoHogCBdDCsqDrNu5njPc1Yh1DfsTUTL5dLbbtE119f4vztxXu6fFCKWRmpqujABdDyGrzMDkhTC38WwSx"); + assertInvalidAddress("MrtcanFnSr4bJFuNoHogCBdDCsqDrNu5Pc1Yh1DfsTUTL5dLbbtE119f4vztxXu6fFCKWRmpqjABdDyGrzMDkhTC3rt4vb8Ww"); + assertInvalidAddress("bBXejHgtfTLWzhyz9fHKBDKTWrsM8MKnebZCKeue8mbDWaKRhnQ8VisGRXUgTvUhsDiwX6PxeP5A22DFf5UVEk431Vjt8m3GM"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MileTest.java b/assets/src/test/java/bisq/asset/coins/MileTest.java new file mode 100644 index 0000000000..fa90b986e0 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MileTest.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class MileTest extends AbstractAssetTest { + + public MileTest() { + super(new Mile()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertValidAddress("NTvSfK1Gr5Jg97UvJo2wvi7BTZo8KqJzgSL2FCGucF6nUH7yq"); + assertValidAddress("ztNdPsuyfDWt1ufCbDqaCDQH3FXvucXNZqVrdzsWvzDHPrkSh"); + assertValidAddress("jkvx3z98rJmuVKqMSktDpKTSBrsqJEtTBW1CBSWJEtchDGkDX"); + assertValidAddress("is2YXBxk91d4Lw4Pet7RoP8KAxCKFHUC6iQyaNgmac5ies6ko"); + assertValidAddress("2NNEr5YLniGxWajoeXiiAZPR68hJXncnhEmC4GWAaV5kwaLRcP"); + assertValidAddress("wGmjgRu8hgjgRsRV8k6h2puis1K9UQCTKWZEPa4yS8mrmJUpU"); + assertValidAddress("i8rc9oMunRtVbSxA4VBESxbYzHnfhP39aM5M1srtxVZ8oBiKD"); + assertValidAddress("vP4w8khXHFQ7cJ2BJNyPbJiV5kFfBHPVivHxKf5nyd8cEgB9U"); + assertValidAddress("QQQZZa46QJ3499RL8CatuqaUx4haKQGUuZ4ZE5SeL13Awkf6m"); + assertValidAddress("qqqfpHD3VbbyZXTHgCW2VX8jvoERcxanzQkCqVyHB8fRBszMn"); + assertValidAddress("BiSQkPqCCET4UovJASnnU1Hk5bnqBxBVi5bjA5wLZpN9HCA6A"); + assertValidAddress("bisqFm6Zbf6ULcpJqQ2ibn2adkL2E9iivQFTAP15Q18daQxnS"); + assertValidAddress("miLEgbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN3cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQ1"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQ"); + assertInvalidAddress("WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQx"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW1"); + assertInvalidAddress("milEgbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + assertInvalidAddress("miLegbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + assertInvalidAddress("1111111"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MirQuiXTest.java b/assets/src/test/java/bisq/asset/coins/MirQuiXTest.java new file mode 100644 index 0000000000..cf86ad6ee5 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MirQuiXTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class MirQuiXTest extends AbstractAssetTest { + + public MirQuiXTest() { + super(new MirQuiX()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("MCfFP5bFtN9riJiRRnH2QRkqCDqgNVC3FX"); + assertValidAddress("MEoLjNvFbNv63NtBW6eyYHUAGgLsJrpJbG"); + assertValidAddress("M84gmHb7mg4PMNBpVt3BeeAWVuKBmH6vtd"); + assertValidAddress("MNurUTgTSgg5ckmCcbjPrkgp7fekouLYgh"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("MCfFP5bFtN9riJiRRnH2QRkqCDqgNVC3FX2"); + assertInvalidAddress("MmEoLjNvFbNv63NtBW6eyYHUAGgLsJrpJbG"); + assertInvalidAddress("M84gmHb7mg4PMNBpVt3BeeAWVuKBmH63vtd"); + assertInvalidAddress("MNurUTgTSgg5ckmCcbjPrkgp7fekouLYfgh"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MoXTest.java b/assets/src/test/java/bisq/asset/coins/MoXTest.java new file mode 100644 index 0000000000..549a9b1606 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MoXTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class MoXTest extends AbstractAssetTest { + + public MoXTest() { + super(new MoX()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("XwoHEJVYYZEBXB99yPP1AWNYYTDLPGHZ11jTia4RWRpwbohuChbpPngF42RCoaKaJciCmhwdKWsBBQPt8Ci5dr9p3BejTRxXV"); + assertValidAddress("XwoG8c8N8VZQy9usuHj88DK5DsezY5YrkZoSCEKg8sFfhKLhFV2NwVMPFNogZkjpPw1RiV16JQ1Mg6ygYpntKADJ2kSRv21Lc"); + assertValidAddress("XwoABgJx6dt96eihXdGwj31AKqsN7dTbb1vMshfmj87YRYxmieBh8zHY26AYnwDE9Ce4Mg4eB4huEHYM26bEWrN72xa6zBf17"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("XwoHEJVYYZEBXB99yPP1AWNYYTDLPGHZ11jTia4RWRpwbohuChbpPngF42RCoaKaJciCmhwdKWsBBQPt8Ci5dr9p3BejTRxX"); + assertInvalidAddress("XwoHEJVYYZEBXB99yPP1AWNYYTDLPGHZ11jTia4RWRpwbohuChbpPngF42RCoaKaJciCmhwdKWsBBQPt8Ci5dr9p3BejTRxXVV"); + assertInvalidAddress("woHEJVYYZEBXB99yPP1AWNYYTDLPGHZ11jTia4RWRpwbohuChbpPngF42RCoaKaJciCmhwdKWsBBQPt8Ci5dr9p3BejTRxXVV"); + assertInvalidAddress("Xizx2PdSDC6B4xwcxr6ZsHAiShnj7XcXSEmf4GQRTmpDFum1MyohsekDvRQpN4eQwyZyCw4Hs2UKyJSygXwA2QhyGcS5NRVsYrM9t2SCPsxzT"); + assertInvalidAddress(""); + assertInvalidAddress("XwoHEJVYYZEBXB99yPP1AWNYYTDLPGHZ11jTia4RWRpwbohuChbpPngF42RCoaKaJciCmhwdKWsBBQPt8Ci5dr9p3BejTRxXV#aFejf"); + assertInvalidAddress("1jRo3rcp9fjdfjdSGpx"); + assertInvalidAddress("GDARp92UtmTWDjZatG8sdurzouheiuRRRTbbRtbr3atrHSXr9vJzjHq2TfPrjateDz9Wc8ZJKuDayqJ$%"); + assertInvalidAddress("F3xQ8Gv6xnvDhUrM57z71bfFvu9HeofXtXpZRLnrCN2s2cKvkQowrWjJTGz4676ymKvU4NzPY8Cadgsdhsdfhg4gfJwL2yhhkJ7"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MobitGlobalTest.java b/assets/src/test/java/bisq/asset/coins/MobitGlobalTest.java new file mode 100644 index 0000000000..92be634d74 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MobitGlobalTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class MobitGlobalTest extends AbstractAssetTest { + + public MobitGlobalTest() { + super(new MobitGlobal()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("MKDLXTdJs1AtAJhoRddLBSimXCE6SXbyMq"); + assertValidAddress("MGr2WYY9kSLPozEcsCWSEumXNX2AJXggUR"); + assertValidAddress("MUe1HzGqzcunR1wUxHTqX9cuQNMnEjiN7D"); + assertValidAddress("MWRqbYKkQcSvtHq4GFrPvYGf8GFGsLNPcE"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("AWGfbG22DNhgP2rsKfqyFxCwi1u68BbHAA1"); + assertInvalidAddress("AWGfbG22DNhgP2rsKfqyFxCwi1u68BbHAB"); + assertInvalidAddress("AWGfbG22DNhgP2rsKfqyFxCwi1u68BbHA#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MoneroTest.java b/assets/src/test/java/bisq/asset/coins/MoneroTest.java new file mode 100644 index 0000000000..48841ee5aa --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MoneroTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class MoneroTest extends AbstractAssetTest { + + public MoneroTest() { + super(new Monero()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPW"); + assertValidAddress("46tM15KsogEW5MiVmBn7waPF8u8ZsB6aHjJk7BAv1wvMKfWhQ2h2so5BCJ9cRakfPt5BFo452oy3K8UK6L2u2v7aJ3Nf7P2"); + assertValidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ"); + + // integrated addresses + assertValidAddress("4LL9oSLmtpccfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2bYXZKKQePHES9khPK"); + assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CWD1FFMXoYHeE6M55P9"); + assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CW82yHFEGvSG3NJRNtH"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqP"); + assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPWW"); + assertInvalidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQ!"); + assertInvalidAddress("76iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MonetaryUnitTest.java b/assets/src/test/java/bisq/asset/coins/MonetaryUnitTest.java new file mode 100644 index 0000000000..b6bb2242b9 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MonetaryUnitTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class MonetaryUnitTest extends AbstractAssetTest { + + public MonetaryUnitTest() { + super(new MonetaryUnit()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("7VjG4Vjnu488k14QdpxswKk1acVgySqV6c"); + assertValidAddress("7U42XyYEf7CsLsaq84mRxMaMfst7f3r4By"); + assertValidAddress("7hbLQSY9SnyHf1RwiTniMt8vT94x7pqJEr"); + assertValidAddress("7oM4HbCStpDQ8imocHPVjNWGVj9gg54erh"); + assertValidAddress("7SUheC6Xp12G9CCgoMJ2dT8e9zwnFRwjrU"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0U42XyYEf7CsLsaq84mRxMaMfst7f3r4By"); + assertInvalidAddress("#7VjG4Vjnu488k14QdpxswKk1acVgySqV6c"); + assertInvalidAddress("7SUheC6Xp12G9CCgoMJ2dT8e9zwnFRwjr"); + assertInvalidAddress("7AUheX6X"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/MyceTest.java b/assets/src/test/java/bisq/asset/coins/MyceTest.java new file mode 100644 index 0000000000..8bb7b8687d --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/MyceTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import org.junit.Test; +import bisq.asset.AbstractAssetTest; + +public class MyceTest extends AbstractAssetTest { + + public MyceTest() { + super(new Myce()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("MCgtattGUWUBAV8n2JAa4uDWCRvbZeVcaD"); + assertValidAddress("MRV2dtuxwo8b1JSkwBXN3uGypJxp85Hbqn"); + assertValidAddress("MEUvfCySnAqzuNvbRh2SZCbSro8e2dxLYK"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("MCgtattGUWUBAV8n2JAa4uDWCRvbZeVcaDx"); + assertInvalidAddress("AUV2dtuxwo8b1JSkwBXN3uGypJxp85Hbqn"); + assertInvalidAddress("SEUvfCySnAqzuNvbRh2SZCbSro8e2dxLYK"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/NamecoinTest.java b/assets/src/test/java/bisq/asset/coins/NamecoinTest.java new file mode 100644 index 0000000000..4f025fab26 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/NamecoinTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import org.junit.Test; +import bisq.asset.AbstractAssetTest; + +public class NamecoinTest extends AbstractAssetTest { + + public NamecoinTest() { + super(new Namecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("N7yhcPhzFduWXPc11AUK9zvtnsL6sgxmRs"); + assertValidAddress("N22FRU9f3fx7Hty641D5cg95kRK6S3sbf3"); + assertValidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("N7yhcPhzFduWXPc11AUK9zvtnsL6sgxmRsx"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQer"); + assertInvalidAddress("bc1qus65zpte6qa2408qu3540lfcyj9cx7dphfcspn"); + assertInvalidAddress("3GyEtTwXhxbjBtmAR3CtzeayAyshtvd44P"); + assertInvalidAddress("1CnXYrivw7pJy3asKftp41wRPgBggF9fBw"); + } +} + diff --git a/assets/src/test/java/bisq/asset/coins/NavcoinTest.java b/assets/src/test/java/bisq/asset/coins/NavcoinTest.java new file mode 100644 index 0000000000..55a1182d8c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/NavcoinTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class NavcoinTest extends AbstractAssetTest { + + public NavcoinTest() { + super(new Navcoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("NNR93HzmuhYKZ4Tnc9TGoD2DK6TVzXG9P7"); + assertValidAddress("NSm5NyCe5BFRuV3gFY5VcfhxWx7GTu9U9F"); + assertValidAddress("NaSdzJ64o8DQo5DMPexVrL4PYFCBZqcmsW"); + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("NNR93HzmuhYKZ4Tnc9TGoD2DK6TVzXG9P"); + assertInvalidAddress("NNR93HzmuhYKZ4TnO9TGoD2DK6TVzXG9P8"); + assertInvalidAddress("NNR93HzmuhYKZ4Tnc9TGoD2DK6TVzXG9P71"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/NdauTest.java b/assets/src/test/java/bisq/asset/coins/NdauTest.java new file mode 100644 index 0000000000..5efa400319 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/NdauTest.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +/* + * Copyright © 2019 Oneiro NA, Inc. + */ + +package bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class NdauTest extends AbstractAssetTest { + public NdauTest() {super(new Ndau());} + + @Test + public void testValidAddresses() { + assertValidAddress("ndaaacj4gbv5xgwikt6adcujqyvd37ksadj4mg9v3jqtbe9f"); + assertValidAddress("ndnbeju3vmcxf9n96rb652eaeri79anqz47budnw8vwv3nyv"); + assertValidAddress("ndeatpdkx5stu28n3v6pie96bma5k8pzbvbdpu8dchyn46nw"); + assertValidAddress("ndxix97gyubjrkqbu4a5m3kpxyz4qhap3c3ui7359pzskwv4"); + assertValidAddress("ndbjhkkcvj88beqcamr439z6d6icm5mjwth5r7vrgfbnxktr"); + assertValidAddress("ndmpdkab97bi4ea73scjh6xpt8njjjhha4rarpr2zzzrv88u"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("ndaaacj4gbv5xgwikt6adcujqyvd37ksadj4mg9v3jqtbe9"); + assertInvalidAddress("ndnbeju3vmcxf9n96rb652eaeri79anqz47budnw8vwv3nyvw"); + assertInvalidAddress("ndpatpdkx5stu28n3v6pie96bma5k8pzbvbdpu8dchyn46nw"); + assertInvalidAddress("ndx1x97gyubjrkqbu4a5m3kpxyz4qhap3c3ui7359pzskwv4"); + assertInvalidAddress("ndbjhklcvj88beqcamr439z6d6icm5mjwth5r7vrgfbnxktr"); + assertInvalidAddress("ndmpdkab97bi4ea73scjh6xpt8njjjhhaArarpr2zzzrv88u"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/NoirTest.java b/assets/src/test/java/bisq/asset/coins/NoirTest.java new file mode 100644 index 0000000000..78bc7e3c67 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/NoirTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class NoirTest extends AbstractAssetTest { + + public NoirTest() { + super(new Noir()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("ZMZ6M64FiFjPjmzXRf7xBuyarorUmT8uyG"); + assertValidAddress("ZHoMM3vccwGrAQocmmp9ZHA7Gjg9Uqkok7"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("21HQQgsvLTgN9xD9hNmAgAreakzVzQUSLSHa"); + assertInvalidAddress("ZHoMM3vccwGrAQocmmp9ZHA7Gjg9Uqkok7*"); + assertInvalidAddress("ZHoMM3vccwGrAQocmmp9ZHA7Gjg9Uqkok7#jHt5jtP"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/NoteBlockchainTest.java b/assets/src/test/java/bisq/asset/coins/NoteBlockchainTest.java new file mode 100644 index 0000000000..76f74a1969 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/NoteBlockchainTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class NoteBlockchainTest extends AbstractAssetTest { + + public NoteBlockchainTest() { + super(new NoteBlockchain()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("NaeSp6oTDFiGBZejFyYJvuCaSqWMnMM44E"); + assertValidAddress("NPCz6bsSnksLUGbp11hbHFWqFuVweEgMWM"); + assertValidAddress("NMNA6oMBExWhYoVEcD2BbcL6qmQ6rs7GN2"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1NMNA6oMBExWhYoVEcD2BbcL6qmQ6rs7GN2"); + assertInvalidAddress("NMNA6oMBExyWhYoVEcD2BbcL6qmQ6rs7GN2"); + assertInvalidAddress("NMNA6oMBExWhYoVEcD2BbcL6qmQ6rs7GN2#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/PENGTest.java b/assets/src/test/java/bisq/asset/coins/PENGTest.java new file mode 100644 index 0000000000..56af71f052 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PENGTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PENGTest extends AbstractAssetTest { + + public PENGTest() { + super(new PENG()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("P9KqnVS9UpcJmLtCF1j4SV3fcccMuGEbhs"); + assertValidAddress("PUTXyY73s3HDvEzNJQekXMnjNjTrzFBzE2"); + assertValidAddress("PEfabj5DzRj6WBpc3jtVDorsVM5nddDxie"); + assertValidAddress("PAvXbSUAdCyd9MEtDPYYSmezmeLGL1HcjG"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("Pp9KqnVS9UpcJmLtCF1j4SV3fcccMuGEbhs"); + assertInvalidAddress("PqUTXyY73s3HDvEzNJQekXMnjNjTrzFBzE2"); + assertInvalidAddress("P8Efabj5DzRj6WBpc3jtVDorsVM5nddDxie"); + assertInvalidAddress("P9AvXbSUAdCyd9MEtDPYYSmezmeLGL1HcjG"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/PIVXTest.java b/assets/src/test/java/bisq/asset/coins/PIVXTest.java new file mode 100644 index 0000000000..7f8e12a3d6 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PIVXTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PIVXTest extends AbstractAssetTest { + + public PIVXTest() { + super(new PIVX()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DFJku78A14HYwPSzC5PtUmda7jMr5pbD2B"); + assertValidAddress("DAeiBSH4nudXgoxS4kY6uhTPobc7ALrWDA"); + assertValidAddress("DRbnCYbuMXdKU4y8dya9EnocL47gFjErWe"); + assertValidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBCDnG6Rc"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("dFJku78A14HYwPSzC5PtUmda7jMr5pbD2B"); + assertInvalidAddress("DAeiBSH4nudXgoxS4kY6uhTPobc7AlrWDA"); + assertInvalidAddress("DRbnCYbuMXdKU4y8dya9EnocL47gFjErWeg"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBODnG6Rc"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCB0DnG6Rc"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBIDnG6Rc"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/PZDCTest.java b/assets/src/test/java/bisq/asset/coins/PZDCTest.java new file mode 100644 index 0000000000..5af789200d --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PZDCTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PZDCTest extends AbstractAssetTest { + + public PZDCTest() { + super(new PZDC()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("PNxERPUbkvCYeuJk44pH8bsdQJenvEWt5J"); + assertValidAddress("PCwCT1PkW2RsxT8jTb21vRnNDQGDRcWNkM"); + assertValidAddress("PPD3mYyS3vsHBkCrbCfrZyrwCGdr6EJHgG"); + assertValidAddress("PTQDhqksrocR7Z516zbpjuXSGVD37iu8gy"); + assertValidAddress("PXtABooQW1ED9NkARTiFcZv6xUnMmrbhpt"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("pGXsg0jSMzh1dSqggRvHjPvE3cnwvuXC7s"); + assertInvalidAddress("PKfRRcjwzKFq3dIqE9gq8Ztxn922W4GZhm"); + assertInvalidAddress("PKfRRcjwzKFq3d0qE9gq8Ztxn922W4GZhm"); + assertInvalidAddress("PKfRRcjwzKFq3dOqE9gq8Ztxn922W4GZhm"); + assertInvalidAddress("PKfRRcjwzKFq3dlqE9gq8Ztxn922W4GZhm"); + assertInvalidAddress("PXP75NnwDryYswQb9RaPFBchqLRSvBmDP"); + assertInvalidAddress("PKr3vQ7S"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ParsiCoinTest.java b/assets/src/test/java/bisq/asset/coins/ParsiCoinTest.java new file mode 100644 index 0000000000..3840990d15 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ParsiCoinTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ParsiCoinTest extends AbstractAssetTest { + + public ParsiCoinTest() { + super(new ParsiCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("PARSGnjdcRG4gY9g4rMTFAEHZLGU7uK8YMiFY3Do1uzoMz4LMA6PqmdPp7ZxDu25b56RyhCevkWjbAMng532iFFj8L5RaPyT4s"); + assertValidAddress("PARSftfY5pwJaUFtaxThVgKY9Sepd4mG44WpyncbtAxTddwTvJ84GCgGfoxYjzG53kLhRm21ENWp3fx5bneArq1D815ZoWNVqA"); + assertValidAddress("PARSju1hCQ5GmXSRbca8weGYDn2pqCypgLyTrENqL4XU3mdEx1mZ2vR7osrVA2hHNGRJRA5pRENF2Q8Pee8BscHoABVrcfkWnx"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("1GfqxEuuFmwwHTFkch3Aq3frEBbdpYfWPP"); + assertInvalidAddress("PARsaUEu1c9HWPQx6WpCcjZNmpS3vMhN4Jws12KrccLhH9vzUw4racG3g7St2FKDYngjcnkNF3N2sKQJ5jv1NYqD2buCpmVKE"); + assertInvalidAddress("PArSeoCiQL2Rjyo9GR39boeLCTM6ou3zGiv8AuFFblGHfNasy5iKfvG6JgnksNby26J6i5sEorRcmG8gF2AxC8bYiHyDGEfD6hp8T9KfwjQxVa"); + assertInvalidAddress("PaRSaUEu1c9HWPQx6WpCcjZNmpS3vMhN4Jws12rccLhH9vzUw4racG3g7St2#FKDYngjcnkNF3N2sKQJ5jv1NYqD2buCpmVKE"); + assertInvalidAddress("pARSeoCiQL2Rjyo9GR39boeLCTM6ou3zGiv8AuFFby5iKfvG6JNby26J6i5s$&*orRcmG8gF2AxC8bYiHyDGEfD6hp8T9KfwjQxVa"); + assertInvalidAddress("hyrjMmPhaznQkJD6C9dcmbBH9y6r9vYAg2aTG9CHSzL1R89xrFi7wj1azmkXyLPiWDBeTCsKGMmr6JzygbP2ZGSN2JqWs1WcK"); + assertInvalidAddress("parsGnjdcRG4gY9g4rMTFAEHZLGU7uK8YMiFY3Do1uzoMz4LMA6PqmdPp7ZxDu25b56RyhCevkWjbAMng532iFFj8L5RaPyT"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/ParticlTest.java b/assets/src/test/java/bisq/asset/coins/ParticlTest.java new file mode 100644 index 0000000000..9e7a398553 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ParticlTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ParticlTest extends AbstractAssetTest { + + public ParticlTest() { + super(new Particl()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("PZdYWHgyhuG7NHVCzEkkx3dcLKurTpvmo6"); + assertValidAddress("RJAPhgckEgRGVPZa9WoGSWW24spskSfLTQ"); + assertValidAddress("PaqMewoBY4vufTkKeSy91su3CNwviGg4EK"); + assertValidAddress("PpWHwrkUKRYvbZbTic57YZ1zjmsV9X9Wu7"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/PersonaTest.java b/assets/src/test/java/bisq/asset/coins/PersonaTest.java new file mode 100644 index 0000000000..ff4dd4a5c6 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PersonaTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PersonaTest extends AbstractAssetTest { + + public PersonaTest() { + super(new Persona()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("PV5PbsyhkM1RkN41QiSXy7cisbZ4kBzm51"); + assertValidAddress("PJACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("LJACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + assertInvalidAddress("TJACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + assertInvalidAddress("PJACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zAA"); + assertInvalidAddress("PlACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + assertInvalidAddress("PIACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + assertInvalidAddress("POACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + assertInvalidAddress("P0ACMZ2tMMZzQ8H9mWPHJcB7uYP47BM2zA"); + } +} + + diff --git a/assets/src/test/java/bisq/asset/coins/PinkcoinTest.java b/assets/src/test/java/bisq/asset/coins/PinkcoinTest.java new file mode 100644 index 0000000000..3d6ec9be67 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PinkcoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PinkcoinTest extends AbstractAssetTest { + + public PinkcoinTest() { + super(new Pinkcoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("2KZEgvipDn5EkDAFB8UR8nVXuKuKt8rmgH"); + assertValidAddress("2KVgwafcbw9LcJngqAzxu8UKpQSRwNhtTH"); + assertValidAddress("2TPDcXRRmvTxJQ4V8xNhP1KmrTmH9KKCkg"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("PPo1gCi4xoC87gZZsnU2Uj6vSgZAAD9com"); + assertInvalidAddress("z4Vg3S5pJEJY45tHX7u6X1r9tv2DEvCShi2"); + assertInvalidAddress("1dQT9U73rNmomYkkxQwcNYhfQr9yy4Ani"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/PlenteumTest.java b/assets/src/test/java/bisq/asset/coins/PlenteumTest.java new file mode 100644 index 0000000000..ce36a0f819 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/PlenteumTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class PlenteumTest extends AbstractAssetTest { + + public PlenteumTest() { + super(new Plenteum()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("PLeah9bvqxEDUWbRFqgNcYDeoL772WH9mcCQu9p29MC23NeCUkbVdUEfwDAtF8SgV81kf2hwCdpxqAJmC9k3nJsA7W4UThrufj"); + assertValidAddress("PLeavHTKHz9UcTCSCmd8eihuLxbsK9a7wSpfcYXPYY87JMpvYwwTH6Df32fRLc1r4rQMKoDLpTvywXx4FUVTggCR4jh9PEhvXb"); + assertValidAddress("PLeazd7iQEoFWJttR6353BMvs1cJfMqDmEUk2Z2XSoDdZigY5CbNLvrFUr7duvnEFdSKRdCQYTDkrcySYD1zaFtT9YMubRjHL2"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("PLev23ymatPTWgN1jncG33hMdJxZBLrBcCWQBGGGC14CFMUCq1nvxiV8d5cW92mmavzw542bpyjYXd8"); + assertInvalidAddress("PLeuxauCnCH7XZrSZSZw7XEEbkgrnZcaE1MK8wLtTYkF3g1J7nciYiaZDsTNYm2oDLTAM2JPq4rrlhVN5cXWpTPYh8P5wKbXNdoh"); + assertInvalidAddress(""); + assertInvalidAddress("PLev3xxpAFfXKwF5ond4sWDX3ATpZngT88KpPCCJKcuRjGktgp5HHTK2yV7NTo8687u5jwMigLmHaoFKho0OhVmF8WP9pVZhBL9kC#RoPOWRwpsx1F"); + assertInvalidAddress("PLeuwafXHTPzj1d2wc7c9X69r3qG1277ecnLnUaZ61M1YV5d3GYAs1Jbc2q4C4fWN$C4fWNLoDLDvADvpjNYdt3sdRB434UidKXimQQn"); + assertInvalidAddress("1jRo3rcp9fjdfjdSGpx"); + assertInvalidAddress("GDARp92UtmTWDjZatG8sduRockSteadyWasHere3atrHSXr9vJzjHq2TfPrjateDz9Wc8ZJKuDayqJ$%"); + assertInvalidAddress("F3xQ8Gv6xnvDhUrM57z71bfFvu9HeofXtXpZRLnrCN2s2cKvkQowrWjJTGz4676ymKvU4NzYT5Aadgsdhsdfhg4gfJwL2yhhkJ7"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/QMCoinTest.java b/assets/src/test/java/bisq/asset/coins/QMCoinTest.java new file mode 100644 index 0000000000..0cdaa1c29a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/QMCoinTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class QMCoinTest extends AbstractAssetTest { + + public QMCoinTest() { + super(new QMCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("QSXwS2opau1PYsvj4PrirPkP6LQHeKbQDx"); + assertValidAddress("QbvD8CPJwAmpQoE8CQhzcfWp1EAmT2E298"); + assertValidAddress("QUAzsb7nqp7XVsRy9vjaE4kTUpgP1pFeoL"); + assertValidAddress("QQDvVM2s3WYa8EZQS1s2esRkR4zmrjy94d"); + assertValidAddress("QgdkWtsy1inr9j8RUrqDeVnrJmhE28WnLX"); + assertValidAddress("Qii56aanBMiEPpjHoaE4zgEW4jPuhGjuj5"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("qSXwS2opau1PYsvj4PrirPkP6LQHeKbQDx"); + assertInvalidAddress("QbvD8CPJwAmpQoE8CQhzcfWp1EAmT2E2989"); + assertInvalidAddress("QUAzsb7nq07XVsRy9vjaE4kTUpgP1pFeoL"); + assertInvalidAddress("QQDvVM2s3WYa8EZQS1s2OsRkR4zmrjy94d"); + assertInvalidAddress("QgdkWtsy1inr9j8RUrqDIVnrJmhE28WnLX"); + assertInvalidAddress("Qii56aanBMiEPpjHoaE4lgEW4jPuhGjuj5"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/QbaseTest.java b/assets/src/test/java/bisq/asset/coins/QbaseTest.java new file mode 100644 index 0000000000..31c43752af --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/QbaseTest.java @@ -0,0 +1,28 @@ +package bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class QbaseTest extends AbstractAssetTest { + public QbaseTest() { + super(new Qbase()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("BBrv1uUkQxpWayMvaVSw9Gr4X7CcdWUtcC"); + assertValidAddress("BNMFjkDk9qqcF2rtoAbqbqWiHa41GPkQ2G"); + assertValidAddress("B73WdFQXx8VGNg8h1BeJj6H2BEa1xrbtsT"); + assertValidAddress("BGq4DH2BnS4kFWuNNQqfmiDLZvjaWtvnWX"); + assertValidAddress("B9b8iTbVVcQrohrEnJ9ho4QUftHS3svB84"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("bBrv1uUkQxpWayMvaVSw9Gr4X7CcdWUtcC"); + assertInvalidAddress("B3rv1uUkQxpWayMvaVSw9Gr4X7CcdWUtcC"); + assertInvalidAddress("PXP75NnwDryYswQb9RaPFBchqLRSvBmDP"); + assertInvalidAddress("PKr3vQ7S"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/QwertycoinTest.java b/assets/src/test/java/bisq/asset/coins/QwertycoinTest.java new file mode 100644 index 0000000000..ed39da44b7 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/QwertycoinTest.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class QwertycoinTest extends AbstractAssetTest { + + public QwertycoinTest() { + super(new Qwertycoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("QWC1NStUeRB9hZiYH8sG5RAWEt7YycyB44YZnpZQBpgq4CLwmLw4vAk9tU3h7Td21NL9aMbLHxseDGKdEv3gRexo2QCodNEZWa"); + assertValidAddress("QWC1anUNJRo2HePBmenLFkGu8rnug4odGLCjHaCqAwMxboiZZS3Gv9ACLfn2zvcsGVcCc51eqZXB8Dot9X5qAt3F53F8BxjDrG"); + assertValidAddress("QWC1hgpbsxsPrpxH9H3wL771p4KdgS7vA369PQTiznHiCC3NjZxKJSmBtJPVCJBBUKE346FcPTsQ18W6fgiDzj762BHNgo2sir"); + assertValidAddress("QWC1YAvWpYBVs8XT2eSt2JV5iAJSdm8CwbQhDruuBeTzRNKSdtdK8Mn3WjaXQrFvjMMWWTf24x89p31mWppJN2Br9uiA5zdYQu"); + assertValidAddress("QWC1YzR91Zmcj7fpf1HRZhSfz6cgXbxqAVTjQTtrUV6Bfv1ysEzb78qgVojE7FuQWSRnVqSb3LyxP9nH2q4vWyo82Fonutfkzr"); + assertValidAddress("QWC1KYAwX6sRXK94HabKLCFNMjfC12KFC74cRjTgFtsD79VUBydTtMd3G2z4xLg2e1LKaXsTt3zkYibH3pBrAMjd5z5ConjRXn"); + assertValidAddress("QWC1ZgSyFwS3tUbmCRPGDBi224ynMZXgXCHxvQ5pEmtuZSCrmid4z1de1DWRjhZKRZXe4E5LYhtP6e7FmpN8R2MM2SHGFvg12z"); + assertValidAddress("QWC1W7223e83cBdseddQp461j49bhr7y4VHh8FTPs7qWArhpqBzNvrYR5QXyFtc3eRaoASo3QVhuT6ogAa6AHhgt4bVMUNpZGh"); + assertValidAddress("QWC1NgBcSwvXghUkEqGttNPSSmPEgEdknXELNLyTG444Fx3cKkV2oJ9iCwzySbps7y9BqqkWAKbkvdkA8FTspfdm29ScDzASK1"); + assertValidAddress("QWC1FVgbYqkafwnpW8KU2gKXLTKoraMXuEJ2c1yG6PNdesh6BA3Wq8d1mgRYqfsbCn53g5VLHuxyLT8CXnGRLxN64wHssuSa9D"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("QW009s5NiYva6XS9bhhVc6jKYgXsH9wuHhZhWsqyAoPoWPUEiwEo9AZCDNbksvknyvZk73zwhHWSiXdgcDGLyhk5teEM7yxTgk"); + assertInvalidAddress("WC115a2NPZy7Xe2WZt3nKMZBsBpgNdevnKfy6PLk99eCjYw5fWQ5nu4uM6LerRBvVKTJpdGp2acZ25X3QDPHc7ocTxz1WfN2Za"); + assertInvalidAddress("34BQWC8imA1UH29uR6PHiGpcz9MYdnL3rku27gGeLosn5XuSedFC7jPBmzaB9DoxmDA5VUZa5hPv6PSe3tJH2MJhBktMEJXaZ8"); + assertInvalidAddress("KWC45Ghz2JRTTrLh8Z4bm6QhzZxVbq7LPiKbgjjhHPNDvVXZAJLop91zRu9A7wJMjyrU89uF7QpZB5kHhniuGZ88MJv7jRZXNi"); + assertInvalidAddress("ABc58FFmFEGcS52mTWmhAskQaKSSiX1BnHo8YcDjuhPdYBpWT9Q6ZCDz54k6cs3jPF2nk6desb1T6vRfHLfthiNf561qPct2SY"); + assertInvalidAddress("2K267rMF5ve4nt2wTHYJ1pZ6j3o2YP5KDBnE7GDxnr6bpem9WcqeHzw9yKWXvtxYdpDXCBbLiX9nm97r4aEtnXq8YNb9WPn15f"); + assertInvalidAddress("798Qr9sWTprQ2sH2y5PGpfV3RAnFxUsJYY2a2VQWCA9GjZ3MiyScD8VEh8ifWk4toYRCcbLZmRJw2dSsJBJAJ1Ava8WBzW7J12"); + assertInvalidAddress("A2o85CQSLDNNKR4HGHwhtsxhm8jheYEvk6ngf44AhqCRWDV2XsaTHr6ittDuyfCjinAP1SzBqnVJfqNhYGDJLzxq4Y7FBVofXV"); + assertInvalidAddress("QW9AeKW87bkao59oadmTXGf8Jv7sMYByPrKahRbnmZEmGzRgoxGRbWqmmXuPDW6jPJSUAdpZRZn6E5B9935LtWD5gHAPpZQA"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/RadiumTest.java b/assets/src/test/java/bisq/asset/coins/RadiumTest.java new file mode 100644 index 0000000000..9c5966729e --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/RadiumTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import org.junit.Test; + + + +import bisq.asset.AbstractAssetTest; + +public class RadiumTest extends AbstractAssetTest { + + public RadiumTest() { + super(new Radium()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("XfrvQw3Uv4oGgc535TYyBCT2uNU7ofHGDU"); + assertValidAddress("Xwgof4wf1t8TnQUJ2UokZRVwHxRt4t6Feb"); + assertValidAddress("Xep8KxEZUsCxQuvCfPdt2VHuHbp43nX7Pm"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + assertInvalidAddress("1K5B7SDcuZvd2oUTaW9d62gwqsZkteXqA4"); + assertInvalidAddress("1GckU1XSCknLBcTGnayBVRjNsDjxqopNav"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/RemixTest.java b/assets/src/test/java/bisq/asset/coins/RemixTest.java new file mode 100644 index 0000000000..ae0aeb921c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/RemixTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class RemixTest extends AbstractAssetTest { + + public RemixTest() { + super(new Remix()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("REMXisBbsyWKYdENidNhiP3bGaVwVgtescK2ZuJMtxed4TqJGH8VX57gMSTyfC43FULSM4XXzmj727SGjDNak16mGaYdban4o4m"); + assertValidAddress("REMXiqQhgfqWtZ1gfxP4iDbXEV4f8cUDFAp2Bz43PztJSJvv2mUqG4Z2YFBMauJV74YCDcJLyqkbCfsC55LNJhQfZxdiE5tGxKq"); + assertValidAddress("SubRM7BgZyGiccN3pKuRPrN52FraE9j7miu17MDwx6wWb7J6XWeDykk48JBZ3QVSXR7GJWr2RdpjK3YCRAUdTbfRL4wGAn7oggi"); + assertValidAddress("SubRM9N9dmoeawsXqNt94jVn6vSurYxxU3E6mEoMnzWvAMB7QjL3Zc9dmKTD64wE5ePFfACVLVLTZZa6GKVp6FuZ7Z9dJheMoJb"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("REMXiqQhgfqWtZ1gfxP4iDbXEV4f8cUDFAp2Bz43PztJSJvv2mUqG4Z2YFBMauJV74YCDcJLyqkbCfsC55LNJhQ"); + assertInvalidAddress("REMXIqQhgfqWtZ1gfxP4iDbXEV4f8cUDFApdfgdfgdfgdfgr4453453453444JV74YCDcJLyqkbCfsC55LNJhQfZxdiE5tGxKq"); + assertInvalidAddress("REMXiqQhgfqWtZ1gfxP4iDbXEV4f8cUDFAp2Bz43PztJS4dssdffffsdfsdfffffdfgdfgsaqkbCfsC4iDbXEV4f8cUDFAp2Bz"); + assertInvalidAddress("SubRM9N9dmoeawsXqNt94jVn6vSurYxxU3E6mEoMnzWvAMB7QL3Zc9dmKTD64wE5ePFfACVLVLTZZa6GKVp6FuZ7Z9dJheMo69"); + assertInvalidAddress("SubRM9N9dmoeawsXqNt94jdfsdfsdfsdfsdfsdfJb"); + assertInvalidAddress("SubrM9N9dmoeawsXqNt94jVn6vSfeet"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/RyoTest.java b/assets/src/test/java/bisq/asset/coins/RyoTest.java new file mode 100644 index 0000000000..6387820cc6 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/RyoTest.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class RyoTest extends AbstractAssetTest { + + public RyoTest() { + super(new Ryo()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("RYoLsinT9duNEtHGqAUicJKD2cmGiB9gB6sqHqWvV6suB4TtPSR8ynyh2vVVvNyDE6g7WEaBxCG8GD1KM2ffWP7FLXgeJbNYrp2"); + assertValidAddress("RYoSrJ7ES1wGsikGHFm69SU6dTTKt8Vi6V7BoC3wsLcc1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPeM"); + assertValidAddress("RYoKst8YBCucSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d4qqpsJ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("RYoLsinT9duNEtHGqAUicJKD2cmGiB9gB6sqHqWvV6suB4TtPSR8ynyh2vVVvNyDE6g7WEaBxCG8GD1KM2ffWP7FLXgeJbNYrp"); + assertInvalidAddress("RYoLsjCoYrxag2pPoDDTB4cRriKCNn8WjhY99kqjYuNTfE4MU2Yo1CPdpyK7PXpxDcAd5YDNerE6WCc4cVQvEbxLaHk4UcvbRp23"); + assertInvalidAddress("RYoLsinT9duNEtHGqAUicJKD2cmGiB9gB6sqHqWvV6suB4TtPSR8ynyh2vVVvNyDE6g7W!!!xCG8GD1KM2ffWP7FLXgeJbNYrp2"); + assertInvalidAddress("RYoSrJ7ES1IIIIIGHFm69SU6dTTKt8Vi6V7BoC3wsLcc1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPeM"); + assertInvalidAddress("RYoSrJ7ES1wGsikGHFm69SU6dTTKt8Vi6V7BoC3wsLcc1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPe"); + assertInvalidAddress("RYoSrJ7ES1wGsikGHFm69SU6dTTKt8Vi6V7BoC3wsLcc1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPeM1"); + assertInvalidAddress("RYoNsBB18NdcSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d6DEu3PcSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d96NjjvBCYU2SZD2of"); + assertInvalidAddress("RYoKst8YBCucSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d4qqpsJC"); + assertInvalidAddress("RYoKst8YBCucSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d4qqps"); + assertInvalidAddress("RYost8YBCucSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d4qqpsJ"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SUB1XTest.java b/assets/src/test/java/bisq/asset/coins/SUB1XTest.java new file mode 100644 index 0000000000..183c2f9c7c --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SUB1XTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class SUB1XTest extends AbstractAssetTest { + + public SUB1XTest() { + super(new SUB1X()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("ZDxdoVuyosZ6vY3LZAP1Z4H4eXMq2ZpLH7"); + assertValidAddress("ZKi6EksPCZoMi6EGXS9vWVed4NeSov2ZS4"); + assertValidAddress("ZT29B3yDJq1jzkCTBs4LnraM3E854MAPRm"); + assertValidAddress("ZZeaSimQwza3CkFWTrRPQDamZcbntf2uMG"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("zKi6EksPCZoMi6EGXS9vWVed4NeSov2ZS4"); + assertInvalidAddress("ZDxdoVuyosZ6vY3LZAP1Z4H4eXMq2ZpAC7"); + assertInvalidAddress("ZKi6EksPCZoMi6EGXS9vWVedqwfov2ZS4"); + assertInvalidAddress("ZT29B3yDJq1jzkqwrwBs4LnraM3E854MAPRm"); + assertInvalidAddress("ZZeaSimQwza3CkFWTqwrfQDamZcbntf2uMG"); + assertInvalidAddress("Z23t23f"); + assertInvalidAddress("ZZeaSimQwza3CkFWTrRPQDavZcbnta2uMGA"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SiaPrimeCoinTest.java b/assets/src/test/java/bisq/asset/coins/SiaPrimeCoinTest.java new file mode 100644 index 0000000000..68d54edfe8 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SiaPrimeCoinTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class SiaPrimeCoinTest extends AbstractAssetTest { + + public SiaPrimeCoinTest() { + super(new SiaPrimeCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("d9fe1331ed2ae1bbdfe0e2942e84d74b7310648e5a5f14c4980ec2c6a19f08af6894b9060e83"); + assertValidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca2031"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca20311"); + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca203"); + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca2031#"); + assertInvalidAddress("bvQpKvb1SswwxVTuyZocHWCVsUeGq7MwoR"); + assertInvalidAddress("d9fe1331ed2ae1bbdfe0e2942e84d74b7310648e5a5f14c4980ec2c6a19f08af6894b9060E83"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SiafundTest.java b/assets/src/test/java/bisq/asset/coins/SiafundTest.java new file mode 100644 index 0000000000..864a05ac12 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SiafundTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class SiafundTest extends AbstractAssetTest { + + public SiafundTest() { + super(new Siafund()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("949f35966a9b5f329f7419f91a02301b71b9f776568b2c767842af22b408eb8662203a02ec53"); + assertValidAddress("4daae3005456559972f4902217ee8394a890e2afede6f0b49015e5cfaecdcb13f466f5543346"); + assertValidAddress("da4f7fdc0fa047851a9860b09bc9b1e7424333c977e53a5d8aad74f5843a20b7cfa77a7794ae"); + + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + assertInvalidAddress("N22FRU9f3fx7Hty641D5cg95kRK6S3sbf3"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SixElevenTest.java b/assets/src/test/java/bisq/asset/coins/SixElevenTest.java new file mode 100644 index 0000000000..a7de8d34e4 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SixElevenTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import org.junit.Test; +import bisq.asset.AbstractAssetTest; + +public class SixElevenTest extends AbstractAssetTest { + + public SixElevenTest() { + super(new SixEleven()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("N7yhcPhzFduWXPc11AUK9zvtnsL6sgxmRs"); + assertValidAddress("N22FRU9f3fx7Hty641D5cg95kRK6S3sbf3"); + assertValidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("N7yhcPhzFduWXPc11AUK9zvtnsL6sgxmRsx"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQer"); + assertInvalidAddress("bc1qus65zpte6qa2408qu3540lfcyj9cx7dphfcspn"); + assertInvalidAddress("3GyEtTwXhxbjBtmAR3CtzeayAyshtvd44P"); + assertInvalidAddress("1CnXYrivw7pJy3asKftp41wRPgBggF9fBw"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SoloTest.java b/assets/src/test/java/bisq/asset/coins/SoloTest.java new file mode 100644 index 0000000000..e95bbcafca --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SoloTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + + import bisq.asset.AbstractAssetTest; + import org.junit.Test; + + public class SoloTest extends AbstractAssetTest { + + public SoloTest() { + super(new Solo()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("SL3UVNhEHuaWK9PwhVgMZWD5yaL6VBC4xRuXLnLFWizxavKvSqbcSpH2fG3dT36uMJEQ6XoKBqvFLUnzWG4Rb5e11yqsioFy8"); + assertValidAddress("Ssy27ePzscCj4spPjgtc8NKGSud9eLFLHGEWNAo8PuC53NnWhDDTX17Cfo3BzFKdYZfU9ovtEYNtQ4ezTtPhAHEuAR5mF8dTqB"); + assertValidAddress("Ssy2WFFnmi3XYJz8UsXPKzHtUxFdVhdSuU3sBGmpTbTLQqpZEMPS8GB486Q8UCaskdbGzxJxwdJYobtJmEPwDawa5mXD5spNbs"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("SL3dqGkkFszKzjzyXSLkYB6X9uqad7ih3DJtTeB8hrzD9iaRjWAUHZ8FA3NErphWM00NzURSTL7FEZ9un9fgLYjK2f7mHRFBn"); + assertInvalidAddress("Ssy2WLjegYxS5P1djMSRmVG8EzXDfHyde6BiZRd3aDyVh1vjwUB2GJHfWhVsvg1i4TjWyGRC9rD4n3kCE2gPA9yx6K34AyzcMZ"); + assertInvalidAddress("Sl3UVNhEHuaWK9PwhVgMZWD5yaL6VBC4xRuXLnLFWizxavKvSXxXSpam8d3dMaDuMJEQ6XoKBqvFLUnzWG4Rb5e11yqsioFy8"); + assertInvalidAddress("Ssy2WFFnmi3XYJz8UsXPKzHtUxFdVhdSuU3sBGmpTbTLQLoLIghGooDdf6QTryaskdbGzxJxwdJYobtJmEPwDawa5mXD5spNbs"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SpaceCashTest.java b/assets/src/test/java/bisq/asset/coins/SpaceCashTest.java new file mode 100644 index 0000000000..942662c58f --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SpaceCashTest.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class SpaceCashTest extends AbstractAssetTest { + + public SpaceCashTest() { + super(new SpaceCash()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("d9fe1331ed2ae1bbdfe0e2942e84d74b7310648e5a5f14c4980ec2c6a19f08af6894b9060e83"); + assertValidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca2031"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca20311"); + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca203"); + assertInvalidAddress("205cf3be0f04397ee6cc1f52d8ae47f589a4ef5936949c158b2555df291efb87db2bbbca2031#"); + assertInvalidAddress("bvQpKvb1SswwxVTuyZocHWCVsUeGq7MwoR"); + assertInvalidAddress("d9fe1331ed2ae1bbdfe0e2942e84d74b7310648e5a5f14c4980ec2c6a19f08af6894b9060E83"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/SpectrecoinTest.java b/assets/src/test/java/bisq/asset/coins/SpectrecoinTest.java new file mode 100644 index 0000000000..b6ad8afcc8 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/SpectrecoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class SpectrecoinTest extends AbstractAssetTest { + + public SpectrecoinTest() { + super(new Spectrecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("SUZRHjTLSCr581qLsGqMqBD5f3oW2JHckn"); + assertValidAddress("SZ4S1oFfUa4a9s9Kg8bNRywucHiDZmcUuz"); + assertValidAddress("SdyjGEmgroK2vxBhkHE1MBUVRbUWpRAdVG"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/StarwelsTest.java b/assets/src/test/java/bisq/asset/coins/StarwelsTest.java new file mode 100644 index 0000000000..e30e93b588 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/StarwelsTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class StarwelsTest extends AbstractAssetTest { + + public StarwelsTest() { + super(new Starwels()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("1F7EixuiBdvi9bVxEPzAgJ11GRJsdH3ihh"); + assertValidAddress("17DdVnWvz3XZPvMYHmSRSycUgt2EEv29So"); + assertValidAddress("1HuoFLoGJQCLodNDH5oCXWaR1kL8DwksJX"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("21HQQgsvLTgN9xD9hNmAgAreakzVzQUSLSHa"); + assertInvalidAddress("1HQQgsvLTgN9xD9hNmAgAreakzVzQUSLSHs"); + assertInvalidAddress("1HQQgsvLTgN9xD9hNmAgAreakzVzQUSLSH#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/TEOTest.java b/assets/src/test/java/bisq/asset/coins/TEOTest.java new file mode 100644 index 0000000000..66f67b605f --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/TEOTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class TEOTest extends AbstractAssetTest { + + public TEOTest() { + super(new TEO()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertValidAddress("0x23c9c5ae8c854e9634a610af82924a5366a360a3"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("8d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertInvalidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68"); + assertInvalidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68k"); + assertInvalidAddress("098d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertInvalidAddress("098d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/TurtleCoinTest.java b/assets/src/test/java/bisq/asset/coins/TurtleCoinTest.java new file mode 100644 index 0000000000..ad064b3755 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/TurtleCoinTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class TurtleCoinTest extends AbstractAssetTest { + + public TurtleCoinTest() { + super(new TurtleCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("TRTLv2X775FNQmN8x2UC3TVzs6trRHwUAcQSL6RUyRXR6JjwFYP8XG8VTCsi7QgPcWBJUWJk2SwaMYvrMk37T4nFVLPigMXcsf8"); + assertValidAddress("TRTLuyTzuoDL9wvoq9VcyGW9Vrp2R3161V3hSa8nZUxAL4iqbTJfFhSXpsrQunXuCGAnA72cZgYGmP7a8zJ6RrwAf5rKjwhUEU8"); + assertValidAddress("TRTLv2YGSbTgmAkZDYvRM8X6bLcJXYr4qMDTXYth9ppc2rHfnNGXPcbBTWxfRxwPTnJvFX1txGh6j9tQ9spJs3US3WwvDzkGsXC"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("TRTLv23ymatPTWgN1jncG33hMdJxZBLrBcCWQBGGGC14CFMUCq1nvxiV8d5cW92mmavzw542bpyjYXd8"); + assertInvalidAddress("TRLuxauCnCH7XZrSZSZw7XEEbkgrnZcaE1MK8wLtTYkF3g1J7nciYiaZDsTNYm2oDLTAM2JPq4rrlhVN5cXWpTPYh8P5wKbXNdoh"); + assertInvalidAddress(""); + assertInvalidAddress("TRTLv3xxpAFfXKwF5ond4sWDX3AVgZngT88KpPCCJKcuRjGktgp5HHTK2yV7NTo8659u5jwMigLmHaoFKho0OhVmF8WP9pVZhBL9kC#RoUKWRwpsx1F"); + assertInvalidAddress("TRTLuwafXHTPzj1d2wc7c9X69r3qG1277ecnLnUaZ61M1YV5d3GYAs1Jbc2q4C4fWN$C4fWNLoDLDvADvpjNYdt3sdRB434UidKXimQQn"); + assertInvalidAddress("1jRo3rcp9fjdfjdSGpx"); + assertInvalidAddress("GDARp92UtmTWDjZatG8sduRockSteadyWasHere3atrHSXr9vJzjHq2TfPrjateDz9Wc8ZJKuDayqJ$%"); + assertInvalidAddress("F3xQ8Gv6xnvDhUrM57z71bfFvu9HeofXtXpZRLnrCN2s2cKvkQowrWjJTGz4676ymKvU4NzPY8Cadgsdhsdfhg4gfJwL2yhhkJ7"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/UnitedCommunityCoinTest.java b/assets/src/test/java/bisq/asset/coins/UnitedCommunityCoinTest.java new file mode 100644 index 0000000000..cee2e4af0e --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/UnitedCommunityCoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class UnitedCommunityCoinTest extends AbstractAssetTest { + + public UnitedCommunityCoinTest() { + super(new UnitedCommunityCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("UX3DVuoiNR9Uwa22NLehu8yVKecjFKn4ii"); + assertValidAddress("URqRRRFY7D6drJCput5UjTRUQYEL8npUwk"); + assertValidAddress("Uha1WUkuYtW9Uapme2E46PBz2sBkM9qV9w"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("UX3DVuoiNR90wa22NLehu8yVKecjFKn4ii"); + assertInvalidAddress("URqRRRFY7D6drJCput5UjTRUQYaEL8npUwk"); + assertInvalidAddress("Uha1WUkuYtW9Uapme2E46PBz2$BkM9qV9w"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/UnobtaniumTest.java b/assets/src/test/java/bisq/asset/coins/UnobtaniumTest.java new file mode 100644 index 0000000000..7a3ec5afe3 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/UnobtaniumTest.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class UnobtaniumTest extends AbstractAssetTest { + + public UnobtaniumTest() { + super(new Unobtanium()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("uXN2S9Soj4dSL7fPAuQi9twdaFmtwYndVP"); + assertValidAddress("uZymbhuxhfvxzc5EDdqRWrrZKvabZibBu1"); + assertValidAddress("uKdudT6DwojHYsBE9JWM43hRV28Rmp1Zm1"); + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + assertInvalidAddress("a1HwTdCmQV3NspP2QqCGpehoFpi8NY4Zg3"); + assertInvalidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/VARIUSTest.java b/assets/src/test/java/bisq/asset/coins/VARIUSTest.java new file mode 100644 index 0000000000..a8b27d1406 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/VARIUSTest.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 bisq.asset.coins; + +import org.junit.Test; + +import bisq.asset.AbstractAssetTest; + +public class VARIUSTest extends AbstractAssetTest { + + public VARIUSTest() { + super(new VARIUS()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("VL85MGBCSfnSeiLxuQwXuvxHArzfr1574H"); + assertValidAddress("VBKxFQULC6bjzWdb2PhZyoRdePq8fs55fi"); + assertValidAddress("VXwmVvzX6KMqfkBJXRXu4VUbgzPhLKdBSq"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("xLfnSeiLxuQwXuvxHArzfr1574H"); + assertInvalidAddress("BBKzWdb2PhZyoRdePq8fs55fi"); + assertInvalidAddress("vXwmVvzX6KMqfkBJXRXu4VUbgzPhLKdBSq"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/VeilTest.java b/assets/src/test/java/bisq/asset/coins/VeilTest.java new file mode 100644 index 0000000000..68e9ca856a --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/VeilTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class VeilTest extends AbstractAssetTest { + + public VeilTest() { + super(new Veil()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("VS2oF2pouKoLPJCjY8D7E1dStmUtitACu7"); + assertValidAddress("VV8VtpWTsYFBnhnvgQVnTvqoTx7XRRevte"); + assertValidAddress("VRZF4Am891FS224uuNirsrEugqMyg3VxjJ"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); + assertInvalidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); + assertInvalidAddress("DRbnCYbuMXdKU4y8dya9EnocL47gFjErWeg"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBODnG6Rc"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCB0DnG6Rc"); + assertInvalidAddress("DTPAqTryNRCE2FgsxzohTtJXfCBIDnG6Rc"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/VertcoinTest.java b/assets/src/test/java/bisq/asset/coins/VertcoinTest.java new file mode 100644 index 0000000000..a938eafac3 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/VertcoinTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class VertcoinTest extends AbstractAssetTest { + + public VertcoinTest() { + super(new Vertcoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("VmVwB5dxph84tbi5XqRUtfX1MfmP8WpWWL"); + assertValidAddress("Vt85c1QcQYE318zXqZUnjUB6fwjTrf1Xkb"); + assertValidAddress("33ny4vAPJHFu5Nic7uMHQrvCACYTKPFJ5p"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("VmVwB5dxph84tb15XqRUtfX1MfmP8WpWWW"); + assertInvalidAddress("Vt85555555555555c1QcQYE318zXqZUnjUB6fwjTrf1Xkb"); + assertInvalidAddress("33ny4vAPJHFu5Nic7uMHQrvCACYTKPFJ6r#"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/WORXTest.java b/assets/src/test/java/bisq/asset/coins/WORXTest.java new file mode 100644 index 0000000000..a981633332 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/WORXTest.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class WORXTest extends AbstractAssetTest { + + public WORXTest() { + super(new WORX()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("WgeBjv4PkmNnsUZ6QqhhT3ynEaqr3xDWuS"); + assertValidAddress("WQDes3h9GBa72R9govQCic3f38m566Jydo"); + assertValidAddress("WeNnnz8KFgmipcLhpbXSM9HT37pSqqeVbk"); + assertValidAddress("WNzf7fZgc2frhBGqVvhVhYpSBMWd2WE6x5"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("WgeBjv4PksmNnsUZ6QqhhT3ynEaqr3xDWuS"); + assertInvalidAddress("W2QDes3h9GBa72R9govQCic3f38m566Jydo"); + assertInvalidAddress("WeNnnz8KFgmipcLhpbXSM9HT37pSqqeVbk3"); + assertInvalidAddress("WNzf7fZgc2frhBGqVvhVhYpSBMWd2WE6x54"); + } +} \ No newline at end of file diff --git a/assets/src/test/java/bisq/asset/coins/WebchainTest.java b/assets/src/test/java/bisq/asset/coins/WebchainTest.java new file mode 100644 index 0000000000..4cee0c6278 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/WebchainTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class WebchainTest extends AbstractAssetTest { + + public WebchainTest() { + super(new Webchain()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertValidAddress("0x23c9c5ae8c854e9634a610af82924a5366a360a3"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("8d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertInvalidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68"); + assertInvalidAddress("0x8d1ba0497c3e3db17143604ab7f5e93a3cbac68k"); + assertInvalidAddress("098d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + assertInvalidAddress("098d1ba0497c3e3db17143604ab7f5e93a3cbac68b"); + } +} + diff --git a/assets/src/test/java/bisq/asset/coins/WrkzCoinTest.java b/assets/src/test/java/bisq/asset/coins/WrkzCoinTest.java new file mode 100644 index 0000000000..3647f5cb27 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/WrkzCoinTest.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class WrkzCoinTest extends AbstractAssetTest { + + public WrkzCoinTest() { + super(new WrkzCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("WrkzjsomAAfH8kotfaTyVYfya7PNQt2oL4regF1VpTV9TSezdyxcQpRW2jGptwPP6zLgQUa7Lem1dBWfGM7LfJqs719UZhX9Hg"); + assertValidAddress("WrkzpRgV26G8p8FUfFzaYbd15Nmq3SsRSVbG8yPjvt4W4D5KBHTV2RHbzQVE1TAt1NV21Tp6xiFATJT8QXoxeEUQ8DPY1Zkjnf"); + assertValidAddress("WrkzmetNqgJG5SwtaVhyTxijdx6JGtUeHELTpwfgC9Ym1Ps4JdQtanXLK8Xk5TeMUTEbsmDJ8taXYiyYZpPHSg5X1wC8ij7fdG"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("WrkzQokcStLUSALE5Ra17v2n6ad65h8wL5vqABKkoWy7Xicz9znqPSgS2MRVkuYtRAaJiMFuyDCFF1oJgT7PHb8i9yM"); + assertInvalidAddress("WrkskixT63cYzLFmDoA5WN7RbihYBwbzJJmjR9zgjD3ZUotbFGBgv1RaUAu1fWWT4QeEEktQfZK9AFPh19t2U8uG49EH3WSVEn"); + assertInvalidAddress(""); + assertInvalidAddress("WrkzUAxg9TSdkh6tfh5pk84XgKeyNe8T4TvaSgk87kk6iCUEitkk2sk6wVtKJXk5BM3kwh2ftnkaVfBWfBPr8igZ2xkn#RoUxF"); + assertInvalidAddress("WrkzXTU4REbRijuLPpds2k4BhcBGgXFpeEaXKs49D7$PFuqBYpQw2tQbAApoQLAp2iWVsoxiPmcERXhHrhtCLnzL4ezB8kAbxH"); + assertInvalidAddress("cccd2bd37455350e7586cf9315c7f3acd3de56321aa356ff3391bd21f0bbf502"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/XDRTest.java b/assets/src/test/java/bisq/asset/coins/XDRTest.java new file mode 100644 index 0000000000..dcf61c808f --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/XDRTest.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class XDRTest extends AbstractAssetTest { + + public XDRTest() { + super(new XDR()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertValidAddress("NTvSfK1Gr5Jg97UvJo2wvi7BTZo8KqJzgSL2FCGucF6nUH7yq"); + assertValidAddress("ztNdPsuyfDWt1ufCbDqaCDQH3FXvucXNZqVrdzsWvzDHPrkSh"); + assertValidAddress("jkvx3z98rJmuVKqMSktDpKTSBrsqJEtTBW1CBSWJEtchDGkDX"); + assertValidAddress("is2YXBxk91d4Lw4Pet7RoP8KAxCKFHUC6iQyaNgmac5ies6ko"); + assertValidAddress("2NNEr5YLniGxWajoeXiiAZPR68hJXncnhEmC4GWAaV5kwaLRcP"); + assertValidAddress("wGmjgRu8hgjgRsRV8k6h2puis1K9UQCTKWZEPa4yS8mrmJUpU"); + assertValidAddress("i8rc9oMunRtVbSxA4VBESxbYzHnfhP39aM5M1srtxVZ8oBiKD"); + assertValidAddress("vP4w8khXHFQ7cJ2BJNyPbJiV5kFfBHPVivHxKf5nyd8cEgB9U"); + assertValidAddress("QQQZZa46QJ3499RL8CatuqaUx4haKQGUuZ4ZE5SeL13Awkf6m"); + assertValidAddress("qqqfpHD3VbbyZXTHgCW2VX8jvoERcxanzQkCqVyHB8fRBszMn"); + assertValidAddress("BiSQkPqCCET4UovJASnnU1Hk5bnqBxBVi5bjA5wLZpN9HCA6A"); + assertValidAddress("bisqFm6Zbf6ULcpJqQ2ibn2adkL2E9iivQFTAP15Q18daQxnS"); + assertValidAddress("miLEgbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + + + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN3cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQ1"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQ"); + assertInvalidAddress("WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW"); + assertInvalidAddress("2WeY8JpRJgrvWQx"); + assertInvalidAddress("2WeY8JpRJgrvWQxbSPuyhsBMjtZMMN7cADEomPHh2bCkdZ7xQW1"); + assertInvalidAddress("milEgbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + assertInvalidAddress("miLegbhGv4ARoPG2kAhTCy8UGqBcFbsY6rr5tXq63nH8RyqcE"); + assertInvalidAddress("1111111"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ZcashTest.java b/assets/src/test/java/bisq/asset/coins/ZcashTest.java new file mode 100644 index 0000000000..44cb2f0c30 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ZcashTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ZcashTest extends AbstractAssetTest { + + public ZcashTest() { + super(new Zcash()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("t1K6LGT7z2uNTLxag6eK6XwGNpdkHbncBaK"); + assertValidAddress("t1ZjdqCGEkqL9nZ8fk9R6KA7bqNvXaVLUpF"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ZcoinTest.java b/assets/src/test/java/bisq/asset/coins/ZcoinTest.java new file mode 100644 index 0000000000..5178a3f672 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ZcoinTest.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +public class ZcoinTest extends AbstractAssetTest { + + public ZcoinTest() { + super(new Zcoin()); + } + + @Override + public void testValidAddresses() { + assertValidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + assertValidAddress("a1HwTdCmQV3NspP2QqCGpehoFpi8NY4Zg3"); + assertValidAddress("aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD"); + } + + @Override + public void testInvalidAddresses() { + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + assertInvalidAddress("N22FRU9f3fx7Hty641D5cg95kRK6S3sbf3"); + assertInvalidAddress("MxmFPEPzF19JFPU3VPrRXvUbPjMQXnQerY"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ZelCashTest.java b/assets/src/test/java/bisq/asset/coins/ZelCashTest.java new file mode 100644 index 0000000000..e2891e1f80 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ZelCashTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ZelCashTest extends AbstractAssetTest { + + public ZelCashTest() { + super(new ZelCash()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("t1K6LGT7z2uNTLxag6eK6XwGNpdkHbncBaK"); + assertValidAddress("t1ZjdqCGEkqL9nZ8fk9R6KA7bqNvXaVLUpF"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ZeroClassicTest.java b/assets/src/test/java/bisq/asset/coins/ZeroClassicTest.java new file mode 100644 index 0000000000..d175790f77 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ZeroClassicTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ZeroClassicTest extends AbstractAssetTest { + + public ZeroClassicTest() { + super(new ZeroClassic()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("t1PLfc14vCYaRz6Nv1zxpKXhn5W5h9vUdUE"); + assertValidAddress("t1MjXvaqL5X2CquP8hLmvyxCiJqCBzuMofS"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/ZeroTest.java b/assets/src/test/java/bisq/asset/coins/ZeroTest.java new file mode 100644 index 0000000000..4fd5309f96 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/ZeroTest.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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class ZeroTest extends AbstractAssetTest { + + public ZeroTest() { + super(new Zero()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("t1cZTNaKS6juH6tGEhCUZmZhtbYGeYeuTrK"); + assertValidAddress("t1ZBPYJwK2UPbshwcYWRiCq7vw8VPDYumWu"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); + assertInvalidAddress("38NwrYsD1HxQW5zfLT0QcUUXGMPvQgzTSn"); + assertInvalidAddress("8tP9rh3SH6n9cSLmV22vnSNNw56LKGpLrB"); + assertInvalidAddress("8Zbvjr"); + } +} diff --git a/assets/src/test/java/bisq/asset/coins/uPlexaTest.java b/assets/src/test/java/bisq/asset/coins/uPlexaTest.java new file mode 100644 index 0000000000..a4950f7ff3 --- /dev/null +++ b/assets/src/test/java/bisq/asset/coins/uPlexaTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.asset.coins; + +import bisq.asset.AbstractAssetTest; +import org.junit.Test; + +public class uPlexaTest extends AbstractAssetTest { + + public uPlexaTest() { + super(new uPlexa()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("UPX1dz81hmfWc7AUhn16JATXJJgZeQZ4zLKA4tnHJHcdS5zoSaKQUoaGqDUQnTXecPL4mjJF1vkwRF3EEq5UJdSw8A84sXDjFP"); + assertValidAddress("UPi1S1uqRRNSgC26PjasZP8FwTBRwnAEmBnx5mAYsbGqRvsU46aficYEA3FAT621EuPeChyKQumS7j6jpF74zW9tLJMve8kUJLP5zUgR5ts8W"); + assertValidAddress("UmV7QTQs5Q47wMPggtuQSMTvuqNie1MRmbD4AG1xJXykZmxBG4P18p4CHqkV5sKDRXauXWbs76835PZoemQmPGJC1Dv2zdF43"); + assertValidAddress("UmWh1MthnAiRP4GuN3DEQxPt6kgeAZfJLUuX1krtufAj2XvUJxDYnuYTAQzEp25V2W8BAJQkfXj8yFNUqQphxddN35nRLnZeE"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress(""); + assertInvalidAddress("UPXLsinT9duNEtHGqAUicJKD2cmGiB9gB6sqHqWvV6suB4TtPSR8ynyh2vVVvNyDE6g7WEaBxCG8GD1KM2ffWPx7FLXgeJbNYrp"); + assertInvalidAddress("UPXsjCoYrxag2pPoDDTB4cRriKCNn8WjhY99kqjYuNdTfE4MU2Yo1CPdpyK7PXpxDcAd5YDNerE6WCc4cVQvEbxLaHk4UcvbRp2"); + assertInvalidAddress("UPXsinT9duNEtHGqAUicJKD2cmGiB9gB6sqHqWvV6suBx4TtPSR8ynyh2vVVvNyDE6g7W!!!xCG8GD1KM2ffWP7FLXgeJbNYrp2"); + assertInvalidAddress("UmVSrJ7ES1IIIIIGHFm69SU6dTTKt8Vi6V7BoC3wsLccd1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTP"); + assertInvalidAddress("UmWrJ7ES1wGsikGHFm69SU6dTTKt8Vi6V7BoC3wsLcc1xY2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPe"); + assertInvalidAddress("UPi12rJ7ES1wGsikGHFm69SU6dTTKt8Vi6V7BoC36sqHqWvwsLcc1Y2CXgQkW7vHSe5uArGU9TjUC5RtvzhCycVDnPPbThTmZA8VqDzTPeM1"); + assertInvalidAddress("UPisBB18NdcSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93vd6DEu3PcSywKDshsywbjc5uCi8ybSUtWgvM3LfzaYe93d96NjjvBCYU2SZD2of"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/AugmintEuroTest.java b/assets/src/test/java/bisq/asset/tokens/AugmintEuroTest.java new file mode 100644 index 0000000000..0a7013da8b --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/AugmintEuroTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class AugmintEuroTest extends AbstractAssetTest { + + public AugmintEuroTest() { + super(new AugmintEuro()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x0d81d9e21bd7c5bb095535624dcb0759e64b3899"); + assertValidAddress("0d81d9e21bd7c5bb095535624dcb0759e64b3899"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x65767ec6d4d3d18a200842352485cdc37cbf3a216"); + assertInvalidAddress("0x65767ec6d4d3d18a200842352485cdc37cbf3a2g"); + assertInvalidAddress("65767ec6d4d3d18a200842352485cdc37cbf3a2g"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/DaiStablecoinTest.java b/assets/src/test/java/bisq/asset/tokens/DaiStablecoinTest.java new file mode 100644 index 0000000000..d567cce57d --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/DaiStablecoinTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class DaiStablecoinTest extends AbstractAssetTest { + + public DaiStablecoinTest() { + super(new DaiStablecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226"); + assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/EtherStoneTest.java b/assets/src/test/java/bisq/asset/tokens/EtherStoneTest.java new file mode 100644 index 0000000000..f5306ff22a --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/EtherStoneTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class EtherStoneTest extends AbstractAssetTest { + + public EtherStoneTest () { + super(new EtherStone()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x0d81d9e21bd7c5bb095535624dcb0759e64b3899"); + assertValidAddress("0d81d9e21bd7c5bb095535624dcb0759e64b3899"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x65767ec6d4d3d18a200842352485cdc37cbf3a216"); + assertInvalidAddress("0x65767ec6d4d3d18a200842352485cdc37cbf3a2g"); + assertInvalidAddress("65767ec6d4d3d18a200842352485cdc37cbf3a2g"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/TrueUSDTest.java b/assets/src/test/java/bisq/asset/tokens/TrueUSDTest.java new file mode 100644 index 0000000000..e86612cdda --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/TrueUSDTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class TrueUSDTest extends AbstractAssetTest { + + public TrueUSDTest() { + super(new TrueUSD()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0xa23579c2f7b462e5fb2e92f8cf02971fe4de4f82"); + assertValidAddress("0xdb59b63738e27e6d689c9d72c92c7a12f22161bb"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/USDCoinTest.java b/assets/src/test/java/bisq/asset/tokens/USDCoinTest.java new file mode 100644 index 0000000000..dd76c40e90 --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/USDCoinTest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class USDCoinTest extends AbstractAssetTest { + + public USDCoinTest() { + super(new USDCoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0xb86bb5fc804768db34f1a37da8b719e19af9dffd"); + assertValidAddress("0xea82afd93ebfc4f6564f3e5bd823cdef710f75dd"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + } +} diff --git a/assets/src/test/java/bisq/asset/tokens/VectorspaceAITest.java b/assets/src/test/java/bisq/asset/tokens/VectorspaceAITest.java new file mode 100644 index 0000000000..4e4e5e4cc4 --- /dev/null +++ b/assets/src/test/java/bisq/asset/tokens/VectorspaceAITest.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.asset.tokens; + +import bisq.asset.AbstractAssetTest; + +import org.junit.Test; + +public class VectorspaceAITest extends AbstractAssetTest { + + public VectorspaceAITest () { + super(new VectorspaceAI()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0xdd88dbdde30b684798881d4f3d9a3752d6c1dd71"); + assertValidAddress("dd88dbdde30b684798881d4f3d9a3752d6c1dd71"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2ecf455d8a2e6baf8d1039204c4f97efeddf27a82"); + assertInvalidAddress("0xh8wheG1jdka0c8b8263758chanbmshj2937zgab"); + assertInvalidAddress("h8wheG1jdka0c8b8263758chanbmshj2937zgab"); + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..1556a6aa31 --- /dev/null +++ b/build.gradle @@ -0,0 +1,714 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' + classpath 'com.google.gradle:osdetector-gradle-plugin:1.6.0' + classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0' + classpath files('gradle/witness/gradle-witness.jar') + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.5.10.RELEASE' + } +} + +configure(rootProject) { + // remove the 'bisq-*' scripts and 'lib' dir generated by the 'installDist' task + task clean { + doLast { + delete fileTree(dir: rootProject.projectDir, include: 'bisq-*'), 'lib' + } + } +} + +configure(subprojects) { + apply plugin: 'java' + apply plugin: 'com.google.osdetector' + + sourceCompatibility = 1.10 + + ext { // in alphabetical order + bcVersion = '1.63' + bitcoinjVersion = '2a80db4' + codecVersion = '1.13' + easybindVersion = '1.0.3' + easyVersion = '4.0.1' + findbugsVersion = '3.0.2' + firebaseVersion = '6.2.0' + fontawesomefxVersion = '8.0.0' + fontawesomefxCommonsVersion = '9.1.2' + fontawesomefxMaterialdesignfontVersion = '2.0.26-9.1.2' + grpcVersion = '1.25.0' + gsonVersion = '2.8.5' + guavaVersion = '28.2-jre' + guiceVersion = '4.2.2' + hamcrestVersion = '1.3' + httpclientVersion = '4.5.12' + httpcoreVersion = '4.4.13' + ioVersion = '2.6' + jacksonVersion = '2.12.1' + javafxVersion = '15' + javaxAnnotationVersion = '1.2' + jcsvVersion = '1.4.0' + jetbrainsAnnotationsVersion = '13.0' + jfoenixVersion = '9.0.10' + joptVersion = '5.0.4' + jsonsimpleVersion = '1.1.1' + jsonrpc4jVersion = '1.6.0.bisq.1' + junitVersion = '4.12' + jupiterVersion = '5.7.0' + kotlinVersion = '1.3.41' + knowmXchangeVersion = '4.4.2' + langVersion = '3.11' + logbackVersion = '1.1.11' + loggingVersion = '1.2' + lombokVersion = '1.18.12' + mockitoVersion = '3.5.15' + netlayerVersion = '8db4a13' // Commit ID from https://github.com/bisq-network/netlayer/commits/externaltor + protobufVersion = '3.10.0' + protocVersion = protobufVersion + pushyVersion = '0.13.2' + qrgenVersion = '1.3' + slf4jVersion = '1.7.30' + sparkVersion = '2.5.2' + springBootVersion = '1.5.10.RELEASE' + + os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os + } + + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } + + dependencies { + testCompile "junit:junit:$junitVersion" + } + + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + } +} + + +configure([project(':cli'), + project(':daemon'), + project(':desktop'), + project(':monitor'), + project(':relay'), + project(':seednode'), + project(':statsnode'), + project(':pricenode'), + project(':inventory'), + project(':apitest')]) { + + apply plugin: 'application' + + build.dependsOn installDist + installDist.destinationDir = file('build/app') + distZip.enabled = false + + // the 'installDist' and 'startScripts' blocks below configure bisq executables to put + // generated shell scripts in the root project directory, such that users can easily + // discover and invoke e.g. ./bisq-desktop, ./bisq-seednode, etc. + // See https://stackoverflow.com/q/46327736 for details. + + installDist { + doLast { + // copy generated shell scripts, e.g. `bisq-desktop` directly to the project + // root directory for discoverability and ease of use + + copy { + from "$destinationDir/bin" + into rootProject.projectDir + } + // copy libs required for generated shell script classpaths to 'lib' dir under + // the project root directory + copy { + from "$destinationDir/lib" + into "${rootProject.projectDir}/lib" + } + + // edit generated shell scripts such that they expect to be executed in the + // project root dir as opposed to a 'bin' subdirectory + def windowsScriptFile = file("${rootProject.projectDir}/bisq-${applicationName}.bat") + windowsScriptFile.text = windowsScriptFile.text.replace( + 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') + + def unixScriptFile = file("${rootProject.projectDir}/bisq-$applicationName") + unixScriptFile.text = unixScriptFile.text.replace( + 'cd "`dirname \\"$PRG\\"`/.." >/dev/null', 'cd "`dirname \\"$PRG\\"`" >/dev/null') + + if (applicationName == 'desktop') { + def script = file("${rootProject.projectDir}/bisq-$applicationName") + script.text = script.text.replace( + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g"') + } + + if (applicationName == 'apitest') { + // Pass the logback config file as a system property to avoid chatty + // logback startup due to multiple logback.xml files in the classpath + // (:daemon & :cli). + def script = file("${rootProject.projectDir}/bisq-$applicationName") + script.text = script.text.replace( + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="' + + '-Dlogback.configurationFile=apitest/build/resources/main/logback.xml"') + } + + if (osdetector.os != 'windows') + delete fileTree(dir: rootProject.projectDir, include: 'bisq-*.bat') + else + delete fileTree(dir: rootProject.projectDir, include: 'bisq-*', exclude: '*.bat') + } + } + + startScripts { + // rename scripts from, e.g. `desktop` to `bisq-desktop` + applicationName = "bisq-$applicationName" + } +} + +configure(project(':proto')) { + apply plugin: 'com.google.protobuf' + + dependencies { + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-protobuf:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation "com.google.guava:guava:$guavaVersion" + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } + + sourceSets.main.java.srcDirs += [ + 'build/generated/source/proto/main/grpc', + 'build/generated/source/proto/main/java' + ] + + protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protocVersion}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } + } +} + +configure(project(':assets')) { + dependencies { + compile("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { + exclude(module: 'jsr305') + exclude(module: 'slf4j-api') + exclude(module: 'guava') + exclude(module: 'protobuf-java') + exclude(module: 'bcprov-jdk15on') + exclude(module: 'okhttp') + exclude(module: 'okio') + } + compile "com.google.guava:guava:$guavaVersion" + compile "org.slf4j:slf4j-api:$slf4jVersion" + compile "org.apache.commons:commons-lang3:$langVersion" + } +} + + +configure(project(':common')) { + dependencies { + compile project(':proto') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + compile "org.openjfx:javafx-base:$javafxVersion:$os" + compile "org.openjfx:javafx-graphics:$javafxVersion:$os" + compile "com.google.protobuf:protobuf-java:$protobufVersion" + compile "com.google.code.gson:gson:$gsonVersion" + compile "net.sf.jopt-simple:jopt-simple:$joptVersion" + compile "org.slf4j:slf4j-api:$slf4jVersion" + compile "ch.qos.logback:logback-core:$logbackVersion" + compile "ch.qos.logback:logback-classic:$logbackVersion" + compile "com.google.code.findbugs:jsr305:$findbugsVersion" + compile "com.google.guava:guava:$guavaVersion" + compile("com.google.inject:guice:$guiceVersion") { + exclude(module: 'guava') + } + compile("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { + exclude(module: 'jsr305') + exclude(module: 'slf4j-api') + exclude(module: 'guava') + exclude(module: 'protobuf-java') + exclude(module: 'bcprov-jdk15on') + exclude(module: 'okhttp') + exclude(module: 'okio') + } + runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + compile "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" + compile "org.bouncycastle:bcpg-jdk15on:$bcVersion" + compile "commons-io:commons-io:$ioVersion" + compile "org.apache.commons:commons-lang3:$langVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompile "org.hamcrest:hamcrest-all:$hamcrestVersion" + } +} + + +configure(project(':p2p')) { + dependencies { + compile project(':common') + compile("com.github.bisq-network.netlayer:tor.native:$netlayerVersion") { + exclude(module: 'slf4j-api') + } + compile("com.github.bisq-network.netlayer:tor.external:$netlayerVersion") { + exclude(module: 'slf4j-api') + } + implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { + exclude(module: 'commons-codec') + } + compile "org.fxmisc.easybind:easybind:$easybindVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompile("org.mockito:mockito-core:$mockitoVersion") + } + + processResources.doFirst { + // Sanity check that Git LFS-managed data store files have actually been sync'd. + // If they have not, e.g. because Git LFS is not installed, they will be text files + // containing a sha256 hash of the remote object, indicating we should stop the + // build and inform the user how to fix the problem. + if (file('src/main/resources/ProposalStore_BTC_MAINNET').text.contains("oid sha256:")) + throw new GradleException("p2p data store files have not been synchronized. " + + "To fix this, ensure you have Git LFS installed and run `git lfs pull`. " + + "See docs/build.md for more information.") + } +} + + +configure(project(':core')) { + dependencies { + compile project(':proto') + compile project(':assets') + compile project(':p2p') + implementation "commons-codec:commons-codec:$codecVersion" + implementation "com.google.code.gson:gson:$gsonVersion" + implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion" + implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { + exclude(module: 'commons-codec') + } + compile "com.google.guava:guava:$guavaVersion" + compile("com.github.bisq-network:jsonrpc4j:$jsonrpc4jVersion") { + exclude(module: 'base64') + exclude(module: 'httpcore-nio') + } + compile "com.fasterxml.jackson.core:jackson-core:$jacksonVersion" + compile "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion" + compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") { + exclude(module: 'jackson-annotations') + } + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testCompile "org.hamcrest:hamcrest-all:$hamcrestVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" + testCompile "com.natpryce:make-it-easy:$easyVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + } + + test { + systemProperty 'jdk.attach.allowAttachSelf', true + } +} + +configure(project(':cli')) { + mainClassName = 'bisq.cli.CliMain' + + dependencies { + compile project(':proto') + implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" + implementation "com.google.guava:guava:$guavaVersion" + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-core:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + } + + test { + useJUnitPlatform() + } +} + +configure(project(':desktop')) { + apply plugin: 'com.github.johnrengelman.shadow' + apply plugin: 'witness' + apply from: '../gradle/witness/gradle-witness.gradle' + apply from: 'package/package.gradle' + + version = '1.6.2-SNAPSHOT' + + jar.manifest.attributes( + "Implementation-Title": project.name, + "Implementation-Version": version) + + mainClassName = 'bisq.desktop.app.BisqAppMain' + + tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true + } + + sourceSets.main.resources.srcDirs += ['src/main/java'] // to copy fxml and css files + + dependencies { + compile project(':core') + compile "net.glxn:qrgen:$qrgenVersion" + compile "de.jensd:fontawesomefx:$fontawesomefxVersion" + compile "de.jensd:fontawesomefx-commons:$fontawesomefxCommonsVersion" + compile "de.jensd:fontawesomefx-materialdesignfont:$fontawesomefxMaterialdesignfontVersion" + compile "com.google.guava:guava:$guavaVersion" + compile "com.googlecode.jcsv:jcsv:$jcsvVersion" + compile "org.openjfx:javafx-controls:$javafxVersion:$os" + compile "org.openjfx:javafx-fxml:$javafxVersion:$os" + compile "org.openjfx:javafx-swing:$javafxVersion:$os" + compile "com.jfoenix:jfoenix:$jfoenixVersion" + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testCompile "org.mockito:mockito-core:$mockitoVersion" + testCompile "com.natpryce:make-it-easy:$easyVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + } + + test { + systemProperty 'jdk.attach.allowAttachSelf', true + } +} + + +configure(project(':monitor')) { + mainClassName = 'bisq.monitor.Monitor' + + test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } + } + + dependencies { + compile project(':core') + compile "org.slf4j:slf4j-api:$slf4jVersion" + compile "ch.qos.logback:logback-core:$logbackVersion" + compile "ch.qos.logback:logback-classic:$logbackVersion" + compile "com.google.guava:guava:$guavaVersion" + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + } +} + + +configure(project(':pricenode')) { + apply plugin: "org.springframework.boot" + + version = file("src/main/resources/version.txt").text.trim() + + jar.manifest.attributes( + "Implementation-Title": project.name, + "Implementation-Version": version) + + dependencies { + compile project(":core") + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + implementation "com.google.code.gson:gson:$gsonVersion" + implementation "commons-codec:commons-codec:$codecVersion" + implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion" + implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { + exclude(module: 'commons-codec') + } + compile("org.knowm.xchange:xchange-bitbay:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-btcmarkets:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-binance:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitfinex:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitflyer:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitstamp:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-cexio:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-coinmate:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-coinmarketcap:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-coinone:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-exmo:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-hitbtc:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-huobi:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-independentreserve:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-kraken:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-luno:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-mercadobitcoin:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-paribu:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-poloniex:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-quoine:$knowmXchangeVersion") + compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion") + compile("org.springframework.boot:spring-boot-starter-actuator") + testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + } + + test { + useJUnitPlatform() + + // Disabled by default, since spot provider tests include connections to external API endpoints + // Can be enabled by adding -Dtest.pricenode.includeSpotProviderTests=true to the gradle command: + // ./gradlew test -Dtest.pricenode.includeSpotProviderTests=true + if (System.properties['test.pricenode.includeSpotProviderTests'] != 'true') { + project.logger.lifecycle('Pricenode: Skipping spot provider tests') + exclude 'bisq/price/spot/providers/**' + } + } + + task stage { + dependsOn assemble + } +} + + +configure(project(':relay')) { + mainClassName = 'bisq.relay.RelayMain' + + dependencies { + compile project(':common') + implementation "io.grpc:grpc-auth:$grpcVersion" + compile "com.sparkjava:spark-core:$sparkVersion" + compile "com.turo:pushy:$pushyVersion" + implementation("com.google.firebase:firebase-admin:$firebaseVersion") { + exclude(module: 'commons-logging') + exclude(module: 'httpclient') + exclude(module: 'httpcore') + exclude(module: 'grpc-auth') + } + compile "commons-codec:commons-codec:$codecVersion" + } +} + + +configure(project(':seednode')) { + apply plugin: 'com.github.johnrengelman.shadow' + + mainClassName = 'bisq.seednode.SeedNodeMain' + + dependencies { + compile project(':core') + compileOnly "org.projectlombok:lombok:$lombokVersion" + implementation "com.google.guava:guava:$guavaVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" + } +} + + +configure(project(':statsnode')) { + mainClassName = 'bisq.statistics.StatisticsMain' + + dependencies { + compile project(':core') + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} + +configure(project(':daemon')) { + mainClassName = 'bisq.daemon.app.BisqDaemonMain' + + dependencies { + compile project(':core') + implementation "com.google.guava:guava:$guavaVersion" + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-protobuf:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + } +} + +configure(project(':inventory')) { + apply plugin: 'com.github.johnrengelman.shadow' + + mainClassName = 'bisq.inventory.InventoryMonitorMain' + + dependencies { + compile project(':core') + compile "com.google.guava:guava:$guavaVersion" + compile "com.sparkjava:spark-core:$sparkVersion" + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} + +configure(project(':apitest')) { + mainClassName = 'bisq.apitest.ApiTestMain' + + // The external dao-setup.gradle file contains tasks to install and clean dao-setup + // files downloaded from + // https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip + // These tasks are not run by the default build, but they can can be run during + // full or partial builds, or by themselves. + // To run the regular clean + build + test (non api), and install dao-setup files: + // ./gradlew clean build :apitest:installDaoSetup + // To install or re-install dao-setup file only: + // ./gradlew :apitest:installDaoSetup -x test + // To clean installed dao-setup files: + // ./gradlew :apitest:cleanDaoSetup -x test + apply from: 'dao-setup.gradle' + + // We have to disable the :apitest 'test' task by default because we do not want + // to interfere with normal builds. To run JUnit tests in this subproject: + // Run a normal build and install dao-setup files first, then run: + // 'gradle :apitest:test -DrunApiTests=true' + test.enabled = System.getProperty("runApiTests") == "true" + + sourceSets { + main { + resources { + exclude 'dao-setup' + exclude 'dao-setup.zip' + } + } + } + + test { + useJUnitPlatform() + outputs.upToDateWhen { false } // Don't use previously cached test outputs. + testLogging { + showStackTraces = true // Show full stack traces in the console. + exceptionFormat = "full" + // Show passed & failed tests, and anything printed to stderr by the tests in the console. + // Do not show skipped tests in the console; they are shown in the html report. + events "passed", "failed", "standardError" + } + + afterSuite { desc, result -> + if (!desc.parent) { + println("${result.resultType} " + + "[${result.testCount} tests, " + + "${result.successfulTestCount} passed, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped] html report contains skipped test info") + + // Show report link if all tests passed in case you want to see more detail, stdout, skipped, etc. + if (result.resultType == TestResult.ResultType.SUCCESS) { + DirectoryReport htmlReport = getReports().getHtml() + String reportUrl = new org.gradle.internal.logging.ConsoleRenderer() + .asClickableFileUrl(htmlReport.getEntryPoint()) + println("REPORT " + reportUrl) + } + } + } + } + + dependencies { + compile project(':proto') + compile project(':common') + compile project(':seednode') + compile project(':desktop') + compile project(':daemon') + compile project(':cli') + implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" + implementation "com.google.guava:guava:$guavaVersion" + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-protobuf:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + + compileOnly "org.projectlombok:lombok:$lombokVersion" + compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + } +} + diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java new file mode 100644 index 0000000000..95bfd0d7b8 --- /dev/null +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -0,0 +1,817 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import io.grpc.StatusRuntimeException; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; + +import java.util.Date; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.CurrencyFormat.formatMarketPrice; +import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo; +import static bisq.cli.CurrencyFormat.toSatoshis; +import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct; +import static bisq.cli.Method.*; +import static bisq.cli.TableFormat.*; +import static bisq.cli.opts.OptLabel.*; +import static java.lang.String.format; +import static java.lang.System.err; +import static java.lang.System.exit; +import static java.lang.System.out; +import static java.util.Collections.singletonList; + + + +import bisq.cli.opts.ArgumentList; +import bisq.cli.opts.CancelOfferOptionParser; +import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; +import bisq.cli.opts.CreateOfferOptionParser; +import bisq.cli.opts.CreatePaymentAcctOptionParser; +import bisq.cli.opts.GetAddressBalanceOptionParser; +import bisq.cli.opts.GetBTCMarketPriceOptionParser; +import bisq.cli.opts.GetBalanceOptionParser; +import bisq.cli.opts.GetOfferOptionParser; +import bisq.cli.opts.GetOffersOptionParser; +import bisq.cli.opts.GetPaymentAcctFormOptionParser; +import bisq.cli.opts.GetTradeOptionParser; +import bisq.cli.opts.GetTransactionOptionParser; +import bisq.cli.opts.RegisterDisputeAgentOptionParser; +import bisq.cli.opts.RemoveWalletPasswordOptionParser; +import bisq.cli.opts.SendBsqOptionParser; +import bisq.cli.opts.SendBtcOptionParser; +import bisq.cli.opts.SetTxFeeRateOptionParser; +import bisq.cli.opts.SetWalletPasswordOptionParser; +import bisq.cli.opts.SimpleMethodOptionParser; +import bisq.cli.opts.TakeOfferOptionParser; +import bisq.cli.opts.UnlockWalletOptionParser; +import bisq.cli.opts.VerifyBsqSentToAddressOptionParser; +import bisq.cli.opts.WithdrawFundsOptionParser; + +/** + * A command-line client for the Bisq gRPC API. + */ +@Slf4j +public class CliMain { + + public static void main(String[] args) { + try { + run(args); + } catch (Throwable t) { + err.println("Error: " + t.getMessage()); + exit(1); + } + } + + public static void run(String[] args) { + var parser = new OptionParser(); + + var helpOpt = parser.accepts(OPT_HELP, "Print this help text") + .forHelp(); + + var hostOpt = parser.accepts(OPT_HOST, "rpc server hostname or ip") + .withRequiredArg() + .defaultsTo("localhost"); + + var portOpt = parser.accepts(OPT_PORT, "rpc server port") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); + + var passwordOpt = parser.accepts(OPT_PASSWORD, "rpc server password") + .withRequiredArg(); + + // Parse the CLI opts host, port, password, method name, and help. The help opt + // may indicate the user is asking for method level help, and will be excluded + // from the parsed options if a method opt is present in String[] args. + OptionSet options = parser.parse(new ArgumentList(args).getCLIArguments()); + @SuppressWarnings("unchecked") + var nonOptionArgs = (List) options.nonOptionArguments(); + + // If neither the help opt nor a method name is present, print CLI level help + // to stderr and throw an exception. + if (!options.has(helpOpt) && nonOptionArgs.isEmpty()) { + printHelp(parser, err); + throw new IllegalArgumentException("no method specified"); + } + + // If the help opt is present, but not a method name, print CLI level help + // to stdout. + if (options.has(helpOpt) && nonOptionArgs.isEmpty()) { + printHelp(parser, out); + return; + } + + var host = options.valueOf(hostOpt); + var port = options.valueOf(portOpt); + var password = options.valueOf(passwordOpt); + if (password == null) + throw new IllegalArgumentException("missing required 'password' option"); + + var methodName = nonOptionArgs.get(0); + Method method; + try { + method = getMethodFromCmd(methodName); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(format("'%s' is not a supported method", methodName)); + } + + GrpcClient client = new GrpcClient(host, port, password); + try { + switch (method) { + case getversion: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var version = client.getVersion(); + out.println(version); + return; + } + case getbalance: { + var opts = new GetBalanceOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var currencyCode = opts.getCurrencyCode(); + var balances = client.getBalances(currencyCode); + switch (currencyCode.toUpperCase()) { + case "BSQ": + out.println(formatBsqBalanceInfoTbl(balances.getBsq())); + break; + case "BTC": + out.println(formatBtcBalanceInfoTbl(balances.getBtc())); + break; + case "": + default: + out.println(formatBalancesTbls(balances)); + break; + } + return; + } + case getaddressbalance: { + var opts = new GetAddressBalanceOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = opts.getAddress(); + var addressBalance = client.getAddressBalance(address); + out.println(formatAddressBalanceTbl(singletonList(addressBalance))); + return; + } + case getbtcprice: { + var opts = new GetBTCMarketPriceOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var currencyCode = opts.getCurrencyCode(); + var price = client.getBtcPrice(currencyCode); + out.println(formatMarketPrice(price)); + return; + } + case getfundingaddresses: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var fundingAddresses = client.getFundingAddresses(); + out.println(formatAddressBalanceTbl(fundingAddresses)); + return; + } + case getunusedbsqaddress: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = client.getUnusedBsqAddress(); + out.println(address); + return; + } + case sendbsq: { + var opts = new SendBsqOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = opts.getAddress(); + var amount = opts.getAmount(); + verifyStringIsValidDecimal(OPT_AMOUNT, amount); + + var txFeeRate = opts.getFeeRate(); + if (!txFeeRate.isEmpty()) + verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate); + + var txInfo = client.sendBsq(address, amount, txFeeRate); + out.printf("%s bsq sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); + return; + } + case sendbtc: { + var opts = new SendBtcOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = opts.getAddress(); + var amount = opts.getAmount(); + verifyStringIsValidDecimal(OPT_AMOUNT, amount); + + var txFeeRate = opts.getFeeRate(); + if (!txFeeRate.isEmpty()) + verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate); + + var memo = opts.getMemo(); + + var txInfo = client.sendBtc(address, amount, txFeeRate, memo); + out.printf("%s btc sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); + return; + } + case verifybsqsenttoaddress: { + var opts = new VerifyBsqSentToAddressOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = opts.getAddress(); + var amount = opts.getAmount(); + verifyStringIsValidDecimal(OPT_AMOUNT, amount); + + var bsqWasSent = client.verifyBsqSentToAddress(address, amount); + out.printf("%s bsq %s sent to address %s%n", + amount, + bsqWasSent ? "has been" : "has not been", + address); + return; + } + case gettxfeerate: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var txFeeRate = client.getTxFeeRate(); + out.println(formatTxFeeRateInfo(txFeeRate)); + return; + } + case settxfeerate: { + var opts = new SetTxFeeRateOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var txFeeRate = client.setTxFeeRate(toLong(opts.getFeeRate())); + out.println(formatTxFeeRateInfo(txFeeRate)); + return; + } + case unsettxfeerate: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var txFeeRate = client.unsetTxFeeRate(); + out.println(formatTxFeeRateInfo(txFeeRate)); + return; + } + case gettransaction: { + var opts = new GetTransactionOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var txId = opts.getTxId(); + var tx = client.getTransaction(txId); + out.println(TransactionFormat.format(tx)); + return; + } + case createoffer: { + var opts = new CreateOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var paymentAcctId = opts.getPaymentAccountId(); + var direction = opts.getDirection(); + var currencyCode = opts.getCurrencyCode(); + var amount = toSatoshis(opts.getAmount()); + var minAmount = toSatoshis(opts.getMinAmount()); + var useMarketBasedPrice = opts.isUsingMktPriceMargin(); + var fixedPrice = opts.getFixedPrice(); + var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); + var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit()); + var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode(); + var offer = client.createOffer(direction, + currencyCode, + amount, + minAmount, + useMarketBasedPrice, + fixedPrice, + marketPriceMargin.doubleValue(), + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode); + out.println(formatOfferTable(singletonList(offer), currencyCode)); + return; + } + case canceloffer: { + var opts = new CancelOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var offerId = opts.getOfferId(); + client.cancelOffer(offerId); + out.println("offer canceled and removed from offer book"); + return; + } + case getoffer: { + var opts = new GetOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var offerId = opts.getOfferId(); + var offer = client.getOffer(offerId); + out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); + return; + } + case getmyoffer: { + var opts = new GetOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var offerId = opts.getOfferId(); + var offer = client.getMyOffer(offerId); + out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); + return; + } + case getoffers: { + var opts = new GetOffersOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var direction = opts.getDirection(); + var currencyCode = opts.getCurrencyCode(); + List offers = client.getOffers(direction, currencyCode); + if (offers.isEmpty()) + out.printf("no %s %s offers found%n", direction, currencyCode); + else + out.println(formatOfferTable(offers, currencyCode)); + + return; + } + case getmyoffers: { + var opts = new GetOffersOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var direction = opts.getDirection(); + var currencyCode = opts.getCurrencyCode(); + List offers = client.getMyOffers(direction, currencyCode); + if (offers.isEmpty()) + out.printf("no %s %s offers found%n", direction, currencyCode); + else + out.println(formatOfferTable(offers, currencyCode)); + + return; + } + case takeoffer: { + var opts = new TakeOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var offerId = opts.getOfferId(); + var paymentAccountId = opts.getPaymentAccountId(); + var takerFeeCurrencyCode = opts.getTakerFeeCurrencyCode(); + var trade = client.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); + out.printf("trade %s successfully taken%n", trade.getTradeId()); + return; + } + case gettrade: { + // TODO make short-id a valid argument? + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + var showContract = opts.getShowContract(); + var trade = client.getTrade(tradeId); + if (showContract) + out.println(trade.getContractAsJson()); + else + out.println(TradeFormat.format(trade)); + + return; + } + case confirmpaymentstarted: { + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + client.confirmPaymentStarted(tradeId); + out.printf("trade %s payment started message sent%n", tradeId); + return; + } + case confirmpaymentreceived: { + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + client.confirmPaymentReceived(tradeId); + out.printf("trade %s payment received message sent%n", tradeId); + return; + } + case keepfunds: { + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + client.keepFunds(tradeId); + out.printf("funds from trade %s saved in bisq wallet%n", tradeId); + return; + } + case withdrawfunds: { + var opts = new WithdrawFundsOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + var address = opts.getAddress(); + // Multi-word memos must be double quoted. + var memo = opts.getMemo(); + client.withdrawFunds(tradeId, address, memo); + out.printf("trade %s funds sent to btc address %s%n", tradeId, address); + return; + } + case getpaymentmethods: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var paymentMethods = client.getPaymentMethods(); + paymentMethods.forEach(p -> out.println(p.getId())); + return; + } + case getpaymentacctform: { + var opts = new GetPaymentAcctFormOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var paymentMethodId = opts.getPaymentMethodId(); + String jsonString = client.getPaymentAcctFormAsJson(paymentMethodId); + File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(), + ".json", + jsonString); + out.printf("payment account form %s%nsaved to %s%n", + jsonString, jsonFile.getAbsolutePath()); + out.println("Edit the file, and use as the argument to a 'createpaymentacct' command."); + return; + } + case createpaymentacct: { + var opts = new CreatePaymentAcctOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var paymentAccountForm = opts.getPaymentAcctForm(); + String jsonString; + try { + jsonString = new String(Files.readAllBytes(paymentAccountForm)); + } catch (IOException e) { + throw new IllegalStateException( + format("could not read %s", paymentAccountForm)); + } + var paymentAccount = client.createPaymentAccount(jsonString); + out.println("payment account saved"); + out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); + return; + } + case createcryptopaymentacct: { + var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var accountName = opts.getAccountName(); + var currencyCode = opts.getCurrencyCode(); + var address = opts.getAddress(); + var isTradeInstant = opts.getIsTradeInstant(); + var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName, + currencyCode, + address, + isTradeInstant); + out.println("payment account saved"); + out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); + return; + } + case getpaymentaccts: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var paymentAccounts = client.getPaymentAccounts(); + if (paymentAccounts.size() > 0) + out.println(formatPaymentAcctTbl(paymentAccounts)); + else + out.println("no payment accounts are saved"); + + return; + } + case lockwallet: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + client.lockWallet(); + out.println("wallet locked"); + return; + } + case unlockwallet: { + var opts = new UnlockWalletOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var walletPassword = opts.getPassword(); + var timeout = opts.getUnlockTimeout(); + client.unlockWallet(walletPassword, timeout); + out.println("wallet unlocked"); + return; + } + case removewalletpassword: { + var opts = new RemoveWalletPasswordOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var walletPassword = opts.getPassword(); + client.removeWalletPassword(walletPassword); + out.println("wallet decrypted"); + return; + } + case setwalletpassword: { + var opts = new SetWalletPasswordOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var walletPassword = opts.getPassword(); + var newWalletPassword = opts.getNewPassword(); + client.setWalletPassword(walletPassword, newWalletPassword); + out.println("wallet encrypted" + (!newWalletPassword.isEmpty() ? " with new password" : "")); + return; + } + case registerdisputeagent: { + var opts = new RegisterDisputeAgentOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var disputeAgentType = opts.getDisputeAgentType(); + var registrationKey = opts.getRegistrationKey(); + client.registerDisputeAgent(disputeAgentType, registrationKey); + out.println(disputeAgentType + " registered"); + return; + } + case stop: { + if (new SimpleMethodOptionParser(args).parse().isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + client.stopServer(); + out.println("server shutdown signal received"); + return; + } + default: { + throw new RuntimeException(format("unhandled method '%s'", method)); + } + } + } catch (StatusRuntimeException ex) { + // Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message + String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", ""); + if (message.equals("io exception")) + throw new RuntimeException(message + ", server may not be running", ex); + else + throw new RuntimeException(message, ex); + } + } + + private static Method getMethodFromCmd(String methodName) { + // TODO if we use const type for enum we need add some mapping. Even if we don't + // change now it is handy to have flexibility in case we change internal code + // and don't want to break user commands. + return Method.valueOf(methodName.toLowerCase()); + } + + @SuppressWarnings("SameParameterValue") + private static void verifyStringIsValidDecimal(String optionLabel, String optionValue) { + try { + Double.parseDouble(optionValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number", + optionLabel, + optionValue, + optionValue)); + } + } + + @SuppressWarnings("SameParameterValue") + private static void verifyStringIsValidLong(String optionLabel, String optionValue) { + try { + Long.parseLong(optionValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number", + optionLabel, + optionValue, + optionValue)); + } + } + + private static long toLong(String param) { + try { + return Long.parseLong(param); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("'%s' is not a number", param)); + } + } + + private static File saveFileToDisk(String prefix, + @SuppressWarnings("SameParameterValue") String suffix, + String text) { + String timestamp = Long.toUnsignedString(new Date().getTime()); + String relativeFileName = prefix + "_" + timestamp + suffix; + try { + Path path = Paths.get(relativeFileName); + if (!Files.exists(path)) { + try (PrintWriter out = new PrintWriter(path.toString())) { + out.println(text); + } + return path.toAbsolutePath().toFile(); + } else { + throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName)); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException(format("could not create file '%s'", relativeFileName)); + } + } + + private static void printHelp(OptionParser parser, @SuppressWarnings("SameParameterValue") PrintStream stream) { + try { + stream.println("Bisq RPC Client"); + stream.println(); + stream.println("Usage: bisq-cli [options] [params]"); + stream.println(); + parser.printHelpOn(stream); + stream.println(); + String rowFormat = "%-25s%-52s%s%n"; + stream.format(rowFormat, "Method", "Params", "Description"); + stream.format(rowFormat, "------", "------", "------------"); + stream.format(rowFormat, getversion.name(), "", "Get server version"); + stream.println(); + stream.format(rowFormat, getbalance.name(), "[--currency-code=]", "Get server wallet balances"); + stream.println(); + stream.format(rowFormat, getaddressbalance.name(), "--address=", "Get server wallet address balance"); + stream.println(); + stream.format(rowFormat, getbtcprice.name(), "--currency-code=", "Get current market btc price"); + stream.println(); + stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses"); + stream.println(); + stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address"); + stream.println(); + stream.format(rowFormat, sendbsq.name(), "--address= --amount= \\", "Send BSQ"); + stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); + stream.println(); + stream.format(rowFormat, sendbtc.name(), "--address= --amount= \\", "Send BTC"); + stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); + stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); + stream.println(); + stream.format(rowFormat, verifybsqsenttoaddress.name(), "--address= --amount=", + "Verify amount was sent to BSQ wallet address"); + stream.println(); + stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte"); + stream.println(); + stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=", "Set custom tx fee rate in sats/byte"); + stream.println(); + stream.format(rowFormat, unsettxfeerate.name(), "", "Unset custom tx fee rate"); + stream.println(); + stream.format(rowFormat, gettransaction.name(), "--transaction-id=", "Get transaction with id"); + stream.println(); + stream.format(rowFormat, createoffer.name(), "--payment-account= \\", "Create and place an offer"); + stream.format(rowFormat, "", "--direction= \\", ""); + stream.format(rowFormat, "", "--currency-code= \\", ""); + stream.format(rowFormat, "", "--amount= \\", ""); + stream.format(rowFormat, "", "[--min-amount=] \\", ""); + stream.format(rowFormat, "", "--fixed-price= | --market-price=margin= \\", ""); + stream.format(rowFormat, "", "--security-deposit= \\", ""); + stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.println(); + stream.format(rowFormat, canceloffer.name(), "--offer-id=", "Cancel offer with id"); + stream.println(); + stream.format(rowFormat, getoffer.name(), "--offer-id=", "Get current offer with id"); + stream.println(); + stream.format(rowFormat, getmyoffer.name(), "--offer-id=", "Get my current offer with id"); + stream.println(); + stream.format(rowFormat, getoffers.name(), "--direction= \\", "Get current offers"); + stream.format(rowFormat, "", "--currency-code=", ""); + stream.println(); + stream.format(rowFormat, getmyoffers.name(), "--direction= \\", "Get my current offers"); + stream.format(rowFormat, "", "--currency-code=", ""); + stream.println(); + stream.format(rowFormat, takeoffer.name(), "--offer-id= \\", "Take offer with id"); + stream.format(rowFormat, "", "--payment-account=", ""); + stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.println(); + stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract"); + stream.format(rowFormat, "", "[--show-contract=]", ""); + stream.println(); + stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=", "Confirm payment started"); + stream.println(); + stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=", "Confirm payment received"); + stream.println(); + stream.format(rowFormat, keepfunds.name(), "--trade-id=", "Keep received funds in Bisq wallet"); + stream.println(); + stream.format(rowFormat, withdrawfunds.name(), "--trade-id= --address= \\", + "Withdraw received funds to external wallet address"); + stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); + stream.println(); + stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids"); + stream.println(); + stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=", "Get a new payment account form"); + stream.println(); + stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account"); + stream.println(); + stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account"); + stream.format(rowFormat, "", "--currency-code= \\", ""); + stream.format(rowFormat, "", "--address=", ""); + stream.format(rowFormat, "", "--trade-instant=", ""); + stream.println(); + stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); + stream.println(); + stream.format(rowFormat, lockwallet.name(), "", "Remove wallet password from memory, locking the wallet"); + stream.println(); + stream.format(rowFormat, unlockwallet.name(), "--wallet-password= --timeout=", + "Store wallet password in memory for timeout seconds"); + stream.println(); + stream.format(rowFormat, setwalletpassword.name(), "--wallet-password= \\", + "Encrypt wallet with password, or set new password on encrypted wallet"); + stream.format(rowFormat, "", "[--new-wallet-password=]", ""); + stream.println(); + stream.format(rowFormat, stop.name(), "", "Shut down the server"); + stream.println(); + stream.println("Method Help Usage: bisq-cli [options] --help"); + stream.println(); + } catch (IOException ex) { + ex.printStackTrace(stream); + } + } +} diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java new file mode 100644 index 0000000000..775221b5ed --- /dev/null +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.cli; + +import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; + +class ColumnHeaderConstants { + + // For inserting 2 spaces between column headers. + static final String COL_HEADER_DELIMITER = " "; + + // Table column header format specs, right padded with two spaces. In some cases + // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the + // expected max data string length is accounted for. In others, column header + // lengths are expected to be greater than any column value length. + static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); + static final String COL_HEADER_AMOUNT = "BTC(min - max)"; + static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; + static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; + static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; + static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance"; + static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance"; + static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance"; + static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance"; + static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance"; + static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; + static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; + static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; + static final String COL_HEADER_IS_USED_ADDRESS = "Is Used"; + static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); + static final String COL_HEADER_CURRENCY = "Currency"; + static final String COL_HEADER_DIRECTION = "Buy/Sell"; + static final String COL_HEADER_NAME = "Name"; + static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; + static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; + static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s"; + static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' '); + static final String COL_HEADER_TRADE_BSQ_BUYER_ADDRESS = "BSQ Buyer Address"; + static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' '); + static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed"; + static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published"; + static final String COL_HEADER_TRADE_PAYMENT_SENT = padEnd("%-3s Sent", 8, ' '); + static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = padEnd("%-3s Received", 12, ' '); + static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published"; + static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn"; + static final String COL_HEADER_TRADE_ROLE = "My Role"; + static final String COL_HEADER_TRADE_SHORT_ID = "ID"; + static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); + static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)"; + static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)"; + + static final String COL_HEADER_TX_ID = "Tx ID"; + static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; + static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)"; + static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)"; + static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)"; + static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed"; + static final String COL_HEADER_TX_MEMO = "Memo"; + + static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); + + static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); +} diff --git a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java new file mode 100644 index 0000000000..cb503d7c07 --- /dev/null +++ b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.cli; + +import java.util.ArrayList; +import java.util.List; + +class CryptoCurrencyUtil { + + public static boolean isSupportedCryptoCurrency(String currencyCode) { + return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); + } + + public static List getSupportedCryptoCurrencies() { + final List result = new ArrayList<>(); + result.add("BSQ"); + result.sort(String::compareTo); + return result; + } +} diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java new file mode 100644 index 0000000000..4abf20276e --- /dev/null +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -0,0 +1,155 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.TxFeeRateInfo; + +import com.google.common.annotations.VisibleForTesting; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.math.BigDecimal; + +import java.util.Locale; + +import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; +import static java.math.RoundingMode.UNNECESSARY; + +@VisibleForTesting +public class CurrencyFormat { + + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); + + static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); + static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); + static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0"); + + static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); + static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); + static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00"); + + static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01"); + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + public static String formatSatoshis(long sats) { + return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); + } + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + public static String formatBsq(long sats) { + return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR)); + } + + public static String formatBsqAmount(long bsqSats) { + // BSQ sats = trade.getOffer().getVolume() + NUMBER_FORMAT.setMinimumFractionDigits(2); + NUMBER_FORMAT.setMaximumFractionDigits(2); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue()); + } + + public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) { + if (txFeeRateInfo.getUseCustomTxFeeRate()) + return format("custom tx fee rate: %s sats/byte, network rate: %s sats/byte, min network rate: %s sats/byte", + formatFeeSatoshis(txFeeRateInfo.getCustomTxFeeRate()), + formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()), + formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate())); + else + return format("tx fee rate: %s sats/byte, min tx fee rate: %s sats/byte", + formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()), + formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate())); + } + + public static String formatAmountRange(long minAmount, long amount) { + return minAmount != amount + ? formatSatoshis(minAmount) + " - " + formatSatoshis(amount) + : formatSatoshis(amount); + } + + public static String formatVolumeRange(long minVolume, long volume) { + return minVolume != volume + ? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume) + : formatOfferVolume(volume); + } + + public static String formatCryptoCurrencyVolumeRange(long minVolume, long volume) { + return minVolume != volume + ? formatCryptoCurrencyOfferVolume(minVolume) + " - " + formatCryptoCurrencyOfferVolume(volume) + : formatCryptoCurrencyOfferVolume(volume); + } + + public static String formatMarketPrice(double price) { + NUMBER_FORMAT.setMinimumFractionDigits(4); + NUMBER_FORMAT.setMaximumFractionDigits(4); + return NUMBER_FORMAT.format(price); + } + + public static String formatPrice(long price) { + NUMBER_FORMAT.setMinimumFractionDigits(4); + NUMBER_FORMAT.setMaximumFractionDigits(4); + NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return NUMBER_FORMAT.format((double) price / 10_000); + } + + public static String formatCryptoCurrencyPrice(long price) { + NUMBER_FORMAT.setMinimumFractionDigits(8); + NUMBER_FORMAT.setMaximumFractionDigits(8); + NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); + } + + public static String formatOfferVolume(long volume) { + NUMBER_FORMAT.setMinimumFractionDigits(0); + NUMBER_FORMAT.setMaximumFractionDigits(0); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return NUMBER_FORMAT.format((double) volume / 10_000); + } + + public static String formatCryptoCurrencyOfferVolume(long volume) { + NUMBER_FORMAT.setMinimumFractionDigits(2); + NUMBER_FORMAT.setMaximumFractionDigits(2); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); + } + + public static long toSatoshis(String btc) { + if (btc.startsWith("-")) + throw new IllegalArgumentException(format("'%s' is not a positive number", btc)); + + try { + return new BigDecimal(btc).multiply(SATOSHI_DIVISOR).longValue(); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(format("'%s' is not a number", btc)); + } + } + + public static double toSecurityDepositAsPct(String securityDepositInput) { + try { + return new BigDecimal(securityDepositInput) + .multiply(SECURITY_DEPOSIT_MULTIPLICAND).doubleValue(); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(format("'%s' is not a number", securityDepositInput)); + } + } + + public static String formatFeeSatoshis(long sats) { + return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats)); + } +} diff --git a/cli/src/main/java/bisq/cli/DirectionFormat.java b/cli/src/main/java/bisq/cli/DirectionFormat.java new file mode 100644 index 0000000000..ac0e5b6c55 --- /dev/null +++ b/cli/src/main/java/bisq/cli/DirectionFormat.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.function.Function; + +import static bisq.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION; +import static java.lang.String.format; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +class DirectionFormat { + + static int getLongestDirectionColWidth(List offers) { + if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("BTC")) + return COL_HEADER_DIRECTION.length(); + else + return 18; // .e.g., "Sell BSQ (Buy BTC)".length() + } + + static final Function directionFormat = (offer) -> { + String baseCurrencyCode = offer.getBaseCurrencyCode(); + boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("BTC"); + if (!isCryptoCurrencyOffer) { + return baseCurrencyCode; + } else { + // Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)". + String direction = offer.getDirection(); + String mirroredDirection = getMirroredDirection(direction); + Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase(); + return format("%s %s (%s %s)", + mixedCase.apply(mirroredDirection), + baseCurrencyCode, + mixedCase.apply(direction), + offer.getCounterCurrencyCode()); + } + }; + + static String getMirroredDirection(String directionAsString) { + return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); + } +} diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java new file mode 100644 index 0000000000..92784e8829 --- /dev/null +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -0,0 +1,516 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.CancelOfferRequest; +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetAddressBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; +import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetMethodHelpRequest; +import bisq.proto.grpc.GetMyOfferRequest; +import bisq.proto.grpc.GetMyOffersRequest; +import bisq.proto.grpc.GetOfferRequest; +import bisq.proto.grpc.GetOffersRequest; +import bisq.proto.grpc.GetPaymentAccountFormRequest; +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsRequest; +import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetTransactionRequest; +import bisq.proto.grpc.GetTxFeeRateRequest; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; +import bisq.proto.grpc.GetVersionRequest; +import bisq.proto.grpc.KeepFundsRequest; +import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.MarketPriceRequest; +import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.RegisterDisputeAgentRequest; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcRequest; +import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.StopRequest; +import bisq.proto.grpc.TakeOfferReply; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TradeInfo; +import bisq.proto.grpc.TxFeeRateInfo; +import bisq.proto.grpc.TxInfo; +import bisq.proto.grpc.UnlockWalletRequest; +import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.VerifyBsqSentToAddressRequest; +import bisq.proto.grpc.WithdrawFundsRequest; + +import protobuf.PaymentAccount; +import protobuf.PaymentMethod; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.CryptoCurrencyUtil.isSupportedCryptoCurrency; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + + +@SuppressWarnings("ResultOfMethodCallIgnored") +@Slf4j +public final class GrpcClient { + + private final GrpcStubs grpcStubs; + + public GrpcClient(String apiHost, int apiPort, String apiPassword) { + this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword); + } + + public String getVersion() { + var request = GetVersionRequest.newBuilder().build(); + return grpcStubs.versionService.getVersion(request).getVersion(); + } + + public BalancesInfo getBalances() { + return getBalances(""); + } + + public BsqBalanceInfo getBsqBalances() { + return getBalances("BSQ").getBsq(); + } + + public BtcBalanceInfo getBtcBalances() { + return getBalances("BTC").getBtc(); + } + + public BalancesInfo getBalances(String currencyCode) { + var request = GetBalancesRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.walletsService.getBalances(request).getBalances(); + } + + public AddressBalanceInfo getAddressBalance(String address) { + var request = GetAddressBalanceRequest.newBuilder() + .setAddress(address).build(); + return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo(); + } + + public double getBtcPrice(String currencyCode) { + var request = MarketPriceRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.priceService.getMarketPrice(request).getPrice(); + } + + public List getFundingAddresses() { + var request = GetFundingAddressesRequest.newBuilder().build(); + return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList(); + } + + public String getUnusedBsqAddress() { + var request = GetUnusedBsqAddressRequest.newBuilder().build(); + return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress(); + } + + public String getUnusedBtcAddress() { + var request = GetFundingAddressesRequest.newBuilder().build(); + var addressBalances = grpcStubs.walletsService.getFundingAddresses(request) + .getAddressBalanceInfoList(); + //noinspection OptionalGetWithoutIsPresent + return addressBalances.stream() + .filter(AddressBalanceInfo::getIsAddressUnused) + .findFirst() + .get() + .getAddress(); + } + + public TxInfo sendBsq(String address, String amount, String txFeeRate) { + var request = SendBsqRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .build(); + return grpcStubs.walletsService.sendBsq(request).getTxInfo(); + } + + public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) { + var request = SendBtcRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .setMemo(memo) + .build(); + return grpcStubs.walletsService.sendBtc(request).getTxInfo(); + } + + public boolean verifyBsqSentToAddress(String address, String amount) { + var request = VerifyBsqSentToAddressRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .build(); + return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived(); + } + + public TxFeeRateInfo getTxFeeRate() { + var request = GetTxFeeRateRequest.newBuilder().build(); + return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo(); + } + + public TxFeeRateInfo setTxFeeRate(long txFeeRate) { + var request = SetTxFeeRatePreferenceRequest.newBuilder() + .setTxFeeRatePreference(txFeeRate) + .build(); + return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo(); + } + + public TxFeeRateInfo unsetTxFeeRate() { + var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); + return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo(); + } + + public TxInfo getTransaction(String txId) { + var request = GetTransactionRequest.newBuilder() + .setTxId(txId) + .build(); + return grpcStubs.walletsService.getTransaction(request).getTxInfo(); + } + + public OfferInfo createFixedPricedOffer(String direction, + String currencyCode, + long amount, + long minAmount, + String fixedPrice, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode) { + return createOffer(direction, + currencyCode, + amount, + minAmount, + false, + fixedPrice, + 0.00, + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode); + } + + public OfferInfo createMarketBasedPricedOffer(String direction, + String currencyCode, + long amount, + long minAmount, + double marketPriceMargin, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode) { + return createOffer(direction, + currencyCode, + amount, + minAmount, + true, + "0", + marketPriceMargin, + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode); + } + + public OfferInfo createOffer(String direction, + String currencyCode, + long amount, + long minAmount, + boolean useMarketBasedPrice, + String fixedPrice, + double marketPriceMargin, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode) { + var request = CreateOfferRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .setAmount(amount) + .setMinAmount(minAmount) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setPrice(fixedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setBuyerSecurityDeposit(securityDeposit) + .setPaymentAccountId(paymentAcctId) + .setMakerFeeCurrencyCode(makerFeeCurrencyCode) + .build(); + return grpcStubs.offersService.createOffer(request).getOffer(); + } + + public void cancelOffer(String offerId) { + var request = CancelOfferRequest.newBuilder() + .setId(offerId) + .build(); + grpcStubs.offersService.cancelOffer(request); + } + + public OfferInfo getOffer(String offerId) { + var request = GetOfferRequest.newBuilder() + .setId(offerId) + .build(); + return grpcStubs.offersService.getOffer(request).getOffer(); + } + + public OfferInfo getMyOffer(String offerId) { + var request = GetMyOfferRequest.newBuilder() + .setId(offerId) + .build(); + return grpcStubs.offersService.getMyOffer(request).getOffer(); + } + + public List getOffers(String direction, String currencyCode) { + if (isSupportedCryptoCurrency(currencyCode)) { + return getCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getOffers(request).getOffersList(); + } + } + + public List getCryptoCurrencyOffers(String direction, String currencyCode) { + return getOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) + .collect(toList()); + } + + public List getOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getOffers(BUY.name(), currencyCode)); + offers.addAll(getOffers(SELL.name(), currencyCode)); + return sortOffersByDate(offers); + } + + public List getOffersSortedByDate(String direction, String currencyCode) { + var offers = getOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); + return sortOffersByDate(offers); + } + + public List getMyOffers(String direction, String currencyCode) { + if (isSupportedCryptoCurrency(currencyCode)) { + return getMyCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetMyOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getMyOffers(request).getOffersList(); + } + } + + public List getMyCryptoCurrencyOffers(String direction, String currencyCode) { + return getMyOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) + .collect(toList()); + } + + public List getMyOffersSortedByDate(String direction, String currencyCode) { + var offers = getMyOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getMyOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyOffers(BUY.name(), currencyCode)); + offers.addAll(getMyOffers(SELL.name(), currencyCode)); + return sortOffersByDate(offers); + } + + public List getMyBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); + return sortOffersByDate(offers); + } + + public OfferInfo getMostRecentOffer(String direction, String currencyCode) { + List offers = getOffersSortedByDate(direction, currencyCode); + return offers.isEmpty() ? null : offers.get(offers.size() - 1); + } + + public List sortOffersByDate(List offerInfoList) { + return offerInfoList.stream() + .sorted(comparing(OfferInfo::getDate)) + .collect(toList()); + } + + public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { + var request = TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .setTakerFeeCurrencyCode(takerFeeCurrencyCode) + .build(); + return grpcStubs.tradesService.takeOffer(request); + } + + public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { + var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode); + if (reply.hasTrade()) + return reply.getTrade(); + else + throw new IllegalStateException(reply.getFailureReason().getDescription()); + } + + public TradeInfo getTrade(String tradeId) { + var request = GetTradeRequest.newBuilder() + .setTradeId(tradeId) + .build(); + return grpcStubs.tradesService.getTrade(request).getTrade(); + } + + public void confirmPaymentStarted(String tradeId) { + var request = ConfirmPaymentStartedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.confirmPaymentStarted(request); + } + + public void confirmPaymentReceived(String tradeId) { + var request = ConfirmPaymentReceivedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.confirmPaymentReceived(request); + } + + public void keepFunds(String tradeId) { + var request = KeepFundsRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.keepFunds(request); + } + + public void withdrawFunds(String tradeId, String address, String memo) { + var request = WithdrawFundsRequest.newBuilder() + .setTradeId(tradeId) + .setAddress(address) + .setMemo(memo) + .build(); + grpcStubs.tradesService.withdrawFunds(request); + } + + public List getPaymentMethods() { + var request = GetPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList(); + } + + public String getPaymentAcctFormAsJson(String paymentMethodId) { + var request = GetPaymentAccountFormRequest.newBuilder() + .setPaymentMethodId(paymentMethodId) + .build(); + return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson(); + } + + public PaymentAccount createPaymentAccount(String json) { + var request = CreatePaymentAccountRequest.newBuilder() + .setPaymentAccountForm(json) + .build(); + return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount(); + } + + public List getPaymentAccounts() { + var request = GetPaymentAccountsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); + } + + public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address, + boolean tradeInstant) { + var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() + .setAccountName(accountName) + .setCurrencyCode(currencyCode) + .setAddress(address) + .setTradeInstant(tradeInstant) + .build(); + return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount(); + } + + public List getCryptoPaymentMethods() { + var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); + } + + public void lockWallet() { + var request = LockWalletRequest.newBuilder().build(); + grpcStubs.walletsService.lockWallet(request); + } + + public void unlockWallet(String walletPassword, long timeout) { + var request = UnlockWalletRequest.newBuilder() + .setPassword(walletPassword) + .setTimeout(timeout).build(); + grpcStubs.walletsService.unlockWallet(request); + } + + public void removeWalletPassword(String walletPassword) { + var request = RemoveWalletPasswordRequest.newBuilder() + .setPassword(walletPassword).build(); + grpcStubs.walletsService.removeWalletPassword(request); + } + + public void setWalletPassword(String walletPassword) { + var request = SetWalletPasswordRequest.newBuilder() + .setPassword(walletPassword).build(); + grpcStubs.walletsService.setWalletPassword(request); + } + + public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { + var request = SetWalletPasswordRequest.newBuilder() + .setPassword(oldWalletPassword) + .setNewPassword(newWalletPassword).build(); + grpcStubs.walletsService.setWalletPassword(request); + } + + public void registerDisputeAgent(String disputeAgentType, String registrationKey) { + var request = RegisterDisputeAgentRequest.newBuilder() + .setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey).build(); + grpcStubs.disputeAgentsService.registerDisputeAgent(request); + } + + public void stopServer() { + var request = StopRequest.newBuilder().build(); + grpcStubs.shutdownService.stop(request); + } + + public String getMethodHelp(Method method) { + var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build(); + return grpcStubs.helpService.getMethodHelp(request).getMethodHelp(); + } +} diff --git a/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java new file mode 100644 index 0000000000..61c7d27829 --- /dev/null +++ b/cli/src/main/java/bisq/cli/GrpcStubs.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.DisputeAgentsGrpc; +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.HelpGrpc; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.PaymentAccountsGrpc; +import bisq.proto.grpc.PriceGrpc; +import bisq.proto.grpc.ShutdownServerGrpc; +import bisq.proto.grpc.TradesGrpc; +import bisq.proto.grpc.WalletsGrpc; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannelBuilder; + +import static java.util.concurrent.TimeUnit.SECONDS; + +public final class GrpcStubs { + + public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService; + public final HelpGrpc.HelpBlockingStub helpService; + public final GetVersionGrpc.GetVersionBlockingStub versionService; + public final OffersGrpc.OffersBlockingStub offersService; + public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; + public final PriceGrpc.PriceBlockingStub priceService; + public final ShutdownServerGrpc.ShutdownServerBlockingStub shutdownService; + public final TradesGrpc.TradesBlockingStub tradesService; + public final WalletsGrpc.WalletsBlockingStub walletsService; + + public GrpcStubs(String apiHost, int apiPort, String apiPassword) { + CallCredentials credentials = new PasswordCallCredentials(apiPassword); + + var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + })); + + this.disputeAgentsService = DisputeAgentsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.helpService = HelpGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.shutdownService = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + } +} diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java new file mode 100644 index 0000000000..cf8b1d7df5 --- /dev/null +++ b/cli/src/main/java/bisq/cli/Method.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.cli; + +/** + * Currently supported api methods. + */ +public enum Method { + canceloffer, + confirmpaymentreceived, + confirmpaymentstarted, + createoffer, + createpaymentacct, + createcryptopaymentacct, + getaddressbalance, + getbalance, + getbtcprice, + getfundingaddresses, + getmyoffer, + getmyoffers, + getoffer, + getoffers, + getpaymentacctform, + getpaymentaccts, + getpaymentmethods, + gettrade, + gettransaction, + gettxfeerate, + getunusedbsqaddress, + getversion, + keepfunds, + lockwallet, + registerdisputeagent, + removewalletpassword, + sendbsq, + sendbtc, + verifybsqsenttoaddress, + settxfeerate, + setwalletpassword, + takeoffer, + unlockwallet, + unsettxfeerate, + withdrawfunds, + stop +} diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java new file mode 100644 index 0000000000..a1de5be556 --- /dev/null +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.cli; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; + +import java.util.concurrent.Executor; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.String.format; + +/** + * Sets the {@value PASSWORD_KEY} rpc call header to a given value. + */ +class PasswordCallCredentials extends CallCredentials { + + public static final String PASSWORD_KEY = "password"; + + private final String passwordValue; + + public PasswordCallCredentials(String passwordValue) { + if (passwordValue == null) + throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); + this.passwordValue = passwordValue; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) { + appExecutor.execute(() -> { + try { + var headers = new Metadata(); + var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); + headers.put(passwordKey, passwordValue); + metadataApplier.apply(headers); + } catch (Throwable ex) { + metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); + } + }); + } + + @Override + public void thisUsesUnstableApi() { + } +} diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java new file mode 100644 index 0000000000..5c123184e9 --- /dev/null +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -0,0 +1,276 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.OfferInfo; + +import protobuf.PaymentAccount; + +import com.google.common.annotations.VisibleForTesting; + +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.Collectors; + +import static bisq.cli.ColumnHeaderConstants.*; +import static bisq.cli.CurrencyFormat.*; +import static bisq.cli.DirectionFormat.directionFormat; +import static bisq.cli.DirectionFormat.getLongestDirectionColWidth; +import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; +import static java.lang.String.format; +import static java.util.Collections.max; +import static java.util.Comparator.comparing; +import static java.util.TimeZone.getTimeZone; + +@VisibleForTesting +public class TableFormat { + + static final TimeZone TZ_UTC = getTimeZone("UTC"); + static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + public static String formatAddressBalanceTbl(List addressBalanceInfo) { + String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER + + COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + + COL_HEADER_IS_USED_ADDRESS + COL_HEADER_DELIMITER + "\n"; + String headerLine = format(headerFormatString, "BTC"); + + String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify + + " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify + + " %" + COL_HEADER_CONFIRMATIONS.length() + "d" // rt justify + + " %-" + COL_HEADER_IS_USED_ADDRESS.length() + "s"; // lt justify + return headerLine + + addressBalanceInfo.stream() + .map(info -> format(colDataFormat, + info.getAddress(), + formatSatoshis(info.getBalance()), + info.getNumConfirmations(), + info.getIsAddressUnused() ? "NO" : "YES")) + .collect(Collectors.joining("\n")); + } + + public static String formatBalancesTbls(BalancesInfo balancesInfo) { + return "BTC" + "\n" + + formatBtcBalanceInfoTbl(balancesInfo.getBtc()) + "\n" + + "BSQ" + "\n" + + formatBsqBalanceInfoTbl(balancesInfo.getBsq()); + } + + public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()), + formatBsq(bsqBalanceInfo.getUnverifiedBalance()), + formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()), + formatBsq(bsqBalanceInfo.getLockedForVotingBalance()), + formatBsq(bsqBalanceInfo.getLockupBondsBalance()), + formatBsq(bsqBalanceInfo.getUnlockingBondsBalance())); + } + + public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatSatoshis(btcBalanceInfo.getAvailableBalance()), + formatSatoshis(btcBalanceInfo.getReservedBalance()), + formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()), + formatSatoshis(btcBalanceInfo.getLockedBalance())); + } + + public static String formatPaymentAcctTbl(List paymentAccounts) { + // Some column values might be longer than header, so we need to calculate them. + int nameColWidth = getLongestColumnSize( + COL_HEADER_NAME.length(), + paymentAccounts.stream().map(PaymentAccount::getAccountName) + .collect(Collectors.toList())); + int paymentMethodColWidth = getLongestColumnSize( + COL_HEADER_PAYMENT_METHOD.length(), + paymentAccounts.stream().map(a -> a.getPaymentMethod().getId()) + .collect(Collectors.toList())); + String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CURRENCY + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%-" + nameColWidth + "s" // left justify + + " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify + + " %-" + paymentMethodColWidth + "s" // left justify + + " %-" + COL_HEADER_UUID.length() + "s"; // left justify + return headerLine + + paymentAccounts.stream() + .map(a -> format(colDataFormat, + a.getAccountName(), + a.getSelectedTradeCurrency().getCode(), + a.getPaymentMethod().getId(), + a.getId())) + .collect(Collectors.joining("\n")); + } + + public static String formatOfferTable(List offers, String currencyCode) { + if (offers == null || offers.isEmpty()) + throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase())); + + String baseCurrencyCode = offers.get(0).getBaseCurrencyCode(); + return baseCurrencyCode.equalsIgnoreCase("BTC") + ? formatFiatOfferTable(offers, currencyCode) + : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode); + } + + private static String formatFiatOfferTable(List offers, String fiatCurrencyCode) { + // Some column values might be longer than header, so we need to calculate them. + int amountColWith = getLongestAmountColWidth(offers); + int volumeColWidth = getLongestVolumeColWidth(offers); + int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); + String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER + + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode + + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + // COL_HEADER_VOLUME includes %s -> fiatCurrencyCode + + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + + COL_HEADER_UUID.trim() + "%n"; + String headerLine = format(headersFormat, + fiatCurrencyCode.toUpperCase(), + fiatCurrencyCode.toUpperCase()); + String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + o.getDirection(), + formatPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } + + private static String formatCryptoCurrencyOfferTable(List offers, String cryptoCurrencyCode) { + // Some column values might be longer than header, so we need to calculate them. + int directionColWidth = getLongestDirectionColWidth(offers); + int amountColWith = getLongestAmountColWidth(offers); + int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers); + int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); + // TODO use memoize function to avoid duplicate the formatting done above? + String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode + + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + // COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode + + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + + COL_HEADER_UUID.trim() + "%n"; + String headerLine = format(headersFormat, + cryptoCurrencyCode.toUpperCase(), + cryptoCurrencyCode.toUpperCase()); + String colDataFormat = "%-" + directionColWidth + "s" + + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + directionFormat.apply(o), + formatCryptoCurrencyPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } + + private static int getLongestPaymentMethodColWidth(List offers) { + return getLongestColumnSize( + COL_HEADER_PAYMENT_METHOD.length(), + offers.stream() + .map(OfferInfo::getPaymentMethodShortName) + .collect(Collectors.toList())); + } + + private static int getLongestAmountColWidth(List offers) { + return getLongestColumnSize( + COL_HEADER_AMOUNT.length(), + offers.stream() + .map(o -> formatAmountRange(o.getMinAmount(), o.getAmount())) + .collect(Collectors.toList())); + } + + private static int getLongestVolumeColWidth(List offers) { + // Pad this col width by 1 space. + return 1 + getLongestColumnSize( + COL_HEADER_VOLUME.length(), + offers.stream() + .map(o -> formatVolumeRange(o.getMinVolume(), o.getVolume())) + .collect(Collectors.toList())); + } + + private static int getLongestCryptoCurrencyVolumeColWidth(List offers) { + // Pad this col width by 1 space. + return 1 + getLongestColumnSize( + COL_HEADER_VOLUME.length(), + offers.stream() + .map(o -> formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume())) + .collect(Collectors.toList())); + } + + // Return size of the longest string value, or the header.len, whichever is greater. + private static int getLongestColumnSize(int headerLength, List strings) { + int longest = max(strings, comparing(String::length)).length(); + return Math.max(longest, headerLength); + } + + private static String formatTimestamp(long timestamp) { + DATE_FORMAT_ISO_8601.setTimeZone(TZ_UTC); + return DATE_FORMAT_ISO_8601.format(new Date(timestamp)); + } +} diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java new file mode 100644 index 0000000000..dbf8dbf4b8 --- /dev/null +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -0,0 +1,221 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.ContractInfo; +import bisq.proto.grpc.TradeInfo; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import static bisq.cli.ColumnHeaderConstants.*; +import static bisq.cli.CurrencyFormat.*; +import static com.google.common.base.Strings.padEnd; + +@VisibleForTesting +public class TradeFormat { + + private static final String YES = "YES"; + private static final String NO = "NO"; + + // TODO add String format(List trades) + + @VisibleForTesting + public static String format(TradeInfo tradeInfo) { + // Some column values might be longer than header, so we need to calculate them. + int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length()); + int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length()); + + // We only show taker fee under its header when user is the taker. + boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker"); + Supplier makerFeeHeader = () -> !isTaker ? + COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER + : ""; + Supplier makerFeeHeaderSpec = () -> !isTaker ? + "%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s" + : ""; + Supplier takerFeeHeader = () -> isTaker ? + COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER + : ""; + Supplier takerFeeHeaderSpec = () -> isTaker ? + "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s" + : ""; + + boolean showBsqBuyerAddress = shouldShowBsqBuyerAddress(tradeInfo, isTaker); + Supplier bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : ""; + Supplier bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%s" : ""; + + String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER + + priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER // includes %s -> currencyCode + + padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER + + makerFeeHeader.get() + // maker or taker fee header, not both + + takerFeeHeader.get() + + COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_BUYER_COST + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_PAYMENT_SENT + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER + + bsqBuyerAddressHeader.get() + + "%n"; + + String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode(); + String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode(); + + String headerLine = String.format(headersFormat, + /* COL_HEADER_PRICE */ priceHeaderCurrencyCode.apply(tradeInfo), + /* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode, + /* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo, isTaker), + /* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode, + /* COL_HEADER_TRADE_PAYMENT_SENT */ paymentStatusHeaderCurrencyCode.apply(tradeInfo), + /* COL_HEADER_TRADE_PAYMENT_RECEIVED */ paymentStatusHeaderCurrencyCode.apply(tradeInfo)); + + String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify + + " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify + + "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify + + "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify + + makerFeeHeaderSpec.get() // rt justify + // OR (one of them is an empty string) + + takerFeeHeaderSpec.get() // rt justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify + + "%" + (COL_HEADER_TRADE_BUYER_COST.length() + 1) + "s" // rt justify + + " %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left + + " %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left + + " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify + + " %-" + (COL_HEADER_TRADE_WITHDRAWN.length() + 2) + "s" + + bsqBuyerAddressHeaderSpec.get(); + + return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress); + } + + private static String formatTradeData(String format, + TradeInfo tradeInfo, + boolean isTaker, + boolean showBsqBuyerAddress) { + return String.format(format, + tradeInfo.getShortId(), + tradeInfo.getRole(), + priceFormat.apply(tradeInfo), + amountFormat.apply(tradeInfo), + makerTakerMinerTxFeeFormat.apply(tradeInfo, isTaker), + makerTakerFeeFormat.apply(tradeInfo, isTaker), + tradeInfo.getIsDepositPublished() ? YES : NO, + tradeInfo.getIsDepositConfirmed() ? YES : NO, + tradeCostFormat.apply(tradeInfo), + tradeInfo.getIsFiatSent() ? YES : NO, + tradeInfo.getIsFiatReceived() ? YES : NO, + tradeInfo.getIsPayoutPublished() ? YES : NO, + tradeInfo.getIsWithdrawn() ? YES : NO, + bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress)); + } + + private static final Function priceHeader = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? COL_HEADER_PRICE + : COL_HEADER_PRICE_OF_ALTCOIN; + + private static final Function priceHeaderCurrencyCode = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + private static final BiFunction makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> { + if (isTaker) { + return t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ"; + } else { + return t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ"; + } + }; + + private static final Function paymentStatusHeaderCurrencyCode = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + private static final Function priceFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatPrice(t.getTradePrice()) + : formatCryptoCurrencyPrice(t.getOffer().getPrice()); + + private static final Function amountFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatSatoshis(t.getTradeAmountAsLong()) + : formatCryptoCurrencyOfferVolume(t.getOffer().getVolume()); + + private static final BiFunction makerTakerMinerTxFeeFormat = (t, isTaker) -> { + if (isTaker) { + return formatSatoshis(t.getTxFeeAsLong()); + } else { + return formatSatoshis(t.getOffer().getTxFee()); + } + }; + + private static final BiFunction makerTakerFeeFormat = (t, isTaker) -> { + if (isTaker) { + return t.getIsCurrencyForTakerFeeBtc() + ? formatSatoshis(t.getTakerFeeAsLong()) + : formatBsq(t.getTakerFeeAsLong()); + } else { + return t.getOffer().getIsCurrencyForMakerFeeBtc() + ? formatSatoshis(t.getOffer().getMakerFee()) + : formatBsq(t.getOffer().getMakerFee()); + } + }; + + private static final Function tradeCostFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatOfferVolume(t.getOffer().getVolume()) + : formatSatoshis(t.getTradeAmountAsLong()); + + private static final BiFunction bsqReceiveAddress = (t, showBsqBuyerAddress) -> { + if (showBsqBuyerAddress) { + ContractInfo contract = t.getContract(); + boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); + return isBuyerMakerAndSellerTaker // (is BTC buyer / maker) + ? contract.getTakerPaymentAccountPayload().getAddress() + : contract.getMakerPaymentAccountPayload().getAddress(); + } else { + return ""; + } + }; + + private static boolean shouldShowBsqBuyerAddress(TradeInfo tradeInfo, boolean isTaker) { + if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) { + return false; + } else { + ContractInfo contract = tradeInfo.getContract(); + // Do not forget buyer and seller refer to BTC buyer and seller, not BSQ + // buyer and seller. If you are buying BSQ, you are the (BTC) seller. + boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); + if (isTaker) { + return !isBuyerMakerAndSellerTaker; + } else { + return isBuyerMakerAndSellerTaker; + } + } + } +} diff --git a/cli/src/main/java/bisq/cli/TransactionFormat.java b/cli/src/main/java/bisq/cli/TransactionFormat.java new file mode 100644 index 0000000000..608c2fcb71 --- /dev/null +++ b/cli/src/main/java/bisq/cli/TransactionFormat.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.cli; + +import bisq.proto.grpc.TxInfo; + +import com.google.common.annotations.VisibleForTesting; + +import static bisq.cli.ColumnHeaderConstants.*; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static com.google.common.base.Strings.padEnd; + +@VisibleForTesting +public class TransactionFormat { + + public static String format(TxInfo txInfo) { + String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER + + COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER + + COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER + + COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER + + COL_HEADER_TX_FEE + COL_HEADER_DELIMITER + + COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER + + (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER) + + "\n"; + + String colDataFormat = "%-" + txInfo.getTxId().length() + "s" + + " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s" + + " %" + COL_HEADER_TX_INPUT_SUM.length() + "s" + + " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s" + + " %" + COL_HEADER_TX_FEE.length() + "s" + + " %" + COL_HEADER_TX_SIZE.length() + "s" + + " %s"; + + return headerLine + + String.format(colDataFormat, + txInfo.getTxId(), + txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed + formatSatoshis(txInfo.getInputSum()), + formatSatoshis(txInfo.getOutputSum()), + formatSatoshis(txInfo.getFee()), + txInfo.getSize(), + txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo()); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java new file mode 100644 index 0000000000..25256eb6a9 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.cli.opts; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.util.List; +import java.util.function.Function; + +import lombok.Getter; + +import static bisq.cli.opts.OptLabel.OPT_HELP; + +abstract class AbstractMethodOptionParser implements MethodOpts { + + // The full command line args passed to CliMain.main(String[] args). + // CLI and Method level arguments are derived from args by an ArgumentList(args). + protected final String[] args; + + protected final OptionParser parser = new OptionParser(); + + // The help option for a specific api method, e.g., takeoffer -help. + protected final OptionSpec helpOpt = parser.accepts(OPT_HELP, "Print method help").forHelp(); + + @Getter + protected OptionSet options; + @Getter + protected List nonOptionArguments; + + protected AbstractMethodOptionParser(String[] args) { + this.args = args; + } + + public AbstractMethodOptionParser parse() { + try { + options = parser.parse(new ArgumentList(args).getMethodArguments()); + //noinspection unchecked + nonOptionArguments = (List) options.nonOptionArguments(); + return this; + } catch (OptionException ex) { + throw new IllegalArgumentException(cliExceptionMessageStyle.apply(ex), ex); + } + } + + public boolean isForHelp() { + return options.has(helpOpt); + } + + private final Function cliExceptionMessageStyle = (ex) -> { + if (ex.getMessage() == null) + return null; + + var optionToken = "option "; + var cliMessage = ex.getMessage().toLowerCase(); + if (cliMessage.startsWith(optionToken) && cliMessage.length() > optionToken.length()) { + cliMessage = cliMessage.substring(cliMessage.indexOf(" ") + 1); + } + return cliMessage; + }; +} diff --git a/cli/src/main/java/bisq/cli/opts/ArgumentList.java b/cli/src/main/java/bisq/cli/opts/ArgumentList.java new file mode 100644 index 0000000000..b416946d64 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/ArgumentList.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.cli.opts; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * Wrapper for an array of command line arguments. + * + * Used to extract CLI connection and authentication arguments, or method arguments + * before parsing CLI or method opts + * + */ +public class ArgumentList { + + private final Predicate isCliOpt = (o) -> + o.startsWith("--password") || o.startsWith("-password") + || o.startsWith("--port") || o.startsWith("-port") + || o.startsWith("--host") || o.startsWith("-host"); + + + // The method name is the only positional opt in a command (easy to identify). + // If the positional argument does not match a Method, or there are more than one + // positional arguments, the joptsimple parser or CLI will fail as expected. + private final Predicate isMethodNameOpt = (o) -> !o.startsWith("-"); + + private final Predicate isHelpOpt = (o) -> o.startsWith("--help") || o.startsWith("-help"); + + private final String[] arguments; + private int currentIndex; + + public ArgumentList(String... arguments) { + this.arguments = arguments.clone(); + } + + /** + * Returns only the CLI connection & authentication, and method name args + * (--password, --host, --port, --help, method name) contained in the original + * String[] args; excludes the method specific arguments. + * + * If String[] args contains both a method name (the only positional opt) and a help + * argument (--help, -help), it is assumed the user wants method help, not CLI help, + * and the help argument is not included in the returned String[]. + */ + public String[] getCLIArguments() { + currentIndex = 0; + Optional methodNameArgument = Optional.empty(); + Optional helpArgument = Optional.empty(); + List prunedArguments = new ArrayList<>(); + + while (hasMore()) { + String arg = peek(); + if (isMethodNameOpt.test(arg)) { + methodNameArgument = Optional.of(arg); + prunedArguments.add(arg); + } + + if (isCliOpt.test(arg)) + prunedArguments.add(arg); + + if (isHelpOpt.test(arg)) + helpArgument = Optional.of(arg); + + next(); + } + + // Include the saved CLI help argument if the positional method name argument + // was not found. + if (!methodNameArgument.isPresent() && helpArgument.isPresent()) + prunedArguments.add(helpArgument.get()); + + return prunedArguments.toArray(new String[0]); + } + + /** + * Returns only the method args contained in the original String[] args; excludes the + * CLI connection & authentication opts (--password, --host, --port), plus the + * positional method name arg. + */ + public String[] getMethodArguments() { + List prunedArguments = new ArrayList<>(); + currentIndex = 0; + while (hasMore()) { + String arg = peek(); + if (!isCliOpt.test(arg) && !isMethodNameOpt.test(arg)) { + prunedArguments.add(arg); + } + next(); + } + return prunedArguments.toArray(new String[0]); + } + + + boolean hasMore() { + return currentIndex < arguments.length; + } + + @SuppressWarnings("UnusedReturnValue") + String next() { + return arguments[currentIndex++]; + } + + String peek() { + return arguments[currentIndex]; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java new file mode 100644 index 0000000000..c35ffc7bfb --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_OFFER_ID; + +public class CancelOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel") + .withRequiredArg(); + + public CancelOfferOptionParser(String[] args) { + super(args); + } + + public CancelOfferOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) + throw new IllegalArgumentException("no offer id specified"); + + return this; + } + + public String getOfferId() { + return options.valueOf(offerIdOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java new file mode 100644 index 0000000000..a37a9f109b --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME; +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; +import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT; + +public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name") + .withRequiredArg(); + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (bsq only)") + .withRequiredArg(); + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "bsq address") + .withRequiredArg(); + + final OptionSpec tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account") + .withOptionalArg() + .ofType(boolean.class) + .defaultsTo(Boolean.FALSE); + + public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) { + super(args); + } + + public CreateCryptoCurrencyPaymentAcctOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty()) + throw new IllegalArgumentException("no payment account name specified"); + + if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) + throw new IllegalArgumentException("no currency code specified"); + + if (!options.valueOf(currencyCodeOpt).equalsIgnoreCase("bsq")) + throw new IllegalArgumentException("api only supports bsq crypto currency payment accounts"); + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no bsq address specified"); + + return this; + } + + public String getAccountName() { + return options.valueOf(accountNameOpt); + } + + public String getCurrencyCode() { + return options.valueOf(currencyCodeOpt); + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public boolean getIsTradeInstant() { + return options.valueOf(tradeInstantOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java new file mode 100644 index 0000000000..42cf8ad155 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java @@ -0,0 +1,144 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import java.math.BigDecimal; + +import static bisq.cli.opts.OptLabel.*; +import static joptsimple.internal.Strings.EMPTY; + +public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, + "id of payment account used for offer") + .withRequiredArg() + .defaultsTo(EMPTY); + + final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") + .withRequiredArg(); + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") + .withRequiredArg(); + + final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell") + .withRequiredArg(); + + final OptionSpec minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell") + .withOptionalArg(); + + final OptionSpec mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)") + .withOptionalArg() + .defaultsTo("0.00"); + + final OptionSpec fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price") + .withOptionalArg() + .defaultsTo("0"); + + final OptionSpec securityDepositOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)") + .withRequiredArg(); + + final OptionSpec makerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "maker fee currency code (bsq|btc)") + .withOptionalArg() + .defaultsTo("btc"); + + public CreateOfferOptionParser(String[] args) { + super(args); + } + + public CreateOfferOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty()) + throw new IllegalArgumentException("no payment account id specified"); + + if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty()) + throw new IllegalArgumentException("no direction (buy|sell) specified"); + + if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) + throw new IllegalArgumentException("no currency code specified"); + + if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) + throw new IllegalArgumentException("no btc amount specified"); + + if (!options.has(mktPriceMarginOpt) && !options.has(fixedPriceOpt)) + throw new IllegalArgumentException("no market price margin or fixed price specified"); + + if (options.has(mktPriceMarginOpt) && options.valueOf(mktPriceMarginOpt).isEmpty()) + throw new IllegalArgumentException("no market price margin specified"); + + if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty()) + throw new IllegalArgumentException("no fixed price specified"); + + if (!options.has(securityDepositOpt) || options.valueOf(securityDepositOpt).isEmpty()) + throw new IllegalArgumentException("no security deposit specified"); + + return this; + } + + public String getPaymentAccountId() { + return options.valueOf(paymentAccountIdOpt); + } + + public String getDirection() { + return options.valueOf(directionOpt); + } + + public String getCurrencyCode() { + return options.valueOf(currencyCodeOpt); + } + + public String getAmount() { + return options.valueOf(amountOpt); + } + + public String getMinAmount() { + return options.has(minAmountOpt) ? options.valueOf(minAmountOpt) : getAmount(); + } + + public boolean isUsingMktPriceMargin() { + return options.has(mktPriceMarginOpt); + } + + @SuppressWarnings("unused") + public String getMktPriceMargin() { + return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00"; + } + + public BigDecimal getMktPriceMarginAsBigDecimal() { + return isUsingMktPriceMargin() ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) : BigDecimal.ZERO; + } + + public String getFixedPrice() { + return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00"; + } + + public String getSecurityDeposit() { + return options.valueOf(securityDepositOpt); + } + + public String getMakerFeeCurrencyCode() { + return options.has(makerFeeCurrencyCodeOpt) ? options.valueOf(makerFeeCurrencyCodeOpt) : "btc"; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java new file mode 100644 index 0000000000..fe91924bb4 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_FORM; +import static java.lang.String.format; + +public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM, + "path to json payment account form") + .withRequiredArg(); + + public CreatePaymentAcctOptionParser(String[] args) { + super(args); + } + + public CreatePaymentAcctOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(paymentAcctFormPathOpt) || options.valueOf(paymentAcctFormPathOpt).isEmpty()) + throw new IllegalArgumentException("no path to json payment account form specified"); + + Path path = Paths.get(options.valueOf(paymentAcctFormPathOpt)); + if (!path.toFile().exists()) + throw new IllegalStateException( + format("json payment account form '%s' could not be found", + path)); + + return this; + } + + public Path getPaymentAcctForm() { + return Paths.get(options.valueOf(paymentAcctFormPathOpt)); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetAddressBalanceOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetAddressBalanceOptionParser.java new file mode 100644 index 0000000000..4ad693ca4c --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetAddressBalanceOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; + +public class GetAddressBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "wallet btc address") + .withRequiredArg(); + + public GetAddressBalanceOptionParser(String[] args) { + super(args); + } + + public GetAddressBalanceOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no address specified"); + + return this; + } + + public String getAddress() { + return options.valueOf(addressOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetBTCMarketPriceOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetBTCMarketPriceOptionParser.java new file mode 100644 index 0000000000..8d6585631b --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetBTCMarketPriceOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; + +public class GetBTCMarketPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency-code") + .withRequiredArg(); + + public GetBTCMarketPriceOptionParser(String[] args) { + super(args); + } + + public GetBTCMarketPriceOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) + throw new IllegalArgumentException("no currency code specified"); + + return this; + } + + public String getCurrencyCode() { + return options.valueOf(currencyCodeOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetBalanceOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetBalanceOptionParser.java new file mode 100644 index 0000000000..206e590c3d --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetBalanceOptionParser.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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; +import static joptsimple.internal.Strings.EMPTY; + +public class GetBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "wallet currency code (bsq|btc)") + .withOptionalArg() + .defaultsTo(EMPTY); + + public GetBalanceOptionParser(String[] args) { + super(args); + } + + public GetBalanceOptionParser parse() { + return (GetBalanceOptionParser) super.parse(); + } + + public String getCurrencyCode() { + return options.has(currencyCodeOpt) ? options.valueOf(currencyCodeOpt) : ""; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetOfferOptionParser.java new file mode 100644 index 0000000000..1a849654cf --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetOfferOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_OFFER_ID; + +public class GetOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to get") + .withRequiredArg(); + + public GetOfferOptionParser(String[] args) { + super(args); + } + + public GetOfferOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) + throw new IllegalArgumentException("no offer id specified"); + + return this; + } + + public String getOfferId() { + return options.valueOf(offerIdOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java new file mode 100644 index 0000000000..f8a4dee839 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; +import static bisq.cli.opts.OptLabel.OPT_DIRECTION; + +public class GetOffersOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") + .withRequiredArg(); + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") + .withRequiredArg(); + + public GetOffersOptionParser(String[] args) { + super(args); + } + + public GetOffersOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty()) + throw new IllegalArgumentException("no direction (buy|sell) specified"); + + if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) + throw new IllegalArgumentException("no currency code specified"); + + return this; + } + + public String getDirection() { + return options.valueOf(directionOpt); + } + + public String getCurrencyCode() { + return options.valueOf(currencyCodeOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java new file mode 100644 index 0000000000..508069c2f3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID; + +public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID, + "id of payment method type used by a payment account") + .withRequiredArg(); + + public GetPaymentAcctFormOptionParser(String[] args) { + super(args); + } + + public GetPaymentAcctFormOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(paymentMethodIdOpt) || options.valueOf(paymentMethodIdOpt).isEmpty()) + throw new IllegalArgumentException("no payment method id specified"); + + return this; + } + + public String getPaymentMethodId() { + return options.valueOf(paymentMethodIdOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetTradeOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetTradeOptionParser.java new file mode 100644 index 0000000000..1419f3ed6a --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetTradeOptionParser.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_SHOW_CONTRACT; +import static bisq.cli.opts.OptLabel.OPT_TRADE_ID; + +public class GetTradeOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade") + .withRequiredArg(); + + final OptionSpec showContractOpt = parser.accepts(OPT_SHOW_CONTRACT, "show trade's json contract") + .withOptionalArg() + .ofType(boolean.class) + .defaultsTo(Boolean.FALSE); + + public GetTradeOptionParser(String[] args) { + super(args); + } + + public GetTradeOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty()) + throw new IllegalArgumentException("no trade id specified"); + + return this; + } + + public String getTradeId() { + return options.valueOf(tradeIdOpt); + } + + public boolean getShowContract() { + return options.has(showContractOpt) ? options.valueOf(showContractOpt) : false; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/GetTransactionOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetTransactionOptionParser.java new file mode 100644 index 0000000000..0b245cb156 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetTransactionOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_TRANSACTION_ID; + +public class GetTransactionOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec txIdOpt = parser.accepts(OPT_TRANSACTION_ID, "id of transaction") + .withRequiredArg(); + + public GetTransactionOptionParser(String[] args) { + super(args); + } + + public GetTransactionOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(txIdOpt) || options.valueOf(txIdOpt).isEmpty()) + throw new IllegalArgumentException("no tx id specified"); + + return this; + } + + public String getTxId() { + return options.valueOf(txIdOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/MethodOpts.java b/cli/src/main/java/bisq/cli/opts/MethodOpts.java new file mode 100644 index 0000000000..9f6c2d1a3e --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/MethodOpts.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.cli.opts; + +public interface MethodOpts { + + MethodOpts parse(); + + boolean isForHelp(); +} diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java new file mode 100644 index 0000000000..084c230aae --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.cli.opts; + +/** + * CLI opt label definitions. + */ +public class OptLabel { + public final static String OPT_ACCOUNT_NAME = "account-name"; + public final static String OPT_ADDRESS = "address"; + public final static String OPT_AMOUNT = "amount"; + public final static String OPT_CURRENCY_CODE = "currency-code"; + public final static String OPT_DIRECTION = "direction"; + public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; + public final static String OPT_FEE_CURRENCY = "fee-currency"; + public final static String OPT_FIXED_PRICE = "fixed-price"; + public final static String OPT_HELP = "help"; + public final static String OPT_HOST = "host"; + public final static String OPT_MEMO = "memo"; + public final static String OPT_MKT_PRICE_MARGIN = "market-price-margin"; + public final static String OPT_MIN_AMOUNT = "min-amount"; + public final static String OPT_OFFER_ID = "offer-id"; + public final static String OPT_PASSWORD = "password"; + public final static String OPT_PAYMENT_ACCOUNT = "payment-account"; + public final static String OPT_PAYMENT_ACCOUNT_FORM = "payment-account-form"; + public final static String OPT_PAYMENT_METHOD_ID = "payment-method-id"; + public final static String OPT_PORT = "port"; + public final static String OPT_REGISTRATION_KEY = "registration-key"; + public final static String OPT_SECURITY_DEPOSIT = "security-deposit"; + public final static String OPT_SHOW_CONTRACT = "show-contract"; + public final static String OPT_TRADE_ID = "trade-id"; + public final static String OPT_TRADE_INSTANT = "trade-instant"; + public final static String OPT_TIMEOUT = "timeout"; + public final static String OPT_TRANSACTION_ID = "transaction-id"; + public final static String OPT_TX_FEE_RATE = "tx-fee-rate"; + public final static String OPT_WALLET_PASSWORD = "wallet-password"; + public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password"; +} diff --git a/cli/src/main/java/bisq/cli/opts/RegisterDisputeAgentOptionParser.java b/cli/src/main/java/bisq/cli/opts/RegisterDisputeAgentOptionParser.java new file mode 100644 index 0000000000..b1c8f0bba8 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/RegisterDisputeAgentOptionParser.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_DISPUTE_AGENT_TYPE; +import static bisq.cli.opts.OptLabel.OPT_REGISTRATION_KEY; + +public class RegisterDisputeAgentOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec disputeAgentTypeOpt = parser.accepts(OPT_DISPUTE_AGENT_TYPE, "dispute agent type") + .withRequiredArg(); + + final OptionSpec registrationKeyOpt = parser.accepts(OPT_REGISTRATION_KEY, "registration key") + .withRequiredArg(); + + public RegisterDisputeAgentOptionParser(String[] args) { + super(args); + } + + public RegisterDisputeAgentOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(disputeAgentTypeOpt) || options.valueOf(disputeAgentTypeOpt).isEmpty()) + throw new IllegalArgumentException("no dispute agent type specified"); + + if (!options.has(registrationKeyOpt) || options.valueOf(registrationKeyOpt).isEmpty()) + throw new IllegalArgumentException("no registration key specified"); + + return this; + } + + public String getDisputeAgentType() { + return options.valueOf(disputeAgentTypeOpt); + } + + public String getRegistrationKey() { + return options.valueOf(registrationKeyOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/RemoveWalletPasswordOptionParser.java b/cli/src/main/java/bisq/cli/opts/RemoveWalletPasswordOptionParser.java new file mode 100644 index 0000000000..db556a0fd8 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/RemoveWalletPasswordOptionParser.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD; + +public class RemoveWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password") + .withRequiredArg(); + + public RemoveWalletPasswordOptionParser(String[] args) { + super(args); + } + + public RemoveWalletPasswordOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) + throw new IllegalArgumentException("no password specified"); + + return this; + } + + public String getPassword() { + return options.valueOf(passwordOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/SendBsqOptionParser.java b/cli/src/main/java/bisq/cli/opts/SendBsqOptionParser.java new file mode 100644 index 0000000000..ad9ab87cbb --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/SendBsqOptionParser.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_AMOUNT; +import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE; +import static joptsimple.internal.Strings.EMPTY; + +public class SendBsqOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "destination bsq address") + .withRequiredArg(); + + final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of bsq to send") + .withRequiredArg(); + + final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)") + .withOptionalArg() + .defaultsTo(EMPTY); + + public SendBsqOptionParser(String[] args) { + super(args); + } + + public SendBsqOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no bsq address specified"); + + if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) + throw new IllegalArgumentException("no bsq amount specified"); + + return this; + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public String getAmount() { + return options.valueOf(amountOpt); + } + + public String getFeeRate() { + return options.has(feeRateOpt) ? options.valueOf(feeRateOpt) : ""; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/SendBtcOptionParser.java b/cli/src/main/java/bisq/cli/opts/SendBtcOptionParser.java new file mode 100644 index 0000000000..f7d8fd5683 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/SendBtcOptionParser.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_AMOUNT; +import static bisq.cli.opts.OptLabel.OPT_MEMO; +import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE; +import static joptsimple.internal.Strings.EMPTY; + +public class SendBtcOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address") + .withRequiredArg(); + + final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to send") + .withRequiredArg(); + + final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)") + .withOptionalArg() + .defaultsTo(EMPTY); + + final OptionSpec memoOpt = parser.accepts(OPT_MEMO, "optional tx memo") + .withOptionalArg() + .defaultsTo(EMPTY); + + public SendBtcOptionParser(String[] args) { + super(args); + } + + public SendBtcOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no btc address specified"); + + if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) + throw new IllegalArgumentException("no btc amount specified"); + + return this; + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public String getAmount() { + return options.valueOf(amountOpt); + } + + public String getFeeRate() { + return options.has(feeRateOpt) ? options.valueOf(feeRateOpt) : ""; + } + + public String getMemo() { + return options.has(memoOpt) ? options.valueOf(memoOpt) : ""; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java b/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java new file mode 100644 index 0000000000..f7ed113cb3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE; + +public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, + "tx fee rate preference (sats/byte)") + .withRequiredArg(); + + public SetTxFeeRateOptionParser(String[] args) { + super(args); + } + + public SetTxFeeRateOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(feeRateOpt) || options.valueOf(feeRateOpt).isEmpty()) + throw new IllegalArgumentException("no tx fee rate specified"); + + return this; + } + + public String getFeeRate() { + return options.valueOf(feeRateOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/SetWalletPasswordOptionParser.java b/cli/src/main/java/bisq/cli/opts/SetWalletPasswordOptionParser.java new file mode 100644 index 0000000000..1caa09232c --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/SetWalletPasswordOptionParser.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_NEW_WALLET_PASSWORD; +import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD; +import static joptsimple.internal.Strings.EMPTY; + +public class SetWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password") + .withRequiredArg(); + + final OptionSpec newPasswordOpt = parser.accepts(OPT_NEW_WALLET_PASSWORD, "new bisq wallet password") + .withOptionalArg() + .defaultsTo(EMPTY); + + public SetWalletPasswordOptionParser(String[] args) { + super(args); + } + + public SetWalletPasswordOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) + throw new IllegalArgumentException("no password specified"); + + return this; + } + + public String getPassword() { + return options.valueOf(passwordOpt); + } + + public String getNewPassword() { + return options.has(newPasswordOpt) ? options.valueOf(newPasswordOpt) : ""; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/SimpleMethodOptionParser.java b/cli/src/main/java/bisq/cli/opts/SimpleMethodOptionParser.java new file mode 100644 index 0000000000..a0e396d63a --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/SimpleMethodOptionParser.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.cli.opts; + + +public class SimpleMethodOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + public SimpleMethodOptionParser(String[] args) { + super(args); + } + + public SimpleMethodOptionParser parse() { + return (SimpleMethodOptionParser) super.parse(); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java new file mode 100644 index 0000000000..67fbdd8c86 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_FEE_CURRENCY; +import static bisq.cli.opts.OptLabel.OPT_OFFER_ID; +import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT; + +public class TakeOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to take") + .withRequiredArg(); + + final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, "id of payment account used for trade") + .withRequiredArg(); + + final OptionSpec takerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "taker fee currency code (bsq|btc)") + .withOptionalArg() + .defaultsTo("btc"); + + public TakeOfferOptionParser(String[] args) { + super(args); + } + + public TakeOfferOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) + throw new IllegalArgumentException("no offer id specified"); + + if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty()) + throw new IllegalArgumentException("no payment account id specified"); + + return this; + } + + public String getOfferId() { + return options.valueOf(offerIdOpt); + } + + public String getPaymentAccountId() { + return options.valueOf(paymentAccountIdOpt); + } + + public String getTakerFeeCurrencyCode() { + return options.has(takerFeeCurrencyCodeOpt) ? options.valueOf(takerFeeCurrencyCodeOpt) : "btc"; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/UnlockWalletOptionParser.java b/cli/src/main/java/bisq/cli/opts/UnlockWalletOptionParser.java new file mode 100644 index 0000000000..2908be42bf --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/UnlockWalletOptionParser.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_TIMEOUT; +import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD; + +public class UnlockWalletOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password") + .withRequiredArg(); + + final OptionSpec unlockTimeoutOpt = parser.accepts(OPT_TIMEOUT, "wallet unlock timeout (s)") + .withRequiredArg() + .ofType(long.class) + .defaultsTo(0L); + + public UnlockWalletOptionParser(String[] args) { + super(args); + } + + public UnlockWalletOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) + throw new IllegalArgumentException("no password specified"); + + if (!options.has(unlockTimeoutOpt) || options.valueOf(unlockTimeoutOpt) <= 0) + throw new IllegalArgumentException("no unlock timeout specified"); + + return this; + } + + public String getPassword() { + return options.valueOf(passwordOpt); + } + + public long getUnlockTimeout() { + return options.valueOf(unlockTimeoutOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java b/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java new file mode 100644 index 0000000000..c2d5ea2b35 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_AMOUNT; + +public class VerifyBsqSentToAddressOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "receiving bsq address") + .withRequiredArg(); + + final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of bsq received") + .withRequiredArg(); + + public VerifyBsqSentToAddressOptionParser(String[] args) { + super(args); + } + + public VerifyBsqSentToAddressOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no bsq address specified"); + + if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) + throw new IllegalArgumentException("no bsq amount specified"); + + return this; + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public String getAmount() { + return options.valueOf(amountOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/WithdrawFundsOptionParser.java b/cli/src/main/java/bisq/cli/opts/WithdrawFundsOptionParser.java new file mode 100644 index 0000000000..bdba643760 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/WithdrawFundsOptionParser.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_MEMO; +import static bisq.cli.opts.OptLabel.OPT_TRADE_ID; +import static joptsimple.internal.Strings.EMPTY; + +public class WithdrawFundsOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade") + .withRequiredArg(); + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address") + .withRequiredArg(); + + final OptionSpec memoOpt = parser.accepts(OPT_MEMO, "optional tx memo") + .withOptionalArg() + .defaultsTo(EMPTY); + + public WithdrawFundsOptionParser(String[] args) { + super(args); + } + + public WithdrawFundsOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty()) + throw new IllegalArgumentException("no trade id specified"); + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no destination address specified"); + + return this; + } + + public String getTradeId() { + return options.valueOf(tradeIdOpt); + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public String getMemo() { + return options.has(memoOpt) ? options.valueOf(memoOpt) : ""; + } +} diff --git a/cli/src/main/resources/logback.xml b/cli/src/main/resources/logback.xml new file mode 100644 index 0000000000..bc8edf0221 --- /dev/null +++ b/cli/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + + diff --git a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java new file mode 100644 index 0000000000..f613aea358 --- /dev/null +++ b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java @@ -0,0 +1,39 @@ +package bisq.cli; + +import static java.lang.System.out; + +/** + Smoke tests for getoffers method. Useful for examining the format of the console output. + + Prerequisites: + + - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` + + This can be run on mainnet. + */ +public class GetOffersSmokeTest { + + public static void main(String[] args) { + + out.println(">>> getoffers buy usd"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=usd"}); + out.println(">>> getoffers sell usd"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=usd"}); + + out.println(">>> getoffers buy eur"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"}); + out.println(">>> getoffers sell eur"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=eur"}); + + out.println(">>> getoffers buy gbp"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=gbp"}); + out.println(">>> getoffers sell gbp"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=gbp"}); + + out.println(">>> getoffers buy brl"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=brl"}); + out.println(">>> getoffers sell brl"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"}); + } + +} diff --git a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java new file mode 100644 index 0000000000..951b56a5e3 --- /dev/null +++ b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java @@ -0,0 +1,267 @@ +package bisq.cli.opt; + +import org.junit.jupiter.api.Test; + +import static bisq.cli.Method.canceloffer; +import static bisq.cli.Method.createcryptopaymentacct; +import static bisq.cli.Method.createoffer; +import static bisq.cli.Method.createpaymentacct; +import static bisq.cli.opts.OptLabel.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + + +import bisq.cli.opts.CancelOfferOptionParser; +import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; +import bisq.cli.opts.CreateOfferOptionParser; +import bisq.cli.opts.CreatePaymentAcctOptionParser; + + +public class OptionParsersTest { + + private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; + + // canceloffer opt parser tests + + @Test + public void testCancelOfferWithMissingOfferIdOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + canceloffer.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CancelOfferOptionParser(args).parse()); + assertEquals("no offer id specified", exception.getMessage()); + } + + @Test + public void testCancelOfferWithEmptyOfferIdOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + canceloffer.name(), + "--" + OPT_OFFER_ID + "=" // missing opt value + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CancelOfferOptionParser(args).parse()); + assertEquals("no offer id specified", exception.getMessage()); + } + + @Test + public void testCancelOfferWithMissingOfferIdValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + canceloffer.name(), + "--" + OPT_OFFER_ID // missing equals sign & opt value + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CancelOfferOptionParser(args).parse()); + assertEquals("offer-id requires an argument", exception.getMessage()); + } + + @Test + public void testValidCancelOfferOpts() { + String[] args = new String[]{ + PASSWORD_OPT, + canceloffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID" + }; + new CancelOfferOptionParser(args).parse(); + } + + // createoffer opt parser tests + + @Test + public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createoffer.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateOfferOptionParser(args).parse()); + assertEquals("no payment account id specified", exception.getMessage()); + } + + @Test + public void testCreateOfferOptParserWithEmptyPaymentAccountIdOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createoffer.name(), + "--" + OPT_PAYMENT_ACCOUNT + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateOfferOptionParser(args).parse()); + assertEquals("payment-account requires an argument", exception.getMessage()); + } + + @Test + public void testCreateOfferOptParserWithMissingDirectionOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createoffer.name(), + "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateOfferOptionParser(args).parse()); + assertEquals("no direction (buy|sell) specified", exception.getMessage()); + } + + + @Test + public void testCreateOfferOptParserWithMissingDirectionOptValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createoffer.name(), + "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123", + "--" + OPT_DIRECTION + "=" + "" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateOfferOptionParser(args).parse()); + assertEquals("no direction (buy|sell) specified", exception.getMessage()); + } + + @Test + public void testValidCreateOfferOpts() { + String[] args = new String[]{ + PASSWORD_OPT, + createoffer.name(), + "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123", + "--" + OPT_DIRECTION + "=" + "BUY", + "--" + OPT_CURRENCY_CODE + "=" + "EUR", + "--" + OPT_AMOUNT + "=" + "0.125", + "--" + OPT_MKT_PRICE_MARGIN + "=" + "0.0", + "--" + OPT_SECURITY_DEPOSIT + "=" + "25.0" + }; + CreateOfferOptionParser parser = new CreateOfferOptionParser(args).parse(); + assertEquals("abc-payment-acct-id-123", parser.getPaymentAccountId()); + assertEquals("BUY", parser.getDirection()); + assertEquals("EUR", parser.getCurrencyCode()); + assertEquals("0.125", parser.getAmount()); + assertEquals("0.0", parser.getMktPriceMargin()); + assertEquals("25.0", parser.getSecurityDeposit()); + } + + // createpaymentacct opt parser tests + + @Test + public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createpaymentacct.name() + // OPT_PAYMENT_ACCOUNT_FORM + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreatePaymentAcctOptionParser(args).parse()); + assertEquals("no path to json payment account form specified", exception.getMessage()); + } + + @Test + public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createpaymentacct.name(), + "--" + OPT_PAYMENT_ACCOUNT_FORM + "=" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreatePaymentAcctOptionParser(args).parse()); + assertEquals("no path to json payment account form specified", exception.getMessage()); + } + + @Test + public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createpaymentacct.name(), + "--" + OPT_PAYMENT_ACCOUNT_FORM + "=" + "/tmp/milkyway/solarsystem/mars" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreatePaymentAcctOptionParser(args).parse()); + if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) + assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found", + exception.getMessage()); + else + assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", + exception.getMessage()); + } + + // createcryptopaymentacct parser tests + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no payment account name specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithEmptyAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("account-name requires an argument", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingCurrencyCodeOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no currency code specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithInvalidCurrencyCodeOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", + "--" + OPT_CURRENCY_CODE + "=" + "xmr" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("api only supports bsq crypto currency payment accounts", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAddressOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", + "--" + OPT_CURRENCY_CODE + "=" + "bsq" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no bsq address specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParser() { + var acctName = "bsq payment account"; + var currencyCode = "bsq"; + var address = "B1nXyZ"; // address is validated on server + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + acctName, + "--" + OPT_CURRENCY_CODE + "=" + currencyCode, + "--" + OPT_ADDRESS + "=" + address + }; + var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + assertEquals(acctName, parser.getAccountName()); + assertEquals(currencyCode, parser.getCurrencyCode()); + assertEquals(address, parser.getAddress()); + } +} diff --git a/common/src/main/java/bisq/common/BisqException.java b/common/src/main/java/bisq/common/BisqException.java new file mode 100644 index 0000000000..4953f308d6 --- /dev/null +++ b/common/src/main/java/bisq/common/BisqException.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.common; + +public class BisqException extends RuntimeException { + + public BisqException(Throwable cause) { + super(cause); + } + + public BisqException(String format, Object... args) { + super(String.format(format, args)); + } + + public BisqException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } +} diff --git a/common/src/main/java/bisq/common/ClockWatcher.java b/common/src/main/java/bisq/common/ClockWatcher.java new file mode 100644 index 0000000000..1b673535fa --- /dev/null +++ b/common/src/main/java/bisq/common/ClockWatcher.java @@ -0,0 +1,95 @@ +/* + * 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 bisq.common; + +import javax.inject.Singleton; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +// Helps configure listener objects that are run by the `UserThread` each second +// and can do per second, per minute and delayed second actions. Also detects when we were in standby, and logs it. +@Slf4j +@Singleton +public class ClockWatcher { + public static final int IDLE_TOLERANCE_MS = 20000; + + public interface Listener { + void onSecondTick(); + + void onMinuteTick(); + + default void onMissedSecondTick(long missedMs) { + } + + default void onAwakeFromStandby(long missedMs) { + } + } + + private Timer timer; + private final List listeners = new LinkedList<>(); + private long counter = 0; + private long lastSecondTick; + + public ClockWatcher() { + } + + public void start() { + if (timer == null) { + lastSecondTick = System.currentTimeMillis(); + timer = UserThread.runPeriodically(() -> { + listeners.forEach(Listener::onSecondTick); + counter++; + if (counter >= 60) { + counter = 0; + listeners.forEach(Listener::onMinuteTick); + } + + long currentTimeMillis = System.currentTimeMillis(); + long diff = currentTimeMillis - lastSecondTick; + if (diff > 1000) { + long missedMs = diff - 1000; + listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); + + if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { + log.info("We have been in standby mode for {} sec", missedMs / 1000); + listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs)); + } + } + lastSecondTick = currentTimeMillis; + }, 1, TimeUnit.SECONDS); + } + } + + public void stop() { + timer.stop(); + timer = null; + counter = 0; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } +} diff --git a/common/src/main/java/bisq/common/Envelope.java b/common/src/main/java/bisq/common/Envelope.java new file mode 100644 index 0000000000..c713e8a26c --- /dev/null +++ b/common/src/main/java/bisq/common/Envelope.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common; + +/** + * Interface for the outside envelope object sent over the network or persisted to disk. + */ +public interface Envelope extends Proto { +} diff --git a/common/src/main/java/bisq/common/FrameRateTimer.java b/common/src/main/java/bisq/common/FrameRateTimer.java new file mode 100644 index 0000000000..cd1ddbf06f --- /dev/null +++ b/common/src/main/java/bisq/common/FrameRateTimer.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.common; + +import java.time.Duration; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * We simulate a global frame rate timer similar to FXTimer to avoid creation of threads for each timer call. + * Used only in headless apps like the seed node. + */ +public class FrameRateTimer implements Timer, Runnable { + private final Logger log = LoggerFactory.getLogger(FrameRateTimer.class); + + private long interval; + private Runnable runnable; + private long startTs; + private boolean isPeriodically; + private final String uid = UUID.randomUUID().toString(); + private volatile boolean stopped; + + public FrameRateTimer() { + } + + @Override + public void run() { + if (!stopped) { + try { + long currentTimeMillis = System.currentTimeMillis(); + if ((currentTimeMillis - startTs) >= interval) { + runnable.run(); + if (isPeriodically) + startTs = currentTimeMillis; + else + stop(); + } + } catch (Throwable t) { + log.error("exception in FrameRateTimer", t); + stop(); + throw t; + } + } + } + + @Override + public Timer runLater(Duration delay, Runnable runnable) { + this.interval = delay.toMillis(); + this.runnable = runnable; + startTs = System.currentTimeMillis(); + MasterTimer.addListener(this); + return this; + } + + @Override + public Timer runPeriodically(Duration interval, Runnable runnable) { + this.interval = interval.toMillis(); + isPeriodically = true; + this.runnable = runnable; + startTs = System.currentTimeMillis(); + MasterTimer.addListener(this); + return this; + } + + @Override + public void stop() { + stopped = true; + MasterTimer.removeListener(this); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FrameRateTimer)) return false; + + FrameRateTimer that = (FrameRateTimer) o; + + return !(uid != null ? !uid.equals(that.uid) : that.uid != null); + + } + + @Override + public int hashCode() { + return uid != null ? uid.hashCode() : 0; + } +} diff --git a/common/src/main/java/bisq/common/MasterTimer.java b/common/src/main/java/bisq/common/MasterTimer.java new file mode 100644 index 0000000000..2e8b773833 --- /dev/null +++ b/common/src/main/java/bisq/common/MasterTimer.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.common; + +import java.util.Set; +import java.util.TimerTask; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Runs all listener objects periodically in a short interval. +public class MasterTimer { + private final static Logger log = LoggerFactory.getLogger(MasterTimer.class); + private static final java.util.Timer timer = new java.util.Timer(); + // frame rate of 60 fps is about 16 ms but we don't need such a short interval, 100 ms should be good enough + public static final long FRAME_INTERVAL_MS = 100; + + static { + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + UserThread.execute(() -> listeners.forEach(Runnable::run)); + } + }, FRAME_INTERVAL_MS, FRAME_INTERVAL_MS); + } + + private static final Set listeners = new CopyOnWriteArraySet<>(); + + public static void addListener(Runnable runnable) { + listeners.add(runnable); + } + + public static void removeListener(Runnable runnable) { + listeners.remove(runnable); + } +} diff --git a/common/src/main/java/bisq/common/Payload.java b/common/src/main/java/bisq/common/Payload.java new file mode 100644 index 0000000000..24f4223386 --- /dev/null +++ b/common/src/main/java/bisq/common/Payload.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common; + +/** + * Interface for objects used inside an Envelope or other Payloads. + */ +public interface Payload extends Proto { +} diff --git a/common/src/main/java/bisq/common/Proto.java b/common/src/main/java/bisq/common/Proto.java new file mode 100644 index 0000000000..a71930f16d --- /dev/null +++ b/common/src/main/java/bisq/common/Proto.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.common; + +import com.google.protobuf.Message; + +/** + * Base interface for Envelope and Payload. + */ +public interface Proto { + Message toProtoMessage(); +} diff --git a/common/src/main/java/bisq/common/Timer.java b/common/src/main/java/bisq/common/Timer.java new file mode 100644 index 0000000000..e93529ee51 --- /dev/null +++ b/common/src/main/java/bisq/common/Timer.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common; + +import java.time.Duration; + +public interface Timer { + Timer runLater(java.time.Duration delay, Runnable action); + + Timer runPeriodically(Duration interval, Runnable runnable); + + void stop(); +} diff --git a/common/src/main/java/bisq/common/UserThread.java b/common/src/main/java/bisq/common/UserThread.java new file mode 100644 index 0000000000..1166769f53 --- /dev/null +++ b/common/src/main/java/bisq/common/UserThread.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.common; + +import com.google.common.util.concurrent.MoreExecutors; + +import java.time.Duration; + +import java.util.Random; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import java.lang.reflect.InvocationTargetException; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + + +/** + * Defines which thread is used as user thread. The user thread is the the main thread in the single threaded context. + * For JavaFX it is usually the Platform::RunLater executor, for a headless application it is any single threaded + * executor. + * Additionally sets a timer class so JavaFX and headless applications can set different timers (UITimer for JavaFX + * otherwise we use the default FrameRateTimer). + *

    + * Provides also methods for delayed and periodic executions. + */ +@Slf4j +public class UserThread { + private static Class timerClass; + @Getter + @Setter + private static Executor executor; + + public static void setTimerClass(Class timerClass) { + UserThread.timerClass = timerClass; + } + + static { + // If not defined we use same thread as caller thread + executor = MoreExecutors.directExecutor(); + timerClass = FrameRateTimer.class; + } + + public static void execute(Runnable command) { + UserThread.executor.execute(command); + } + + // Prefer FxTimer if a delay is needed in a JavaFx class (gui module) + public static Timer runAfterRandomDelay(Runnable runnable, long minDelayInSec, long maxDelayInSec) { + return UserThread.runAfterRandomDelay(runnable, minDelayInSec, maxDelayInSec, TimeUnit.SECONDS); + } + + @SuppressWarnings("WeakerAccess") + public static Timer runAfterRandomDelay(Runnable runnable, long minDelay, long maxDelay, TimeUnit timeUnit) { + return UserThread.runAfter(runnable, new Random().nextInt((int) (maxDelay - minDelay)) + minDelay, timeUnit); + } + + public static Timer runAfter(Runnable runnable, long delayInSec) { + return UserThread.runAfter(runnable, delayInSec, TimeUnit.SECONDS); + } + + public static Timer runAfter(Runnable runnable, long delay, TimeUnit timeUnit) { + return getTimer().runLater(Duration.ofMillis(timeUnit.toMillis(delay)), runnable); + } + + public static Timer runPeriodically(Runnable runnable, long intervalInSec) { + return UserThread.runPeriodically(runnable, intervalInSec, TimeUnit.SECONDS); + } + + public static Timer runPeriodically(Runnable runnable, long interval, TimeUnit timeUnit) { + return getTimer().runPeriodically(Duration.ofMillis(timeUnit.toMillis(interval)), runnable); + } + + private static Timer getTimer() { + try { + return timerClass.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + String message = "Could not instantiate timer bsTimerClass=" + timerClass; + log.error(message, e); + throw new RuntimeException(message); + } + } +} diff --git a/common/src/main/java/bisq/common/app/AppModule.java b/common/src/main/java/bisq/common/app/AppModule.java new file mode 100644 index 0000000000..1001caf691 --- /dev/null +++ b/common/src/main/java/bisq/common/app/AppModule.java @@ -0,0 +1,66 @@ +/* + * 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 bisq.common.app; + +import bisq.common.config.Config; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AppModule extends AbstractModule { + + protected final Config config; + + private final List modules = new ArrayList<>(); + + protected AppModule(Config config) { + this.config = config; + } + + protected void install(AppModule module) { + super.install(module); + modules.add(module); + } + + /** + * Close any instances this module is responsible for and recursively close any + * sub-modules installed via {@link #install(AppModule)}. This method + * must be called manually, e.g. at the end of a main() method or in the stop() method + * of a JavaFX Application; alternatively it may be registered as a JVM shutdown hook. + * + * @param injector the Injector originally initialized with this module + * @see #doClose(com.google.inject.Injector) + */ + public final void close(Injector injector) { + modules.forEach(module -> module.close(injector)); + doClose(injector); + } + + /** + * Actually perform closing of any instances this module is responsible for. Called by + * {@link #close(Injector)}. + * + * @param injector the Injector originally initialized with this module + */ + @SuppressWarnings({"WeakerAccess", "EmptyMethod", "UnusedParameters"}) + protected void doClose(Injector injector) { + } +} diff --git a/common/src/main/java/bisq/common/app/AsciiLogo.java b/common/src/main/java/bisq/common/app/AsciiLogo.java new file mode 100644 index 0000000000..1bac8fdb10 --- /dev/null +++ b/common/src/main/java/bisq/common/app/AsciiLogo.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.common.app; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AsciiLogo { + public static void showAsciiLogo() { + String ls = System.lineSeparator(); + log.info(ls + ls + + " ........ ...... " + ls + + " .............. ...... " + ls + + " ................. ...... " + ls + + " ...... .......... .. ...... " + ls + + " ...... ...... ...... ............... ..... ......... .......... " + ls + + " ....... ........ .................. ..... ............. ............... " + ls + + " ...... ........ .......... ....... ..... ...... ... ........ ....... " + ls + + " ...... ..... ....... ..... ..... ..... ..... ...... " + ls + + " ...... ... ... ...... ...... ..... ........... ...... ...... " + ls + + " ...... ..... .... ...... ...... ..... ............ ..... ...... " + ls + + " ...... ..... ...... ..... ........ ...... ...... " + ls + + " ...... .... ... ...... ...... ..... .. ...... ...... ........ " + ls + + " ........ .. ....... ................. ..... .............. ................... " + ls + + " .......... ......... ............. ..... ............ ................. " + ls + + " ...................... ..... .... .... ...... " + ls + + " ................ ...... " + ls + + " .... ...... " + ls + + " ...... " + ls + + ls + ls); + } +} diff --git a/common/src/main/java/bisq/common/app/Capabilities.java b/common/src/main/java/bisq/common/app/Capabilities.java new file mode 100644 index 0000000000..d0b3e50a3f --- /dev/null +++ b/common/src/main/java/bisq/common/app/Capabilities.java @@ -0,0 +1,201 @@ +/* + * 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 bisq.common.app; + +import com.google.common.base.Joiner; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +/** + * hold a set of capabilities and offers appropriate comparison methods. + * + * @author Florian Reimair + */ +@EqualsAndHashCode +@Slf4j +public class Capabilities { + + /** + * The global set of capabilities, i.e. the capabilities if the local app. + */ + public static final Capabilities app = new Capabilities(); + + // Defines which most recent capability any node need to support. + // This helps to clean network from very old inactive but still running nodes. + @SuppressWarnings("deprecation") + private static final Capability MANDATORY_CAPABILITY = Capability.DAO_STATE; + + protected final Set capabilities = new HashSet<>(); + + public Capabilities(Capability... capabilities) { + this(Arrays.asList(capabilities)); + } + + public Capabilities(Capabilities capabilities) { + this(capabilities.capabilities); + } + + public Capabilities(Collection capabilities) { + this.capabilities.addAll(capabilities); + } + + public void set(Capability... capabilities) { + set(Arrays.asList(capabilities)); + } + + public void set(Capabilities capabilities) { + set(capabilities.capabilities); + } + + public void set(Collection capabilities) { + this.capabilities.clear(); + this.capabilities.addAll(capabilities); + } + + public void addAll(Capability... capabilities) { + this.capabilities.addAll(Arrays.asList(capabilities)); + } + + public void addAll(Capabilities capabilities) { + if (capabilities != null) + this.capabilities.addAll(capabilities.capabilities); + } + + public boolean containsAll(final Set requiredItems) { + return capabilities.containsAll(requiredItems); + } + + public boolean containsAll(final Capabilities capabilities) { + return containsAll(capabilities.capabilities); + } + + public boolean containsAll(Capability... capabilities) { + return this.capabilities.containsAll(Arrays.asList(capabilities)); + } + + public boolean contains(Capability capability) { + return this.capabilities.contains(capability); + } + + public boolean isEmpty() { + return capabilities.isEmpty(); + } + + + /** + * helper for protobuffer stuff + * + * @param capabilities + * @return int list of Capability ordinals + */ + public static List toIntList(Capabilities capabilities) { + return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); + } + + /** + * helper for protobuffer stuff + * + * @param capabilities a list of Capability ordinals + * @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())); + } + + /** + * + * @param list Comma separated list of Capability ordinals. + * @return Capabilities + */ + public static Capabilities fromStringList(String list) { + if (list == null || list.isEmpty()) + return new Capabilities(); + + List entries = List.of(list.replace(" ", "").split(",")); + List capabilitiesList = entries.stream() + .map(c -> { + try { + return Integer.parseInt(c); + } catch (Throwable e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return Capabilities.fromIntList(capabilitiesList); + } + + /** + * @return Converts capabilities to list of ordinals as comma separated strings + */ + public String toStringList() { + return Joiner.on(", ").join(Capabilities.toIntList(this)); + } + + public static boolean hasMandatoryCapability(Capabilities capabilities) { + return hasMandatoryCapability(capabilities, MANDATORY_CAPABILITY); + } + + public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { + return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); + } + + @Override + public String toString() { + return Arrays.toString(Capabilities.toIntList(this).toArray()); + } + + public String prettyPrint() { + return capabilities.stream() + .sorted(Comparator.comparingInt(Enum::ordinal)) + .map(e -> e.name() + " [" + e.ordinal() + "]") + .collect(Collectors.joining(", ")); + } + + public int size() { + return capabilities.size(); + } + + // We return true if our capabilities have less capabilities than the parameter value + public boolean hasLess(Capabilities other) { + return findHighestCapability(this) < findHighestCapability(other); + } + + // We use the sum of all capabilities. Alternatively we could use the highest entry. + // 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(); + } +} diff --git a/common/src/main/java/bisq/common/app/Capability.java b/common/src/main/java/bisq/common/app/Capability.java new file mode 100644 index 0000000000..1c9aebd889 --- /dev/null +++ b/common/src/main/java/bisq/common/app/Capability.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.common.app; + +// We can define here special features the client is supporting. +// Useful for updates to new versions where a new data type would break backwards compatibility or to +// limit a node to certain behaviour and roles like the seed nodes. +// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. +// We use the ordinal integer instead. +// Sequence in the enum must not be changed (append only). +public enum Capability { + @Deprecated TRADE_STATISTICS, // Not required anymore as no old clients out there not having that support + @Deprecated TRADE_STATISTICS_2, // Not required anymore as no old clients out there not having that support + @Deprecated ACCOUNT_AGE_WITNESS, // Not required anymore as no old clients out there not having that support + SEED_NODE, // Node is a seed node + DAO_FULL_NODE, // DAO full node can deliver BSQ blocks + @Deprecated PROPOSAL, // Not required anymore as no old clients out there not having that support + @Deprecated BLIND_VOTE, // Not required anymore as no old clients out there not having that support + @Deprecated ACK_MSG, // Not required anymore as no old clients out there not having that support + RECEIVE_BSQ_BLOCK, // Signaling that node which wants to receive BSQ blocks (DAO lite node) + @Deprecated DAO_STATE, // Not required anymore as no old clients out there not having that support + + @Deprecated BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval + + SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature + MEDIATION, // Supports mediation feature + REFUND_AGENT, // Supports refund agents + TRADE_STATISTICS_HASH_UPDATE, // We changed the hash method in 1.2.0 and that requires update to 1.2.2 for handling it correctly, otherwise the seed nodes have to process too much data. + NO_ADDRESS_PRE_FIX, // At 1.4.0 we removed the prefix filter for mailbox messages. If a peer has that capability we do not sent the prefix. + TRADE_STATISTICS_3 // We used a new reduced trade statistics model from v1.4.0 on +} diff --git a/common/src/main/java/bisq/common/app/DevEnv.java b/common/src/main/java/bisq/common/app/DevEnv.java new file mode 100644 index 0000000000..9bf852d97b --- /dev/null +++ b/common/src/main/java/bisq/common/app/DevEnv.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.common.app; + +import bisq.common.config.Config; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DevEnv { + + // The UI got set the private dev key so the developer does not need to do anything and can test those features. + // Features: Arbitration registration (alt+R at account), Alert/Update (alt+m), private message to a + // peer (click user icon and alt+r), filter/block offers by various data like offer ID (cmd + f). + // The user can set a program argument to ignore all of those privileged network_messages. They are intended for + // emergency cases only (beside update message and arbitrator registration). + public static final String DEV_PRIVILEGE_PUB_KEY = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee"; + public static final String DEV_PRIVILEGE_PRIV_KEY = "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a"; + + public static void setup(Config config) { + DevEnv.setDevMode(config.useDevMode); + DevEnv.setDaoActivated(config.daoActivated); + } + + // If set to true we ignore several UI behavior like confirmation popups as well dummy accounts are created and + // offers are filled with default values. Intended to make dev testing faster. + private static boolean devMode = false; + + public static boolean isDevMode() { + return devMode; + } + + public static void setDevMode(boolean devMode) { + DevEnv.devMode = devMode; + } + + private static boolean daoActivated = true; + + public static boolean isDaoActivated() { + return daoActivated; + } + + public static void setDaoActivated(boolean daoActivated) { + DevEnv.daoActivated = daoActivated; + } + + public static void logErrorAndThrowIfDevMode(String msg) { + log.error(msg); + if (devMode) + throw new RuntimeException(msg); + } + + public static boolean isDaoTradingActivated() { + return true; + } +} diff --git a/common/src/main/java/bisq/common/app/HasCapabilities.java b/common/src/main/java/bisq/common/app/HasCapabilities.java new file mode 100644 index 0000000000..9652f8825c --- /dev/null +++ b/common/src/main/java/bisq/common/app/HasCapabilities.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common.app; + +/** + * Holds a set of {@link Capabilities}. + * + * @author Florian Reimair + */ +public interface HasCapabilities { + + Capabilities getCapabilities(); +} diff --git a/common/src/main/java/bisq/common/app/Log.java b/common/src/main/java/bisq/common/app/Log.java new file mode 100644 index 0000000000..a594292289 --- /dev/null +++ b/common/src/main/java/bisq/common/app/Log.java @@ -0,0 +1,92 @@ +/* + * 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 bisq.common.app; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; +import ch.qos.logback.core.util.FileSize; + +public class Log { + private static Logger logbackLogger; + + public static void setLevel(Level logLevel) { + logbackLogger.setLevel(logLevel); + } + + public static void setup(String fileName) { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + + RollingFileAppender appender = new RollingFileAppender<>(); + appender.setContext(loggerContext); + appender.setFile(fileName + ".log"); + + FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); + rollingPolicy.setContext(loggerContext); + rollingPolicy.setParent(appender); + rollingPolicy.setFileNamePattern(fileName + "_%i.log"); + rollingPolicy.setMinIndex(1); + rollingPolicy.setMaxIndex(20); + rollingPolicy.start(); + + SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); + triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB")); + triggeringPolicy.start(); + + PatternLayoutEncoder encoder = new PatternLayoutEncoder(); + encoder.setContext(loggerContext); + encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n"); + encoder.start(); + + appender.setEncoder(encoder); + appender.setRollingPolicy(rollingPolicy); + appender.setTriggeringPolicy(triggeringPolicy); + appender.start(); + + logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + logbackLogger.addAppender(appender); + logbackLogger.setLevel(Level.INFO); + + // log errors in separate file + // not working as expected still.... damn logback... + /* FileAppender errorAppender = new FileAppender(); + errorAppender.setEncoder(encoder); + errorAppender.setName("Error"); + errorAppender.setContext(loggerContext); + errorAppender.setFile(fileName + "_error.log"); + LevelFilter levelFilter = new LevelFilter(); + levelFilter.setLevel(Level.ERROR); + levelFilter.setOnMatch(FilterReply.ACCEPT); + levelFilter.setOnMismatch(FilterReply.DENY); + levelFilter.start(); + errorAppender.addFilter(levelFilter); + errorAppender.start(); + logbackLogger.addAppender(errorAppender);*/ + } + + public static void setCustomLogLevel(String pattern, Level logLevel) { + ((Logger) LoggerFactory.getLogger(pattern)).setLevel(logLevel); + } +} diff --git a/common/src/main/java/bisq/common/app/Version.java b/common/src/main/java/bisq/common/app/Version.java new file mode 100644 index 0000000000..9d3bc83a94 --- /dev/null +++ b/common/src/main/java/bisq/common/app/Version.java @@ -0,0 +1,145 @@ +/* + * 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 bisq.common.app; + +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class Version { + // The application versions + // VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update + // Therefore all sub versions start again with 1 + // We use semantic versioning with major, minor and patch + public static final String VERSION = "1.6.2"; + + /** + * Holds a list of the tagged resource files for optimizing the getData requests. + * This must not contain each version but only those where we add new version-tagged resource files for + * historical data stores. + */ + public static final List HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0", "1.5.2", + "1.5.5", "1.5.7", "1.6.0"); + + public static int getMajorVersion(String version) { + return getSubVersion(version, 0); + } + + public static int getMinorVersion(String version) { + return getSubVersion(version, 1); + } + + public static int getPatchVersion(String version) { + return getSubVersion(version, 2); + } + + public static boolean isNewVersion(String newVersion) { + return isNewVersion(newVersion, VERSION); + } + + public static boolean isNewVersion(String newVersion, String currentVersion) { + if (newVersion.equals(currentVersion)) + return false; + else if (getMajorVersion(newVersion) > getMajorVersion(currentVersion)) + return true; + else if (getMajorVersion(newVersion) < getMajorVersion(currentVersion)) + return false; + else if (getMinorVersion(newVersion) > getMinorVersion(currentVersion)) + return true; + else if (getMinorVersion(newVersion) < getMinorVersion(currentVersion)) + return false; + else if (getPatchVersion(newVersion) > getPatchVersion(currentVersion)) + return true; + else if (getPatchVersion(newVersion) < getPatchVersion(currentVersion)) + return false; + else + return false; + } + + 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); + return Integer.parseInt(split[index]); + } + + // The version no. for the objects sent over the network. A change will break the serialization of old objects. + // If objects are used for both network and database the network version is applied. + // VERSION = 0.5.0 -> P2P_NETWORK_VERSION = 1 + // With version 1.2.2 we change to version 2 (new trade protocol) + public static final int P2P_NETWORK_VERSION = 1; + + // The version no. of the serialized data stored to disc. A change will break the serialization of old objects. + // VERSION = 0.5.0 -> LOCAL_DB_VERSION = 1 + public static final int LOCAL_DB_VERSION = 1; + + // The version no. of the current protocol. The offer holds that version. + // A taker will check the version of the offers to see if his version is compatible. + // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. + // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening + // the Bisq app. + // VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1 + // Version 1.2.2 -> TRADE_PROTOCOL_VERSION = 2 + // Version 1.5.0 -> TRADE_PROTOCOL_VERSION = 3 + public static final int TRADE_PROTOCOL_VERSION = 3; + private static int p2pMessageVersion; + + public static final String BSQ_TX_VERSION = "1"; + + public static int getP2PMessageVersion() { + return p2pMessageVersion; + } + + // The version for the crypto network (BTC_Mainnet = 0, BTC_TestNet = 1, BTC_Regtest = 2, ...) + private static int BASE_CURRENCY_NETWORK; + + public static void setBaseCryptoNetworkId(int baseCryptoNetworkId) { + BASE_CURRENCY_NETWORK = baseCryptoNetworkId; + + // CRYPTO_NETWORK_ID is ordinal of enum. We use for changes at NETWORK_PROTOCOL_VERSION a multiplication with 10 + // to not mix up networks: + p2pMessageVersion = BASE_CURRENCY_NETWORK + 10 * P2P_NETWORK_VERSION; + } + + public static int getBaseCurrencyNetwork() { + return BASE_CURRENCY_NETWORK; + } + + public static void printVersion() { + log.info("Version{" + + "VERSION=" + VERSION + + ", P2P_NETWORK_VERSION=" + P2P_NETWORK_VERSION + + ", LOCAL_DB_VERSION=" + LOCAL_DB_VERSION + + ", TRADE_PROTOCOL_VERSION=" + TRADE_PROTOCOL_VERSION + + ", BASE_CURRENCY_NETWORK=" + BASE_CURRENCY_NETWORK + + ", getP2PNetworkId()=" + getP2PMessageVersion() + + '}'); + } + + public static final byte COMPENSATION_REQUEST = (byte) 0x01; + public static final byte REIMBURSEMENT_REQUEST = (byte) 0x01; + public static final byte PROPOSAL = (byte) 0x01; + public static final byte BLIND_VOTE = (byte) 0x01; + public static final byte VOTE_REVEAL = (byte) 0x01; + public static final byte LOCKUP = (byte) 0x01; + public static final byte ASSET_LISTING_FEE = (byte) 0x01; + public static final byte PROOF_OF_BURN = (byte) 0x01; +} diff --git a/common/src/main/java/bisq/common/config/BaseCurrencyNetwork.java b/common/src/main/java/bisq/common/config/BaseCurrencyNetwork.java new file mode 100644 index 0000000000..7836855a34 --- /dev/null +++ b/common/src/main/java/bisq/common/config/BaseCurrencyNetwork.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.common.config; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; + +import lombok.Getter; + +public enum BaseCurrencyNetwork { + BTC_MAINNET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"), + BTC_TESTNET(TestNet3Params.get(), "BTC", "TESTNET", "Bitcoin"), + BTC_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), + BTC_DAO_TESTNET(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), // server side regtest until v0.9.5 + BTC_DAO_BETANET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"), // mainnet test genesis + BTC_DAO_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"); // server side regtest after v0.9.5, had breaking code changes so we started over again + + @Getter + private final NetworkParameters parameters; + @Getter + private final String currencyCode; + @Getter + private final String network; + @Getter + private final String currencyName; + + BaseCurrencyNetwork(NetworkParameters parameters, String currencyCode, String network, String currencyName) { + this.parameters = parameters; + this.currencyCode = currencyCode; + this.network = network; + this.currencyName = currencyName; + } + + public boolean isMainnet() { + return "BTC_MAINNET".equals(name()); + } + + public boolean isTestnet() { + return "BTC_TESTNET".equals(name()); + } + + public boolean isDaoTestNet() { + return "BTC_DAO_TESTNET".equals(name()); + } + + public boolean isDaoRegTest() { + return "BTC_DAO_REGTEST".equals(name()); + } + + public boolean isDaoBetaNet() { + return "BTC_DAO_BETANET".equals(name()); + } + + public boolean isRegtest() { + return "BTC_REGTEST".equals(name()); + } + + public long getDefaultMinFeePerVbyte() { + return 15; // 2021-02-22 due to mempool congestion, increased from 2 + } +} diff --git a/common/src/main/java/bisq/common/config/BisqHelpFormatter.java b/common/src/main/java/bisq/common/config/BisqHelpFormatter.java new file mode 100644 index 0000000000..acc2285982 --- /dev/null +++ b/common/src/main/java/bisq/common/config/BisqHelpFormatter.java @@ -0,0 +1,131 @@ +/* + * 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 bisq.common.config; + +import joptsimple.HelpFormatter; +import joptsimple.OptionDescriptor; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BisqHelpFormatter implements HelpFormatter { + + private final String fullName; + private final String scriptName; + private final String version; + + public BisqHelpFormatter(String fullName, String scriptName, String version) { + this.fullName = fullName; + this.scriptName = scriptName; + this.version = version; + } + + public String format(Map descriptors) { + + StringBuilder output = new StringBuilder(); + output.append(String.format("%s version %s\n\n", fullName, version)); + output.append(String.format("Usage: %s [options]\n\n", scriptName)); + output.append("Options:\n\n"); + + for (Map.Entry entry : descriptors.entrySet()) { + String optionName = entry.getKey(); + OptionDescriptor optionDesc = entry.getValue(); + + if (optionDesc.representsNonOptions()) + continue; + + output.append(String.format("%s\n", formatOptionSyntax(optionName, optionDesc))); + output.append(String.format("%s\n", formatOptionDescription(optionDesc))); + } + + return output.toString(); + } + + private String formatOptionSyntax(String optionName, OptionDescriptor optionDesc) { + StringBuilder result = new StringBuilder(String.format(" --%s", optionName)); + + if (optionDesc.acceptsArguments()) + result.append(String.format("=<%s>", formatArgDescription(optionDesc))); + + List defaultValues = optionDesc.defaultValues(); + if (defaultValues.size() > 0) + result.append(String.format(" (default: %s)", formatDefaultValues(defaultValues))); + + return result.toString(); + } + + private String formatArgDescription(OptionDescriptor optionDesc) { + String argDescription = optionDesc.argumentDescription(); + + if (argDescription.length() > 0) + return argDescription; + + String typeIndicator = optionDesc.argumentTypeIndicator(); + + if (typeIndicator == null) + return "value"; + + try { + Class type = Class.forName(typeIndicator); + return type.isEnum() ? + Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(Collectors.joining("|")) : + typeIndicator.substring(typeIndicator.lastIndexOf('.') + 1); + } catch (ClassNotFoundException ex) { + // typeIndicator is something other than a class name, which can occur + // in certain cases e.g. where OptionParser.withValuesConvertedBy is used. + return typeIndicator; + } + } + + private Object formatDefaultValues(List defaultValues) { + return defaultValues.size() == 1 ? + defaultValues.get(0) : + defaultValues.toString(); + } + + private String formatOptionDescription(OptionDescriptor optionDesc) { + StringBuilder output = new StringBuilder(); + + String remainder = optionDesc.description().trim(); + + // Wrap description text at 80 characters with 8 spaces of indentation and a + // maximum of 72 chars of text, wrapping on spaces. Strings longer than 72 chars + // without any spaces (e.g. a URL) are allowed to overflow the 80-char margin. + while (remainder.length() > 72) { + int idxFirstSpace = remainder.indexOf(' '); + int chunkLen = idxFirstSpace == -1 ? remainder.length() : Math.max(idxFirstSpace, 73); + String chunk = remainder.substring(0, chunkLen); + int idxLastSpace = chunk.lastIndexOf(' '); + int idxBreak = idxLastSpace > 0 ? idxLastSpace : chunk.length(); + String line = remainder.substring(0, idxBreak); + output.append(formatLine(line)); + remainder = remainder.substring(chunk.length() - (chunk.length() - idxBreak)).trim(); + } + + if (remainder.length() > 0) + output.append(formatLine(remainder)); + + return output.toString(); + } + + private String formatLine(String line) { + return String.format(" %s\n", line.trim()); + } +} diff --git a/common/src/main/java/bisq/common/config/CompositeOptionSet.java b/common/src/main/java/bisq/common/config/CompositeOptionSet.java new file mode 100644 index 0000000000..0b3f39687b --- /dev/null +++ b/common/src/main/java/bisq/common/config/CompositeOptionSet.java @@ -0,0 +1,58 @@ +package bisq.common.config; + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * Composes multiple JOptSimple {@link OptionSet} instances such that calls to + * {@link #valueOf(OptionSpec)} and co will search all instances in the order they were + * added and return any value explicitly set, otherwise returning the default value for + * the given option or null if no default has been set. The API found here loosely + * emulates the {@link OptionSet} API without going through the unnecessary work of + * actually extending it. In practice, this class is used to compose options provided at + * the command line with those provided via config file, such that those provided at the + * command line take precedence over those provided in the config file. + */ +@VisibleForTesting +public class CompositeOptionSet { + + private final List optionSets = new ArrayList<>(); + + public void addOptionSet(OptionSet optionSet) { + optionSets.add(optionSet); + } + + public boolean has(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return true; + + return false; + } + + public V valueOf(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return optionSet.valueOf(option); + + // None of the provided option sets specified the given option so fall back to + // the default value (if any) provided by the first specified OptionSet + return optionSets.get(0).valueOf(option); + } + + public List valuesOf(ArgumentAcceptingOptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return optionSet.valuesOf(option); + + // None of the provided option sets specified the given option so fall back to + // the default value (if any) provided by the first specified OptionSet + return optionSets.get(0).valuesOf(option); + } +} diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java new file mode 100644 index 0000000000..bc21c70a13 --- /dev/null +++ b/common/src/main/java/bisq/common/config/Config.java @@ -0,0 +1,944 @@ +package bisq.common.config; + +import org.bitcoinj.core.NetworkParameters; + +import joptsimple.AbstractOptionSpec; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.HelpFormatter; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.OptionSpecBuilder; +import joptsimple.util.PathConverter; +import joptsimple.util.PathProperties; +import joptsimple.util.RegexMatcher; + +import java.nio.file.Files; +import java.nio.file.Path; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; + +import java.util.List; +import java.util.Optional; + +import ch.qos.logback.classic.Level; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; + +/** + * Parses and provides access to all Bisq configuration options specified at the command + * line and/or via the {@value DEFAULT_CONFIG_FILE_NAME} config file, including any + * default values. Constructing a {@link Config} instance is generally side-effect free, + * with one key exception being that {@value APP_DATA_DIR} and its subdirectories will + * be created if they do not already exist. Care is taken to avoid inadvertent creation or + * modification of the actual system user data directory and/or the production Bisq + * application data directory. Calling code must explicitly specify these values; they are + * never assumed. + *

    + * Note that this class deviates from typical JavaBean conventions in that fields + * representing configuration options are public and have no corresponding accessor + * ("getter") method. This is because all such fields are final and therefore not subject + * to modification by calling code and because eliminating the accessor methods means + * eliminating hundreds of lines of boilerplate code and one less touchpoint to deal with + * when adding or modifying options. Furthermore, while accessor methods are often useful + * when mocking an object in a testing context, this class is designed for testability + * without needing to be mocked. See {@code ConfigTests} for examples. + * @see #Config(String...) + * @see #Config(String, File, String...) + */ +public class Config { + + // Option name constants + public static final String HELP = "help"; + public static final String APP_NAME = "appName"; + public static final String USER_DATA_DIR = "userDataDir"; + public static final String APP_DATA_DIR = "appDataDir"; + public static final String CONFIG_FILE = "configFile"; + public static final String MAX_MEMORY = "maxMemory"; + public static final String LOG_LEVEL = "logLevel"; + public static final String BANNED_BTC_NODES = "bannedBtcNodes"; + public static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; + public static final String BANNED_SEED_NODES = "bannedSeedNodes"; + public static final String BASE_CURRENCY_NETWORK = "baseCurrencyNetwork"; + public static final String REFERRAL_ID = "referralId"; + public static final String USE_DEV_MODE = "useDevMode"; + public static final String USE_DEV_MODE_HEADER = "useDevModeHeader"; + public static final String TOR_DIR = "torDir"; + public static final String STORAGE_DIR = "storageDir"; + public static final String KEY_STORAGE_DIR = "keyStorageDir"; + public static final String WALLET_DIR = "walletDir"; + public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys"; + public static final String DUMP_STATISTICS = "dumpStatistics"; + public static final String IGNORE_DEV_MSG = "ignoreDevMsg"; + public static final String PROVIDERS = "providers"; + public static final String SEED_NODES = "seedNodes"; + public static final String BAN_LIST = "banList"; + public static final String NODE_PORT = "nodePort"; + public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P"; + public static final String MAX_CONNECTIONS = "maxConnections"; + public static final String SOCKS_5_PROXY_BTC_ADDRESS = "socks5ProxyBtcAddress"; + public static final String SOCKS_5_PROXY_HTTP_ADDRESS = "socks5ProxyHttpAddress"; + public static final String USE_TOR_FOR_BTC = "useTorForBtc"; + public static final String TORRC_FILE = "torrcFile"; + public static final String TORRC_OPTIONS = "torrcOptions"; + public static final String TOR_CONTROL_PORT = "torControlPort"; + public static final String TOR_CONTROL_PASSWORD = "torControlPassword"; + public static final String TOR_CONTROL_COOKIE_FILE = "torControlCookieFile"; + public static final String TOR_CONTROL_USE_SAFE_COOKIE_AUTH = "torControlUseSafeCookieAuth"; + public static final String TOR_STREAM_ISOLATION = "torStreamIsolation"; + public static final String MSG_THROTTLE_PER_SEC = "msgThrottlePerSec"; + public static final String MSG_THROTTLE_PER_10_SEC = "msgThrottlePer10Sec"; + public static final String SEND_MSG_THROTTLE_TRIGGER = "sendMsgThrottleTrigger"; + public static final String SEND_MSG_THROTTLE_SLEEP = "sendMsgThrottleSleep"; + public static final String IGNORE_LOCAL_BTC_NODE = "ignoreLocalBtcNode"; + public static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost"; + public static final String BTC_NODES = "btcNodes"; + public static final String SOCKS5_DISCOVER_MODE = "socks5DiscoverMode"; + public static final String USE_ALL_PROVIDED_NODES = "useAllProvidedNodes"; + public static final String USER_AGENT = "userAgent"; + public static final String NUM_CONNECTIONS_FOR_BTC = "numConnectionsForBtc"; + public static final String RPC_USER = "rpcUser"; + public static final String RPC_PASSWORD = "rpcPassword"; + public static final String RPC_HOST = "rpcHost"; + public static final String RPC_PORT = "rpcPort"; + public static final String RPC_BLOCK_NOTIFICATION_PORT = "rpcBlockNotificationPort"; + public static final String RPC_BLOCK_NOTIFICATION_HOST = "rpcBlockNotificationHost"; + public static final String DUMP_BLOCKCHAIN_DATA = "dumpBlockchainData"; + public static final String FULL_DAO_NODE = "fullDaoNode"; + public static final String GENESIS_TX_ID = "genesisTxId"; + public static final String GENESIS_BLOCK_HEIGHT = "genesisBlockHeight"; + public static final String GENESIS_TOTAL_SUPPLY = "genesisTotalSupply"; + public static final String DAO_ACTIVATED = "daoActivated"; + public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs"; + public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs"; + public static final String API_PASSWORD = "apiPassword"; + public static final String API_PORT = "apiPort"; + public static final String PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE = "preventPeriodicShutdownAtSeedNode"; + public static final String REPUBLISH_MAILBOX_ENTRIES = "republishMailboxEntries"; + public static final String BTC_TX_FEE = "btcTxFee"; + public static final String BTC_MIN_TX_FEE = "btcMinTxFee"; + public static final String BTC_FEES_TS = "bitcoinFeesTs"; + public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; + + // Default values for certain options + public static final int UNSPECIFIED_PORT = -1; + public static final String DEFAULT_REGTEST_HOST = "localhost"; + public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12 + public static final boolean DEFAULT_FULL_DAO_NODE = false; + static final String DEFAULT_CONFIG_FILE_NAME = "bisq.properties"; + + // Static fields that provide access to Config properties in locations where injecting + // a Config instance is not feasible. See Javadoc for corresponding static accessors. + private static File APP_DATA_DIR_VALUE; + private static BaseCurrencyNetwork BASE_CURRENCY_NETWORK_VALUE = BaseCurrencyNetwork.BTC_MAINNET; + + // Default "data dir properties", i.e. properties that can determine the location of + // Bisq's application data directory (appDataDir) + public final String defaultAppName; + public final File defaultUserDataDir; + public final File defaultAppDataDir; + public final File defaultConfigFile; + + // Options supported only at the command-line interface (cli) + public final boolean helpRequested; + public final File configFile; + + // Options supported on cmd line and in the config file + public final String appName; + public final File userDataDir; + public final File appDataDir; + public final int nodePort; + public final int maxMemory; + public final String logLevel; + public final List bannedBtcNodes; + public final List bannedPriceRelayNodes; + public final List bannedSeedNodes; + public final BaseCurrencyNetwork baseCurrencyNetwork; + public final NetworkParameters networkParameters; + public final boolean ignoreLocalBtcNode; + public final String bitcoinRegtestHost; + public final boolean daoActivated; + public final String referralId; + public final boolean useDevMode; + public final boolean useDevModeHeader; + public final boolean useDevPrivilegeKeys; + public final boolean dumpStatistics; + public final boolean ignoreDevMsg; + public final List providers; + public final List seedNodes; + public final List banList; + public final boolean useLocalhostForP2P; + public final int maxConnections; + public final String socks5ProxyBtcAddress; + public final String socks5ProxyHttpAddress; + public final File torrcFile; + public final String torrcOptions; + public final int torControlPort; + public final String torControlPassword; + public final File torControlCookieFile; + public final boolean useTorControlSafeCookieAuth; + public final boolean torStreamIsolation; + public final int msgThrottlePerSec; + public final int msgThrottlePer10Sec; + public final int sendMsgThrottleTrigger; + public final int sendMsgThrottleSleep; + public final String btcNodes; + public final boolean useTorForBtc; + public final boolean useTorForBtcOptionSetExplicitly; + public final String socks5DiscoverMode; + public final boolean useAllProvidedNodes; + public final String userAgent; + public final int numConnectionsForBtc; + public final String rpcUser; + public final String rpcPassword; + public final String rpcHost; + public final int rpcPort; + public final int rpcBlockNotificationPort; + public final String rpcBlockNotificationHost; + public final boolean dumpBlockchainData; + public final boolean fullDaoNode; + public final boolean fullDaoNodeOptionSetExplicitly; + public final String genesisTxId; + public final int genesisBlockHeight; + public final long genesisTotalSupply; + public final boolean dumpDelayedPayoutTxs; + public final boolean allowFaultyDelayedTxs; + public final String apiPassword; + public final int apiPort; + public final boolean preventPeriodicShutdownAtSeedNode; + public final boolean republishMailboxEntries; + public final boolean bypassMempoolValidation; + + // Properties derived from options but not exposed as options themselves + public final File torDir; + public final File walletDir; + public final File storageDir; + public final File keyStorageDir; + + // The parser that will be used to parse both cmd line and config file options + private final OptionParser parser = new OptionParser(); + + /** + * Create a new {@link Config} instance using a randomly-generated default + * {@value APP_NAME} and a newly-created temporary directory as the default + * {@value USER_DATA_DIR} along with any command line arguments. This constructor is + * primarily useful in test code, where no references or modifications should be made + * to the actual system user data directory and/or real Bisq application data + * directory. Most production use cases will favor calling the + * {@link #Config(String, File, String...)} constructor directly. + * @param args zero or more command line arguments in the form "--optName=optValue" + * @throws ConfigException if any problems are encountered during option parsing + * @see #Config(String, File, String...) + */ + public Config(String... args) { + this(randomAppName(), tempUserDataDir(), args); + } + + /** + * Create a new {@link Config} instance with the given default {@value APP_NAME} and + * {@value USER_DATA_DIR} values along with any command line arguments, typically + * those supplied via a Bisq application's main() method. + *

    + * This constructor performs all parsing of command line options and config file + * options, assuming the default config file exists or a custom config file has been + * specified via the {@value CONFIG_FILE} option and exists. For any options that + * are present both at the command line and in the config file, the command line value + * will take precedence. Note that the {@value HELP} and {@value CONFIG_FILE} options + * are supported only at the command line and are disallowed within the config file + * itself. + * @param defaultAppName typically "Bisq" or similar + * @param defaultUserDataDir typically the OS-specific user data directory location + * @param args zero or more command line arguments in the form "--optName=optValue" + * @throws ConfigException if any problems are encountered during option parsing + */ + public Config(String defaultAppName, File defaultUserDataDir, String... args) { + this.defaultAppName = defaultAppName; + this.defaultUserDataDir = defaultUserDataDir; + this.defaultAppDataDir = new File(defaultUserDataDir, defaultAppName); + this.defaultConfigFile = absoluteConfigFile(defaultAppDataDir, DEFAULT_CONFIG_FILE_NAME); + + AbstractOptionSpec helpOpt = + parser.accepts(HELP, "Print this help text") + .forHelp(); + + ArgumentAcceptingOptionSpec configFileOpt = + parser.accepts(CONFIG_FILE, format("Specify configuration file. " + + "Relative paths will be prefixed by %s location.", APP_DATA_DIR)) + .withRequiredArg() + .ofType(String.class) + .defaultsTo(DEFAULT_CONFIG_FILE_NAME); + + ArgumentAcceptingOptionSpec appNameOpt = + parser.accepts(APP_NAME, "Application name") + .withRequiredArg() + .ofType(String.class) + .defaultsTo(this.defaultAppName); + + ArgumentAcceptingOptionSpec userDataDirOpt = + parser.accepts(USER_DATA_DIR, "User data directory") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(this.defaultUserDataDir); + + ArgumentAcceptingOptionSpec appDataDirOpt = + parser.accepts(APP_DATA_DIR, "Application data directory") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(defaultAppDataDir); + + ArgumentAcceptingOptionSpec nodePortOpt = + parser.accepts(NODE_PORT, "Port to listen on") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9999); + + ArgumentAcceptingOptionSpec maxMemoryOpt = + parser.accepts(MAX_MEMORY, "Max. permitted memory (used only by headless versions)") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(1200); + + ArgumentAcceptingOptionSpec logLevelOpt = + parser.accepts(LOG_LEVEL, "Set logging level") + .withRequiredArg() + .ofType(String.class) + .describedAs("OFF|ALL|ERROR|WARN|INFO|DEBUG|TRACE") + .defaultsTo(Level.INFO.levelStr); + + ArgumentAcceptingOptionSpec bannedBtcNodesOpt = + parser.accepts(BANNED_BTC_NODES, "List Bitcoin nodes to ban") + .withRequiredArg() + .ofType(String.class) + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + ArgumentAcceptingOptionSpec bannedPriceRelayNodesOpt = + parser.accepts(BANNED_PRICE_RELAY_NODES, "List Bisq price nodes to ban") + .withRequiredArg() + .ofType(String.class) + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + ArgumentAcceptingOptionSpec bannedSeedNodesOpt = + parser.accepts(BANNED_SEED_NODES, "List Bisq seed nodes to ban") + .withRequiredArg() + .ofType(String.class) + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + //noinspection rawtypes + ArgumentAcceptingOptionSpec baseCurrencyNetworkOpt = + parser.accepts(BASE_CURRENCY_NETWORK, "Base currency network") + .withRequiredArg() + .ofType(BaseCurrencyNetwork.class) + .withValuesConvertedBy(new EnumValueConverter(BaseCurrencyNetwork.class)) + .defaultsTo(BaseCurrencyNetwork.BTC_MAINNET); + + ArgumentAcceptingOptionSpec ignoreLocalBtcNodeOpt = + parser.accepts(IGNORE_LOCAL_BTC_NODE, + "If set to true a Bitcoin Core node running locally will be ignored") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = + parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core node when using BTC_REGTEST network") + .withRequiredArg() + .ofType(String.class) + .describedAs("host[:port]") + .defaultsTo(""); + + ArgumentAcceptingOptionSpec referralIdOpt = + parser.accepts(REFERRAL_ID, "Optional Referral ID (e.g. for API users or pro market makers)") + .withRequiredArg() + .ofType(String.class) + .defaultsTo(""); + + ArgumentAcceptingOptionSpec useDevModeOpt = + parser.accepts(USE_DEV_MODE, + "Enables dev mode which is used for convenience for developer testing") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec useDevModeHeaderOpt = + parser.accepts(USE_DEV_MODE_HEADER, + "Use dev mode css scheme to distinguish dev instances.") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec useDevPrivilegeKeysOpt = + parser.accepts(USE_DEV_PRIVILEGE_KEYS, "If set to true all privileged features requiring a private " + + "key to be enabled are overridden by a dev key pair (This is for developers only!)") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec dumpStatisticsOpt = + parser.accepts(DUMP_STATISTICS, "If set to true dump trade statistics to a json file in appDataDir") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec ignoreDevMsgOpt = + parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " + + "network_messages from bisq developers are ignored (Global " + + "alert, Version update alert, Filters for offers, nodes or " + + "trading account data)") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec providersOpt = + parser.accepts(PROVIDERS, "List custom pricenodes") + .withRequiredArg() + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + ArgumentAcceptingOptionSpec seedNodesOpt = + parser.accepts(SEED_NODES, "Override hard coded seed nodes as comma separated list e.g. " + + "'rxdkppp3vicnbgqt.onion:8002,mfla72c4igh5ta2t.onion:8002'") + .withRequiredArg() + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + ArgumentAcceptingOptionSpec banListOpt = + parser.accepts(BAN_LIST, "Nodes to exclude from network connections.") + .withRequiredArg() + .withValuesSeparatedBy(',') + .describedAs("host:port[,...]"); + + ArgumentAcceptingOptionSpec useLocalhostForP2POpt = + parser.accepts(USE_LOCALHOST_FOR_P2P, "Use localhost P2P network for development. Only available for non-BTC_MAINNET configuration.") + .availableIf(BASE_CURRENCY_NETWORK) + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec maxConnectionsOpt = + parser.accepts(MAX_CONNECTIONS, "Max. connections a peer will try to keep") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(12); + + ArgumentAcceptingOptionSpec socks5ProxyBtcAddressOpt = + parser.accepts(SOCKS_5_PROXY_BTC_ADDRESS, "A proxy address to be used for Bitcoin network.") + .withRequiredArg() + .describedAs("host:port") + .defaultsTo(""); + + ArgumentAcceptingOptionSpec socks5ProxyHttpAddressOpt = + parser.accepts(SOCKS_5_PROXY_HTTP_ADDRESS, + "A proxy address to be used for Http requests (should be non-Tor)") + .withRequiredArg() + .describedAs("host:port") + .defaultsTo(""); + + ArgumentAcceptingOptionSpec torrcFileOpt = + parser.accepts(TORRC_FILE, "An existing torrc-file to be sourced for Tor. Note that torrc-entries, " + + "which are critical to Bisq's correct operation, cannot be overwritten.") + .withRequiredArg() + .describedAs("File") + .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); + + ArgumentAcceptingOptionSpec torrcOptionsOpt = + parser.accepts(TORRC_OPTIONS, "A list of torrc-entries to amend to Bisq's torrc. Note that " + + "torrc-entries, which are critical to Bisq's flawless operation, cannot be overwritten. " + + "[torrc options line, torrc option, ...]") + .withRequiredArg() + .withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$")) + .defaultsTo(""); + + ArgumentAcceptingOptionSpec torControlPortOpt = + parser.accepts(TOR_CONTROL_PORT, + "The control port of an already running Tor service to be used by Bisq.") + .availableUnless(TORRC_FILE, TORRC_OPTIONS) + .withRequiredArg() + .ofType(int.class) + .describedAs("port") + .defaultsTo(UNSPECIFIED_PORT); + + ArgumentAcceptingOptionSpec torControlPasswordOpt = + parser.accepts(TOR_CONTROL_PASSWORD, "The password for controlling the already running Tor service.") + .availableIf(TOR_CONTROL_PORT) + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec torControlCookieFileOpt = + parser.accepts(TOR_CONTROL_COOKIE_FILE, "The cookie file for authenticating against the already " + + "running Tor service. Use in conjunction with --" + TOR_CONTROL_USE_SAFE_COOKIE_AUTH) + .availableIf(TOR_CONTROL_PORT) + .availableUnless(TOR_CONTROL_PASSWORD) + .withRequiredArg() + .describedAs("File") + .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); + + OptionSpecBuilder torControlUseSafeCookieAuthOpt = + parser.accepts(TOR_CONTROL_USE_SAFE_COOKIE_AUTH, + "Use the SafeCookie method when authenticating to the already running Tor service.") + .availableIf(TOR_CONTROL_COOKIE_FILE); + + OptionSpecBuilder torStreamIsolationOpt = + parser.accepts(TOR_STREAM_ISOLATION, "Use stream isolation for Tor [experimental!]."); + + ArgumentAcceptingOptionSpec msgThrottlePerSecOpt = + parser.accepts(MSG_THROTTLE_PER_SEC, "Message throttle per sec for connection class") + .withRequiredArg() + .ofType(int.class) + // With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 40MB/sec or 5 mbit/sec + .defaultsTo(200); + + ArgumentAcceptingOptionSpec msgThrottlePer10SecOpt = + parser.accepts(MSG_THROTTLE_PER_10_SEC, "Message throttle per 10 sec for connection class") + .withRequiredArg() + .ofType(int.class) + // With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 20MB/sec or 2.5 mbit/sec + .defaultsTo(1000); + + ArgumentAcceptingOptionSpec sendMsgThrottleTriggerOpt = + parser.accepts(SEND_MSG_THROTTLE_TRIGGER, "Time in ms when we trigger a sleep if 2 messages are sent") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(20); // Time in ms when we trigger a sleep if 2 messages are sent + + ArgumentAcceptingOptionSpec sendMsgThrottleSleepOpt = + parser.accepts(SEND_MSG_THROTTLE_SLEEP, "Pause in ms to sleep if we get too many messages to send") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(50); // Pause in ms to sleep if we get too many messages to send + + ArgumentAcceptingOptionSpec btcNodesOpt = + parser.accepts(BTC_NODES, "Custom nodes used for BitcoinJ as comma separated IP addresses.") + .withRequiredArg() + .describedAs("ip[,...]") + .defaultsTo(""); + + ArgumentAcceptingOptionSpec useTorForBtcOpt = + parser.accepts(USE_TOR_FOR_BTC, "If set to true BitcoinJ is routed over tor (socks 5 proxy).") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec socks5DiscoverModeOpt = + parser.accepts(SOCKS5_DISCOVER_MODE, "Specify discovery mode for Bitcoin nodes. " + + "One or more of: [ADDR, DNS, ONION, ALL] (comma separated, they get OR'd together).") + .withRequiredArg() + .describedAs("mode[,...]") + .defaultsTo("ALL"); + + ArgumentAcceptingOptionSpec useAllProvidedNodesOpt = + parser.accepts(USE_ALL_PROVIDED_NODES, + "Set to true if connection of bitcoin nodes should include clear net nodes") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec userAgentOpt = + parser.accepts(USER_AGENT, + "User agent at btc node connections") + .withRequiredArg() + .defaultsTo("Bisq"); + + ArgumentAcceptingOptionSpec numConnectionsForBtcOpt = + parser.accepts(NUM_CONNECTIONS_FOR_BTC, "Number of connections to the Bitcoin network") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(DEFAULT_NUM_CONNECTIONS_FOR_BTC); + + ArgumentAcceptingOptionSpec rpcUserOpt = + parser.accepts(RPC_USER, "Bitcoind rpc username") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec rpcPasswordOpt = + parser.accepts(RPC_PASSWORD, "Bitcoind rpc password") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec rpcHostOpt = + parser.accepts(RPC_HOST, "Bitcoind rpc host") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec rpcPortOpt = + parser.accepts(RPC_PORT, "Bitcoind rpc port") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(UNSPECIFIED_PORT); + + ArgumentAcceptingOptionSpec rpcBlockNotificationPortOpt = + parser.accepts(RPC_BLOCK_NOTIFICATION_PORT, "Bitcoind rpc port for block notifications") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(UNSPECIFIED_PORT); + + ArgumentAcceptingOptionSpec rpcBlockNotificationHostOpt = + parser.accepts(RPC_BLOCK_NOTIFICATION_HOST, + "Bitcoind rpc accepted incoming host for block notifications") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec dumpBlockchainDataOpt = + parser.accepts(DUMP_BLOCKCHAIN_DATA, "If set to true the blockchain data " + + "from RPC requests to Bitcoin Core are stored as json file in the data dir.") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec fullDaoNodeOpt = + parser.accepts(FULL_DAO_NODE, "If set to true the node requests the blockchain data via RPC requests " + + "from Bitcoin Core and provide the validated BSQ txs to the network. It requires that the " + + "other RPC properties are set as well.") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(DEFAULT_FULL_DAO_NODE); + + ArgumentAcceptingOptionSpec genesisTxIdOpt = + parser.accepts(GENESIS_TX_ID, "Genesis transaction ID when not using the hard coded one") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec genesisBlockHeightOpt = + parser.accepts(GENESIS_BLOCK_HEIGHT, + "Genesis transaction block height when not using the hard coded one") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(-1); + + ArgumentAcceptingOptionSpec genesisTotalSupplyOpt = + parser.accepts(GENESIS_TOTAL_SUPPLY, "Genesis total supply when not using the hard coded one") + .withRequiredArg() + .ofType(long.class) + .defaultsTo(-1L); + + ArgumentAcceptingOptionSpec daoActivatedOpt = + parser.accepts(DAO_ACTIVATED, "Developer flag. If true it enables dao phase 2 features.") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(true); + + ArgumentAcceptingOptionSpec dumpDelayedPayoutTxsOpt = + parser.accepts(DUMP_DELAYED_PAYOUT_TXS, "Dump delayed payout transactions to file") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec allowFaultyDelayedTxsOpt = + parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " + + "payout transactions") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec apiPasswordOpt = + parser.accepts(API_PASSWORD, "gRPC API password") + .withRequiredArg() + .defaultsTo(""); + + ArgumentAcceptingOptionSpec apiPortOpt = + parser.accepts(API_PORT, "gRPC API port") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); + + ArgumentAcceptingOptionSpec preventPeriodicShutdownAtSeedNodeOpt = + parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE, + "Prevents periodic shutdown at seed nodes") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec republishMailboxEntriesOpt = + parser.accepts(REPUBLISH_MAILBOX_ENTRIES, + "Republish mailbox messages at startup") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec bypassMempoolValidationOpt = + parser.accepts(BYPASS_MEMPOOL_VALIDATION, + "Prevents mempool check of trade parameters") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + + try { + CompositeOptionSet options = new CompositeOptionSet(); + + // Parse command line options + OptionSet cliOpts = parser.parse(args); + options.addOptionSet(cliOpts); + + // Option parsing is strict at the command line, but we relax it now for any + // subsequent config file processing. This is for compatibility with pre-1.2.6 + // versions that allowed unrecognized options in the bisq.properties config + // file and because it follows suit with Bitcoin Core's config file behavior. + parser.allowsUnrecognizedOptions(); + + // Parse config file specified at the command line only if it was specified as + // an absolute path. Otherwise, the config file will be processed later below. + File configFile = null; + OptionSpec[] disallowedOpts = new OptionSpec[]{helpOpt, configFileOpt}; + final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt); + boolean configFileHasBeenProcessed = false; + if (cliHasConfigFileOpt) { + configFile = new File(cliOpts.valueOf(configFileOpt)); + if (configFile.isAbsolute()) { + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + if (configFileOpts.isPresent()) { + options.addOptionSet(configFileOpts.get()); + configFileHasBeenProcessed = true; + } + } + } + + // Assign values to the following "data dir properties". If a + // relatively-pathed config file was specified at the command line, any + // entries it has for these options will be ignored, as it has not been + // processed yet. + this.appName = options.valueOf(appNameOpt); + this.userDataDir = options.valueOf(userDataDirOpt); + this.appDataDir = mkAppDataDir(options.has(appDataDirOpt) ? + options.valueOf(appDataDirOpt) : + new File(userDataDir, appName)); + + // If the config file has not yet been processed, either because a relative + // path was provided at the command line, or because no value was provided at + // the command line, attempt to process the file now, falling back to the + // default config file location if none was specified at the command line. + if (!configFileHasBeenProcessed) { + configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ? + absoluteConfigFile(appDataDir, configFile.getPath()) : + absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME); + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + configFileOpts.ifPresent(options::addOptionSet); + } + + // Assign all remaining properties, with command line options taking + // precedence over those provided in the config file (if any) + this.helpRequested = options.has(helpOpt); + this.configFile = configFile; + this.nodePort = options.valueOf(nodePortOpt); + this.maxMemory = options.valueOf(maxMemoryOpt); + this.logLevel = options.valueOf(logLevelOpt); + this.bannedBtcNodes = options.valuesOf(bannedBtcNodesOpt); + this.bannedPriceRelayNodes = options.valuesOf(bannedPriceRelayNodesOpt); + this.bannedSeedNodes = options.valuesOf(bannedSeedNodesOpt); + this.baseCurrencyNetwork = (BaseCurrencyNetwork) options.valueOf(baseCurrencyNetworkOpt); + this.networkParameters = baseCurrencyNetwork.getParameters(); + this.ignoreLocalBtcNode = options.valueOf(ignoreLocalBtcNodeOpt); + this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt); + this.torrcFile = options.has(torrcFileOpt) ? options.valueOf(torrcFileOpt).toFile() : null; + this.torrcOptions = options.valueOf(torrcOptionsOpt); + this.torControlPort = options.valueOf(torControlPortOpt); + this.torControlPassword = options.valueOf(torControlPasswordOpt); + this.torControlCookieFile = options.has(torControlCookieFileOpt) ? + options.valueOf(torControlCookieFileOpt).toFile() : null; + this.useTorControlSafeCookieAuth = options.has(torControlUseSafeCookieAuthOpt); + this.torStreamIsolation = options.has(torStreamIsolationOpt); + this.referralId = options.valueOf(referralIdOpt); + this.useDevMode = options.valueOf(useDevModeOpt); + this.useDevModeHeader = options.valueOf(useDevModeHeaderOpt); + this.useDevPrivilegeKeys = options.valueOf(useDevPrivilegeKeysOpt); + this.dumpStatistics = options.valueOf(dumpStatisticsOpt); + this.ignoreDevMsg = options.valueOf(ignoreDevMsgOpt); + this.providers = options.valuesOf(providersOpt); + this.seedNodes = options.valuesOf(seedNodesOpt); + this.banList = options.valuesOf(banListOpt); + this.useLocalhostForP2P = !this.baseCurrencyNetwork.isMainnet() && options.valueOf(useLocalhostForP2POpt); + this.maxConnections = options.valueOf(maxConnectionsOpt); + this.socks5ProxyBtcAddress = options.valueOf(socks5ProxyBtcAddressOpt); + this.socks5ProxyHttpAddress = options.valueOf(socks5ProxyHttpAddressOpt); + this.msgThrottlePerSec = options.valueOf(msgThrottlePerSecOpt); + this.msgThrottlePer10Sec = options.valueOf(msgThrottlePer10SecOpt); + this.sendMsgThrottleTrigger = options.valueOf(sendMsgThrottleTriggerOpt); + this.sendMsgThrottleSleep = options.valueOf(sendMsgThrottleSleepOpt); + this.btcNodes = options.valueOf(btcNodesOpt); + this.useTorForBtc = options.valueOf(useTorForBtcOpt); + this.useTorForBtcOptionSetExplicitly = options.has(useTorForBtcOpt); + this.socks5DiscoverMode = options.valueOf(socks5DiscoverModeOpt); + this.useAllProvidedNodes = options.valueOf(useAllProvidedNodesOpt); + this.userAgent = options.valueOf(userAgentOpt); + this.numConnectionsForBtc = options.valueOf(numConnectionsForBtcOpt); + this.rpcUser = options.valueOf(rpcUserOpt); + this.rpcPassword = options.valueOf(rpcPasswordOpt); + this.rpcHost = options.valueOf(rpcHostOpt); + this.rpcPort = options.valueOf(rpcPortOpt); + this.rpcBlockNotificationPort = options.valueOf(rpcBlockNotificationPortOpt); + this.rpcBlockNotificationHost = options.valueOf(rpcBlockNotificationHostOpt); + this.dumpBlockchainData = options.valueOf(dumpBlockchainDataOpt); + this.fullDaoNode = options.valueOf(fullDaoNodeOpt); + this.fullDaoNodeOptionSetExplicitly = options.has(fullDaoNodeOpt); + this.genesisTxId = options.valueOf(genesisTxIdOpt); + this.genesisBlockHeight = options.valueOf(genesisBlockHeightOpt); + this.genesisTotalSupply = options.valueOf(genesisTotalSupplyOpt); + this.daoActivated = options.valueOf(daoActivatedOpt); + this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt); + this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt); + this.apiPassword = options.valueOf(apiPasswordOpt); + this.apiPort = options.valueOf(apiPortOpt); + this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt); + this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); + this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); + } catch (OptionException ex) { + throw new ConfigException("problem parsing option '%s': %s", + ex.options().get(0), + ex.getCause() != null ? + ex.getCause().getMessage() : + ex.getMessage()); + } + + // 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"); + + // Assign values to special-case static fields + APP_DATA_DIR_VALUE = appDataDir; + BASE_CURRENCY_NETWORK_VALUE = baseCurrencyNetwork; + } + + private static File absoluteConfigFile(File parentDir, String relativeConfigFilePath) { + return new File(parentDir, relativeConfigFilePath); + } + + private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { + if (!configFile.exists()) { + if (!configFile.equals(absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME))) + throw new ConfigException("The specified config file '%s' does not exist.", configFile); + return Optional.empty(); + } + + ConfigFileReader configFileReader = new ConfigFileReader(configFile); + String[] optionLines = configFileReader.getOptionLines().stream() + .map(o -> "--" + o) // prepend dashes expected by jopt parser below + .collect(toList()) + .toArray(new String[]{}); + + OptionSet configFileOpts = parser.parse(optionLines); + for (OptionSpec disallowedOpt : disallowedOpts) + if (configFileOpts.has(disallowedOpt)) + throw new ConfigException("The '%s' option is disallowed in config files", + disallowedOpt.options().get(0)); + + return Optional.of(configFileOpts); + } + + public void printHelp(OutputStream sink, HelpFormatter formatter) { + try { + parser.formatHelpWith(formatter); + parser.printHelpOn(sink); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + + // == STATIC UTILS =================================================================== + + private static String randomAppName() { + try { + File file = Files.createTempFile("Bisq", "Temp").toFile(); + //noinspection ResultOfMethodCallIgnored + file.delete(); + return file.toPath().getFileName().toString(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static File tempUserDataDir() { + try { + return Files.createTempDirectory("BisqTempUserData").toFile(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Creates {@value APP_DATA_DIR} including any nonexistent parent directories. Does + * nothing if the directory already exists. + * @return the given directory, now guaranteed to exist + */ + private static File mkAppDataDir(File dir) { + if (!dir.exists()) { + try { + Files.createDirectories(dir.toPath()); + } catch (IOException ex) { + throw new UncheckedIOException(format("Application data directory '%s' could not be created", dir), ex); + } + } + return dir; + } + + /** + * Creates child directory assuming parent directories already exist. Does nothing if + * the directory already exists. + * @return the child directory, now guaranteed to exist + */ + private static File mkdir(File parent, String child) { + File dir = new File(parent, child); + if (!dir.exists()) { + try { + Files.createDirectory(dir.toPath()); + } catch (IOException ex) { + throw new UncheckedIOException(format("Directory '%s' could not be created", dir), ex); + } + } + return dir; + } + + + // == STATIC ACCESSORS ====================================================================== + + /** + * Static accessor that returns the same value as the non-static + * {@link #appDataDir} property. For use only in the {@code Overlay} class, where + * because of its large number of subclasses, injecting the Guice-managed + * {@link Config} class is not worth the effort. {@link #appDataDir} should be + * favored in all other cases. + * @throws NullPointerException if the static value has not yet been assigned, i.e. if + * the Guice-managed {@link Config} class has not yet been instantiated elsewhere. + * This should never be the case, as Guice wiring always happens before any + * {@code Overlay} class is instantiated. + */ + public static File appDataDir() { + return checkNotNull(APP_DATA_DIR_VALUE, "The static appDataDir has not yet " + + "been assigned. A Config instance must be instantiated (usually by " + + "Guice) before calling this method."); + } + + /** + * Static accessor that returns either the default base currency network value of + * {@link BaseCurrencyNetwork#BTC_MAINNET} or the value assigned via the + * {@value BASE_CURRENCY_NETWORK} option. The non-static + * {@link #baseCurrencyNetwork} property should be favored whenever possible and + * this static accessor should be used only in code locations where it is infeasible + * or too cumbersome to inject the normal Guice-managed singleton {@link Config} + * instance. + */ + public static BaseCurrencyNetwork baseCurrencyNetwork() { + return BASE_CURRENCY_NETWORK_VALUE; + } + + /** + * Static accessor that returns the value of + * {@code baseCurrencyNetwork().getParameters()} for convenience and to avoid violating + * the Law of Demeter. The + * non-static {@link #baseCurrencyNetwork} property should be favored whenever + * possible. + * @see #baseCurrencyNetwork() + */ + public static NetworkParameters baseCurrencyNetworkParameters() { + return BASE_CURRENCY_NETWORK_VALUE.getParameters(); + } +} diff --git a/common/src/main/java/bisq/common/config/ConfigException.java b/common/src/main/java/bisq/common/config/ConfigException.java new file mode 100644 index 0000000000..0d709dcf53 --- /dev/null +++ b/common/src/main/java/bisq/common/config/ConfigException.java @@ -0,0 +1,10 @@ +package bisq.common.config; + +import bisq.common.BisqException; + +public class ConfigException extends BisqException { + + public ConfigException(String format, Object... args) { + super(format, args); + } +} diff --git a/common/src/main/java/bisq/common/config/ConfigFileEditor.java b/common/src/main/java/bisq/common/config/ConfigFileEditor.java new file mode 100644 index 0000000000..f067430165 --- /dev/null +++ b/common/src/main/java/bisq/common/config/ConfigFileEditor.java @@ -0,0 +1,87 @@ +package bisq.common.config; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +import java.util.List; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Logger; + +public class ConfigFileEditor { + + private static final Logger log = (Logger) LoggerFactory.getLogger(ConfigFileEditor.class); + + private final File file; + private final ConfigFileReader reader; + + public ConfigFileEditor(File file) { + this.file = file; + this.reader = new ConfigFileReader(file); + } + + public void setOption(String name) { + setOption(name, null); + } + + public void setOption(String name, String arg) { + tryCreate(file); + List lines = reader.getLines(); + try (PrintWriter writer = new PrintWriter(file)) { + boolean fileAlreadyContainsTargetOption = false; + for (String line : lines) { + if (ConfigFileOption.isOption(line)) { + ConfigFileOption existingOption = ConfigFileOption.parse(line); + if (existingOption.name.equals(name)) { + fileAlreadyContainsTargetOption = true; + if (!existingOption.arg.equals(arg)) { + ConfigFileOption newOption = new ConfigFileOption(name, arg); + writer.println(newOption); + log.warn("Overwrote existing config file option '{}' as '{}'", existingOption, newOption); + continue; + } + } + } + writer.println(line); + } + if (!fileAlreadyContainsTargetOption) + writer.println(new ConfigFileOption(name, arg)); + } catch (FileNotFoundException ex) { + throw new UncheckedIOException(ex); + } + } + + public void clearOption(String name) { + if (!file.exists()) + return; + + List lines = reader.getLines(); + try (PrintWriter writer = new PrintWriter(file)) { + for (String line : lines) { + if (ConfigFileOption.isOption(line)) { + ConfigFileOption option = ConfigFileOption.parse(line); + if (option.name.equals(name)) { + log.warn("Cleared existing config file option '{}'", option); + continue; + } + } + writer.println(line); + } + } catch (FileNotFoundException ex) { + throw new UncheckedIOException(ex); + } + } + + private void tryCreate(File file) { + try { + if (file.createNewFile()) + log.info("Created config file '{}'", file); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } +} diff --git a/common/src/main/java/bisq/common/config/ConfigFileOption.java b/common/src/main/java/bisq/common/config/ConfigFileOption.java new file mode 100644 index 0000000000..a35ddc33a1 --- /dev/null +++ b/common/src/main/java/bisq/common/config/ConfigFileOption.java @@ -0,0 +1,36 @@ +package bisq.common.config; + +class ConfigFileOption { + + public final String name; + public final String arg; + + public ConfigFileOption(String name, String arg) { + this.name = name; + this.arg = arg; + } + + public static boolean isOption(String line) { + return !line.isEmpty() && !line.startsWith("#"); + } + + public static ConfigFileOption parse(String option) { + if (!option.contains("=")) + return new ConfigFileOption(option, null); + + String[] tokens = clean(option).split("="); + String name = tokens[0].trim(); + String arg = tokens.length > 1 ? tokens[1].trim() : ""; + return new ConfigFileOption(name, arg); + } + + public String toString() { + return String.format("%s%s", name, arg != null ? ('=' + arg) : ""); + } + + public static String clean(String option) { + return option + .trim() + .replace("\\:", ":"); + } +} diff --git a/common/src/main/java/bisq/common/config/ConfigFileReader.java b/common/src/main/java/bisq/common/config/ConfigFileReader.java new file mode 100644 index 0000000000..b6ec89b557 --- /dev/null +++ b/common/src/main/java/bisq/common/config/ConfigFileReader.java @@ -0,0 +1,46 @@ +package bisq.common.config; + +import java.nio.file.Files; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +class ConfigFileReader { + + private final File file; + + public ConfigFileReader(File file) { + this.file = file; + } + + public List getLines() { + if (!file.exists()) + throw new ConfigException("Config file %s does not exist", file); + + if (!file.canRead()) + throw new ConfigException("Config file %s is not readable", file); + + try { + return Files.readAllLines(file.toPath()).stream() + .map(ConfigFileReader::cleanLine) + .collect(toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public List getOptionLines() { + return getLines().stream() + .filter(ConfigFileOption::isOption) + .collect(toList()); + } + + private static String cleanLine(String line) { + return ConfigFileOption.isOption(line) ? ConfigFileOption.clean(line) : line; + } +} diff --git a/common/src/main/java/bisq/common/config/EnumValueConverter.java b/common/src/main/java/bisq/common/config/EnumValueConverter.java new file mode 100644 index 0000000000..95e9765724 --- /dev/null +++ b/common/src/main/java/bisq/common/config/EnumValueConverter.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.common.config; + +import joptsimple.ValueConverter; + +import com.google.common.base.Enums; +import com.google.common.base.Optional; +import com.google.common.collect.Sets; + +import java.util.Set; + +/** + * A {@link joptsimple.ValueConverter} that supports case-insensitive conversion from + * String to an enum label. Useful in conjunction with + * {@link joptsimple.ArgumentAcceptingOptionSpec#ofType(Class)} when the type in question + * is an enum. + */ +class EnumValueConverter implements ValueConverter { + + private final Class enumType; + + public EnumValueConverter(Class enumType) { + this.enumType = enumType; + } + + /** + * Attempt to resolve an enum of the specified type by looking for a label with the + * given value, trying all case variations in the process. + * + * @return the matching enum label (if any) + * @throws ConfigException if no such label matching the given value is found. + */ + @Override + public Enum convert(String value) { + Set candidates = Sets.newHashSet(value, value.toUpperCase(), value.toLowerCase()); + for (String candidate : candidates) { + Optional result = Enums.getIfPresent(enumType, candidate); + if (result.isPresent()) + return result.get(); + } + throw new ConfigException("Enum label %s.{%s} does not exist", + enumType.getSimpleName(), String.join("|", candidates)); + } + + @Override + public Class valueType() { + return enumType; + } + + @Override + public String valuePattern() { + return null; + } +} diff --git a/common/src/main/java/bisq/common/consensus/UsedForTradeContractJson.java b/common/src/main/java/bisq/common/consensus/UsedForTradeContractJson.java new file mode 100644 index 0000000000..94105d34f3 --- /dev/null +++ b/common/src/main/java/bisq/common/consensus/UsedForTradeContractJson.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common.consensus; + +/** + * Marker interface for classes which are used in the trade contract. + * Any change of the class fields would breaking backward compatibility. + * If a field needs to get added it needs to be annotated with @JsonExclude (thus excluded from the contract JSON). + * Better to use the excludeFromJsonDataMap (annotated with @JsonExclude; used in PaymentAccountPayload) to + * add a key/value pair. + */ +public interface UsedForTradeContractJson { +} diff --git a/common/src/main/java/bisq/common/crypto/CryptoException.java b/common/src/main/java/bisq/common/crypto/CryptoException.java new file mode 100644 index 0000000000..930009c995 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/CryptoException.java @@ -0,0 +1,32 @@ +/* + * 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 bisq.common.crypto; + +public class CryptoException extends Exception { + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + + public CryptoException(Throwable cause) { + super(cause); + } +} diff --git a/common/src/main/java/bisq/common/crypto/CryptoUtils.java b/common/src/main/java/bisq/common/crypto/CryptoUtils.java new file mode 100644 index 0000000000..83ab4317f4 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/CryptoUtils.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.common.crypto; + +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.X509EncodedKeySpec; + +import java.util.Base64; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CryptoUtils { + public static String pubKeyToString(PublicKey publicKey) { + final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded()); + return Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded()); + } + + public static byte[] getRandomBytes(int size) { + byte[] bytes = new byte[size]; + new SecureRandom().nextBytes(bytes); + return bytes; + } +} diff --git a/common/src/main/java/bisq/common/crypto/Encryption.java b/common/src/main/java/bisq/common/crypto/Encryption.java new file mode 100644 index 0000000000..8be207fe4c --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/Encryption.java @@ -0,0 +1,250 @@ +/* + * 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 bisq.common.crypto; + +import bisq.common.util.Hex; +import bisq.common.util.Utilities; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import javax.crypto.spec.SecretKeySpec; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.X509EncodedKeySpec; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Encryption { + private static final Logger log = LoggerFactory.getLogger(Encryption.class); + + public static final String ASYM_KEY_ALGO = "RSA"; + private static final String ASYM_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1PADDING"; + + private static final String SYM_KEY_ALGO = "AES"; + private static final String SYM_CIPHER = "AES"; + + private static final String HMAC = "HmacSHA256"; + + public static KeyPair generateKeyPair() { + long ts = System.currentTimeMillis(); + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ASYM_KEY_ALGO); + keyPairGenerator.initialize(2048); + return keyPairGenerator.genKeyPair(); + } catch (Throwable e) { + log.error("Could not create key.", e); + throw new RuntimeException("Could not create key."); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Symmetric + /////////////////////////////////////////////////////////////////////////////////////////// + + public static byte[] encrypt(byte[] payload, SecretKey secretKey) throws CryptoException { + try { + Cipher cipher = Cipher.getInstance(SYM_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return cipher.doFinal(payload); + } catch (Throwable e) { + log.error("error in encrypt", e); + throw new CryptoException(e); + } + } + + public static byte[] decrypt(byte[] encryptedPayload, SecretKey secretKey) throws CryptoException { + try { + Cipher cipher = Cipher.getInstance(SYM_CIPHER); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return cipher.doFinal(encryptedPayload); + } catch (Throwable e) { + throw new CryptoException(e); + } + } + + public static SecretKey getSecretKeyFromBytes(byte[] secretKeyBytes) { + return new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, SYM_KEY_ALGO); + } + + public static byte[] getSecretKeyBytes(SecretKey secretKey) { + return secretKey.getEncoded(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Hmac + /////////////////////////////////////////////////////////////////////////////////////////// + + private static byte[] getPayloadWithHmac(byte[] payload, SecretKey secretKey) { + byte[] payloadWithHmac; + try { + + ByteArrayOutputStream outputStream = null; + try { + byte[] hmac = getHmac(payload, secretKey); + outputStream = new ByteArrayOutputStream(); + outputStream.write(payload); + outputStream.write(hmac); + outputStream.flush(); + payloadWithHmac = outputStream.toByteArray().clone(); + } catch (IOException | NoSuchProviderException e) { + log.error("Could not create hmac", e); + throw new RuntimeException("Could not create hmac"); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ignored) { + } + } + } + } catch (Throwable e) { + log.error("Could not create hmac", e); + throw new RuntimeException("Could not create hmac"); + } + return payloadWithHmac; + } + + + private static boolean verifyHmac(byte[] message, byte[] hmac, SecretKey secretKey) { + try { + byte[] hmacTest = getHmac(message, secretKey); + return Arrays.equals(hmacTest, hmac); + } catch (Throwable e) { + log.error("Could not create cipher", e); + throw new RuntimeException("Could not create cipher"); + } + } + + private static byte[] getHmac(byte[] payload, SecretKey secretKey) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + Mac mac = Mac.getInstance(HMAC); + mac.init(secretKey); + return mac.doFinal(payload); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Symmetric with Hmac + /////////////////////////////////////////////////////////////////////////////////////////// + + + public static byte[] encryptPayloadWithHmac(byte[] payload, SecretKey secretKey) throws CryptoException { + return encrypt(getPayloadWithHmac(payload, secretKey), secretKey); + } + + public static byte[] decryptPayloadWithHmac(byte[] encryptedPayloadWithHmac, SecretKey secretKey) throws CryptoException { + byte[] payloadWithHmac = decrypt(encryptedPayloadWithHmac, secretKey); + String payloadWithHmacAsHex = Hex.encode(payloadWithHmac); + // first part is raw message + int length = payloadWithHmacAsHex.length(); + int sep = length - 64; + String payloadAsHex = payloadWithHmacAsHex.substring(0, sep); + // last 64 bytes is hmac + String hmacAsHex = payloadWithHmacAsHex.substring(sep, length); + if (verifyHmac(Hex.decode(payloadAsHex), Hex.decode(hmacAsHex), secretKey)) { + return Hex.decode(payloadAsHex); + } else { + throw new CryptoException("Hmac does not match."); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Asymmetric + /////////////////////////////////////////////////////////////////////////////////////////// + + public static byte[] encryptSecretKey(SecretKey secretKey, PublicKey publicKey) throws CryptoException { + try { + Cipher cipher = Cipher.getInstance(ASYM_CIPHER); + OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", + MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); + cipher.init(Cipher.WRAP_MODE, publicKey, oaepParameterSpec); + return cipher.wrap(secretKey); + } catch (Throwable e) { + log.error("Couldn't encrypt payload", e); + throw new CryptoException("Couldn't encrypt payload"); + } + } + + public static SecretKey decryptSecretKey(byte[] encryptedSecretKey, PrivateKey privateKey) throws CryptoException { + try { + Cipher cipher = Cipher.getInstance(ASYM_CIPHER); + OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", + MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); + cipher.init(Cipher.UNWRAP_MODE, privateKey, oaepParameterSpec); + return (SecretKey) cipher.unwrap(encryptedSecretKey, "AES", Cipher.SECRET_KEY); + } catch (Throwable e) { + // errors when trying to decrypt foreign network_messages are normal + throw new CryptoException(e); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Hybrid with signature of asymmetric key + /////////////////////////////////////////////////////////////////////////////////////////// + + public static SecretKey generateSecretKey(int bits) { + try { + KeyGenerator keyPairGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO); + keyPairGenerator.init(bits); + return keyPairGenerator.generateKey(); + } catch (Throwable e) { + log.error("Couldn't generate key", e); + throw new RuntimeException("Couldn't generate key"); + } + } + + public static byte[] getPublicKeyBytes(PublicKey encryptionPubKey) { + return new X509EncodedKeySpec(encryptionPubKey.getEncoded()).getEncoded(); + } + + /** + * @param encryptionPubKeyBytes + * @return + */ + public static PublicKey getPublicKeyFromBytes(byte[] encryptionPubKeyBytes) { + try { + return KeyFactory.getInstance(Encryption.ASYM_KEY_ALGO).generatePublic(new X509EncodedKeySpec(encryptionPubKeyBytes)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(encryptionPubKeyBytes), e); + throw new KeyConversionException(e); + } + } +} + diff --git a/common/src/main/java/bisq/common/crypto/Hash.java b/common/src/main/java/bisq/common/crypto/Hash.java new file mode 100644 index 0000000000..b859654d2a --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/Hash.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.common.crypto; + +import org.bitcoinj.core.Utils; + +import com.google.common.base.Charsets; + +import org.bouncycastle.crypto.digests.RIPEMD160Digest; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.nio.ByteBuffer; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Hash { + + /** + * @param data Data as byte array + * @return Hash of data + */ + public static byte[] getSha256Hash(byte[] data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(data, 0, data.length); + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + log.error("Could not create MessageDigest for hash. ", e); + throw new RuntimeException(e); + } + } + + /** + * @param message UTF-8 encoded message + * @return Hash of data + */ + public static byte[] getSha256Hash(String message) { + return getSha256Hash(message.getBytes(Charsets.UTF_8)); + } + + /** + * @param data data as Integer + * @return Hash of data + */ + public static byte[] getSha256Hash(Integer data) { + return getSha256Hash(ByteBuffer.allocate(4).putInt(data).array()); + } + + /** + * Calculates RIPEMD160(SHA256(data)). + */ + public static byte[] getSha256Ripemd160hash(byte[] data) { + return Utils.sha256hash160(data); + } + + /** + * Calculates RIPEMD160(data). + */ + public static byte[] getRipemd160hash(byte[] data) { + RIPEMD160Digest digest = new RIPEMD160Digest(); + digest.update(data, 0, data.length); + byte[] out = new byte[20]; + digest.doFinal(out, 0); + return out; + } +} + diff --git a/common/src/main/java/bisq/common/crypto/KeyConversionException.java b/common/src/main/java/bisq/common/crypto/KeyConversionException.java new file mode 100644 index 0000000000..bf02ff5934 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/KeyConversionException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common.crypto; + +public class KeyConversionException extends RuntimeException { + public KeyConversionException(Throwable cause) { + super(cause); + } + + public KeyConversionException(String msg) { + super(msg); + } +} diff --git a/common/src/main/java/bisq/common/crypto/KeyRing.java b/common/src/main/java/bisq/common/crypto/KeyRing.java new file mode 100644 index 0000000000..07b2cbf9c8 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/KeyRing.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.common.crypto; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.security.KeyPair; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@EqualsAndHashCode +@Slf4j +@Singleton +public final class KeyRing { + private final KeyPair signatureKeyPair; + private final KeyPair encryptionKeyPair; + private final PubKeyRing pubKeyRing; + + @Inject + public KeyRing(KeyStorage keyStorage) { + if (keyStorage.allKeyFilesExist()) { + signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE); + encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION); + } else { + // First time we create key pairs + signatureKeyPair = Sig.generateKeyPair(); + encryptionKeyPair = Encryption.generateKeyPair(); + keyStorage.saveKeyRing(this); + } + pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic()); + } + + // Don't print keys for security reasons + @Override + public String toString() { + return "KeyRing{" + + "signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() + + ", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() + + ", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() + + '}'; + } +} diff --git a/common/src/main/java/bisq/common/crypto/KeyStorage.java b/common/src/main/java/bisq/common/crypto/KeyStorage.java new file mode 100644 index 0000000000..7b4ecece08 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/KeyStorage.java @@ -0,0 +1,171 @@ +/* + * 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 bisq.common.crypto; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; + +import com.google.inject.Inject; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import java.math.BigInteger; + +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +import static bisq.common.util.Preconditions.checkDir; + +@Singleton +public class KeyStorage { + private static final Logger log = LoggerFactory.getLogger(KeyStorage.class); + + public enum KeyEntry { + MSG_SIGNATURE("sig", Sig.KEY_ALGO), + MSG_ENCRYPTION("enc", Encryption.ASYM_KEY_ALGO); + + private final String fileName; + private final String algorithm; + + KeyEntry(String fileName, String algorithm) { + this.fileName = fileName; + this.algorithm = algorithm; + } + + public String getFileName() { + return fileName; + } + + public String getAlgorithm() { + return algorithm; + } + + @NotNull + @Override + public String toString() { + return "Key{" + + "fileName='" + fileName + '\'' + + ", algorithm='" + algorithm + '\'' + + '}'; + } + } + + private final File storageDir; + + @Inject + public KeyStorage(@Named(Config.KEY_STORAGE_DIR) File storageDir) { + this.storageDir = checkDir(storageDir); + } + + public boolean allKeyFilesExist() { + return fileExists(KeyEntry.MSG_SIGNATURE) && fileExists(KeyEntry.MSG_ENCRYPTION); + } + + private boolean fileExists(KeyEntry keyEntry) { + return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists(); + } + + public KeyPair loadKeyPair(KeyEntry keyEntry) { + FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20); + // long now = System.currentTimeMillis(); + try { + KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm()); + PublicKey publicKey; + PrivateKey privateKey; + + File filePrivateKey = new File(storageDir + "/" + keyEntry.getFileName() + ".key"); + try (FileInputStream fis = new FileInputStream(filePrivateKey.getPath())) { + byte[] encodedPrivateKey = new byte[(int) filePrivateKey.length()]; + //noinspection ResultOfMethodCallIgnored + fis.read(encodedPrivateKey); + + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); + privateKey = keyFactory.generatePrivate(privateKeySpec); + } catch (InvalidKeySpecException | IOException e) { + log.error("Could not load key " + keyEntry.toString(), e.getMessage()); + throw new RuntimeException("Could not load key " + keyEntry.toString(), e); + } + + if (privateKey instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey; + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent()); + publicKey = keyFactory.generatePublic(publicKeySpec); + } else if (privateKey instanceof DSAPrivateKey) { + DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) privateKey; + DSAParams dsaParams = dsaPrivateKey.getParams(); + BigInteger p = dsaParams.getP(); + BigInteger q = dsaParams.getQ(); + BigInteger g = dsaParams.getG(); + BigInteger y = g.modPow(dsaPrivateKey.getX(), p); + KeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g); + publicKey = keyFactory.generatePublic(publicKeySpec); + } else { + throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm()); + } + + log.debug("load completed in {} msec", System.currentTimeMillis() - new Date().getTime()); + return new KeyPair(publicKey, privateKey); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + log.error("Could not load key " + keyEntry.toString(), e); + throw new RuntimeException("Could not load key " + keyEntry.toString(), e); + } + } + + public void saveKeyRing(KeyRing keyRing) { + savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName()); + savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName()); + } + + private void savePrivateKey(PrivateKey privateKey, String name) { + if (!storageDir.exists()) + //noinspection ResultOfMethodCallIgnored + storageDir.mkdir(); + + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); + try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) { + fos.write(pkcs8EncodedKeySpec.getEncoded()); + } catch (IOException e) { + log.error("Could not save key " + name, e); + throw new RuntimeException("Could not save key " + name, e); + } + } +} diff --git a/common/src/main/java/bisq/common/crypto/PubKeyRing.java b/common/src/main/java/bisq/common/crypto/PubKeyRing.java new file mode 100644 index 0000000000..0cfd5bfaac --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/PubKeyRing.java @@ -0,0 +1,89 @@ +/* + * 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 bisq.common.crypto; + +import bisq.common.consensus.UsedForTradeContractJson; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import com.google.common.annotations.VisibleForTesting; + +import java.security.PublicKey; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Same as KeyRing but with public keys only. + * Used to send public keys over the wire to other peer. + */ +@Slf4j +@EqualsAndHashCode +@Getter +public final class PubKeyRing implements NetworkPayload, UsedForTradeContractJson { + private final byte[] signaturePubKeyBytes; + private final byte[] encryptionPubKeyBytes; + + private transient PublicKey signaturePubKey; + private transient PublicKey encryptionPubKey; + + public PubKeyRing(PublicKey signaturePubKey, PublicKey encryptionPubKey) { + this.signaturePubKeyBytes = Sig.getPublicKeyBytes(signaturePubKey); + this.encryptionPubKeyBytes = Encryption.getPublicKeyBytes(encryptionPubKey); + this.signaturePubKey = signaturePubKey; + this.encryptionPubKey = encryptionPubKey; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + public PubKeyRing(byte[] signaturePubKeyBytes, byte[] encryptionPubKeyBytes) { + this.signaturePubKeyBytes = signaturePubKeyBytes; + this.encryptionPubKeyBytes = encryptionPubKeyBytes; + signaturePubKey = Sig.getPublicKeyFromBytes(signaturePubKeyBytes); + encryptionPubKey = Encryption.getPublicKeyFromBytes(encryptionPubKeyBytes); + } + + @Override + public protobuf.PubKeyRing toProtoMessage() { + return protobuf.PubKeyRing.newBuilder() + .setSignaturePubKeyBytes(ByteString.copyFrom(signaturePubKeyBytes)) + .setEncryptionPubKeyBytes(ByteString.copyFrom(encryptionPubKeyBytes)) + .build(); + } + + public static PubKeyRing fromProto(protobuf.PubKeyRing proto) { + return new PubKeyRing( + proto.getSignaturePubKeyBytes().toByteArray(), + proto.getEncryptionPubKeyBytes().toByteArray()); + } + + @Override + public String toString() { + return "PubKeyRing{" + + "signaturePubKeyHex=" + Utilities.bytesAsHexString(signaturePubKeyBytes) + + ", encryptionPubKeyHex=" + Utilities.bytesAsHexString(encryptionPubKeyBytes) + + "}"; + } +} diff --git a/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java b/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java new file mode 100644 index 0000000000..534ea69a5b --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java @@ -0,0 +1,19 @@ +package bisq.common.crypto; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class PubKeyRingProvider implements Provider { + + private final PubKeyRing pubKeyRing; + + @Inject + public PubKeyRingProvider(KeyRing keyRing) { + pubKeyRing = keyRing.getPubKeyRing(); + } + + @Override + public PubKeyRing get() { + return pubKeyRing; + } +} diff --git a/common/src/main/java/bisq/common/crypto/SealedAndSigned.java b/common/src/main/java/bisq/common/crypto/SealedAndSigned.java new file mode 100644 index 0000000000..c248064f4a --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/SealedAndSigned.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.common.crypto; + +import bisq.common.proto.network.NetworkPayload; + +import com.google.protobuf.ByteString; + +import java.security.PublicKey; + +import lombok.Value; + +@Value +public final class SealedAndSigned implements NetworkPayload { + private final byte[] encryptedSecretKey; + private final byte[] encryptedPayloadWithHmac; + private final byte[] signature; + private final byte[] sigPublicKeyBytes; + transient private final PublicKey sigPublicKey; + + public SealedAndSigned(byte[] encryptedSecretKey, + byte[] encryptedPayloadWithHmac, + byte[] signature, + PublicKey sigPublicKey) { + this.encryptedSecretKey = encryptedSecretKey; + this.encryptedPayloadWithHmac = encryptedPayloadWithHmac; + this.signature = signature; + this.sigPublicKey = sigPublicKey; + + sigPublicKeyBytes = Sig.getPublicKeyBytes(sigPublicKey); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SealedAndSigned(byte[] encryptedSecretKey, + byte[] encryptedPayloadWithHmac, + byte[] signature, + byte[] sigPublicKeyBytes) { + this.encryptedSecretKey = encryptedSecretKey; + this.encryptedPayloadWithHmac = encryptedPayloadWithHmac; + this.signature = signature; + this.sigPublicKeyBytes = sigPublicKeyBytes; + + sigPublicKey = Sig.getPublicKeyFromBytes(sigPublicKeyBytes); + } + + public protobuf.SealedAndSigned toProtoMessage() { + return protobuf.SealedAndSigned.newBuilder() + .setEncryptedSecretKey(ByteString.copyFrom(encryptedSecretKey)) + .setEncryptedPayloadWithHmac(ByteString.copyFrom(encryptedPayloadWithHmac)) + .setSignature(ByteString.copyFrom(signature)) + .setSigPublicKeyBytes(ByteString.copyFrom(sigPublicKeyBytes)) + .build(); + } + + public static SealedAndSigned fromProto(protobuf.SealedAndSigned proto) { + return new SealedAndSigned(proto.getEncryptedSecretKey().toByteArray(), + proto.getEncryptedPayloadWithHmac().toByteArray(), + proto.getSignature().toByteArray(), + proto.getSigPublicKeyBytes().toByteArray()); + } +} diff --git a/common/src/main/java/bisq/common/crypto/Sig.java b/common/src/main/java/bisq/common/crypto/Sig.java new file mode 100644 index 0000000000..c910025786 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/Sig.java @@ -0,0 +1,141 @@ +/* + * 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 bisq.common.crypto; + +import bisq.common.util.Base64; +import bisq.common.util.Utilities; + +import com.google.common.base.Charsets; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * StorageSignatureKeyPair/STORAGE_SIGN_KEY_ALGO: That is used for signing the data to be stored to the P2P network (by flooding). + * The algo is selected because it originated from the TomP2P version which used DSA. + * Changing to EC keys might be considered. + *

    + * MsgSignatureKeyPair/MSG_SIGN_KEY_ALGO/MSG_SIGN_ALGO: That is used when sending a message to a peer which is encrypted and signed. + * Changing to EC keys might be considered. + */ +public class Sig { + private static final Logger log = LoggerFactory.getLogger(Sig.class); + + public static final String KEY_ALGO = "DSA"; + private static final String ALGO = "SHA256withDSA"; + + + /** + * @return keyPair + */ + public static KeyPair generateKeyPair() { + long ts = System.currentTimeMillis(); + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGO); + keyPairGenerator.initialize(1024); + return keyPairGenerator.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + log.error("Could not create key.", e); + throw new RuntimeException("Could not create key."); + } + } + + + /** + * @param privateKey + * @param data + * @return + */ + public static byte[] sign(PrivateKey privateKey, byte[] data) throws CryptoException { + try { + Signature sig = Signature.getInstance(ALGO); + sig.initSign(privateKey); + sig.update(data); + return sig.sign(); + } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoException("Signing failed. " + e.getMessage()); + } + } + + /** + * @param privateKey + * @param message UTF-8 encoded message to sign + * @return Base64 encoded signature + */ + public static String sign(PrivateKey privateKey, String message) throws CryptoException { + byte[] sigAsBytes = sign(privateKey, message.getBytes(Charsets.UTF_8)); + return Base64.encode(sigAsBytes); + } + + /** + * @param publicKey + * @param data + * @param signature + * @return + */ + public static boolean verify(PublicKey publicKey, byte[] data, byte[] signature) throws CryptoException { + try { + Signature sig = Signature.getInstance(ALGO); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoException("Signature verification failed", e); + } + } + + /** + * @param publicKey + * @param message UTF-8 encoded message + * @param signature Base64 encoded signature + * @return + */ + public static boolean verify(PublicKey publicKey, String message, String signature) throws CryptoException { + return verify(publicKey, message.getBytes(Charsets.UTF_8), Base64.decode(signature)); + } + + /** + * @param sigPublicKeyBytes + * @return + */ + public static PublicKey getPublicKeyFromBytes(byte[] sigPublicKeyBytes) { + try { + return KeyFactory.getInstance(Sig.KEY_ALGO).generatePublic(new X509EncodedKeySpec(sigPublicKeyBytes)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(sigPublicKeyBytes), e); + e.printStackTrace(); + throw new KeyConversionException(e); + } + } + + public static byte[] getPublicKeyBytes(PublicKey sigPublicKey) { + return new X509EncodedKeySpec(sigPublicKey.getEncoded()).getEncoded(); + } +} diff --git a/common/src/main/java/bisq/common/file/CorruptedStorageFileHandler.java b/common/src/main/java/bisq/common/file/CorruptedStorageFileHandler.java new file mode 100644 index 0000000000..1a1c7d623e --- /dev/null +++ b/common/src/main/java/bisq/common/file/CorruptedStorageFileHandler.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.common.file; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class CorruptedStorageFileHandler { + private final List files = new ArrayList<>(); + + @Inject + public CorruptedStorageFileHandler() { + } + + public void addFile(String fileName) { + files.add(fileName); + } + + public Optional> getFiles() { + if (files.isEmpty()) { + return Optional.empty(); + } + + if (files.size() == 1 && files.get(0).equals("ViewPathAsString")) { + log.debug("We detected incompatible data base file for Navigation. " + + "That is a minor issue happening with refactoring of UI classes " + + "and we don't display a warning popup to the user."); + return Optional.empty(); + } + + return Optional.of(files); + } +} diff --git a/common/src/main/java/bisq/common/file/FileUtil.java b/common/src/main/java/bisq/common/file/FileUtil.java new file mode 100644 index 0000000000..f69c2df88d --- /dev/null +++ b/common/src/main/java/bisq/common/file/FileUtil.java @@ -0,0 +1,224 @@ +/* + * 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 bisq.common.file; + +import bisq.common.util.Utilities; + +import com.google.common.io.Files; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class FileUtil { + public static void rollingBackup(File dir, String fileName, int numMaxBackupFiles) { + if (dir.exists()) { + File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString()); + if (!backupDir.exists()) + if (!backupDir.mkdir()) + log.warn("make dir failed.\nBackupDir=" + backupDir.getAbsolutePath()); + + File origFile = new File(Paths.get(dir.getAbsolutePath(), fileName).toString()); + if (origFile.exists()) { + String dirName = "backups_" + fileName; + if (dirName.contains(".")) + dirName = dirName.replace(".", "_"); + File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); + if (!backupFileDir.exists()) + if (!backupFileDir.mkdir()) + log.warn("make backupFileDir failed.\nBackupFileDir=" + backupFileDir.getAbsolutePath()); + + File backupFile = new File(Paths.get(backupFileDir.getAbsolutePath(), new Date().getTime() + "_" + fileName).toString()); + + try { + Files.copy(origFile, backupFile); + + pruneBackup(backupFileDir, numMaxBackupFiles); + } catch (IOException e) { + log.error("Backup key failed: " + e.getMessage()); + e.printStackTrace(); + } + } + } + } + + private static void pruneBackup(File backupDir, int numMaxBackupFiles) { + if (backupDir.isDirectory()) { + File[] files = backupDir.listFiles(); + if (files != null) { + List filesList = Arrays.asList(files); + if (filesList.size() > numMaxBackupFiles) { + filesList.sort(Comparator.comparing(File::getName)); + File file = filesList.get(0); + if (file.isFile()) { + if (!file.delete()) + log.error("Failed to delete file: " + file); + else + pruneBackup(backupDir, numMaxBackupFiles); + + } else { + pruneBackup(new File(Paths.get(backupDir.getAbsolutePath(), file.getName()).toString()), numMaxBackupFiles); + } + } + } + } + } + + public static void deleteDirectory(File file) throws IOException { + deleteDirectory(file, null, true); + } + + public static void deleteDirectory(File file, + @Nullable File exclude, + boolean ignoreLockedFiles) throws IOException { + boolean excludeFileFound = false; + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) + for (File f : files) { + boolean excludeFileFoundLocal = exclude != null && f.getAbsolutePath().equals(exclude.getAbsolutePath()); + excludeFileFound |= excludeFileFoundLocal; + if (!excludeFileFoundLocal) + deleteDirectory(f, exclude, ignoreLockedFiles); + } + } + // Finally delete main file/dir if exclude file was not found in directory + if (!excludeFileFound && !(exclude != null && file.getAbsolutePath().equals(exclude.getAbsolutePath()))) { + try { + deleteFileIfExists(file, ignoreLockedFiles); + } catch (Throwable t) { + log.error("Could not delete file. Error=" + t.toString()); + throw new IOException(t); + } + } + } + + public static void deleteFileIfExists(File file) throws IOException { + deleteFileIfExists(file, true); + } + + public static void deleteFileIfExists(File file, boolean ignoreLockedFiles) throws IOException { + try { + if (Utilities.isWindows()) + file = file.getCanonicalFile(); + + if (file.exists() && !file.delete()) { + if (ignoreLockedFiles) { + // We check if file is locked. On Windows all open files are locked by the OS, so we + if (isFileLocked(file)) + log.info("Failed to delete locked file: " + file.getAbsolutePath()); + } else { + final String message = "Failed to delete file: " + file.getAbsolutePath(); + log.error(message); + throw new IOException(message); + } + } + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + throw new IOException(t); + } + } + + private static boolean isFileLocked(File file) { + return !file.canWrite(); + } + + public static void resourceToFile(String resourcePath, + File destinationFile) throws ResourceNotFoundException, IOException { + try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) { + if (inputStream == null) { + throw new ResourceNotFoundException(resourcePath); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(destinationFile)) { + IOUtils.copy(inputStream, fileOutputStream); + } + } + } + + public static void renameFile(File oldFile, File newFile) throws IOException { + if (Utilities.isWindows()) { + // Work around an issue on Windows whereby you can't rename over existing files. + final File canonical = newFile.getCanonicalFile(); + if (canonical.exists() && !canonical.delete()) { + throw new IOException("Failed to delete canonical file for replacement with save"); + } + if (!oldFile.renameTo(canonical)) { + throw new IOException("Failed to rename " + oldFile + " to " + canonical); + } + } else if (!oldFile.renameTo(newFile)) { + throw new IOException("Failed to rename " + oldFile + " to " + newFile); + } + } + + public static void copyFile(File origin, File target) throws IOException { + if (!origin.exists()) { + return; + } + + try { + Files.copy(origin, target); + } catch (IOException e) { + log.error("Copy file failed", e); + throw new IOException("Failed to copy " + origin + " to " + target); + } + + } + + public static void copyDirectory(File source, File destination) throws IOException { + FileUtils.copyDirectory(source, destination); + } + + public static File createNewFile(Path path) throws IOException { + File file = path.toFile(); + if (!file.createNewFile()) { + throw new IOException("There already exists a file with path: " + path); + } + return file; + } + + public static void removeAndBackupFile(File dbDir, File storageFile, String fileName, String backupFolderName) + throws IOException { + File corruptedBackupDir = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName).toString()); + if (!corruptedBackupDir.exists() && !corruptedBackupDir.mkdir()) { + log.warn("make dir failed"); + } + + File corruptedFile = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName, fileName).toString()); + if (storageFile.exists()) { + renameFile(storageFile, corruptedFile); + } + } +} diff --git a/common/src/main/java/bisq/common/file/JsonFileManager.java b/common/src/main/java/bisq/common/file/JsonFileManager.java new file mode 100644 index 0000000000..4a10f508e3 --- /dev/null +++ b/common/src/main/java/bisq/common/file/JsonFileManager.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.common.file; + +import bisq.common.util.Utilities; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.PrintWriter; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Slf4j +public class JsonFileManager { + private final static List INSTANCES = new ArrayList<>(); + + public static void shutDownAllInstances() { + INSTANCES.forEach(JsonFileManager::shutDown); + } + + + @Nullable + private ThreadPoolExecutor executor; + private final File dir; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public JsonFileManager(File dir) { + this.dir = dir; + + if (!dir.exists() && !dir.mkdir()) { + log.warn("make dir failed"); + } + + INSTANCES.add(this); + } + + @NotNull + protected ThreadPoolExecutor getExecutor() { + if (executor == null) { + executor = Utilities.getThreadPoolExecutor("JsonFileManagerExecutor", 5, 50, 60); + } + return executor; + } + + public void shutDown() { + if (executor != null) { + executor.shutdown(); + } + } + + public void writeToDiscThreaded(String json, String fileName) { + getExecutor().execute(() -> writeToDisc(json, fileName)); + } + + public void writeToDisc(String json, String fileName) { + File jsonFile = new File(Paths.get(dir.getAbsolutePath(), fileName + ".json").toString()); + File tempFile = null; + PrintWriter printWriter = null; + try { + tempFile = File.createTempFile("temp", null, dir); + tempFile.deleteOnExit(); + + printWriter = new PrintWriter(tempFile); + printWriter.println(json); + + // This close call and comment is borrowed from FileManager. Not 100% sure it that is really needed but + // seems that had fixed in the past and we got reported issues on Windows so that fix might be still + // required. + // Close resources before replacing file with temp file because otherwise it causes problems on windows + // when rename temp file + printWriter.close(); + + FileUtil.renameFile(tempFile, jsonFile); + } catch (Throwable t) { + log.error("storageFile " + jsonFile.toString()); + t.printStackTrace(); + } finally { + if (tempFile != null && tempFile.exists()) { + log.warn("Temp file still exists after failed save. We will delete it now. storageFile=" + fileName); + if (!tempFile.delete()) + log.error("Cannot delete temp file."); + } + + if (printWriter != null) + printWriter.close(); + } + } +} diff --git a/common/src/main/java/bisq/common/file/ResourceNotFoundException.java b/common/src/main/java/bisq/common/file/ResourceNotFoundException.java new file mode 100644 index 0000000000..fbe7797ff9 --- /dev/null +++ b/common/src/main/java/bisq/common/file/ResourceNotFoundException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common.file; + +public class ResourceNotFoundException extends Exception { + public ResourceNotFoundException(String path) { + super("Resource not found: path = " + path); + } +} diff --git a/common/src/main/java/bisq/common/handlers/ErrorMessageHandler.java b/common/src/main/java/bisq/common/handlers/ErrorMessageHandler.java new file mode 100644 index 0000000000..4fd32a7993 --- /dev/null +++ b/common/src/main/java/bisq/common/handlers/ErrorMessageHandler.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.common.handlers; + +/** + * For reporting error message only (UI) + */ +public interface ErrorMessageHandler { + void handleErrorMessage(String errorMessage); +} diff --git a/common/src/main/java/bisq/common/handlers/ExceptionHandler.java b/common/src/main/java/bisq/common/handlers/ExceptionHandler.java new file mode 100644 index 0000000000..a8147e969d --- /dev/null +++ b/common/src/main/java/bisq/common/handlers/ExceptionHandler.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.common.handlers; + +/** + * For reporting throwable objects only + */ +public interface ExceptionHandler { + void handleException(Throwable throwable); +} diff --git a/common/src/main/java/bisq/common/handlers/FaultHandler.java b/common/src/main/java/bisq/common/handlers/FaultHandler.java new file mode 100644 index 0000000000..bdc4a9192d --- /dev/null +++ b/common/src/main/java/bisq/common/handlers/FaultHandler.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.common.handlers; + +/** + * For reporting a description message and throwable + */ +public interface FaultHandler { + void handleFault(String errorMessage, Throwable throwable); +} diff --git a/common/src/main/java/bisq/common/handlers/ResultHandler.java b/common/src/main/java/bisq/common/handlers/ResultHandler.java new file mode 100644 index 0000000000..d9fa12609c --- /dev/null +++ b/common/src/main/java/bisq/common/handlers/ResultHandler.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.common.handlers; + +public interface ResultHandler { + void handleResult(); +} diff --git a/common/src/main/java/bisq/common/persistence/PersistenceManager.java b/common/src/main/java/bisq/common/persistence/PersistenceManager.java new file mode 100644 index 0000000000..04b5a95ac2 --- /dev/null +++ b/common/src/main/java/bisq/common/persistence/PersistenceManager.java @@ -0,0 +1,522 @@ +/* + * 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 bisq.common.persistence; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.file.FileUtil; +import bisq.common.handlers.ResultHandler; +import bisq.common.proto.persistable.PersistableEnvelope; +import bisq.common.proto.persistable.PersistenceProtoResolver; +import bisq.common.util.Utilities; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import java.nio.file.Path; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.common.util.Preconditions.checkDir; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Responsible for reading persisted data and writing it on disk. We read usually only at start-up and keep data in RAM. + * We write all data which got a request for persistence at shut down at the very last moment when all other services + * are shut down, so allowing changes to the data in the very last moment. For critical data we set {@link Source} + * to HIGH which causes a timer to trigger a write to disk after 1 minute. We use that for not very frequently altered + * data and data which cannot be recovered from the network. + * + * We decided to not use threading (as it was in previous versions) as the read operation happens only at start-up and + * with the modified model that data is written at shut down we eliminate frequent and expensive disk I/O. Risks of + * deadlock or data inconsistency and a more complex model have been a further argument for that model. In fact + * previously we wasted a lot of resources as way too many threads have been created without doing actual work as well + * the write operations got triggered way too often specially for the very frequent changes at SequenceNumberMap and + * the very large DaoState (at dao blockchain sync that slowed down sync). + * + * + * @param The type of the {@link PersistableEnvelope} to be written or read from disk + */ +@Slf4j +public class PersistenceManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + public static final Map> ALL_PERSISTENCE_MANAGERS = new HashMap<>(); + private static boolean flushAtShutdownCalled; + private static final AtomicBoolean allServicesInitialized = new AtomicBoolean(false); + + public static void onAllServicesInitialized() { + allServicesInitialized.set(true); + + ALL_PERSISTENCE_MANAGERS.values().forEach(persistenceManager -> { + // In case we got a requestPersistence call before we got initialized we trigger the timer for the + // persist call + if (persistenceManager.persistenceRequested) { + persistenceManager.maybeStartTimerForPersistence(); + } + }); + } + + public static void flushAllDataToDiskAtBackup(ResultHandler completeHandler) { + flushAllDataToDisk(completeHandler, false); + } + + public static void flushAllDataToDiskAtShutdown(ResultHandler completeHandler) { + flushAllDataToDisk(completeHandler, true); + } + + // We require being called only once from the global shutdown routine. As the shutdown routine has a timeout + // and error condition where we call the method as well beside the standard path and it could be that those + // alternative code paths call our method after it was called already, so it is a valid but rare case. + // We add a guard to prevent repeated calls. + private static void flushAllDataToDisk(ResultHandler completeHandler, boolean doShutdown) { + if (!allServicesInitialized.get()) { + log.warn("Application has not completed start up yet so we do not flush data to disk."); + completeHandler.handleResult(); + return; + } + + + // We don't know from which thread we are called so we map to user thread + UserThread.execute(() -> { + if (doShutdown) { + if (flushAtShutdownCalled) { + log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call."); + return; + } + + flushAtShutdownCalled = true; + } + + log.info("Start flushAllDataToDisk"); + AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size()); + + if (openInstances.get() == 0) { + log.info("No PersistenceManager instances have been created yet."); + completeHandler.handleResult(); + } + + new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> { + // For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed + // a requestPersistence call after an important state update. Those are usually rather small data stores. + // Otherwise we only persist if requestPersistence was called since the last persist call. + // We also check if we have called read already to avoid a very early write attempt before we have ever + // read the data, which would lead to a write of empty data + // (fixes https://github.com/bisq-network/bisq/issues/4844). + if (persistenceManager.readCalled.get() && + (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested)) { + // We always get our completeHandler called even if exceptions happen. In case a file write fails + // we still call our shutdown and count down routine as the completeHandler is triggered in any case. + + // We get our result handler called from the write thread so we map back to user thread. + persistenceManager.persistNow(() -> + UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown))); + } else { + onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown); + } + }); + }); + } + + // We get called always from user thread here. + private static void onWriteCompleted(ResultHandler completeHandler, + AtomicInteger openInstances, + PersistenceManager persistenceManager, + boolean doShutdown) { + if (doShutdown) { + persistenceManager.shutdown(); + } + + if (openInstances.decrementAndGet() == 0) { + log.info("flushAllDataToDisk completed"); + completeHandler.handleResult(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Enum + /////////////////////////////////////////////////////////////////////////////////////////// + + public enum Source { + // For data stores we received from the network and which could be rebuilt. We store only for avoiding too much network traffic. + NETWORK(1, TimeUnit.MINUTES.toMillis(5), false), + + // For data stores which are created from private local data. This data could only be rebuilt from backup files. + PRIVATE(10, 200, true), + + // For data stores which are created from private local data. Loss of that data would not have critical consequences. + PRIVATE_LOW_PRIO(4, TimeUnit.MINUTES.toMillis(1), false); + + + @Getter + private final int numMaxBackupFiles; + @Getter + private final long delay; + @Getter + private final boolean flushAtShutDown; + + Source(int numMaxBackupFiles, long delay, boolean flushAtShutDown) { + this.numMaxBackupFiles = numMaxBackupFiles; + this.delay = delay; + this.flushAtShutDown = flushAtShutDown; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final File dir; + private final PersistenceProtoResolver persistenceProtoResolver; + private final CorruptedStorageFileHandler corruptedStorageFileHandler; + private File storageFile; + private T persistable; + private String fileName; + private Source source = Source.PRIVATE_LOW_PRIO; + private Path usedTempFilePath; + private volatile boolean persistenceRequested; + @Nullable + private Timer timer; + private ExecutorService writeToDiskExecutor; + public final AtomicBoolean initCalled = new AtomicBoolean(false); + public final AtomicBoolean readCalled = new AtomicBoolean(false); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PersistenceManager(@Named(Config.STORAGE_DIR) File dir, + PersistenceProtoResolver persistenceProtoResolver, + CorruptedStorageFileHandler corruptedStorageFileHandler) { + this.dir = checkDir(dir); + this.persistenceProtoResolver = persistenceProtoResolver; + this.corruptedStorageFileHandler = corruptedStorageFileHandler; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void initialize(T persistable, Source source) { + this.initialize(persistable, persistable.getDefaultStorageFileName(), source); + } + + public void initialize(T persistable, String fileName, Source source) { + if (flushAtShutdownCalled) { + log.warn("We have started the shut down routine already. We ignore that initialize call."); + return; + } + + if (ALL_PERSISTENCE_MANAGERS.containsKey(fileName)) { + RuntimeException runtimeException = new RuntimeException("We must not create multiple " + + "PersistenceManager instances for file " + fileName + "."); + // We want to get logged from where we have been called so lets print the stack trace. + runtimeException.printStackTrace(); + throw runtimeException; + } + + if (initCalled.get()) { + RuntimeException runtimeException = new RuntimeException("We must not call initialize multiple times. " + + "PersistenceManager for file: " + fileName + "."); + // We want to get logged from where we have been called so lets print the stack trace. + runtimeException.printStackTrace(); + throw runtimeException; + } + + initCalled.set(true); + + this.persistable = persistable; + this.fileName = fileName; + this.source = source; + storageFile = new File(dir, fileName); + ALL_PERSISTENCE_MANAGERS.put(fileName, this); + } + + public void shutdown() { + ALL_PERSISTENCE_MANAGERS.remove(fileName); + + if (timer != null) { + timer.stop(); + } + + if (writeToDiskExecutor != null) { + writeToDiskExecutor.shutdown(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Reading file + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Read persisted file in a thread. + * + * @param resultHandler Consumer of persisted data once it was read from disk. + * @param orElse Called if no file exists or reading of file failed. + */ + public void readPersisted(Consumer resultHandler, Runnable orElse) { + readPersisted(checkNotNull(fileName), resultHandler, orElse); + } + + /** + * Read persisted file in a thread. + * We map result handler calls to UserThread, so clients don't need to worry about threading + * + * @param fileName File name of our persisted data. + * @param resultHandler Consumer of persisted data once it was read from disk. + * @param orElse Called if no file exists or reading of file failed. + */ + public void readPersisted(String fileName, Consumer resultHandler, Runnable orElse) { + if (flushAtShutdownCalled) { + log.warn("We have started the shut down routine already. We ignore that readPersisted call."); + return; + } + + new Thread(() -> { + T persisted = getPersisted(fileName); + if (persisted != null) { + UserThread.execute(() -> resultHandler.accept(persisted)); + } else { + UserThread.execute(orElse); + } + }, "PersistenceManager-read-" + fileName).start(); + } + + // API for synchronous reading of data. Not recommended to be used in application code. + // Currently used by tests and monitor. Should be converted to the threaded API as well. + @Nullable + public T getPersisted() { + return getPersisted(checkNotNull(fileName)); + } + + @Nullable + public T getPersisted(String fileName) { + if (flushAtShutdownCalled) { + log.warn("We have started the shut down routine already. We ignore that getPersisted call."); + return null; + } + + readCalled.set(true); + + File storageFile = new File(dir, fileName); + if (!storageFile.exists()) { + return null; + } + + long ts = System.currentTimeMillis(); + try (FileInputStream fileInputStream = new FileInputStream(storageFile)) { + protobuf.PersistableEnvelope proto = protobuf.PersistableEnvelope.parseDelimitedFrom(fileInputStream); + //noinspection unchecked + T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto); + log.info("Reading {} completed in {} ms", fileName, System.currentTimeMillis() - ts); + return persistableEnvelope; + } catch (Throwable t) { + log.error("Reading {} failed with {}.", fileName, t.getMessage()); + try { + // We keep a backup which might be used for recovery + FileUtil.removeAndBackupFile(dir, storageFile, fileName, "backup_of_corrupted_data"); + DevEnv.logErrorAndThrowIfDevMode(t.toString()); + } catch (IOException e1) { + e1.printStackTrace(); + log.error(e1.getMessage()); + // We swallow Exception if backup fails + } + if (corruptedStorageFileHandler != null) { + corruptedStorageFileHandler.addFile(storageFile.getName()); + } + } + return null; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Write file to disk + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestPersistence() { + if (flushAtShutdownCalled) { + log.warn("We have started the shut down routine already. We ignore that requestPersistence call."); + return; + } + + persistenceRequested = true; + + // If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at + // onAllServicesInitialized + if (!allServicesInitialized.get()) { + return; + } + + maybeStartTimerForPersistence(); + } + + private void maybeStartTimerForPersistence() { + // We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays + // can be rather long. + if (timer == null) { + timer = UserThread.runAfter(() -> { + persistNow(null); + UserThread.execute(() -> timer = null); + }, source.delay, TimeUnit.MILLISECONDS); + } + } + + public void persistNow(@Nullable Runnable completeHandler) { + long ts = System.currentTimeMillis(); + try { + // The serialisation is done on the user thread to avoid threading issue with potential mutations of the + // persistable object. Keeping it on the user thread we are in a synchronize model. + protobuf.PersistableEnvelope serialized = (protobuf.PersistableEnvelope) persistable.toPersistableMessage(); + + // For the write to disk task we use a thread. We do not have any issues anymore if the persistable objects + // gets mutated while the thread is running as we have serialized it already and do not operate on the + // reference to the persistable object. + getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler)); + + long duration = System.currentTimeMillis() - ts; + if (duration > 100) { + log.info("Serializing {} took {} msec", fileName, duration); + } + } catch (Throwable e) { + log.error("Error in saveToFile toProtoMessage: {}, {}", persistable.getClass().getSimpleName(), fileName); + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) { + if (!allServicesInitialized.get()) { + log.warn("Application has not completed start up yet so we do not permit writing data to disk."); + UserThread.execute(completeHandler); + return; + } + + long ts = System.currentTimeMillis(); + File tempFile = null; + FileOutputStream fileOutputStream = null; + + try { + // Before we write we backup existing file + FileUtil.rollingBackup(dir, fileName, source.getNumMaxBackupFiles()); + + if (!dir.exists() && !dir.mkdir()) + log.warn("make dir failed {}", fileName); + + tempFile = usedTempFilePath != null + ? FileUtil.createNewFile(usedTempFilePath) + : File.createTempFile("temp_" + fileName, null, dir); + // Don't use a new temp file path each time, as that causes the delete-on-exit hook to leak memory: + tempFile.deleteOnExit(); + + fileOutputStream = new FileOutputStream(tempFile); + + serialized.writeDelimitedTo(fileOutputStream); + + // Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide + // to not write through to physical media for at least a few seconds, but this is the best we can do. + fileOutputStream.flush(); + fileOutputStream.getFD().sync(); + + // Close resources before replacing file with temp file because otherwise it causes problems on windows + // when rename temp file + fileOutputStream.close(); + + FileUtil.renameFile(tempFile, storageFile); + usedTempFilePath = tempFile.toPath(); + } catch (Throwable t) { + // If an error occurred, don't attempt to reuse this path again, in case temp file cleanup fails. + usedTempFilePath = null; + log.error("Error at saveToFile, storageFile={}", fileName, t); + } finally { + if (tempFile != null && tempFile.exists()) { + log.warn("Temp file still exists after failed save. We will delete it now. storageFile={}", fileName); + if (!tempFile.delete()) { + log.error("Cannot delete temp file."); + } + } + + try { + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException e) { + // We swallow that + e.printStackTrace(); + log.error("Cannot close resources." + e.getMessage()); + } + long duration = System.currentTimeMillis() - ts; + if (duration > 100) { + log.info("Writing the serialized {} completed in {} msec", fileName, duration); + } + persistenceRequested = false; + if (completeHandler != null) { + UserThread.execute(completeHandler); + } + } + } + + private ExecutorService getWriteToDiskExecutor() { + if (writeToDiskExecutor == null) { + String name = "Write-" + fileName + "_to-disk"; + writeToDiskExecutor = Utilities.getSingleThreadExecutor(name); + } + return writeToDiskExecutor; + } + + @Override + public String toString() { + return "PersistenceManager{" + + "\n fileName='" + fileName + '\'' + + ",\n dir=" + dir + + ",\n storageFile=" + storageFile + + ",\n persistable=" + persistable + + ",\n source=" + source + + ",\n usedTempFilePath=" + usedTempFilePath + + ",\n persistenceRequested=" + persistenceRequested + + "\n}"; + } +} diff --git a/common/src/main/java/bisq/common/proto/ProtoResolver.java b/common/src/main/java/bisq/common/proto/ProtoResolver.java new file mode 100644 index 0000000000..679c1b3e5a --- /dev/null +++ b/common/src/main/java/bisq/common/proto/ProtoResolver.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.common.proto; + +import bisq.common.Payload; +import bisq.common.proto.persistable.PersistablePayload; + +public interface ProtoResolver { + Payload fromProto(protobuf.PaymentAccountPayload proto); + + PersistablePayload fromProto(protobuf.PersistableNetworkPayload proto); +} diff --git a/common/src/main/java/bisq/common/proto/ProtoUtil.java b/common/src/main/java/bisq/common/proto/ProtoUtil.java new file mode 100644 index 0000000000..d7450e9d50 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/ProtoUtil.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.common.proto; + +import bisq.common.Proto; +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import com.google.protobuf.ProtocolStringList; + +import com.google.common.base.Enums; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class ProtoUtil { + + public static Set byteSetFromProtoByteStringList(List byteStringList) { + return byteStringList.stream().map(ByteString::toByteArray).collect(Collectors.toSet()); + } + + /** + * Returns the input String, except when it's the empty string: "", then null is returned. + * Note: "" is the default value for a protobuffer string, so this means it's not filled in. + */ + @Nullable + public static String stringOrNullFromProto(String proto) { + return "".equals(proto) ? null : proto; + } + + @Nullable + public static byte[] byteArrayOrNullFromProto(ByteString proto) { + return proto.isEmpty() ? null : proto.toByteArray(); + } + + /** + * Get a Java enum from a Protobuf enum in a safe way. + * + * @param enumType the class of the enum, e.g: BlaEnum.class + * @param name the name of the enum entry, e.g: proto.getWinner().name() + * @param the enum Type + * @return an enum + */ + @Nullable + public static > E enumFromProto(Class enumType, String name) { + String enumName = name != null ? name : "UNDEFINED"; + E result = Enums.getIfPresent(enumType, enumName).orNull(); + if (result == null) { + result = Enums.getIfPresent(enumType, "UNDEFINED").orNull(); + log.debug("We try to lookup for an enum entry with name 'UNDEFINED' and use that if available, " + + "otherwise the enum is null. enum={}", result); + return result; + } + return result; + } + + public static Iterable collectionToProto(Collection collection, + Class messageType) { + return collection.stream() + .map(e -> { + final Message message = e.toProtoMessage(); + try { + return messageType.cast(message); + } catch (ClassCastException t) { + log.error("Message could not be cast. message={}, messageType={}", message, messageType); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public static Iterable collectionToProto(Collection collection, + Function extra) { + return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList()); + } + + public static List protocolStringListToList(ProtocolStringList protocolStringList) { + return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList); + } + + public static Set protocolStringListToSet(ProtocolStringList protocolStringList) { + return CollectionUtils.isEmpty(protocolStringList) ? new HashSet<>() : new HashSet<>(protocolStringList); + } +} diff --git a/common/src/main/java/bisq/common/proto/ProtobufferException.java b/common/src/main/java/bisq/common/proto/ProtobufferException.java new file mode 100644 index 0000000000..a89e865d12 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/ProtobufferException.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.common.proto; + +import java.io.IOException; + +public class ProtobufferException extends IOException { + public ProtobufferException(String message) { + super(message); + } + + public ProtobufferException(String message, Throwable e) { + super(message, e); + } +} diff --git a/common/src/main/java/bisq/common/proto/ProtobufferRuntimeException.java b/common/src/main/java/bisq/common/proto/ProtobufferRuntimeException.java new file mode 100644 index 0000000000..019ca90a7d --- /dev/null +++ b/common/src/main/java/bisq/common/proto/ProtobufferRuntimeException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common.proto; + +public class ProtobufferRuntimeException extends RuntimeException { + public ProtobufferRuntimeException(String message) { + super(message); + } + + public ProtobufferRuntimeException(String message, Throwable e) { + super(message, e); + } +} diff --git a/common/src/main/java/bisq/common/proto/network/NetworkEnvelope.java b/common/src/main/java/bisq/common/proto/network/NetworkEnvelope.java new file mode 100644 index 0000000000..330fbd7216 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/network/NetworkEnvelope.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.common.proto.network; + +import bisq.common.Envelope; + +import com.google.protobuf.Message; + +import lombok.EqualsAndHashCode; + +import static com.google.common.base.Preconditions.checkArgument; + +@EqualsAndHashCode +public abstract class NetworkEnvelope implements Envelope { + + protected final int messageVersion; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected NetworkEnvelope(int messageVersion) { + this.messageVersion = messageVersion; + } + + public protobuf.NetworkEnvelope.Builder getNetworkEnvelopeBuilder() { + return protobuf.NetworkEnvelope.newBuilder().setMessageVersion(messageVersion); + } + + @Override + public Message toProtoMessage() { + return getNetworkEnvelopeBuilder().build(); + } + + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder().build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public int getMessageVersion() { + // -1 is used for the case that we use an envelope message as payload (mailbox) + // so we check only against 0 which is the default value if not set + checkArgument(messageVersion != 0, "messageVersion is not set (0)."); + return messageVersion; + } + + @Override + public String toString() { + return "NetworkEnvelope{" + + "\n messageVersion=" + messageVersion + + "\n}"; + } +} diff --git a/common/src/main/java/bisq/common/proto/network/NetworkPayload.java b/common/src/main/java/bisq/common/proto/network/NetworkPayload.java new file mode 100644 index 0000000000..65169c40d4 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/network/NetworkPayload.java @@ -0,0 +1,26 @@ +/* + * 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 bisq.common.proto.network; + +import bisq.common.Payload; + +/** + * Interface for objects used inside WireEnvelope or other WirePayloads. + */ +public interface NetworkPayload extends Payload { +} diff --git a/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java b/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java new file mode 100644 index 0000000000..8df36611d5 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java @@ -0,0 +1,34 @@ +/* + * 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 bisq.common.proto.network; + +import bisq.common.proto.ProtoResolver; +import bisq.common.proto.ProtobufferException; + +import java.time.Clock; + + +public interface NetworkProtoResolver extends ProtoResolver { + NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException; + + NetworkPayload fromProto(protobuf.StoragePayload proto); + + NetworkPayload fromProto(protobuf.StorageEntryWrapper proto); + + Clock getClock(); +} diff --git a/common/src/main/java/bisq/common/proto/persistable/NavigationPath.java b/common/src/main/java/bisq/common/proto/persistable/NavigationPath.java new file mode 100644 index 0000000000..1cb22d428a --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/NavigationPath.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.common.proto.persistable; + +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.Message; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class NavigationPath implements PersistableEnvelope { + private List path = List.of(); + + @Override + public Message toProtoMessage() { + final protobuf.NavigationPath.Builder builder = protobuf.NavigationPath.newBuilder(); + if (!CollectionUtils.isEmpty(path)) builder.addAllPath(path); + return protobuf.PersistableEnvelope.newBuilder().setNavigationPath(builder).build(); + } + + public static NavigationPath fromProto(protobuf.NavigationPath proto) { + return new NavigationPath(List.copyOf(proto.getPathList())); + } +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistableEnvelope.java b/common/src/main/java/bisq/common/proto/persistable/PersistableEnvelope.java new file mode 100644 index 0000000000..8087d0df77 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistableEnvelope.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.common.proto.persistable; + +import bisq.common.Envelope; + +import com.google.protobuf.Message; + +/** + * Interface for the outside envelope object persisted to disk. + */ +public interface PersistableEnvelope extends Envelope { + + default Message toPersistableMessage() { + return toProtoMessage(); + } + + default String getDefaultStorageFileName() { + return this.getClass().getSimpleName(); + } +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistableList.java b/common/src/main/java/bisq/common/proto/persistable/PersistableList.java new file mode 100644 index 0000000000..9cdfc1348c --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistableList.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.common.proto.persistable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import lombok.Getter; + +public abstract class PersistableList implements PersistableEnvelope { + + @Getter + public final List list = createList(); + + protected List createList() { + return new ArrayList<>(); + } + + public PersistableList() { + } + + protected PersistableList(Collection collection) { + setAll(collection); + } + + public void setAll(Collection collection) { + this.list.clear(); + this.list.addAll(collection); + } + + public boolean add(T item) { + if (!list.contains(item)) { + list.add(item); + return true; + } + return false; + } + + public boolean remove(T item) { + return list.remove(item); + } + + public Stream stream() { + return list.stream(); + } + + public int size() { + return list.size(); + } + + public boolean contains(T item) { + return list.contains(item); + } + + public boolean isEmpty() { + return list.isEmpty(); + } + + public void forEach(Consumer action) { + list.forEach(action); + } + + public void clear() { + list.clear(); + } +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistableListAsObservable.java b/common/src/main/java/bisq/common/proto/persistable/PersistableListAsObservable.java new file mode 100644 index 0000000000..782e44b9e8 --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistableListAsObservable.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.common.proto.persistable; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.Collection; +import java.util.List; + +public abstract class PersistableListAsObservable extends PersistableList { + + public PersistableListAsObservable() { + } + + protected PersistableListAsObservable(Collection collection) { + super(collection); + } + + protected List createList() { + return FXCollections.observableArrayList(); + } + + public ObservableList getObservableList() { + return (ObservableList) getList(); + } + + public void addListener(ListChangeListener listener) { + ((ObservableList) getList()).addListener(listener); + } + + public void removeListener(ListChangeListener listener) { + ((ObservableList) getList()).removeListener(listener); + } +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistablePayload.java b/common/src/main/java/bisq/common/proto/persistable/PersistablePayload.java new file mode 100644 index 0000000000..64bd36f3fd --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistablePayload.java @@ -0,0 +1,26 @@ +/* + * 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 bisq.common.proto.persistable; + +import bisq.common.Payload; + +/** + * Interface for objects used inside Envelope or other Payloads. + */ +public interface PersistablePayload extends Payload { +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java b/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java new file mode 100644 index 0000000000..0aca05732d --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.common.proto.persistable; + +public interface PersistedDataHost { + void readPersisted(Runnable completeHandler); +} diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistenceProtoResolver.java b/common/src/main/java/bisq/common/proto/persistable/PersistenceProtoResolver.java new file mode 100644 index 0000000000..1a7e8a0e9e --- /dev/null +++ b/common/src/main/java/bisq/common/proto/persistable/PersistenceProtoResolver.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.common.proto.persistable; + +import bisq.common.proto.ProtoResolver; + + +public interface PersistenceProtoResolver extends ProtoResolver { + PersistableEnvelope fromProto(protobuf.PersistableEnvelope persistable); +} diff --git a/common/src/main/java/bisq/common/reactfx/FxTimer.java b/common/src/main/java/bisq/common/reactfx/FxTimer.java new file mode 100644 index 0000000000..dfd4aabb78 --- /dev/null +++ b/common/src/main/java/bisq/common/reactfx/FxTimer.java @@ -0,0 +1,104 @@ +package bisq.common.reactfx; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.util.Duration; + +/** + * Provides factory methods for timers that are manipulated from and execute + * their action on the JavaFX application thread. + * + * Copied from: + * https://github.com/TomasMikula/ReactFX/blob/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/FxTimer.java + * + */ +public class FxTimer implements Timer { + + /** + * Prepares a (stopped) timer that lasts for {@code delay} and whose action runs when timer ends. + */ + public static Timer create(java.time.Duration delay, Runnable action) { + return new FxTimer(delay, delay, action, 1); + } + + /** + * Equivalent to {@code create(delay, action).restart()}. + */ + public static Timer runLater(java.time.Duration delay, Runnable action) { + Timer timer = create(delay, action); + timer.restart(); + return timer; + } + + /** + * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically + * when the timer ends. + */ + public static Timer createPeriodic(java.time.Duration interval, Runnable action) { + return new FxTimer(interval, interval, action, Animation.INDEFINITE); + } + + /** + * Equivalent to {@code createPeriodic(interval, action).restart()}. + */ + public static Timer runPeriodically(java.time.Duration interval, Runnable action) { + Timer timer = createPeriodic(interval, action); + timer.restart(); + return timer; + } + + /** + * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically + * when the timer starts. + */ + public static Timer createPeriodic0(java.time.Duration interval, Runnable action) { + return new FxTimer(java.time.Duration.ZERO, interval, action, Animation.INDEFINITE); + } + + /** + * Equivalent to {@code createPeriodic0(interval, action).restart()}. + */ + public static Timer runPeriodically0(java.time.Duration interval, Runnable action) { + Timer timer = createPeriodic0(interval, action); + timer.restart(); + return timer; + } + + private final Duration actionTime; + private final Timeline timeline; + private final Runnable action; + + private long seq = 0; + + private FxTimer(java.time.Duration actionTime, java.time.Duration period, Runnable action, int cycles) { + this.actionTime = Duration.millis(actionTime.toMillis()); + this.timeline = new Timeline(); + this.action = action; + + timeline.getKeyFrames().add(new KeyFrame(this.actionTime)); // used as placeholder + if (period != actionTime) { + timeline.getKeyFrames().add(new KeyFrame(Duration.millis(period.toMillis()))); + } + + timeline.setCycleCount(cycles); + } + + @Override + public void restart() { + stop(); + long expected = seq; + timeline.getKeyFrames().set(0, new KeyFrame(actionTime, ae -> { + if(seq == expected) { + action.run(); + } + })); + timeline.play(); + } + + @Override + public void stop() { + timeline.stop(); + ++seq; + } +} diff --git a/common/src/main/java/bisq/common/reactfx/LICENSE b/common/src/main/java/bisq/common/reactfx/LICENSE new file mode 100644 index 0000000000..801693cd47 --- /dev/null +++ b/common/src/main/java/bisq/common/reactfx/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2013-2014, Tomas Mikula +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/common/src/main/java/bisq/common/reactfx/README.md b/common/src/main/java/bisq/common/reactfx/README.md new file mode 100644 index 0000000000..965f0afbd9 --- /dev/null +++ b/common/src/main/java/bisq/common/reactfx/README.md @@ -0,0 +1,6 @@ +This package is a very minimal subset of the external library `org.reactfx`. + +Two small files from `org.reactfx` were embedded into the project +to avoid having it as dependency: + +[https://github.com/TomasMikula/ReactFX] diff --git a/common/src/main/java/bisq/common/reactfx/Timer.java b/common/src/main/java/bisq/common/reactfx/Timer.java new file mode 100644 index 0000000000..b7ae0a23ea --- /dev/null +++ b/common/src/main/java/bisq/common/reactfx/Timer.java @@ -0,0 +1,66 @@ +package bisq.common.reactfx; + +/** + * Timer represents a delayed action. This means that every timer has an + * associated action and an associated delay. Action and delay are specified + * on timer creation. + * + *

    Every timer also has an associated thread (such as JavaFX application + * thread or a single-thread executor's thread). Timer may only be accessed + * from its associated thread. Timer's action is executed on its associated + * thread, too. This design allows to implement guarantees provided by + * {@link #stop()}. + * + * Copied from: + * https://raw.githubusercontent.com/TomasMikula/ReactFX/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/Timer.java* + */ +public interface Timer { + /** + * Schedules the associated action to be executed after the associated + * delay. If the action is already scheduled but hasn't been executed yet, + * the timeout is reset, so that the action won't be executed before the + * full delay from now. + */ + void restart(); + + /** + * If the associated action has been scheduled for execution but not yet + * executed, this method prevents it from being executed at all. This is + * also true in case the timer's timeout has already expired, but the + * associated action hasn't had a chance to be executed on the associated + * thread. Note that this is a stronger guarantee than the one given by + * {@link javafx.animation.Animation#stop()}: + * + *

    +     * {@code
    +     * Timeline timeline = new Timeline(new KeyFrame(
    +     *         Duration.millis(1000),
    +     *         ae -> System.out.println("FIRED ANYWAY")));
    +     * timeline.play();
    +     *
    +     * // later on the JavaFX application thread,
    +     * // but still before the action has been executed
    +     * timeline.stop();
    +     *
    +     * // later, "FIRED ANYWAY" may still be printed
    +     * }
    +     * 
    + * + * In contrast, using the {@link FxTimer}, the action is guaranteed not to + * be executed after {@code stop()}: + *
    +     * {@code
    +     * Timer timer = FxTimer.runLater(
    +     *         Duration.ofMillis(1000),
    +     *         () -> System.out.println("FIRED"));
    +     *
    +     * // later on the JavaFX application thread,
    +     * // but still before the action has been executed
    +     * timer.stop();
    +     *
    +     * // "FIRED" is guaranteed *not* to be printed
    +     * }
    +     * 
    + */ + void stop(); +} diff --git a/common/src/main/java/bisq/common/setup/CommonSetup.java b/common/src/main/java/bisq/common/setup/CommonSetup.java new file mode 100644 index 0000000000..090d755043 --- /dev/null +++ b/common/src/main/java/bisq/common/setup/CommonSetup.java @@ -0,0 +1,129 @@ +/* + * 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 bisq.common.setup; + +import bisq.common.UserThread; +import bisq.common.app.AsciiLogo; +import bisq.common.app.DevEnv; +import bisq.common.app.Log; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.Profiler; +import bisq.common.util.Utilities; + +import org.bitcoinj.store.BlockStoreException; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.net.URISyntaxException; + +import java.nio.file.Paths; + +import java.util.concurrent.TimeUnit; + +import ch.qos.logback.classic.Level; + +import lombok.extern.slf4j.Slf4j; + + + +import sun.misc.Signal; + +@Slf4j +public class CommonSetup { + + public static void setup(Config config, GracefulShutDownHandler gracefulShutDownHandler) { + setupLog(config); + AsciiLogo.showAsciiLogo(); + Version.setBaseCryptoNetworkId(config.baseCurrencyNetwork.ordinal()); + Version.printVersion(); + maybePrintPathOfCodeSource(); + Profiler.printSystemLoad(); + + setSystemProperties(); + setupSigIntHandlers(gracefulShutDownHandler); + + DevEnv.setup(config); + } + + public static void printSystemLoadPeriodically(int delayMin) { + UserThread.runPeriodically(Profiler::printSystemLoad, delayMin, TimeUnit.MINUTES); + } + + public static void setupUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + // Might come from another thread + if (throwable.getCause() != null && throwable.getCause().getCause() != null && + throwable.getCause().getCause() instanceof BlockStoreException) { + log.error(throwable.getMessage()); + } else if (throwable instanceof ClassCastException && + "sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) { + log.warn(throwable.getMessage()); + } else if (throwable instanceof UnsupportedOperationException && + "The system tray is not supported on the current platform.".equals(throwable.getMessage())) { + log.warn(throwable.getMessage()); + } else { + log.error("Uncaught Exception from thread " + Thread.currentThread().getName()); + log.error("throwableMessage= " + throwable.getMessage()); + log.error("throwableClass= " + throwable.getClass()); + log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable)); + throwable.printStackTrace(); + UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false)); + } + }; + Thread.setDefaultUncaughtExceptionHandler(handler); + Thread.currentThread().setUncaughtExceptionHandler(handler); + } + + private static void setupLog(Config config) { + String logPath = Paths.get(config.appDataDir.getPath(), "bisq").toString(); + Log.setup(logPath); + Utilities.printSysInfo(); + Log.setLevel(Level.toLevel(config.logLevel)); + } + + protected static void setSystemProperties() { + if (Utilities.isLinux()) + System.setProperty("prism.lcdtext", "false"); + } + + protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) { + Signal.handle(new Signal("INT"), signal -> { + log.info("Received {}", signal); + UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { + })); + }); + + Signal.handle(new Signal("TERM"), signal -> { + log.info("Received {}", signal); + UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { + })); + }); + } + + protected static void maybePrintPathOfCodeSource() { + try { + final String pathOfCodeSource = Utilities.getPathOfCodeSource(); + if (!pathOfCodeSource.endsWith("classes")) + log.info("Path to Bisq jar file: " + pathOfCodeSource); + } catch (URISyntaxException e) { + log.error(e.toString()); + e.printStackTrace(); + } + } +} diff --git a/common/src/main/java/bisq/common/setup/GracefulShutDownHandler.java b/common/src/main/java/bisq/common/setup/GracefulShutDownHandler.java new file mode 100644 index 0000000000..825bdf3084 --- /dev/null +++ b/common/src/main/java/bisq/common/setup/GracefulShutDownHandler.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common.setup; + +import bisq.common.handlers.ResultHandler; + +public interface GracefulShutDownHandler { + void gracefulShutDown(ResultHandler resultHandler); +} diff --git a/common/src/main/java/bisq/common/setup/UncaughtExceptionHandler.java b/common/src/main/java/bisq/common/setup/UncaughtExceptionHandler.java new file mode 100644 index 0000000000..ed9dd7dd39 --- /dev/null +++ b/common/src/main/java/bisq/common/setup/UncaughtExceptionHandler.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.common.setup; + +public interface UncaughtExceptionHandler { + void handleUncaughtException(Throwable throwable, boolean doShutDown); +} diff --git a/common/src/main/java/bisq/common/taskrunner/InterceptTaskException.java b/common/src/main/java/bisq/common/taskrunner/InterceptTaskException.java new file mode 100644 index 0000000000..905a08e48a --- /dev/null +++ b/common/src/main/java/bisq/common/taskrunner/InterceptTaskException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common.taskrunner; + +public class InterceptTaskException extends RuntimeException { + public InterceptTaskException(String message) { + super(message); + } +} diff --git a/common/src/main/java/bisq/common/taskrunner/Model.java b/common/src/main/java/bisq/common/taskrunner/Model.java new file mode 100644 index 0000000000..4a0ead244c --- /dev/null +++ b/common/src/main/java/bisq/common/taskrunner/Model.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.common.taskrunner; + +public interface Model { + void onComplete(); +} diff --git a/common/src/main/java/bisq/common/taskrunner/Task.java b/common/src/main/java/bisq/common/taskrunner/Task.java new file mode 100644 index 0000000000..e6fce41550 --- /dev/null +++ b/common/src/main/java/bisq/common/taskrunner/Task.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.common.taskrunner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class Task { + private static final Logger log = LoggerFactory.getLogger(Task.class); + + public static Class taskToIntercept; + + private final TaskRunner taskHandler; + protected final T model; + protected String errorMessage = "An error occurred at task: " + getClass().getSimpleName(); + protected boolean completed; + + public Task(TaskRunner taskHandler, T model) { + this.taskHandler = taskHandler; + this.model = model; + } + + protected abstract void run(); + + protected void runInterceptHook() { + if (getClass() == taskToIntercept) + throw new InterceptTaskException("Task intercepted for testing purpose. Task = " + getClass().getSimpleName()); + } + + protected void appendToErrorMessage(String message) { + errorMessage += "\n" + message; + } + + protected void appendExceptionToErrorMessage(Throwable t) { + if (t.getMessage() != null) + errorMessage += "\nException message: " + t.getMessage(); + else + errorMessage += "\nException: " + t.toString(); + } + + protected void complete() { + completed = true; + taskHandler.handleComplete(); + } + + protected void failed(String message) { + appendToErrorMessage(message); + failed(); + } + + protected void failed(Throwable t) { + log.error(errorMessage, t); + taskHandler.handleErrorMessage(errorMessage); + } + + protected void failed() { + log.error(errorMessage); + taskHandler.handleErrorMessage(errorMessage); + } + +} diff --git a/common/src/main/java/bisq/common/taskrunner/TaskRunner.java b/common/src/main/java/bisq/common/taskrunner/TaskRunner.java new file mode 100644 index 0000000000..d8870b09f4 --- /dev/null +++ b/common/src/main/java/bisq/common/taskrunner/TaskRunner.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.common.taskrunner; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import java.util.Arrays; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TaskRunner { + private final Queue>> tasks = new LinkedBlockingQueue<>(); + private final T sharedModel; + private final Class sharedModelClass; + private final ResultHandler resultHandler; + private final ErrorMessageHandler errorMessageHandler; + private boolean failed = false; + private boolean isCanceled; + + private Class> currentTask; + + + public TaskRunner(T sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + //noinspection unchecked + this(sharedModel, (Class) sharedModel.getClass(), resultHandler, errorMessageHandler); + } + + public TaskRunner(T sharedModel, Class sharedModelClass, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + this.sharedModel = sharedModel; + this.resultHandler = resultHandler; + this.errorMessageHandler = errorMessageHandler; + this.sharedModelClass = sharedModelClass; + } + + @SafeVarargs + public final void addTasks(Class>... items) { + tasks.addAll(Arrays.asList(items)); + } + + public void run() { + next(); + } + + private void next() { + if (!failed && !isCanceled) { + if (tasks.size() > 0) { + try { + currentTask = tasks.poll(); + log.info("Run task: " + currentTask.getSimpleName()); + currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + handleErrorMessage("Error at taskRunner: " + throwable.getMessage()); + } + } else { + resultHandler.handleResult(); + } + } + } + + public void cancel() { + isCanceled = true; + } + + void handleComplete() { + next(); + } + + void handleErrorMessage(String errorMessage) { + log.error("Task failed: " + currentTask.getSimpleName() + " / errorMessage: " + errorMessage); + failed = true; + errorMessageHandler.handleErrorMessage(errorMessage); + } +} diff --git a/common/src/main/java/bisq/common/util/Base64.java b/common/src/main/java/bisq/common/util/Base64.java new file mode 100644 index 0000000000..a7c36d8518 --- /dev/null +++ b/common/src/main/java/bisq/common/util/Base64.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.common.util; + +/** + * We use Java 8 builtin Base64 because it is much faster than Guava and Apache versions: + * http://java-performance.info/base64-encoding-and-decoding-performance/ + */ +public class Base64 { + + public static byte[] decode(String base64) { + return java.util.Base64.getDecoder().decode(base64); + } + + public static String encode(byte[] bytes) { + return java.util.Base64.getEncoder().encodeToString(bytes); + } +} diff --git a/common/src/main/java/bisq/common/util/CollectionUtils.java b/common/src/main/java/bisq/common/util/CollectionUtils.java new file mode 100644 index 0000000000..9b4a41e6cb --- /dev/null +++ b/common/src/main/java/bisq/common/util/CollectionUtils.java @@ -0,0 +1,35 @@ +package bisq.common.util; + +import java.util.Collection; +import java.util.Map; + +/** + * Collection utility methods copied from Spring Framework v4.3.6's + * {@code org.springframework.util.CollectionUtils} class in order to make it possible to + * drop Bisq's dependency on Spring altogether. The name of the class and methods have + * been preserved here to minimize the impact to the Bisq codebase of making this change. + * All that is necessary to swap this implementation in is to change the CollectionUtils + * import statement. + */ +public class CollectionUtils { + + /** + * Return {@code true} if the supplied Collection is {@code null} or empty. + * Otherwise, return {@code false}. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return {@code true} if the supplied Map is {@code null} or empty. + * Otherwise, return {@code false}. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } +} diff --git a/common/src/main/java/bisq/common/util/DesktopUtil.java b/common/src/main/java/bisq/common/util/DesktopUtil.java new file mode 100644 index 0000000000..ae695e526d --- /dev/null +++ b/common/src/main/java/bisq/common/util/DesktopUtil.java @@ -0,0 +1,175 @@ +/* + * 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 bisq.common.util; + +import java.net.URI; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +// Taken form https://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform, +// originally net.mightypork.rpack.utils.DesktopApi +@Slf4j +class DesktopUtil { + + public static boolean browse(URI uri) { + return openSystemSpecific(uri.toString()); + } + + + public static boolean open(File file) { + return openSystemSpecific(file.getPath()); + } + + + public static boolean edit(File file) { + // you can try something like + // runCommand("gimp", "%s", file.getPath()) + // based on user preferences. + return openSystemSpecific(file.getPath()); + } + + + private static boolean openSystemSpecific(String what) { + EnumOS os = getOs(); + if (os.isLinux()) { + if (runCommand("kde-open", "%s", what)) return true; + if (runCommand("gnome-open", "%s", what)) return true; + if (runCommand("xdg-open", "%s", what)) return true; + } + + if (os.isMac()) { + if (runCommand("open", "%s", what)) return true; + } + + if (os.isWindows()) { + return runCommand("explorer", "%s", "\"" + what + "\""); + } + + return false; + } + + + @SuppressWarnings("SameParameterValue") + private static boolean runCommand(String command, String args, String file) { + + log.info("Trying to exec: cmd = {} args = {} file = {}", command, args, file); + + String[] parts = prepareCommand(command, args, file); + + try { + Process p = Runtime.getRuntime().exec(parts); + if (p == null) return false; + + try { + int value = p.exitValue(); + if (value == 0) { + log.warn("Process ended immediately."); + } else { + log.warn("Process crashed."); + } + return false; + } catch (IllegalThreadStateException e) { + log.info("Process is running."); + return true; + } + } catch (IOException e) { + log.warn("Error running command. {}", e.toString()); + return false; + } + } + + + private static String[] prepareCommand(String command, String args, String file) { + + List parts = new ArrayList<>(); + parts.add(command); + + if (args != null) { + for (String s : args.split(" ")) { + s = String.format(s, file); // put in the filename thing + + parts.add(s.trim()); + } + } + + return parts.toArray(new String[parts.size()]); + } + + public enum EnumOS { + linux, + macos, + solaris, + unknown, + windows; + + public boolean isLinux() { + + return this == linux || this == solaris; + } + + + public boolean isMac() { + + return this == macos; + } + + + public boolean isWindows() { + + return this == windows; + } + } + + + private static EnumOS getOs() { + + String s = System.getProperty("os.name").toLowerCase(); + + if (s.contains("win")) { + return EnumOS.windows; + } + + if (s.contains("mac")) { + return EnumOS.macos; + } + + if (s.contains("solaris")) { + return EnumOS.solaris; + } + + if (s.contains("sunos")) { + return EnumOS.solaris; + } + + if (s.contains("linux")) { + return EnumOS.linux; + } + + if (s.contains("unix")) { + return EnumOS.linux; + } else { + return EnumOS.unknown; + } + } +} diff --git a/common/src/main/java/bisq/common/util/DoubleSummaryStatisticsWithStdDev.java b/common/src/main/java/bisq/common/util/DoubleSummaryStatisticsWithStdDev.java new file mode 100644 index 0000000000..c4a43bdfe4 --- /dev/null +++ b/common/src/main/java/bisq/common/util/DoubleSummaryStatisticsWithStdDev.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.common.util; + +import java.util.DoubleSummaryStatistics; + +/* Adds logic to DoubleSummaryStatistics for keeping track of sum of squares + * and computing population variance and population standard deviation. + * Kahan summation algorithm (for `getSumOfSquares`) sourced from the DoubleSummaryStatistics class. + * Incremental variance algorithm sourced from https://math.stackexchange.com/a/1379804/316756 + */ +public class DoubleSummaryStatisticsWithStdDev extends DoubleSummaryStatistics { + private double sumOfSquares; + private double sumOfSquaresCompensation; // Low order bits of sum of squares + private double simpleSumOfSquares; // Used to compute right sum of squares for non-finite inputs + + @Override + public void accept(double value) { + super.accept(value); + double valueSquared = value * value; + simpleSumOfSquares += valueSquared; + sumOfSquaresWithCompensation(valueSquared); + } + + public void combine(DoubleSummaryStatisticsWithStdDev other) { + super.combine(other); + simpleSumOfSquares += other.simpleSumOfSquares; + sumOfSquaresWithCompensation(other.sumOfSquares); + sumOfSquaresWithCompensation(other.sumOfSquaresCompensation); + } + + /* Incorporate a new squared double value using Kahan summation / + * compensated summation. + */ + private void sumOfSquaresWithCompensation(double valueSquared) { + double tmp = valueSquared - sumOfSquaresCompensation; + double velvel = sumOfSquares + tmp; // Little wolf of rounding error + sumOfSquaresCompensation = (velvel - sumOfSquares) - tmp; + sumOfSquares = velvel; + } + + private double getSumOfSquares() { + // Better error bounds to add both terms as the final sum of squares + double tmp = sumOfSquares + sumOfSquaresCompensation; + if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquares)) + // If the compensated sum of squares is spuriously NaN from + // accumulating one or more same-signed infinite values, + // return the correctly-signed infinity stored in + // simpleSumOfSquares. + return simpleSumOfSquares; + else + return tmp; + } + + private double getVariance() { + double sumOfSquares = getSumOfSquares(); + long count = getCount(); + double mean = getAverage(); + return (sumOfSquares / count) - (mean * mean); + } + + public final double getStandardDeviation() { + double variance = getVariance(); + return Math.sqrt(variance); + } + +} diff --git a/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java b/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java new file mode 100644 index 0000000000..df15855716 --- /dev/null +++ b/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.common.util; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Validator for extraDataMap fields used in network payloads. + * Ensures that we don't get the network attacked by huge data inserted there. + */ +@Slf4j +public class ExtraDataMapValidator { + // ExtraDataMap is only used for exceptional cases to not break backward compatibility. + // We don't expect many entries there. + public final static int MAX_SIZE = 10; + public final static int MAX_KEY_LENGTH = 100; + public final static int MAX_VALUE_LENGTH = 100000; // 100 kb + + public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap) { + return getValidatedExtraDataMap(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); + } + + public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap, int maxSize, + int maxKeyLength, int maxValueLength) { + if (extraDataMap == null) + return null; + + try { + checkArgument(extraDataMap.entrySet().size() <= maxSize, + "Size of map must not exceed " + maxSize); + extraDataMap.forEach((key, value) -> { + checkArgument(key.length() <= maxKeyLength, + "Length of key must not exceed " + maxKeyLength); + checkArgument(value.length() <= maxValueLength, + "Length of value must not exceed " + maxValueLength); + }); + return extraDataMap; + } catch (Throwable t) { + return new HashMap<>(); + } + } + + public static void validate(@Nullable Map extraDataMap) { + validate(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); + } + + public static void validate(@Nullable Map extraDataMap, int maxSize, int maxKeyLength, + int maxValueLength) { + if (extraDataMap == null) + return; + + checkArgument(extraDataMap.entrySet().size() <= maxSize, + "Size of map must not exceed " + maxSize); + extraDataMap.forEach((key, value) -> { + checkArgument(key.length() <= maxKeyLength, + "Length of key must not exceed " + maxKeyLength); + checkArgument(value.length() <= maxValueLength, + "Length of value must not exceed " + maxValueLength); + }); + } +} diff --git a/common/src/main/java/bisq/common/util/Hex.java b/common/src/main/java/bisq/common/util/Hex.java new file mode 100644 index 0000000000..5edcce6602 --- /dev/null +++ b/common/src/main/java/bisq/common/util/Hex.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.common.util; + +import com.google.common.io.BaseEncoding; + +public class Hex { + + public static byte[] decode(String hex) { + return BaseEncoding.base16().lowerCase().decode(hex.toLowerCase()); + } + + public static String encode(byte[] bytes) { + return BaseEncoding.base16().lowerCase().encode(bytes); + } +} diff --git a/common/src/main/java/bisq/common/util/InvalidVersionException.java b/common/src/main/java/bisq/common/util/InvalidVersionException.java new file mode 100644 index 0000000000..d63bc31af0 --- /dev/null +++ b/common/src/main/java/bisq/common/util/InvalidVersionException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.common.util; + +public class InvalidVersionException extends Exception { + public InvalidVersionException(String msg) { + super(msg); + } +} diff --git a/common/src/main/java/bisq/common/util/JsonExclude.java b/common/src/main/java/bisq/common/util/JsonExclude.java new file mode 100644 index 0000000000..afa65d49c1 --- /dev/null +++ b/common/src/main/java/bisq/common/util/JsonExclude.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.common.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface JsonExclude { +} diff --git a/common/src/main/java/bisq/common/util/MathUtils.java b/common/src/main/java/bisq/common/util/MathUtils.java new file mode 100644 index 0000000000..77c702b65d --- /dev/null +++ b/common/src/main/java/bisq/common/util/MathUtils.java @@ -0,0 +1,182 @@ +/* + * 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 bisq.common.util; + +import com.google.common.math.DoubleMath; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; + +public class MathUtils { + private static final Logger log = LoggerFactory.getLogger(MathUtils.class); + + public static double roundDouble(double value, int precision) { + return roundDouble(value, precision, RoundingMode.HALF_UP); + } + + @SuppressWarnings("SameParameterValue") + public static double roundDouble(double value, int precision, RoundingMode roundingMode) { + if (precision < 0) + throw new IllegalArgumentException(); + if (!Double.isFinite(value)) + throw new IllegalArgumentException("Expected a finite double, but found " + value); + + try { + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(precision, roundingMode); + return bd.doubleValue(); + } catch (Throwable t) { + log.error(t.toString()); + return 0; + } + } + + public static long roundDoubleToLong(double value) { + return roundDoubleToLong(value, RoundingMode.HALF_UP); + } + + @SuppressWarnings("SameParameterValue") + public static long roundDoubleToLong(double value, RoundingMode roundingMode) { + return DoubleMath.roundToLong(value, roundingMode); + } + + public static int roundDoubleToInt(double value) { + return roundDoubleToInt(value, RoundingMode.HALF_UP); + } + + @SuppressWarnings("SameParameterValue") + public static int roundDoubleToInt(double value, RoundingMode roundingMode) { + return DoubleMath.roundToInt(value, roundingMode); + } + + public static long doubleToLong(double value) { + return Double.valueOf(value).longValue(); + } + + public static double scaleUpByPowerOf10(double value, int exponent) { + double factor = Math.pow(10, exponent); + return value * factor; + } + + public static double scaleUpByPowerOf10(long value, int exponent) { + double factor = Math.pow(10, exponent); + return ((double) value) * factor; + } + + public static double scaleDownByPowerOf10(double value, int exponent) { + double factor = Math.pow(10, exponent); + return value / factor; + } + + public static double scaleDownByPowerOf10(long value, int exponent) { + double factor = Math.pow(10, exponent); + return ((double) value) / factor; + } + + public static double exactMultiply(double value1, double value2) { + return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); + } + + public static long getMedian(Long[] list) { + if (list.length == 0) { + return 0L; + } + + int middle = list.length / 2; + long median; + if (list.length % 2 == 1) { + median = list[middle]; + } else { + median = MathUtils.roundDoubleToLong((list[middle - 1] + list[middle]) / 2.0); + } + return median; + } + + public static class MovingAverage { + final Deque window; + private final int size; + private long sum; + private final double outlier; + + // Outlier as ratio + public MovingAverage(int size, double outlier) { + this.size = size; + window = new ArrayDeque<>(size); + this.outlier = outlier; + sum = 0; + } + + public Optional next(long val) { + try { + var fullAtStart = isFull(); + if (fullAtStart) { + if (outlier > 0) { + // Return early if it's an outlier + checkArgument(size != 0); + var avg = (double) sum / size; + if (Math.abs(avg - val) / avg > outlier) { + return Optional.empty(); + } + } + sum -= window.remove(); + } + window.add(val); + sum += val; + if (!fullAtStart && isFull() && outlier != 0) { + removeInitialOutlier(); + } + // When discarding outliers, the first n non discarded elements return Optional.empty() + return outlier > 0 && !isFull() ? Optional.empty() : current(); + } catch (Throwable t) { + log.error(t.toString()); + return Optional.empty(); + } + } + + boolean isFull() { + return window.size() == size; + } + + private void removeInitialOutlier() { + var element = window.iterator(); + while (element.hasNext()) { + var val = element.next(); + int div = size - 1; + checkArgument(div != 0); + var avgExVal = (double) (sum - val) / div; + if (Math.abs(avgExVal - val) / avgExVal > outlier) { + element.remove(); + break; + } + } + } + + public Optional current() { + return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size()); + } + } +} diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java new file mode 100644 index 0000000000..a7c3e98023 --- /dev/null +++ b/common/src/main/java/bisq/common/util/PermutationUtil.java @@ -0,0 +1,177 @@ +/* + * 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 bisq.common.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PermutationUtil { + + /** + * @param list Original list + * @param indicesToRemove List of indices to remove + * @param Type of List items + * @return Partial list where items at indices of indicesToRemove have been removed + */ + public static List getPartialList(List list, List indicesToRemove) { + List altered = new ArrayList<>(list); + + // Eliminate duplicates + indicesToRemove = new ArrayList<>(new HashSet<>(indicesToRemove)); + + // Sort + Collections.sort(indicesToRemove); + + // Reverse list. + // We need to remove from highest index downwards to not change order of remaining indices + Collections.reverse(indicesToRemove); + + indicesToRemove.forEach(index -> { + if (altered.size() > index && index >= 0) + altered.remove((int) index); + }); + return altered; + } + + public static List findMatchingPermutation(R targetValue, + List list, + BiFunction, Boolean> predicate, + int maxIterations) { + if (predicate.apply(targetValue, list)) { + return list; + } else { + return findMatchingPermutation(targetValue, + list, + new ArrayList<>(), + predicate, + new AtomicInteger(maxIterations)); + } + } + + private static List findMatchingPermutation(R targetValue, + List list, + List> lists, + BiFunction, Boolean> predicate, + AtomicInteger maxIterations) { + for (int level = 0; level < list.size(); level++) { + // Test one level at a time + var result = checkLevel(targetValue, list, predicate, level, 0, maxIterations); + if (!result.isEmpty()) { + return result; + } + } + + return new ArrayList<>(); + } + + @NonNull + private static List checkLevel(R targetValue, + List previousLevel, + BiFunction, Boolean> predicate, + int level, + int permutationIndex, + AtomicInteger maxIterations) { + if (previousLevel.size() == 1) { + return new ArrayList<>(); + } + for (int i = permutationIndex; i < previousLevel.size(); i++) { + if (maxIterations.get() <= 0) { + return new ArrayList<>(); + } + List newList = new ArrayList<>(previousLevel); + newList.remove(i); + if (level == 0) { + maxIterations.decrementAndGet(); + // Check all permutations on this level + if (predicate.apply(targetValue, newList)) { + return newList; + } + } else { + // Test next level + var result = checkLevel(targetValue, newList, predicate, level - 1, i, maxIterations); + if (!result.isEmpty()) { + return result; + } + } + } + return new ArrayList<>(); + } + + //TODO optimize algorithm so that it starts from all objects and goes down instead starting with from the bottom. + // That should help that we are not hitting the iteration limit so easily. + + /** + * Returns a list of all possible permutations of a give sorted list ignoring duplicates. + * E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]] + * Number of variations and iterations grows with 2^n - 1 where n is the number of items in the list. + * With 20 items we reach about 1 million iterations and it takes about 0.5 sec. + * To avoid performance issues we added the maxIterations parameter to stop once the number of iterations has + * reached the maxIterations and return in such a case the list of permutations we have been able to create. + * Depending on the type of object which is stored in the list the memory usage should be considered as well for + * choosing the right maxIterations value. + * + * @param list List from which we create permutations + * @param maxIterations Max. number of iterations including inner iterations + * @param Type of list items + * @return List of possible permutations of the original list + */ + public static List> findAllPermutations(List list, int maxIterations) { + List> result = new ArrayList<>(); + int counter = 0; + long ts = System.currentTimeMillis(); + for (T item : list) { + counter++; + if (counter > maxIterations) { + log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + + "counter={}", counter); + return result; + } + + List> subLists = new ArrayList<>(); + for (int n = 0; n < result.size(); n++) { + counter++; + if (counter > maxIterations) { + log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + + "counter={}", counter); + return result; + } + List subList = new ArrayList<>(result.get(n)); + subList.add(item); + subLists.add(subList); + } + + // add single item + result.add(new ArrayList<>(Collections.singletonList(item))); + + // add subLists + result.addAll(subLists); + } + + log.info("findAllPermutations took {} ms for {} items and {} iterations. Heap size used: {} MB", + (System.currentTimeMillis() - ts), list.size(), counter, Profiler.getUsedMemoryInMB()); + return result; + } +} diff --git a/common/src/main/java/bisq/common/util/Preconditions.java b/common/src/main/java/bisq/common/util/Preconditions.java new file mode 100644 index 0000000000..e78215d540 --- /dev/null +++ b/common/src/main/java/bisq/common/util/Preconditions.java @@ -0,0 +1,32 @@ +package bisq.common.util; + +import java.io.File; + +import static java.lang.String.format; + +/** + * Custom preconditions similar to those found in + * {@link com.google.common.base.Preconditions}. + */ +public class Preconditions { + + /** + * Ensures that {@code dir} is a non-null, existing and read-writeable directory. + * @param dir the directory to check + * @return the given directory, now validated + */ + public static File checkDir(File dir) { + if (dir == null) + throw new IllegalArgumentException("Directory must not be null"); + if (!dir.exists()) + throw new IllegalArgumentException(format("Directory '%s' does not exist", dir)); + if (!dir.isDirectory()) + throw new IllegalArgumentException(format("Directory '%s' is not a directory", dir)); + if (!dir.canRead()) + throw new IllegalArgumentException(format("Directory '%s' is not readable", dir)); + if (!dir.canWrite()) + throw new IllegalArgumentException(format("Directory '%s' is not writeable", dir)); + + return dir; + } +} diff --git a/common/src/main/java/bisq/common/util/Profiler.java b/common/src/main/java/bisq/common/util/Profiler.java new file mode 100644 index 0000000000..28150ae9ec --- /dev/null +++ b/common/src/main/java/bisq/common/util/Profiler.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.common.util; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Profiler { + public static void printSystemLoad() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory() / 1024 / 1024; + long total = runtime.totalMemory() / 1024 / 1024; + long used = total - free; + + log.info("System report: Used memory: {} MB; Free memory: {} MB; Total memory: {} MB; No. of threads: {}", + used, free, total, Thread.activeCount()); + } + + public static long getUsedMemoryInMB() { + return getUsedMemoryInBytes() / 1024 / 1024; + } + + public static long getUsedMemoryInBytes() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory(); + long total = runtime.totalMemory(); + return total - free; + } + + public static long getFreeMemoryInMB() { + return Runtime.getRuntime().freeMemory() / 1024 / 1024; + } + + public static long getTotalMemoryInMB() { + return Runtime.getRuntime().totalMemory() / 1024 / 1024; + } +} diff --git a/common/src/main/java/bisq/common/util/ReflectionUtils.java b/common/src/main/java/bisq/common/util/ReflectionUtils.java new file mode 100644 index 0000000000..c9f83a8f41 --- /dev/null +++ b/common/src/main/java/bisq/common/util/ReflectionUtils.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.common.util; + + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static org.apache.commons.lang3.StringUtils.capitalize; + +@Slf4j +public class ReflectionUtils { + + /** + * Recursively loads a list of fields for a given class and its superclasses, + * using a filter predicate to exclude any unwanted fields. + * + * @param fields The list of fields being loaded for a class hierarchy. + * @param clazz The lowest level class in a hierarchy; excluding Object.class. + * @param isExcludedField The field exclusion predicate. + */ + public static void loadFieldListForClassHierarchy(List fields, + Class clazz, + Predicate isExcludedField) { + fields.addAll(stream(clazz.getDeclaredFields()) + .filter(f -> !isExcludedField.test(f)) + .collect(Collectors.toList())); + + Class superclass = clazz.getSuperclass(); + if (!Objects.equals(superclass, Object.class)) + loadFieldListForClassHierarchy(fields, + superclass, + isExcludedField); + } + + /** + * Returns an Optional of a setter method for a given field and a class hierarchy, + * or Optional.empty() if it does not exist. + * + * @param field The field used to find a setter method. + * @param clazz The lowest level class in a hierarchy; excluding Object.class. + * @return Optional of the setter method for a field in the class hierarchy, + * or Optional.empty() if it does not exist. + */ + public static Optional getSetterMethodForFieldInClassHierarchy(Field field, + Class clazz) { + Optional setter = stream(clazz.getDeclaredMethods()) + .filter((m) -> isSetterForField(m, field)) + .findFirst(); + + if (setter.isPresent()) + return setter; + + Class superclass = clazz.getSuperclass(); + if (!Objects.equals(superclass, Object.class)) { + setter = getSetterMethodForFieldInClassHierarchy(field, superclass); + if (setter.isPresent()) + return setter; + } + + return Optional.empty(); + } + + public static boolean isSetterForField(Method m, Field f) { + return m.getName().startsWith("set") + && m.getName().endsWith(capitalize(f.getName())) + && m.getReturnType().getName().equals("void") + && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().equals(f.getType().getName()); + } + + public static boolean isSetterOnClass(Method setter, Class clazz) { + return clazz.equals(setter.getDeclaringClass()); + } + + public static String getVisibilityModifierAsString(Field field) { + if (Modifier.isPrivate(field.getModifiers())) + return "private"; + else if (Modifier.isProtected(field.getModifiers())) + return "protected"; + else if (Modifier.isPublic(field.getModifiers())) + return "public"; + else + return ""; + } + + public static Field getField(String name, Class clazz) { + Optional field = stream(clazz.getDeclaredFields()) + .filter(f -> f.getName().equals(name)).findFirst(); + return field.orElseThrow(() -> + new IllegalArgumentException(format("field %s not found in class %s", + name, + clazz.getSimpleName()))); + } + + public static Method getMethod(String name, Class clazz) { + Optional method = stream(clazz.getDeclaredMethods()) + .filter(m -> m.getName().equals(name)).findFirst(); + return method.orElseThrow(() -> + new IllegalArgumentException(format("method %s not found in class %s", + name, + clazz.getSimpleName()))); + } + + public static void handleSetFieldValueError(Object object, + Field field, + ReflectiveOperationException ex) { + String errMsg = format("cannot set value of field %s, on class %s", + field.getName(), + object.getClass().getSimpleName()); + log.error(capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } +} diff --git a/common/src/main/java/bisq/common/util/RestartUtil.java b/common/src/main/java/bisq/common/util/RestartUtil.java new file mode 100644 index 0000000000..1f8009d586 --- /dev/null +++ b/common/src/main/java/bisq/common/util/RestartUtil.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.common.util; + +import java.io.File; +import java.io.IOException; + +import java.util.List; + +import java.lang.management.ManagementFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Borrowed from: https://dzone.com/articles/programmatically-restart-java +public class RestartUtil { + private static final Logger log = LoggerFactory.getLogger(RestartUtil.class); + + /** + * Sun property pointing the main class and its arguments. + * Might not be defined on non Hotspot VM implementations. + */ + public static final String SUN_JAVA_COMMAND = "sun.java.command"; + + public static void restartApplication(String logPath) throws IOException { + try { + String java = System.getProperty("java.home") + "/bin/java"; + List vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); + StringBuilder vmArgsOneLine = new StringBuilder(); + // if it's the agent argument : we ignore it otherwise the +// address of the old application and the new one will be in conflict + vmArguments.stream().filter(arg -> !arg.contains("-agentlib")).forEach(arg -> { + vmArgsOneLine.append(arg); + vmArgsOneLine.append(" "); + }); + // init the command to execute, add the vm args + final StringBuilder cmd = new StringBuilder(java + " " + vmArgsOneLine); + + // program main and program arguments + String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" "); + // program main is a jar + if (mainCommand[0].endsWith(".jar")) { + // if it's a jar, add -jar mainJar + cmd.append("-jar ").append(new File(mainCommand[0]).getPath()); + } else { + // else it's a .class, add the classpath and mainClass + cmd.append("-cp \"").append(System.getProperty("java.class.path")).append("\" ").append(mainCommand[0]); + } + // finally add program arguments + for (int i = 1; i < mainCommand.length; i++) { + cmd.append(" "); + cmd.append(mainCommand[i]); + } + + try { + final String command = "nohup " + cmd.toString() + " >/dev/null 2>" + logPath + " &"; + log.warn("\n\n############################################################\n" + + "Executing cmd for restart: {}" + + "\n############################################################\n\n", + command); + Runtime.getRuntime().exec(command); + } catch (IOException e) { + e.printStackTrace(); + } + } catch (Exception e) { + throw new IOException("Error while trying to restart the application", e); + } + } +} diff --git a/common/src/main/java/bisq/common/util/Tuple2.java b/common/src/main/java/bisq/common/util/Tuple2.java new file mode 100644 index 0000000000..b7a2c5d908 --- /dev/null +++ b/common/src/main/java/bisq/common/util/Tuple2.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.common.util; + +import java.io.Serializable; + +import java.util.Objects; + +public class Tuple2 implements Serializable { + private static final long serialVersionUID = 1; + + final public A first; + final public B second; + + public Tuple2(A first, B second) { + this.first = first; + this.second = second; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple2)) return false; + + Tuple2 tuple2 = (Tuple2) o; + + if (!Objects.equals(first, tuple2.first)) return false; + return Objects.equals(second, tuple2.second); + + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Tuple2{" + + "\n first=" + first + + ",\n second=" + second + + "\n}"; + } +} diff --git a/common/src/main/java/bisq/common/util/Tuple3.java b/common/src/main/java/bisq/common/util/Tuple3.java new file mode 100644 index 0000000000..92f8009e15 --- /dev/null +++ b/common/src/main/java/bisq/common/util/Tuple3.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.common.util; + +import java.util.Objects; + +public class Tuple3 { + final public A first; + final public B second; + final public C third; + + public Tuple3(A first, B second, C third) { + this.first = first; + this.second = second; + this.third = third; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple3)) return false; + + Tuple3 tuple3 = (Tuple3) o; + + if (!Objects.equals(first, tuple3.first)) return false; + if (!Objects.equals(second, tuple3.second)) return false; + return Objects.equals(third, tuple3.third); + + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + result = 31 * result + (third != null ? third.hashCode() : 0); + return result; + } +} diff --git a/common/src/main/java/bisq/common/util/Tuple4.java b/common/src/main/java/bisq/common/util/Tuple4.java new file mode 100644 index 0000000000..3f045c6f6e --- /dev/null +++ b/common/src/main/java/bisq/common/util/Tuple4.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.common.util; + +import java.util.Objects; + +public class Tuple4 { + final public A first; + final public B second; + final public C third; + final public D fourth; + + public Tuple4(A first, B second, C third, D fourth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple4)) return false; + + Tuple4 tuple4 = (Tuple4) o; + + if (!Objects.equals(first, tuple4.first)) return false; + if (!Objects.equals(second, tuple4.second)) return false; + if (!Objects.equals(third, tuple4.third)) return false; + return Objects.equals(fourth, tuple4.fourth); + + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + result = 31 * result + (third != null ? third.hashCode() : 0); + result = 31 * result + (fourth != null ? fourth.hashCode() : 0); + return result; + } +} diff --git a/common/src/main/java/bisq/common/util/Tuple5.java b/common/src/main/java/bisq/common/util/Tuple5.java new file mode 100644 index 0000000000..5f2bea611e --- /dev/null +++ b/common/src/main/java/bisq/common/util/Tuple5.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.common.util; + +import java.util.Objects; + +public class Tuple5 { + final public A first; + final public B second; + final public C third; + final public D fourth; + final public E fifth; + + public Tuple5(A first, B second, C third, D fourth, E fifth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple5)) return false; + + Tuple5 tuple5 = (Tuple5) o; + + if (!Objects.equals(first, tuple5.first)) return false; + if (!Objects.equals(second, tuple5.second)) return false; + if (!Objects.equals(third, tuple5.third)) return false; + if (!Objects.equals(fourth, tuple5.fourth)) return false; + return Objects.equals(fifth, tuple5.fifth); + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + result = 31 * result + (third != null ? third.hashCode() : 0); + result = 31 * result + (fourth != null ? fourth.hashCode() : 0); + result = 31 * result + (fifth != null ? fifth.hashCode() : 0); + return result; + } +} diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java new file mode 100644 index 0000000000..3d530b918d --- /dev/null +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -0,0 +1,594 @@ +/* + * 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 bisq.common.util; + +import org.bitcoinj.core.Utils; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import com.google.common.base.Splitter; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DurationFormatUtils; + +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; + +import java.text.DecimalFormat; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class Utilities { + public static String objectToJson(Object object) { + Gson gson = new GsonBuilder() + .setExclusionStrategies(new AnnotationExclusionStrategy()) + /*.excludeFieldsWithModifiers(Modifier.TRANSIENT)*/ + /* .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)*/ + .setPrettyPrinting() + .create(); + return gson.toJson(object); + } + + public static ExecutorService getSingleThreadExecutor(String name) { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .build(); + return Executors.newSingleThreadExecutor(threadFactory); + } + + public static ListeningExecutorService getSingleThreadListeningExecutor(String name) { + return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name)); + } + + public static ListeningExecutorService getListeningExecutorService(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec)); + } + + public static ListeningExecutorService getListeningExecutorService(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec, + BlockingQueue workQueue) { + return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, workQueue)); + } + + public static ThreadPoolExecutor getThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, + new ArrayBlockingQueue<>(maximumPoolSize)); + } + + private static ThreadPoolExecutor getThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec, + BlockingQueue workQueue) { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .build(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTimeInSec, + TimeUnit.SECONDS, workQueue, threadFactory); + executor.allowCoreThreadTimeOut(true); + executor.setRejectedExecutionHandler((r, e) -> log.debug("RejectedExecutionHandler called")); + return executor; + } + + @SuppressWarnings("SameParameterValue") + public static ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .setPriority(Thread.MIN_PRIORITY) + .build(); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); + executor.setKeepAliveTime(keepAliveTimeInSec, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + executor.setMaximumPoolSize(maximumPoolSize); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRejectedExecutionHandler((r, e) -> log.debug("RejectedExecutionHandler called")); + return executor; + } + + // TODO: Can some/all of the uses of this be replaced by guava MoreExecutors.shutdownAndAwaitTermination(..)? + public static void shutdownAndAwaitTermination(ExecutorService executor, long timeout, TimeUnit unit) { + executor.shutdown(); + try { + if (!executor.awaitTermination(timeout, unit)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + } + } + + public static FutureCallback failureCallback(Consumer errorHandler) { + return new FutureCallback<>() { + @Override + public void onSuccess(V result) { + } + + @Override + public void onFailure(@NotNull Throwable t) { + errorHandler.accept(t); + } + }; + } + + /** + * @return true if defaults read -g AppleInterfaceStyle has an exit status of 0 (i.e. _not_ returning "key not found"). + */ + public static boolean isMacMenuBarDarkMode() { + try { + // check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents.. + Process process = Runtime.getRuntime().exec(new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"}); + process.waitFor(100, TimeUnit.MILLISECONDS); + return process.exitValue() == 0; + } catch (IOException | InterruptedException | IllegalThreadStateException ex) { + // IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate + return false; + } + } + + public static boolean isUnix() { + return isOSX() || isLinux() || getOSName().contains("freebsd"); + } + + public static boolean isWindows() { + return getOSName().contains("win"); + } + + /** + * @return True, if Bisq is running on a virtualized OS within Qubes, false otherwise + */ + public static boolean isQubesOS() { + // For Linux qubes, "os.version" looks like "4.19.132-1.pvops.qubes.x86_64" + // The presence of the "qubes" substring indicates this Linux is running as a qube + // This is the case for all 3 virtualization modes (PV, PVH, HVM) + // In addition, this works for both simple AppVMs, as well as for StandaloneVMs + // TODO This might not work for detecting Qubes virtualization for other OSes + // like Windows + return getOSVersion().contains("qubes"); + } + + public static boolean isOSX() { + return getOSName().contains("mac") || getOSName().contains("darwin"); + } + + public static boolean isLinux() { + return getOSName().contains("linux"); + } + + public static boolean isDebianLinux() { + return isLinux() && new File("/etc/debian_version").isFile(); + } + + public static boolean isRedHatLinux() { + return isLinux() && new File("/etc/redhat-release").isFile(); + } + + private static String getOSName() { + return System.getProperty("os.name").toLowerCase(Locale.US); + } + + public static String getOSVersion() { + return System.getProperty("os.version").toLowerCase(Locale.US); + } + + /** + * Returns the well-known "user data directory" for the current operating system. + */ + public static File getUserDataDir() { + if (Utilities.isWindows()) + return new File(System.getenv("APPDATA")); + + if (Utilities.isOSX()) + return Paths.get(System.getProperty("user.home"), "Library", "Application Support").toFile(); + + // *nix + return Paths.get(System.getProperty("user.home"), ".local", "share").toFile(); + } + + public static int getMinorVersion() throws InvalidVersionException { + String version = getOSVersion(); + String[] tokens = version.split("\\."); + try { + checkArgument(tokens.length > 1); + return Integer.parseInt(tokens[1]); + } catch (IllegalArgumentException e) { + printSysInfo(); + throw new InvalidVersionException("Version is not in expected format. Version=" + version); + } + } + + public static int getMajorVersion() throws InvalidVersionException { + String version = getOSVersion(); + String[] tokens = version.split("\\."); + try { + checkArgument(tokens.length > 0); + return Integer.parseInt(tokens[0]); + } catch (IllegalArgumentException e) { + printSysInfo(); + throw new InvalidVersionException("Version is not in expected format. Version=" + version); + } + } + + public static String getOSArchitecture() { + String osArch = System.getProperty("os.arch"); + if (isWindows()) { + // See: Like always windows needs extra treatment + // https://stackoverflow.com/questions/20856694/how-to-find-the-os-bit-type + String arch = System.getenv("PROCESSOR_ARCHITECTURE"); + String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); + return arch.endsWith("64") + || wow64Arch != null && wow64Arch.endsWith("64") + ? "64" : "32"; + } else if (osArch.contains("arm")) { + // armv8 is 64 bit, armv7l is 32 bit + return osArch.contains("64") || osArch.contains("v8") ? "64" : "32"; + } else if (isLinux()) { + return osArch.startsWith("i") ? "32" : "64"; + } else { + return osArch.contains("64") ? "64" : osArch; + } + } + + public static void printSysInfo() { + log.info("System info: os.name={}; os.version={}; os.arch={}; sun.arch.data.model={}; JRE={}; JVM={}", + System.getProperty("os.name"), + System.getProperty("os.version"), + System.getProperty("os.arch"), + getJVMArchitecture(), + (System.getProperty("java.runtime.version", "-") + " (" + System.getProperty("java.vendor", "-") + ")"), + (System.getProperty("java.vm.version", "-") + " (" + System.getProperty("java.vm.name", "-") + ")") + ); + } + + public static String getJVMArchitecture() { + return System.getProperty("sun.arch.data.model"); + } + + public static boolean isCorrectOSArchitecture() { + boolean result = getOSArchitecture().endsWith(getJVMArchitecture()); + if (!result) { + log.warn("System.getProperty(\"os.arch\") " + System.getProperty("os.arch")); + log.warn("System.getenv(\"ProgramFiles(x86)\") " + System.getenv("ProgramFiles(x86)")); + log.warn("System.getenv(\"PROCESSOR_ARCHITECTURE\")" + System.getenv("PROCESSOR_ARCHITECTURE")); + log.warn("System.getenv(\"PROCESSOR_ARCHITEW6432\") " + System.getenv("PROCESSOR_ARCHITEW6432")); + log.warn("System.getProperty(\"sun.arch.data.model\") " + System.getProperty("sun.arch.data.model")); + } + return result; + } + + public static void openURI(URI uri) throws IOException { + if (!DesktopUtil.browse(uri)) + throw new IOException("Failed to open URI: " + uri.toString()); + } + + public static void openFile(File file) throws IOException { + if (!DesktopUtil.open(file)) + throw new IOException("Failed to open file: " + file.toString()); + } + + public static String getDownloadOfHomeDir() { + File file = new File(getSystemHomeDirectory() + "/Downloads"); + if (file.exists()) + return file.getAbsolutePath(); + else + return getSystemHomeDirectory(); + } + + + public static void copyToClipboard(String content) { + try { + if (content != null && content.length() > 0) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(content); + clipboard.setContent(clipboardContent); + } + } catch (Throwable e) { + log.error("copyToClipboard failed " + e.getMessage()); + e.printStackTrace(); + } + } + + public static void setThreadName(String name) { + Thread.currentThread().setName(name + "-" + new Random().nextInt(10000)); + } + + public static boolean isDirectory(String path) { + return new File(path).isDirectory(); + } + + public static String getSystemHomeDirectory() { + return Utilities.isWindows() ? System.getenv("USERPROFILE") : System.getProperty("user.home"); + } + + public static String encodeToHex(@Nullable byte[] bytes, boolean allowNullable) { + if (allowNullable) + return bytes != null ? Utils.HEX.encode(bytes) : "null"; + else + return Utils.HEX.encode(checkNotNull(bytes, "bytes must not be null at encodeToHex")); + } + + public static String bytesAsHexString(@Nullable byte[] bytes) { + return encodeToHex(bytes, true); + } + + public static String encodeToHex(@Nullable byte[] bytes) { + return encodeToHex(bytes, false); + } + + public static byte[] decodeFromHex(String encoded) { + return Utils.HEX.decode(encoded); + } + + public static boolean isAltOrCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) { + return isAltPressed(keyCode, keyEvent) || isCtrlPressed(keyCode, keyEvent); + } + + public static boolean isCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) { + return new KeyCodeCombination(keyCode, KeyCombination.SHORTCUT_DOWN).match(keyEvent) || + new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN).match(keyEvent); + } + + public static boolean isAltPressed(KeyCode keyCode, KeyEvent keyEvent) { + return new KeyCodeCombination(keyCode, KeyCombination.ALT_DOWN).match(keyEvent); + } + + public static boolean isCtrlShiftPressed(KeyCode keyCode, KeyEvent keyEvent) { + return new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent); + } + + public static byte[] concatenateByteArrays(byte[] array1, byte[] array2) { + return ArrayUtils.addAll(array1, array2); + } + + public static Date getUTCDate(int year, int month, int dayOfMonth) { + GregorianCalendar calendar = new GregorianCalendar(year, month, dayOfMonth); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + return calendar.getTime(); + } + + /** + * @param stringList String of comma separated tokens. + * @param allowWhitespace If white space inside the list tokens is allowed. If not the token will be ignored. + * @return Set of tokens + */ + public static Set commaSeparatedListToSet(String stringList, boolean allowWhitespace) { + if (stringList != null) { + return Splitter.on(",") + .splitToList(allowWhitespace ? stringList : StringUtils.deleteWhitespace(stringList)) + .stream() + .filter(e -> !e.isEmpty()) + .collect(Collectors.toSet()); + } else { + return new HashSet<>(); + } + } + + public static String getPathOfCodeSource() throws URISyntaxException { + return new File(Utilities.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); + } + + private static class AnnotationExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(JsonExclude.class) != null; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + } + + public static String toTruncatedString(Object message) { + return toTruncatedString(message, 200, true); + } + + public static String toTruncatedString(Object message, int maxLength) { + return toTruncatedString(message, maxLength, true); + } + + public static String toTruncatedString(Object message, int maxLength, boolean removeLineBreaks) { + if (message == null) + return "null"; + + + String result = StringUtils.abbreviate(message.toString(), maxLength); + if (removeLineBreaks) + return result.replace("\n", ""); + + return result; + + } + + public static String getRandomPrefix(int minLength, int maxLength) { + int length = minLength + new Random().nextInt(maxLength - minLength + 1); + String result; + switch (new Random().nextInt(3)) { + case 0: + result = RandomStringUtils.randomAlphabetic(length); + break; + case 1: + result = RandomStringUtils.randomNumeric(length); + break; + case 2: + default: + result = RandomStringUtils.randomAlphanumeric(length); + } + + switch (new Random().nextInt(3)) { + case 0: + result = result.toUpperCase(); + break; + case 1: + result = result.toLowerCase(); + break; + case 2: + default: + } + + return result; + } + + public static String getShortId(String id) { + return getShortId(id, "-"); + } + + @SuppressWarnings("SameParameterValue") + public static String getShortId(String id, String sep) { + String[] chunks = id.split(sep); + if (chunks.length > 0) + return chunks[0]; + else + return id.substring(0, Math.min(8, id.length())); + } + + public static byte[] integerToByteArray(int intValue, int numBytes) { + byte[] bytes = new byte[numBytes]; + for (int i = numBytes - 1; i >= 0; i--) { + bytes[i] = ((byte) (intValue & 0xFF)); + intValue >>>= 8; + } + return bytes; + } + + public static int byteArrayToInteger(byte[] bytes) { + int result = 0; + for (byte aByte : bytes) { + result = result << 8 | aByte & 0xff; + } + return result; + } + + // Helper to filter unique elements by key + public static Predicate distinctByKey(Function keyExtractor) { + Map map = new ConcurrentHashMap<>(); + return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + + public static String readableFileSize(long size) { + if (size <= 0) return "0"; + String[] units = new String[]{"B", "kB", "MB", "GB", "TB"}; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.###").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } + + // Substitute for FormattingUtils if there is no dependency to core + public static String formatDurationAsWords(long durationMillis) { + String format = ""; + String second = "second"; + String minute = "minute"; + String hour = "hour"; + String day = "day"; + String days = "days"; + String hours = "hours"; + String minutes = "minutes"; + String seconds = "seconds"; + + if (durationMillis >= TimeUnit.DAYS.toMillis(1)) { + format = "d\' " + days + ", \'"; + } + + format += "H\' " + hours + ", \'m\' " + minutes + ", \'s\'.\'S\' " + seconds + "\'"; + + String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : ""; + + duration = StringUtils.replacePattern(duration, "^1 " + seconds + "|\\b1 " + seconds, "1 " + second); + duration = StringUtils.replacePattern(duration, "^1 " + minutes + "|\\b1 " + minutes, "1 " + minute); + duration = StringUtils.replacePattern(duration, "^1 " + hours + "|\\b1 " + hours, "1 " + hour); + duration = StringUtils.replacePattern(duration, "^1 " + days + "|\\b1 " + days, "1 " + day); + + duration = duration.replace(", 0 seconds", ""); + duration = duration.replace(", 0 minutes", ""); + duration = duration.replace(", 0 hours", ""); + duration = StringUtils.replacePattern(duration, "^0 days, ", ""); + duration = StringUtils.replacePattern(duration, "^0 hours, ", ""); + duration = StringUtils.replacePattern(duration, "^0 minutes, ", ""); + duration = StringUtils.replacePattern(duration, "^0 seconds, ", ""); + + String result = duration.trim(); + if (result.isEmpty()) { + result = "0.000 seconds"; + } + return result; + } +} diff --git a/common/src/test/java/bisq/common/app/CapabilitiesTest.java b/common/src/test/java/bisq/common/app/CapabilitiesTest.java new file mode 100644 index 0000000000..f939ff9cd8 --- /dev/null +++ b/common/src/test/java/bisq/common/app/CapabilitiesTest.java @@ -0,0 +1,153 @@ +/* + * 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 bisq.common.app; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; + +import static bisq.common.app.Capability.DAO_FULL_NODE; +import static bisq.common.app.Capability.SEED_NODE; +import static bisq.common.app.Capability.TRADE_STATISTICS; +import static bisq.common.app.Capability.TRADE_STATISTICS_2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CapabilitiesTest { + + @Test + public void testNoCapabilitiesAvailable() { + Capabilities DUT = new Capabilities(); + + assertTrue(DUT.containsAll(new HashSet<>())); + assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); + } + + @Test + public void testHasLess() { + assertTrue(new Capabilities().hasLess(new Capabilities(SEED_NODE))); + assertFalse(new Capabilities().hasLess(new Capabilities())); + assertFalse(new Capabilities(SEED_NODE).hasLess(new Capabilities())); + assertTrue(new Capabilities(SEED_NODE).hasLess(new Capabilities(DAO_FULL_NODE))); + assertFalse(new Capabilities(DAO_FULL_NODE).hasLess(new Capabilities(SEED_NODE))); + + Capabilities all = new Capabilities( + TRADE_STATISTICS, + TRADE_STATISTICS_2, + Capability.ACCOUNT_AGE_WITNESS, + Capability.ACK_MSG, + Capability.PROPOSAL, + Capability.BLIND_VOTE, + Capability.DAO_STATE, + Capability.BUNDLE_OF_ENVELOPES, + Capability.MEDIATION, + Capability.SIGNED_ACCOUNT_AGE_WITNESS, + Capability.REFUND_AGENT, + Capability.TRADE_STATISTICS_HASH_UPDATE + ); + Capabilities other = new Capabilities( + TRADE_STATISTICS, + TRADE_STATISTICS_2, + Capability.ACCOUNT_AGE_WITNESS, + Capability.ACK_MSG, + Capability.PROPOSAL, + Capability.BLIND_VOTE, + Capability.DAO_STATE, + Capability.BUNDLE_OF_ENVELOPES, + Capability.MEDIATION, + Capability.SIGNED_ACCOUNT_AGE_WITNESS, + Capability.REFUND_AGENT, + Capability.TRADE_STATISTICS_HASH_UPDATE, + Capability.NO_ADDRESS_PRE_FIX + ); + + assertTrue(all.hasLess(other)); + } + + @Test + public void testO() { + Capabilities DUT = new Capabilities(TRADE_STATISTICS); + + assertTrue(DUT.containsAll(new HashSet<>())); + } + + @Test + public void testSingleMatch() { + Capabilities DUT = new Capabilities(TRADE_STATISTICS); + + // single match + assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS))); + assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); + } + + @Test + public void testMultiMatch() { + Capabilities DUT = new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2); + + assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS))); + assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); + assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2))); + assertFalse(DUT.containsAll(new Capabilities(SEED_NODE, TRADE_STATISTICS_2))); + } + + @Test + public void testToIntList() { + assertEquals(Collections.emptyList(), Capabilities.toIntList(new Capabilities())); + assertEquals(Collections.singletonList(12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION))); + assertEquals(Arrays.asList(6, 12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE))); + } + + @Test + public void testFromIntList() { + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.emptyList())); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Collections.singletonList(12))); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(6, 12))); + + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(-1))); + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(99))); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(-6, 12))); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(12, 99))); + } + + @Test + public void testToStringList() { + assertEquals("", new Capabilities().toStringList()); + assertEquals("12", new Capabilities(Capability.MEDIATION).toStringList()); + assertEquals("6, 12", new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION).toStringList()); + // capabilities gets sorted, independent of our order + assertEquals("6, 12", new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE).toStringList()); + } + + @Test + public void testFromStringList() { + assertEquals(new Capabilities(), Capabilities.fromStringList(null)); + assertEquals(new Capabilities(), Capabilities.fromStringList("")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12")); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("6,12")); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("12, 6")); + assertEquals(new Capabilities(), Capabilities.fromStringList("a")); + assertEquals(new Capabilities(), Capabilities.fromStringList("99")); + assertEquals(new Capabilities(), Capabilities.fromStringList("-1")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, a")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, 99")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("a,12, 99")); + } +} diff --git a/common/src/test/java/bisq/common/app/VersionTest.java b/common/src/test/java/bisq/common/app/VersionTest.java new file mode 100644 index 0000000000..bd01e840a8 --- /dev/null +++ b/common/src/test/java/bisq/common/app/VersionTest.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.common.app; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class VersionTest { + + @Test + public void testVersionNumber() { + assertEquals(0, Version.getMajorVersion("0.0.0")); + assertEquals(1, Version.getMajorVersion("1.0.0")); + + assertEquals(0, Version.getMinorVersion("0.0.0")); + assertEquals(5, Version.getMinorVersion("0.5.0")); + + assertEquals(0, Version.getPatchVersion("0.0.0")); + assertEquals(5, Version.getPatchVersion("0.0.5")); + } + + @Test + public void testIsNewVersion() { + assertFalse(Version.isNewVersion("0.0.0", "0.0.0")); + assertTrue(Version.isNewVersion("0.1.0", "0.0.0")); + assertTrue(Version.isNewVersion("0.0.1", "0.0.0")); + assertTrue(Version.isNewVersion("1.0.0", "0.0.0")); + assertTrue(Version.isNewVersion("0.5.1", "0.5.0")); + assertFalse(Version.isNewVersion("0.5.0", "0.5.1")); + assertTrue(Version.isNewVersion("0.6.0", "0.5.0")); + assertTrue(Version.isNewVersion("0.6.0", "0.5.1")); + assertFalse(Version.isNewVersion("0.5.0", "1.5.0")); + assertFalse(Version.isNewVersion("0.4.9", "0.5.0")); + } +} diff --git a/common/src/test/java/bisq/common/config/ConfigFileEditorTests.java b/common/src/test/java/bisq/common/config/ConfigFileEditorTests.java new file mode 100644 index 0000000000..4819c1bd84 --- /dev/null +++ b/common/src/test/java/bisq/common/config/ConfigFileEditorTests.java @@ -0,0 +1,120 @@ +package bisq.common.config; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class ConfigFileEditorTests { + + private File file; + private PrintWriter writer; + private ConfigFileReader reader; + private ConfigFileEditor editor; + + @Before + public void setUp() throws IOException { + file = File.createTempFile("bisq", "properties"); + reader = new ConfigFileReader(file); + editor = new ConfigFileEditor(file); + writer = new PrintWriter(file); + } + + @Test + public void whenFileDoesNotExist_thenSetOptionCreatesItAndAppendsOneLine() { + writer.close(); + assertTrue(file.delete()); + + editor.setOption("opt1", "val1"); + + assertThat(reader.getLines(), contains("opt1=val1")); + } + + @Test + public void whenFileContainsOptionBeingSet_thenSetOptionOverwritesIt() { + writer.println("opt1=val1"); + writer.println("opt2=val2"); + writer.println("opt3=val3"); + writer.flush(); + + editor.setOption("opt2", "newval2"); + + assertThat(reader.getLines(), contains( + "opt1=val1", + "opt2=newval2", + "opt3=val3")); + } + + @Test + public void whenOptionBeingSetHasNoArg_thenSetOptionWritesItWithNoEqualsSign() { + writer.println("opt1=val1"); + writer.println("opt2=val2"); + writer.flush(); + + editor.setOption("opt3"); + + assertThat(reader.getLines(), contains( + "opt1=val1", + "opt2=val2", + "opt3")); + } + + @Test + public void whenFileHasBlankOrCommentLines_thenTheyArePreserved() { + writer.println("# Comment 1"); + writer.println("opt1=val1"); + writer.println(); + writer.println("# Comment 2"); + writer.println("opt2=val2"); + writer.flush(); + + editor.setOption("opt3=val3"); + + assertThat(reader.getLines(), contains( + "# Comment 1", + "opt1=val1", + "", + "# Comment 2", + "opt2=val2", + "opt3=val3")); + } + + @Test + public void whenFileContainsOptionBeingCleared_thenClearOptionRemovesIt() { + writer.println("opt1=val1"); + writer.println("opt2=val2"); + writer.flush(); + + editor.clearOption("opt2"); + + assertThat(reader.getLines(), contains("opt1=val1")); + } + + @Test + public void whenFileDoesNotContainOptionBeingCleared_thenClearOptionIsNoOp() { + writer.println("opt1=val1"); + writer.println("opt2=val2"); + writer.flush(); + + editor.clearOption("opt3"); + + assertThat(reader.getLines(), contains( + "opt1=val1", + "opt2=val2")); + } + + @Test + public void whenFileDoesNotExist_thenClearOptionIsNoOp() { + writer.close(); + assertTrue(file.delete()); + editor.clearOption("opt1"); + assertFalse(file.exists()); + } +} diff --git a/common/src/test/java/bisq/common/config/ConfigFileOptionTests.java b/common/src/test/java/bisq/common/config/ConfigFileOptionTests.java new file mode 100644 index 0000000000..a8de344b27 --- /dev/null +++ b/common/src/test/java/bisq/common/config/ConfigFileOptionTests.java @@ -0,0 +1,45 @@ +package bisq.common.config; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ConfigFileOptionTests { + + @Test + public void whenOptionHasWhitespaceAroundEqualsSign_thenItGetsTrimmed() { + String value = "name1 = arg1"; + ConfigFileOption option = ConfigFileOption.parse(value); + assertThat(option.name, equalTo("name1")); + assertThat(option.arg, equalTo("arg1")); + assertThat(option.toString(), equalTo("name1=arg1")); + } + + @Test + public void whenOptionHasLeadingOrTrailingWhitespace_thenItGetsTrimmed() { + String value = " name1=arg1 "; + ConfigFileOption option = ConfigFileOption.parse(value); + assertThat(option.name, equalTo("name1")); + assertThat(option.arg, equalTo("arg1")); + assertThat(option.toString(), equalTo("name1=arg1")); + } + + @Test + public void whenOptionHasEscapedColons_thenTheyGetUnescaped() { + String value = "host1=example.com\\:8080"; + ConfigFileOption option = ConfigFileOption.parse(value); + assertThat(option.name, equalTo("host1")); + assertThat(option.arg, equalTo("example.com:8080")); + assertThat(option.toString(), equalTo("host1=example.com:8080")); + } + + @Test + public void whenOptionHasNoValue_thenItSetsEmptyValue() { + String value = "host1="; + ConfigFileOption option = ConfigFileOption.parse(value); + assertThat(option.name, equalTo("host1")); + assertThat(option.arg, equalTo("")); + assertThat(option.toString(), equalTo("host1=")); + } +} diff --git a/common/src/test/java/bisq/common/config/ConfigFileReaderTests.java b/common/src/test/java/bisq/common/config/ConfigFileReaderTests.java new file mode 100644 index 0000000000..e0076bc822 --- /dev/null +++ b/common/src/test/java/bisq/common/config/ConfigFileReaderTests.java @@ -0,0 +1,79 @@ +package bisq.common.config; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class ConfigFileReaderTests { + + private File file; + private PrintWriter writer; + private ConfigFileReader reader; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() throws IOException { + file = File.createTempFile("bisq", "properties"); + reader = new ConfigFileReader(file); + writer = new PrintWriter(file); + } + + @Test + public void whenFileDoesNotExist_thenGetLinesThrows() { + writer.close(); + assertTrue(file.delete()); + + exception.expect(ConfigException.class); + exception.expectMessage(containsString("Config file")); + exception.expectMessage(containsString("does not exist")); + + reader.getLines(); + } + + @Test + public void whenOptionHasWhitespaceAroundEqualsSign_thenGetLinesPreservesIt() { + writer.println("name1 =arg1"); + writer.println("name2= arg2"); + writer.println("name3 = arg3"); + writer.flush(); + + assertThat(reader.getLines(), contains( + "name1 =arg1", + "name2= arg2", + "name3 = arg3")); + } + + @Test + public void whenOptionHasEscapedColons_thenTheyGetUnescaped() { + writer.println("host1=example.com\\:8080"); + writer.println("host2=example.org:8080"); + writer.flush(); + + assertThat(reader.getLines(), contains( + "host1=example.com:8080", + "host2=example.org:8080")); + } + + @Test + public void whenFileContainsNonOptionLines_getOptionLinesReturnsOnlyOptionLines() { + writer.println("# Comment"); + writer.println(""); + writer.println("name1=arg1"); + writer.println("noArgOpt"); + writer.flush(); + + assertThat(reader.getOptionLines(), contains("name1=arg1", "noArgOpt")); + } +} diff --git a/common/src/test/java/bisq/common/config/ConfigTests.java b/common/src/test/java/bisq/common/config/ConfigTests.java new file mode 100644 index 0000000000..fbc4015193 --- /dev/null +++ b/common/src/test/java/bisq/common/config/ConfigTests.java @@ -0,0 +1,294 @@ +package bisq.common.config; + +import java.nio.file.Files; +import java.nio.file.Path; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static bisq.common.config.Config.*; +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.isEmptyString; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class ConfigTests { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + // Note: "DataDirProperties" in the test method names below represent the group of + // configuration options that influence the location of a Bisq node's data directory. + // These options include appName, userDataDir, appDataDir and configFile + + @Test + public void whenNoArgCtorIsCalled_thenDefaultAppNameIsSetToTempValue() { + Config config = new Config(); + String defaultAppName = config.defaultAppName; + String regex = "Bisq\\d{2,}Temp"; + assertTrue(format("Temp app name '%s' failed to match '%s'", defaultAppName, regex), + defaultAppName.matches(regex)); + } + + @Test + public void whenAppNameOptionIsSet_thenAppNamePropertyDiffersFromDefaultAppNameProperty() { + Config config = configWithOpts(opt(APP_NAME, "My-Bisq")); + assertThat(config.appName, equalTo("My-Bisq")); + assertThat(config.appName, not(equalTo(config.defaultAppName))); + } + + @Test + public void whenNoOptionsAreSet_thenDataDirPropertiesEqualDefaultValues() { + Config config = new Config(); + assertThat(config.appName, equalTo(config.defaultAppName)); + assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); + assertThat(config.appDataDir, equalTo(config.defaultAppDataDir)); + assertThat(config.configFile, equalTo(config.defaultConfigFile)); + } + + @Test + public void whenAppNameOptionIsSet_thenDataDirPropertiesReflectItsValue() { + Config config = configWithOpts(opt(APP_NAME, "My-Bisq")); + assertThat(config.appName, equalTo("My-Bisq")); + assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); + assertThat(config.appDataDir, equalTo(new File(config.userDataDir, "My-Bisq"))); + assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); + } + + @Test + public void whenAppDataDirOptionIsSet_thenDataDirPropertiesReflectItsValue() throws IOException { + File appDataDir = Files.createTempDirectory("myapp").toFile(); + Config config = configWithOpts(opt(APP_DATA_DIR, appDataDir)); + assertThat(config.appName, equalTo(config.defaultAppName)); + assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); + assertThat(config.appDataDir, equalTo(appDataDir)); + assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); + } + + @Test + public void whenUserDataDirOptionIsSet_thenDataDirPropertiesReflectItsValue() throws IOException { + File userDataDir = Files.createTempDirectory("myuserdata").toFile(); + Config config = configWithOpts(opt(USER_DATA_DIR, userDataDir)); + assertThat(config.appName, equalTo(config.defaultAppName)); + assertThat(config.userDataDir, equalTo(userDataDir)); + assertThat(config.appDataDir, equalTo(new File(userDataDir, config.defaultAppName))); + assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); + } + + @Test + public void whenAppNameAndAppDataDirOptionsAreSet_thenDataDirPropertiesReflectTheirValues() throws IOException { + File appDataDir = Files.createTempDirectory("myapp").toFile(); + Config config = configWithOpts(opt(APP_NAME, "My-Bisq"), opt(APP_DATA_DIR, appDataDir)); + assertThat(config.appName, equalTo("My-Bisq")); + assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); + assertThat(config.appDataDir, equalTo(appDataDir)); + assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); + } + + @Test + public void whenOptionIsSetAtCommandLineAndInConfigFile_thenCommandLineValueTakesPrecedence() throws IOException { + File configFile = File.createTempFile("bisq", "properties"); + try (PrintWriter writer = new PrintWriter(configFile)) { + writer.println(new ConfigFileOption(APP_NAME, "Bisq-configFileValue")); + } + Config config = configWithOpts(opt(APP_NAME, "Bisq-commandLineValue")); + assertThat(config.appName, equalTo("Bisq-commandLineValue")); + } + + @Test + public void whenUnrecognizedOptionIsSet_thenConfigExceptionIsThrown() { + exceptionRule.expect(ConfigException.class); + exceptionRule.expectMessage("problem parsing option 'bogus': bogus is not a recognized option"); + configWithOpts(opt("bogus")); + } + + @Test + public void whenUnrecognizedOptionIsSetInConfigFile_thenNoExceptionIsThrown() throws IOException { + File configFile = File.createTempFile("bisq", "properties"); + try (PrintWriter writer = new PrintWriter(configFile)) { + writer.println(new ConfigFileOption("bogusOption", "bogusValue")); + writer.println(new ConfigFileOption(APP_NAME, "BisqTest")); + } + Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); + assertThat(config.appName, equalTo("BisqTest")); + } + + @Test + public void whenOptionFileArgumentDoesNotExist_thenConfigExceptionIsThrown() { + String filepath = "/does/not/exist"; + if (System.getProperty("os.name").startsWith("Windows")) { + filepath = "C:\\does\\not\\exist"; + } + exceptionRule.expect(ConfigException.class); + exceptionRule.expectMessage(format("problem parsing option 'torrcFile': File [%s] does not exist", filepath)); + configWithOpts(opt(TORRC_FILE, filepath)); + } + + @Test + public void whenConfigFileOptionIsSetToNonExistentFile_thenConfigExceptionIsThrown() { + String filepath = "/no/such/bisq.properties"; + if (System.getProperty("os.name").startsWith("Windows")) { + filepath = "C:\\no\\such\\bisq.properties"; + } + exceptionRule.expect(ConfigException.class); + exceptionRule.expectMessage(format("The specified config file '%s' does not exist", filepath)); + configWithOpts(opt(CONFIG_FILE, filepath)); + } + + @Test + public void whenConfigFileOptionIsSetInConfigFile_thenConfigExceptionIsThrown() throws IOException { + File configFile = File.createTempFile("bisq", "properties"); + try (PrintWriter writer = new PrintWriter(configFile)) { + writer.println(new ConfigFileOption(CONFIG_FILE, "/tmp/other.bisq.properties")); + } + exceptionRule.expect(ConfigException.class); + exceptionRule.expectMessage(format("The '%s' option is disallowed in config files", CONFIG_FILE)); + configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); + } + + @Test + public void whenConfigFileOptionIsSetToExistingFile_thenConfigFilePropertyReflectsItsValue() throws IOException { + File configFile = File.createTempFile("bisq", "properties"); + Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); + assertThat(config.configFile, equalTo(configFile)); + } + + @Test + public void whenConfigFileOptionIsSetToRelativePath_thenThePathIsPrefixedByAppDataDir() throws IOException { + File configFile = Files.createTempFile("my-bisq", ".properties").toFile(); + File appDataDir = configFile.getParentFile(); + String relativeConfigFilePath = configFile.getName(); + Config config = configWithOpts(opt(APP_DATA_DIR, appDataDir), opt(CONFIG_FILE, relativeConfigFilePath)); + assertThat(config.configFile, equalTo(configFile)); + } + + @Test + public void whenAppNameIsSetInConfigFile_thenDataDirPropertiesReflectItsValue() throws IOException { + File configFile = File.createTempFile("bisq", "properties"); + try (PrintWriter writer = new PrintWriter(configFile)) { + writer.println(new ConfigFileOption(APP_NAME, "My-Bisq")); + } + Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); + assertThat(config.appName, equalTo("My-Bisq")); + assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); + assertThat(config.appDataDir, equalTo(new File(config.userDataDir, config.appName))); + assertThat(config.configFile, equalTo(configFile)); + } + + @Test + public void whenBannedBtcNodesOptionIsSet_thenBannedBtcNodesPropertyReturnsItsValue() { + Config config = configWithOpts(opt(BANNED_BTC_NODES, "foo.onion:8333,bar.onion:8333")); + assertThat(config.bannedBtcNodes, contains("foo.onion:8333", "bar.onion:8333")); + } + + @Test + public void whenHelpOptionIsSet_thenIsHelpRequestedIsTrue() { + assertFalse(new Config().helpRequested); + assertTrue(configWithOpts(opt(HELP)).helpRequested); + } + + @Test + public void whenConfigIsConstructed_thenNoConsoleOutputSideEffectsShouldOccur() { + PrintStream outOrig = System.out; + PrintStream errOrig = System.err; + ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); + ByteArrayOutputStream errBytes = new ByteArrayOutputStream(); + try (PrintStream outTest = new PrintStream(outBytes); + PrintStream errTest = new PrintStream(errBytes)) { + System.setOut(outTest); + System.setErr(errTest); + new Config(); + assertThat(outBytes.toString(), isEmptyString()); + assertThat(errBytes.toString(), isEmptyString()); + } finally { + System.setOut(outOrig); + System.setErr(errOrig); + } + } + + @Test + public void whenConfigIsConstructed_thenAppDataDirAndSubdirsAreCreated() { + Config config = new Config(); + assertTrue(config.appDataDir.exists()); + assertTrue(config.keyStorageDir.exists()); + assertTrue(config.storageDir.exists()); + assertTrue(config.torDir.exists()); + assertTrue(config.walletDir.exists()); + } + + @Test + public void whenAppDataDirCannotBeCreated_thenUncheckedIoExceptionIsThrown() throws IOException { + // set a userDataDir that is actually a file so appDataDir cannot be created + File aFile = Files.createTempFile("A", "File").toFile(); + exceptionRule.expect(UncheckedIOException.class); + exceptionRule.expectMessage(containsString("Application data directory")); + exceptionRule.expectMessage(containsString("could not be created")); + configWithOpts(opt(USER_DATA_DIR, aFile)); + } + + @Test + public void whenAppDataDirIsSymbolicLink_thenAppDataDirCreationIsNoOp() throws IOException { + Path parentDir = Files.createTempDirectory("parent"); + Path targetDir = parentDir.resolve("target"); + Path symlink = parentDir.resolve("symlink"); + Files.createDirectory(targetDir); + try { + Files.createSymbolicLink(symlink, targetDir); + } catch (Throwable ex) { + // An error occurred trying to create a symbolic link, likely because the + // operating system (e.g. Windows) does not support it, so we abort the test. + return; + } + configWithOpts(opt(APP_DATA_DIR, symlink)); + } + + + // == TEST SUPPORT FACILITIES ======================================================== + + static Config configWithOpts(Opt... opts) { + String[] args = new String[opts.length]; + for (int i = 0; i < opts.length; i++) + args[i] = opts[i].toString(); + return new Config(args); + } + + static Opt opt(String name) { + return new Opt(name); + } + + static Opt opt(String name, Object arg) { + return new Opt(name, arg.toString()); + } + + static class Opt { + private final String name; + private final String arg; + + public Opt(String name) { + this(name, null); + } + + public Opt(String name, String arg) { + this.name = name; + this.arg = arg; + } + + @Override + public String toString() { + return format("--%s%s", name, arg != null ? ("=" + arg) : ""); + } + } +} diff --git a/common/src/test/java/bisq/common/util/MathUtilsTest.java b/common/src/test/java/bisq/common/util/MathUtilsTest.java new file mode 100644 index 0000000000..7b3da8d843 --- /dev/null +++ b/common/src/test/java/bisq/common/util/MathUtilsTest.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.common.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class MathUtilsTest { + + @Test(expected = IllegalArgumentException.class) + public void testRoundDoubleWithInfiniteArg() { + MathUtils.roundDouble(Double.POSITIVE_INFINITY, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testRoundDoubleWithNaNArg() { + MathUtils.roundDouble(Double.NaN, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testRoundDoubleWithNegativePrecision() { + MathUtils.roundDouble(3, -1); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Test + public void testMovingAverageWithoutOutlierExclusion() { + var values = new int[]{4, 5, 3, 1, 2, 4}; + // Moving average = 4, 4.5, 4, 3, 2, 7/3 + var movingAverage = new MathUtils.MovingAverage(3, 0); + int i = 0; + assertEquals(4, movingAverage.next(values[i++]).get(),0.001); + assertEquals(4.5, movingAverage.next(values[i++]).get(),0.001); + assertEquals(4, movingAverage.next(values[i++]).get(),0.001); + assertEquals(3, movingAverage.next(values[i++]).get(),0.001); + assertEquals(2, movingAverage.next(values[i++]).get(),0.001); + assertEquals((double) 7 / 3, movingAverage.next(values[i]).get(),0.001); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Test + public void testMovingAverageWithOutlierExclusion() { + var values = new int[]{100, 102, 95, 101, 120, 115}; + // Moving average = N/A, N/A, 99, 99.333..., N/A, 103.666... + var movingAverage = new MathUtils.MovingAverage(3, 0.2); + int i = 0; + assertFalse(movingAverage.next(values[i++]).isPresent()); + assertFalse(movingAverage.next(values[i++]).isPresent()); + assertEquals(99, movingAverage.next(values[i++]).get(),0.001); + assertEquals(99.333, movingAverage.next(values[i++]).get(),0.001); + assertFalse(movingAverage.next(values[i++]).isPresent()); + assertEquals(103.666, movingAverage.next(values[i]).get(),0.001); + } +} diff --git a/common/src/test/java/bisq/common/util/PermutationTest.java b/common/src/test/java/bisq/common/util/PermutationTest.java new file mode 100644 index 0000000000..dc3c65c513 --- /dev/null +++ b/common/src/test/java/bisq/common/util/PermutationTest.java @@ -0,0 +1,478 @@ +/* + * 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 bisq.common.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class PermutationTest { + + + // @Test + public void testGetPartialList() { + String blindVote0 = "blindVote0"; + String blindVote1 = "blindVote1"; + String blindVote2 = "blindVote2"; + String blindVote3 = "blindVote3"; + String blindVote4 = "blindVote4"; + String blindVote5 = "blindVote5"; + + List list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); + List indicesToRemove = Arrays.asList(0, 3); + List expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote4, blindVote5)); + List result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove nothing + indicesToRemove = new ArrayList<>(); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove first + indicesToRemove = Collections.singletonList(0); + expected = new ArrayList<>(list); + expected.remove(0); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove last + indicesToRemove = Collections.singletonList(5); + expected = new ArrayList<>(list); + expected.remove(5); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove all + indicesToRemove = Arrays.asList(0, 1, 2, 3, 4, 5); + expected = new ArrayList<>(); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // wrong sorting of indices + indicesToRemove = Arrays.asList(4, 0, 1); + expected = expected = new ArrayList<>(Arrays.asList(blindVote2, blindVote3, blindVote5)); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // wrong sorting of indices + indicesToRemove = Arrays.asList(0, 0); + expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // don't remove as invalid index + indicesToRemove = Collections.singletonList(9); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // don't remove as invalid index + indicesToRemove = Collections.singletonList(-2); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + } + + @Test + public void testFindMatchingPermutation() { + String a = "A"; + String b = "B"; + String c = "C"; + String d = "D"; + String e = "E"; + int limit = 1048575; + List result; + List list; + List expected; + BiFunction, Boolean> predicate = (target, variationList) -> variationList.toString().equals(target); + + list = Arrays.asList(a, b, c, d, e); + + expected = Arrays.asList(a); + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + + + expected = Arrays.asList(a, c, e); + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + } + + @Test + public void testBreakAtLimit() { + BiFunction, Boolean> predicate = + (target, variationList) -> variationList.toString().equals(target); + var list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"); + var expected = Arrays.asList("b", "g", "m"); + + // Takes around 32508 tries starting from longer strings + var limit = 100000; + var result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + limit = 1000; + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(result.isEmpty()); + } + + + // @Test + public void testFindAllPermutations() { + String blindVote0 = "blindVote0"; + String blindVote1 = "blindVote1"; + String blindVote2 = "blindVote2"; + String blindVote3 = "blindVote3"; + String blindVote4 = "blindVote4"; + + // Up to about 1M iterations performance is acceptable (0.5 sec) + // findAllPermutations took 580 ms for 20 items and 1048575 iterations + // findAllPermutations took 10 ms for 15 items and 32767 iterations + // findAllPermutations took 0 ms for 10 items and 1023 iterations + // int limit = 1048575; + int limit = 1048575000; + List list; + List> expected; + List> result; + List subList; + + + list = new ArrayList<>(); + /* for (int i = 0; i < 4; i++) { + list.add("blindVote" + i); + }*/ + + PermutationUtil.findAllPermutations(list, limit); + + + list = new ArrayList<>(); + expected = new ArrayList<>(); + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + list = new ArrayList<>(Arrays.asList(blindVote0)); + expected = new ArrayList<>(); + expected.add(list); + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 2 items -> 3 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 3 items -> 7 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 4 items -> 15 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(3))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + + // 5 items -> 31 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(3))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(4))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + + } + + +} diff --git a/common/src/test/java/bisq/common/util/PreconditionsTests.java b/common/src/test/java/bisq/common/util/PreconditionsTests.java new file mode 100644 index 0000000000..23616faf47 --- /dev/null +++ b/common/src/test/java/bisq/common/util/PreconditionsTests.java @@ -0,0 +1,38 @@ +package bisq.common.util; + +import java.nio.file.Files; + +import java.io.File; +import java.io.IOException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static bisq.common.util.Preconditions.checkDir; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertSame; + +public class PreconditionsTests { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void whenDirIsValid_thenDirIsReturned() throws IOException { + File dir = Files.createTempDirectory("TestDir").toFile(); + File ret = checkDir(dir); + assertSame(dir, ret); + } + + @Test + public void whenDirDoesNotExist_thenThrow() { + String filepath = "/does/not/exist"; + if (System.getProperty("os.name").startsWith("Windows")) { + filepath = "C:\\does\\not\\exist"; + } + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage(equalTo(String.format("Directory '%s' does not exist", filepath))); + checkDir(new File(filepath)); + } +} diff --git a/common/src/test/java/bisq/common/util/UtilitiesTest.java b/common/src/test/java/bisq/common/util/UtilitiesTest.java new file mode 100644 index 0000000000..7c315d70c2 --- /dev/null +++ b/common/src/test/java/bisq/common/util/UtilitiesTest.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.common.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class UtilitiesTest { + + @Test + public void testToStringList() { + assertTrue(Utilities.commaSeparatedListToSet(null, false).isEmpty()); + assertTrue(Utilities.commaSeparatedListToSet(null, true).isEmpty()); + assertTrue(Utilities.commaSeparatedListToSet("", false).isEmpty()); + assertTrue(Utilities.commaSeparatedListToSet("", true).isEmpty()); + assertTrue(Utilities.commaSeparatedListToSet(" ", false).isEmpty()); + assertEquals(1, Utilities.commaSeparatedListToSet(" ", true).size()); + assertTrue(Utilities.commaSeparatedListToSet(",", false).isEmpty()); + assertTrue(Utilities.commaSeparatedListToSet(",", true).isEmpty()); + assertEquals(1, Utilities.commaSeparatedListToSet(",test1", false).size()); + assertEquals(1, Utilities.commaSeparatedListToSet(", , test1", false).size()); + assertEquals(2, Utilities.commaSeparatedListToSet(", , test1", true).size()); + assertEquals(1, Utilities.commaSeparatedListToSet("test1,", false).size()); + assertEquals(1, Utilities.commaSeparatedListToSet("test1, ,", false).size()); + assertEquals(1, Utilities.commaSeparatedListToSet("test1", false).size()); + assertEquals(2, Utilities.commaSeparatedListToSet("test1, test2", false).size()); + } + + @Test + public void testIntegerToByteArray() { + assertEquals("0000", Utilities.bytesAsHexString(Utilities.integerToByteArray(0, 2))); + assertEquals("ffff", Utilities.bytesAsHexString(Utilities.integerToByteArray(65535, 2))); + assertEquals("0011", Utilities.bytesAsHexString(Utilities.integerToByteArray(17, 2))); + assertEquals("1100", Utilities.bytesAsHexString(Utilities.integerToByteArray(4352, 2))); + assertEquals("dd22", Utilities.bytesAsHexString(Utilities.integerToByteArray(56610, 2))); + assertEquals("7fffffff", Utilities.bytesAsHexString(Utilities.integerToByteArray(2147483647, 4))); // Integer.MAX_VALUE + assertEquals("80000000", Utilities.bytesAsHexString(Utilities.integerToByteArray(-2147483648, 4))); // Integer.MIN_VALUE + assertEquals("00110011", Utilities.bytesAsHexString(Utilities.integerToByteArray(1114129, 4))); + assertEquals("ffeeffef", Utilities.bytesAsHexString(Utilities.integerToByteArray(-1114129, 4))); + } + + @Test + public void testByteArrayToInteger() { + assertEquals(0, Utilities.byteArrayToInteger(Utilities.decodeFromHex("0000"))); + assertEquals(65535, Utilities.byteArrayToInteger(Utilities.decodeFromHex("ffff"))); + assertEquals(4352, Utilities.byteArrayToInteger(Utilities.decodeFromHex("1100"))); + assertEquals(17, Utilities.byteArrayToInteger(Utilities.decodeFromHex("0011"))); + assertEquals(56610, Utilities.byteArrayToInteger(Utilities.decodeFromHex("dd22"))); + assertEquals(2147483647, Utilities.byteArrayToInteger(Utilities.decodeFromHex("7fffffff"))); + assertEquals(-2147483648, Utilities.byteArrayToInteger(Utilities.decodeFromHex("80000000"))); + assertEquals(1114129, Utilities.byteArrayToInteger(Utilities.decodeFromHex("00110011"))); + assertEquals(-1114129, Utilities.byteArrayToInteger(Utilities.decodeFromHex("ffeeffef"))); + } +} diff --git a/core/.tx/config b/core/.tx/config new file mode 100644 index 0000000000..d5c34aeba3 --- /dev/null +++ b/core/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[bisq-desktop.displaystringsproperties] +file_filter = translations/bisq-desktop.displaystringsproperties/.properties +source_lang = en +type = UNICODEPROPERTIES + diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitness.java b/core/src/main/java/bisq/core/account/sign/SignedWitness.java new file mode 100644 index 0000000000..b7e908699d --- /dev/null +++ b/core/src/main/java/bisq/core/account/sign/SignedWitness.java @@ -0,0 +1,188 @@ +/* + * 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 bisq.core.account.sign; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; +import bisq.network.p2p.storage.payload.DateTolerantPayload; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.crypto.Hash; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; + +import java.time.Clock; +import java.time.Instant; + +import java.util.concurrent.TimeUnit; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +// Supports signatures made from EC key (arbitrators) and signature created with DSA key. +@Slf4j +@Value +public class SignedWitness implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, + DateTolerantPayload, CapabilityRequiringPayload { + + public enum VerificationMethod { + ARBITRATOR, + TRADE; + + public static SignedWitness.VerificationMethod fromProto(protobuf.SignedWitness.VerificationMethod method) { + return ProtoUtil.enumFromProto(SignedWitness.VerificationMethod.class, method.name()); + } + + public static protobuf.SignedWitness.VerificationMethod toProtoMessage(SignedWitness.VerificationMethod method) { + return protobuf.SignedWitness.VerificationMethod.valueOf(method.name()); + } + } + + private static final long TOLERANCE = TimeUnit.DAYS.toMillis(1); + + private final VerificationMethod verificationMethod; + private final byte[] accountAgeWitnessHash; + private final byte[] signature; + private final byte[] signerPubKey; + private final byte[] witnessOwnerPubKey; + private final long date; + private final long tradeAmount; + + transient private final byte[] hash; + + public SignedWitness(VerificationMethod verificationMethod, + byte[] accountAgeWitnessHash, + byte[] signature, + byte[] signerPubKey, + byte[] witnessOwnerPubKey, + long date, + long tradeAmount) { + this.verificationMethod = verificationMethod; + this.accountAgeWitnessHash = accountAgeWitnessHash.clone(); + this.signature = signature.clone(); + this.signerPubKey = signerPubKey.clone(); + this.witnessOwnerPubKey = witnessOwnerPubKey.clone(); + this.date = date; + this.tradeAmount = tradeAmount; + + // The hash is only using the data which does not change in repeated trades between identical users (no date or amount). + // We only want to store the first and oldest one and will ignore others. That will also help to protect privacy + // so that the total number of trades is not revealed. We use putIfAbsent when we store the data so first + // object will win. We consider one signed trade with one peer enough and do not consider repeated trades with + // same peer to add more security as if that one would be colluding it would be not detected anyway. The total + // number of signed trades with different peers is still available and can be considered more valuable data for + // security. + byte[] data = Utilities.concatenateByteArrays(accountAgeWitnessHash, signature); + data = Utilities.concatenateByteArrays(data, signerPubKey); + hash = Hash.getSha256Ripemd160hash(data); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTOBUF + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + final protobuf.SignedWitness.Builder builder = protobuf.SignedWitness.newBuilder() + .setVerificationMethod(VerificationMethod.toProtoMessage(verificationMethod)) + .setAccountAgeWitnessHash(ByteString.copyFrom(accountAgeWitnessHash)) + .setSignature(ByteString.copyFrom(signature)) + .setSignerPubKey(ByteString.copyFrom(signerPubKey)) + .setWitnessOwnerPubKey(ByteString.copyFrom(witnessOwnerPubKey)) + .setDate(date) + .setTradeAmount(tradeAmount); + return protobuf.PersistableNetworkPayload.newBuilder().setSignedWitness(builder).build(); + } + + public protobuf.SignedWitness toProtoSignedWitness() { + return toProtoMessage().getSignedWitness(); + } + + public static SignedWitness fromProto(protobuf.SignedWitness proto) { + return new SignedWitness( + SignedWitness.VerificationMethod.fromProto(proto.getVerificationMethod()), + proto.getAccountAgeWitnessHash().toByteArray(), + proto.getSignature().toByteArray(), + proto.getSignerPubKey().toByteArray(), + proto.getWitnessOwnerPubKey().toByteArray(), + proto.getDate(), + proto.getTradeAmount()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isDateInTolerance(Clock clock) { + // We don't allow older or newer than 1 day. + // Preventing forward dating is also important to protect against a sophisticated attack + return Math.abs(clock.millis() - date) <= TOLERANCE; + } + + @Override + public boolean verifyHashSize() { + return hash.length == 20; + } + + // Pre 1.0.1 version don't know the new message type and throw an error which leads to disconnecting the peer. + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.SIGNED_ACCOUNT_AGE_WITNESS); + } + + @Override + public byte[] getHash() { + return hash; + } + + public boolean isSignedByArbitrator() { + return verificationMethod == VerificationMethod.ARBITRATOR; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public P2PDataStorage.ByteArray getHashAsByteArray() { + return new P2PDataStorage.ByteArray(hash); + } + + @Override + public String toString() { + return "SignedWitness{" + + "\n verificationMethod=" + verificationMethod + + ",\n witnessHash=" + Utilities.bytesAsHexString(accountAgeWitnessHash) + + ",\n signature=" + Utilities.bytesAsHexString(signature) + + ",\n signerPubKey=" + Utilities.bytesAsHexString(signerPubKey) + + ",\n witnessOwnerPubKey=" + Utilities.bytesAsHexString(witnessOwnerPubKey) + + ",\n date=" + Instant.ofEpochMilli(date) + + ",\n tradeAmount=" + Coin.valueOf(tradeAmount).toFriendlyString() + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java new file mode 100644 index 0000000000..f33dd7d234 --- /dev/null +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -0,0 +1,589 @@ +/* + * 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 bisq.core.account.sign; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.user.User; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.UserThread; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + +import java.security.PublicKey; +import java.security.SignatureException; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Stack; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SignedWitnessService { + public static final long SIGNER_AGE_DAYS = 30; + private static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis(); + public static final Coin MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Coin.parseCoin("0.0025"); + + private final KeyRing keyRing; + private final P2PService p2PService; + private final ArbitratorManager arbitratorManager; + private final SignedWitnessStorageService signedWitnessStorageService; + private final User user; + private final FilterManager filterManager; + + private final Map signedWitnessMap = new HashMap<>(); + + // This map keeps all SignedWitnesses with the same AccountAgeWitnessHash in a Set. + // This avoids iterations over the signedWitnessMap for getting the set of such SignedWitnesses. + private final Map> signedWitnessSetByAccountAgeWitnessHash = new HashMap<>(); + + // Iterating over all SignedWitnesses and do a byte array comparison is a bit expensive and + // it is called at filtering the offer book many times, so we use a lookup map for fast + // access to the set of SignedWitness which match the ownerPubKey. + private final Map> signedWitnessSetByOwnerPubKey = new HashMap<>(); + + // The signature verification calls are rather expensive and called at filtering the offer book many times, + // so we cache the results using the hash as key. The hash is created from the accountAgeWitnessHash and the + // signature. + private final Map verifySignatureWithDSAKeyResultCache = new HashMap<>(); + private final Map verifySignatureWithECKeyResultCache = new HashMap<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public SignedWitnessService(KeyRing keyRing, + P2PService p2PService, + ArbitratorManager arbitratorManager, + SignedWitnessStorageService signedWitnessStorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + User user, + FilterManager filterManager) { + this.keyRing = keyRing; + this.p2PService = p2PService; + this.arbitratorManager = arbitratorManager; + this.signedWitnessStorageService = signedWitnessStorageService; + this.user = user; + this.filterManager = filterManager; + + // We need to add that early (before onAllServicesInitialized) as it will be used at startup. + appendOnlyDataStoreService.addService(signedWitnessStorageService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { + if (payload instanceof SignedWitness) + addToMap((SignedWitness) payload); + }); + + // At startup the P2PDataStorage initializes earlier, otherwise we get the listener called. + signedWitnessStorageService.getMap().values().forEach(e -> { + if (e instanceof SignedWitness) + addToMap((SignedWitness) e); + }); + + if (p2PService.isBootstrapped()) { + onBootstrapComplete(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + onBootstrapComplete(); + } + }); + } + // TODO: Enable cleaning of signed witness list when necessary + // cleanSignedWitnesses(); + } + + private void onBootstrapComplete() { + if (user.getRegisteredArbitrator() != null) { + UserThread.runAfter(this::doRepublishAllSignedWitnesses, 60); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Collection getSignedWitnessMapValues() { + return signedWitnessMap.values(); + } + + /** + * List of dates as long when accountAgeWitness was signed + * + * Witnesses that were added but are no longer considered signed won't be shown + */ + public List getVerifiedWitnessDateList(AccountAgeWitness accountAgeWitness) { + if (!isSignedAccountAgeWitness(accountAgeWitness)) { + return new ArrayList<>(); + } + return getSignedWitnessSet(accountAgeWitness).stream() + .filter(this::verifySignature) + .map(SignedWitness::getDate) + .sorted() + .collect(Collectors.toList()); + } + + /** + * List of dates as long when accountAgeWitness was signed + * Not verifying that signatures are correct + */ + public List getWitnessDateList(AccountAgeWitness accountAgeWitness) { + // We do not validate as it would not make sense to cheat one self... + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::getDate) + .sorted() + .collect(Collectors.toList()); + } + + public boolean isSignedByArbitrator(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::isSignedByArbitrator) + .findAny() + .orElse(false); + } + + public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::getWitnessOwnerPubKey) + .anyMatch(ownerPubKey -> filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); + } + + private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::getWitnessOwnerPubKey) + .findFirst() + .orElse(null); + } + + public String ownerPubKeyAsString(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(signedWitness -> Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey())) + .findFirst() + .orElse(""); + } + + @VisibleForTesting + public Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey) { + return getSignedWitnessMapValues().stream() + .filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey)) + .collect(Collectors.toSet()); + } + + public boolean publishOwnSignedWitness(SignedWitness signedWitness) { + if (!Arrays.equals(signedWitness.getWitnessOwnerPubKey(), keyRing.getPubKeyRing().getSignaturePubKeyBytes()) || + !verifySigner(signedWitness)) { + return false; + } + + log.info("Publish own signedWitness {}", signedWitness); + publishSignedWitness(signedWitness); + return true; + } + + // Arbitrators sign with EC key + public void signAndPublishAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + ECKey key, + PublicKey peersPubKey) { + signAndPublishAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey.getEncoded(), new Date().getTime()); + } + + // Arbitrators sign with EC key + public String signAndPublishAccountAgeWitness(AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] peersPubKey, + long time) { + var witnessPubKey = peersPubKey == null ? ownerPubKey(accountAgeWitness) : peersPubKey; + return signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, key, witnessPubKey, time); + } + + // Arbitrators sign with EC key + public String signTraderPubKey(ECKey key, + byte[] peersPubKey, + long childSignTime) { + var time = childSignTime - SIGNER_AGE - 1; + var dummyAccountAgeWitness = new AccountAgeWitness(Hash.getRipemd160hash(peersPubKey), time); + return signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, dummyAccountAgeWitness, key, peersPubKey, time); + } + + // Arbitrators sign with EC key + private String signAndPublishAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] peersPubKey, + long time) { + if (isSignedAccountAgeWitness(accountAgeWitness)) { + var err = "Arbitrator trying to sign already signed accountagewitness " + accountAgeWitness.toString(); + log.warn(err); + return err; + } + if (peersPubKey == null) { + var err = "Trying to sign accountAgeWitness " + accountAgeWitness.toString() + "\nwith owner pubkey=null"; + log.warn(err); + return err; + } + + String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash()); + String signatureBase64 = key.signMessage(accountAgeWitnessHashAsHex); + SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.ARBITRATOR, + accountAgeWitness.getHash(), + signatureBase64.getBytes(Charsets.UTF_8), + key.getPubKey(), + peersPubKey, + time, + tradeAmount.value); + publishSignedWitness(signedWitness); + log.info("Arbitrator signed witness {}", signedWitness.toString()); + return ""; + } + + public void selfSignAndPublishAccountAgeWitness(AccountAgeWitness accountAgeWitness) throws CryptoException { + log.info("Sign own accountAgeWitness {}", accountAgeWitness); + signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, + keyRing.getSignatureKeyPair().getPublic()); + } + + // Any peer can sign with DSA key + public Optional signAndPublishAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + PublicKey peersPubKey) throws CryptoException { + if (isSignedAccountAgeWitness(accountAgeWitness)) { + log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); + return Optional.empty(); + } + + if (!isSufficientTradeAmountForSigning(tradeAmount)) { + log.warn("Trader tried to sign account with too little trade amount"); + return Optional.empty(); + } + + byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); + SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE, + accountAgeWitness.getHash(), + signature, + keyRing.getSignatureKeyPair().getPublic().getEncoded(), + peersPubKey.getEncoded(), + new Date().getTime(), + tradeAmount.value); + publishSignedWitness(signedWitness); + log.info("Trader signed witness {}", signedWitness.toString()); + return Optional.of(signedWitness); + } + + public boolean verifySignature(SignedWitness signedWitness) { + if (signedWitness.isSignedByArbitrator()) { + return verifySignatureWithECKey(signedWitness); + } else { + return verifySignatureWithDSAKey(signedWitness); + } + } + + private boolean verifySignatureWithECKey(SignedWitness signedWitness) { + P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(signedWitness.getHash()); + if (verifySignatureWithECKeyResultCache.containsKey(hash)) { + return verifySignatureWithECKeyResultCache.get(hash); + } + try { + 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()))) { + key.verifyMessage(message, signatureBase64); + verifySignatureWithECKeyResultCache.put(hash, true); + return true; + } else { + log.warn("Provided EC key is not in list of valid arbitrators."); + verifySignatureWithECKeyResultCache.put(hash, false); + return false; + } + } catch (SignatureException e) { + log.warn("verifySignature signedWitness failed. signedWitness={}", signedWitness); + log.warn("Caused by ", e); + verifySignatureWithECKeyResultCache.put(hash, false); + return false; + } + } + + private boolean verifySignatureWithDSAKey(SignedWitness signedWitness) { + P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(signedWitness.getHash()); + if (verifySignatureWithDSAKeyResultCache.containsKey(hash)) { + return verifySignatureWithDSAKeyResultCache.get(hash); + } + try { + PublicKey signaturePubKey = Sig.getPublicKeyFromBytes(signedWitness.getSignerPubKey()); + Sig.verify(signaturePubKey, signedWitness.getAccountAgeWitnessHash(), signedWitness.getSignature()); + verifySignatureWithDSAKeyResultCache.put(hash, true); + return true; + } catch (CryptoException e) { + log.warn("verifySignature signedWitness failed. signedWitness={}", signedWitness); + log.warn("Caused by ", e); + verifySignatureWithDSAKeyResultCache.put(hash, false); + return false; + } + } + + public Set getSignedWitnessSet(AccountAgeWitness accountAgeWitness) { + P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(accountAgeWitness.getHash()); + return signedWitnessSetByAccountAgeWitnessHash.getOrDefault(key, new HashSet<>()); + } + + // SignedWitness objects signed by arbitrators + public Set getArbitratorsSignedWitnessSet(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .filter(SignedWitness::isSignedByArbitrator) + .collect(Collectors.toSet()); + } + + // SignedWitness objects signed by any other peer + public Set getTrustedPeerSignedWitnessSet(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .filter(e -> !e.isSignedByArbitrator()) + .collect(Collectors.toSet()); + } + + public Set getRootSignedWitnessSet(boolean includeSignedByArbitrator) { + return getSignedWitnessMapValues().stream() + .filter(witness -> getSignedWitnessSetByOwnerPubKey(witness.getSignerPubKey(), new Stack<>()).isEmpty()) + .filter(witness -> includeSignedByArbitrator || + witness.getVerificationMethod() != SignedWitness.VerificationMethod.ARBITRATOR) + .collect(Collectors.toSet()); + } + + // Find first (in time) SignedWitness per missing signer + public Set getUnsignedSignerPubKeys() { + var oldestUnsignedSigners = new HashMap(); + getRootSignedWitnessSet(false).forEach(signedWitness -> + oldestUnsignedSigners.compute(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey()), + (key, oldValue) -> oldValue == null ? signedWitness : + oldValue.getDate() > signedWitness.getDate() ? signedWitness : oldValue)); + return new HashSet<>(oldestUnsignedSigners.values()); + } + + // We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as + // witnessOwnerPubKey + private Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, + Stack excluded) { + P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(ownerPubKey); + if (signedWitnessSetByOwnerPubKey.containsKey(key)) { + return signedWitnessSetByOwnerPubKey.get(key).stream() + .filter(e -> !excluded.contains(new P2PDataStorage.ByteArray(e.getSignerPubKey()))) + .collect(Collectors.toSet()); + + } else { + return new HashSet<>(); + } + } + + public boolean isSignedAccountAgeWitness(AccountAgeWitness accountAgeWitness) { + return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime() + SIGNER_AGE); + } + + public boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness) { + return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime()); + } + + public boolean isSufficientTradeAmountForSigning(Coin tradeAmount) { + return !tradeAmount.isLessThan(MINIMUM_TRADE_AMOUNT_FOR_SIGNING); + } + + private boolean verifySigner(SignedWitness signedWitness) { + return getSignedWitnessSetByOwnerPubKey(signedWitness.getWitnessOwnerPubKey(), new Stack<>()).stream() + .anyMatch(w -> isValidSignerWitnessInternal(w, signedWitness.getDate(), new Stack<>())); + } + + /** + * Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator and is allowed to sign + * other accounts. + * + * @param accountAgeWitness accountAgeWitness + * @param time time of signing + * @return true if accountAgeWitness is allowed to sign at time, false otherwise. + */ + private boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness, long time) { + Stack excludedPubKeys = new Stack<>(); + Set signedWitnessSet = getSignedWitnessSet(accountAgeWitness); + for (SignedWitness signedWitness : signedWitnessSet) { + if (isValidSignerWitnessInternal(signedWitness, time, excludedPubKeys)) { + return true; + } + } + // If we have not returned in the loops or they have been empty we have not found a valid signer. + return false; + } + + /** + * Helper to isValidAccountAgeWitness(accountAgeWitness) + * + * @param signedWitness the signedWitness to validate + * @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leaf. + * @param excludedPubKeys stack to prevent recursive loops + * @return true if signedWitness is valid, false otherwise. + */ + private boolean isValidSignerWitnessInternal(SignedWitness signedWitness, + long childSignedWitnessDateMillis, + Stack excludedPubKeys) { + if (filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { + return false; + } + if (!verifySignature(signedWitness)) { + return false; + } + if (signedWitness.isSignedByArbitrator()) { + // If signed by an arbitrator we don't have to check anything else. + return true; + } else { + if (!verifyDate(signedWitness, childSignedWitnessDateMillis)) { + return false; + } + if (excludedPubKeys.size() >= 2000) { + // Prevent DoS attack: an attacker floods the SignedWitness db with a long chain that takes lots of time to verify. + return false; + } + excludedPubKeys.push(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey())); + excludedPubKeys.push(new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey())); + // Iterate over signedWitness signers + Set signerSignedWitnessSet = getSignedWitnessSetByOwnerPubKey(signedWitness.getSignerPubKey(), excludedPubKeys); + for (SignedWitness signerSignedWitness : signerSignedWitnessSet) { + if (isValidSignerWitnessInternal(signerSignedWitness, signedWitness.getDate(), excludedPubKeys)) { + return true; + } + } + excludedPubKeys.pop(); + excludedPubKeys.pop(); + } + // If we have not returned in the loops or they have been empty we have not found a valid signer. + return false; + } + + private boolean verifyDate(SignedWitness signedWitness, long childSignedWitnessDateMillis) { + long childSignedWitnessDateMinusChargebackPeriodMillis = Instant.ofEpochMilli( + childSignedWitnessDateMillis).minus(SIGNER_AGE, ChronoUnit.MILLIS).toEpochMilli(); + long signedWitnessDateMillis = signedWitness.getDate(); + return signedWitnessDateMillis <= childSignedWitnessDateMinusChargebackPeriodMillis; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + public void addToMap(SignedWitness signedWitness) { + signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness); + + P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash()); + signedWitnessSetByAccountAgeWitnessHash.putIfAbsent(accountAgeWitnessHash, new HashSet<>()); + signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash).add(signedWitness); + + P2PDataStorage.ByteArray ownerPubKey = new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey()); + signedWitnessSetByOwnerPubKey.putIfAbsent(ownerPubKey, new HashSet<>()); + signedWitnessSetByOwnerPubKey.get(ownerPubKey).add(signedWitness); + } + + private void publishSignedWitness(SignedWitness signedWitness) { + if (!signedWitnessMap.containsKey(signedWitness.getHashAsByteArray())) { + log.info("broadcast signed witness {}", signedWitness.toString()); + // We set reBroadcast to true to achieve better resilience. + p2PService.addPersistableNetworkPayload(signedWitness, true); + addToMap(signedWitness); + } + } + + private void doRepublishAllSignedWitnesses() { + getSignedWitnessMapValues() + .forEach(signedWitness -> p2PService.addPersistableNetworkPayload(signedWitness, true)); + } + + @VisibleForTesting + public void removeSignedWitness(SignedWitness signedWitness) { + signedWitnessMap.remove(signedWitness.getHashAsByteArray()); + + P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash()); + if (signedWitnessSetByAccountAgeWitnessHash.containsKey(accountAgeWitnessHash)) { + Set set = signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash); + set.remove(signedWitness); + if (set.isEmpty()) { + signedWitnessSetByAccountAgeWitnessHash.remove(accountAgeWitnessHash); + } + } + + P2PDataStorage.ByteArray ownerPubKey = new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey()); + if (signedWitnessSetByOwnerPubKey.containsKey(ownerPubKey)) { + Set set = signedWitnessSetByOwnerPubKey.get(ownerPubKey); + set.remove(signedWitness); + if (set.isEmpty()) { + signedWitnessSetByOwnerPubKey.remove(ownerPubKey); + } + } + } + + // Remove SignedWitnesses that are signed by TRADE that also have an ARBITRATOR signature + // for the same ownerPubKey and AccountAgeWitnessHash +// private void cleanSignedWitnesses() { +// var orphans = getRootSignedWitnessSet(false); +// var signedWitnessesCopy = new HashSet<>(signedWitnessMap.values()); +// signedWitnessesCopy.forEach(sw -> orphans.forEach(orphan -> { +// if (sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR && +// Arrays.equals(sw.getWitnessOwnerPubKey(), orphan.getWitnessOwnerPubKey()) && +// Arrays.equals(sw.getAccountAgeWitnessHash(), orphan.getAccountAgeWitnessHash())) { +// signedWitnessMap.remove(orphan.getHashAsByteArray()); +// log.info("Remove duplicate SignedWitness: {}", orphan.toString()); +// } +// })); +// } +} diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java new file mode 100644 index 0000000000..a27c0ade90 --- /dev/null +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.account.sign; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.MapStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SignedWitnessStorageService extends MapStoreService { + private static final String FILE_NAME = "SignedWitnessStore"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public SignedWitnessStorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + public Map getMap() { + return store.getMap(); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof SignedWitness; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected SignedWitnessStore createStore() { + return new SignedWitnessStore(); + } +} diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java new file mode 100644 index 0000000000..d764ed3409 --- /dev/null +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.core.account.sign; + + +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuf + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class SignedWitnessStore extends PersistableNetworkPayloadStore { + + SignedWitnessStore() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SignedWitnessStore(List list) { + super(list); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setSignedWitnessStore(getBuilder()) + .build(); + } + + private protobuf.SignedWitnessStore.Builder getBuilder() { + final List protoList = map.values().stream() + .map(payload -> (SignedWitness) payload) + .map(SignedWitness::toProtoSignedWitness) + .collect(Collectors.toList()); + return protobuf.SignedWitnessStore.newBuilder().addAllItems(protoList); + } + + public static SignedWitnessStore fromProto(protobuf.SignedWitnessStore proto) { + List list = proto.getItemsList().stream() + .map(SignedWitness::fromProto).collect(Collectors.toList()); + return new SignedWitnessStore(list); + } +} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitness.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitness.java new file mode 100644 index 0000000000..131567da3f --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitness.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 bisq.core.account.witness; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.DateTolerantPayload; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; + +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.time.Clock; +import java.time.Instant; + +import java.util.concurrent.TimeUnit; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +// Object has 28 raw bytes (33 bytes is size of ProtoBuffer object in storage list, 5 byte extra for list -> totalBytes = 5 + n*33) +// With 1 000 000 entries we get about 33 MB of data. Old entries will be shipped with the resource file, +// so only the newly added objects since the last release will be retrieved over the P2P network. +@Slf4j +@Value +public class AccountAgeWitness implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, DateTolerantPayload { + private static final long TOLERANCE = TimeUnit.DAYS.toMillis(1); + + private final byte[] hash; // Ripemd160(Sha256(concatenated accountHash, signature and sigPubKey)); 20 bytes + private final long date; // 8 byte + + public AccountAgeWitness(byte[] hash, + long date) { + this.hash = hash; + this.date = date; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + final protobuf.AccountAgeWitness.Builder builder = protobuf.AccountAgeWitness.newBuilder() + .setHash(ByteString.copyFrom(hash)) + .setDate(date); + return protobuf.PersistableNetworkPayload.newBuilder().setAccountAgeWitness(builder).build(); + } + + protobuf.AccountAgeWitness toProtoAccountAgeWitness() { + return toProtoMessage().getAccountAgeWitness(); + } + + public static AccountAgeWitness fromProto(protobuf.AccountAgeWitness proto) { + byte[] hash = proto.getHash().toByteArray(); + if (hash.length != 20) { + log.warn("We got a a hash which is not 20 bytes"); + hash = new byte[0]; + } + return new AccountAgeWitness( + hash, + proto.getDate()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isDateInTolerance(Clock clock) { + // We don't allow older or newer than 1 day. + // Preventing forward dating is also important to protect against a sophisticated attack + return Math.abs(clock.millis() - date) <= TOLERANCE; + } + + @Override + public boolean verifyHashSize() { + return hash.length == 20; + } + + @Override + public byte[] getHash() { + return hash; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + P2PDataStorage.ByteArray getHashAsByteArray() { + return new P2PDataStorage.ByteArray(hash); + } + + @Override + public String toString() { + return "AccountAgeWitness{" + + "\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n date=" + Instant.ofEpochMilli(date) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java new file mode 100644 index 0000000000..b634f01a8d --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -0,0 +1,948 @@ +/* + * 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 bisq.core.account.witness; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferRestrictions; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.ChargeBackRisk; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.TraderDataItem; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.user.User; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.UserThread; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; + +import java.security.PublicKey; + +import java.time.Clock; + +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AccountAgeWitnessService { + private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11); + private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.MARCH, 1).getTime(); + + public enum AccountAge { + UNVERIFIED, + LESS_ONE_MONTH, + ONE_TO_TWO_MONTHS, + TWO_MONTHS_OR_MORE + } + + public enum SignState { + UNSIGNED(Res.get("offerbook.timeSinceSigning.notSigned")), + ARBITRATOR(Res.get("offerbook.timeSinceSigning.info.arbitrator")), + PEER_INITIAL(Res.get("offerbook.timeSinceSigning.info.peer")), + PEER_LIMIT_LIFTED(Res.get("offerbook.timeSinceSigning.info.peerLimitLifted")), + PEER_SIGNER(Res.get("offerbook.timeSinceSigning.info.signer")), + BANNED(Res.get("offerbook.timeSinceSigning.info.banned")); + + private String displayString; + private String hash = ""; + private long daysUntilLimitLifted = 0; + + SignState(String displayString) { + this.displayString = displayString; + } + + public SignState addHash(String hash) { + this.hash = hash; + return this; + } + + public SignState setDaysUntilLimitLifted(long days) { + this.daysUntilLimitLifted = days; + return this; + } + + public String getDisplayString() { + if (!hash.isEmpty()) { // Only showing in DEBUG mode + return displayString + " " + hash; + } + return String.format(displayString, daysUntilLimitLifted); + } + + } + + private final KeyRing keyRing; + private final P2PService p2PService; + private final User user; + private final SignedWitnessService signedWitnessService; + private final ChargeBackRisk chargeBackRisk; + private final AccountAgeWitnessStorageService accountAgeWitnessStorageService; + private final Clock clock; + private final FilterManager filterManager; + @Getter + private final AccountAgeWitnessUtils accountAgeWitnessUtils; + + private final Map accountAgeWitnessMap = new HashMap<>(); + + // The accountAgeWitnessMap is very large (70k items) and access is a bit expensive. We usually only access less + // than 100 items, those who have offers online. So we use a cache for a fast lookup and only if + // not found there we use the accountAgeWitnessMap and put then the new item into our cache. + private final Map accountAgeWitnessCache = new ConcurrentHashMap<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Inject + public AccountAgeWitnessService(KeyRing keyRing, + P2PService p2PService, + User user, + SignedWitnessService signedWitnessService, + ChargeBackRisk chargeBackRisk, + AccountAgeWitnessStorageService accountAgeWitnessStorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + Clock clock, + FilterManager filterManager) { + this.keyRing = keyRing; + this.p2PService = p2PService; + this.user = user; + this.signedWitnessService = signedWitnessService; + this.chargeBackRisk = chargeBackRisk; + this.accountAgeWitnessStorageService = accountAgeWitnessStorageService; + this.clock = clock; + this.filterManager = filterManager; + + accountAgeWitnessUtils = new AccountAgeWitnessUtils( + this, + signedWitnessService, + keyRing); + + // We need to add that early (before onAllServicesInitialized) as it will be used at startup. + appendOnlyDataStoreService.addService(accountAgeWitnessStorageService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { + if (payload instanceof AccountAgeWitness) + addToMap((AccountAgeWitness) payload); + }); + + // At startup the P2PDataStorage initializes earlier, otherwise we get the listener called. + accountAgeWitnessStorageService.getMapOfAllData().values().stream() + .filter(e -> e instanceof AccountAgeWitness) + .map(e -> (AccountAgeWitness) e) + .forEach(this::addToMap); + + if (p2PService.isBootstrapped()) { + onBootStrapped(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + onBootStrapped(); + } + }); + } + } + + private void onBootStrapped() { + republishAllFiatAccounts(); + signAndPublishSameNameAccounts(); + } + + + // At startup we re-publish the witness data of all fiat accounts to ensure we got our data well distributed. + private void republishAllFiatAccounts() { + if (user.getPaymentAccounts() != null) + user.getPaymentAccounts().stream() + .filter(account -> !(account instanceof AssetAccount)) + .forEach(account -> { + AccountAgeWitness myWitness = getMyWitness(account.getPaymentAccountPayload()); + // We only publish if the date of our witness is inside the date tolerance. + // It would be rejected otherwise from the peers. + if (myWitness.isDateInTolerance(clock)) { + // We delay with a random interval of 20-60 sec to ensure to be better connected and don't + // stress the P2P network with publishing all at once at startup time. + int delayInSec = 20 + new Random().nextInt(40); + UserThread.runAfter(() -> + p2PService.addPersistableNetworkPayload(myWitness, true), delayInSec); + } + }); + } + + @VisibleForTesting + public void addToMap(AccountAgeWitness accountAgeWitness) { + accountAgeWitnessMap.putIfAbsent(accountAgeWitness.getHashAsByteArray(), accountAgeWitness); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Generic + /////////////////////////////////////////////////////////////////////////////////////////// + + public void publishMyAccountAgeWitness(PaymentAccountPayload paymentAccountPayload) { + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccountPayload); + P2PDataStorage.ByteArray hash = accountAgeWitness.getHashAsByteArray(); + + // We use first our fast lookup cache. If its in accountAgeWitnessCache it is also in accountAgeWitnessMap + // and we do not publish. + if (accountAgeWitnessCache.containsKey(hash)) { + return; + } + + if (!accountAgeWitnessMap.containsKey(hash)) { + p2PService.addPersistableNetworkPayload(accountAgeWitness, false); + } + } + + public byte[] getPeerAccountAgeWitnessHash(Trade trade) { + return findTradePeerWitness(trade) + .map(AccountAgeWitness::getHash) + .orElse(null); + } + + byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) { + return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(), + paymentAccountPayload.getSalt()); + } + + @VisibleForTesting + public AccountAgeWitness getNewWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { + byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); + byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, + pubKeyRing.getSignaturePubKeyBytes())); + return new AccountAgeWitness(hash, new Date().getTime()); + } + + Optional findWitness(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { + byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); + byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, + pubKeyRing.getSignaturePubKeyBytes())); + + return getWitnessByHash(hash); + } + + public Optional findWitness(Offer offer) { + final Optional accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex(); + return accountAgeWitnessHash.isPresent() ? + getWitnessByHashAsHex(accountAgeWitnessHash.get()) : + Optional.empty(); + } + + private Optional findTradePeerWitness(Trade trade) { + TradingPeer tradingPeer = trade.getProcessModel().getTradingPeer(); + return (tradingPeer == null || + tradingPeer.getPaymentAccountPayload() == null || + tradingPeer.getPubKeyRing() == null) ? + Optional.empty() : + findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing()); + } + + private Optional getWitnessByHash(byte[] hash) { + P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash); + + // First we look up in our fast lookup cache + if (accountAgeWitnessCache.containsKey(hashAsByteArray)) { + return Optional.of(accountAgeWitnessCache.get(hashAsByteArray)); + } + + if (accountAgeWitnessMap.containsKey(hashAsByteArray)) { + AccountAgeWitness accountAgeWitness = accountAgeWitnessMap.get(hashAsByteArray); + + // We add it to our fast lookup cache + accountAgeWitnessCache.put(hashAsByteArray, accountAgeWitness); + + return Optional.of(accountAgeWitness); + } + + return Optional.empty(); + } + + private Optional getWitnessByHashAsHex(String hashAsHex) { + return getWitnessByHash(Utilities.decodeFromHex(hashAsHex)); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Witness age + /////////////////////////////////////////////////////////////////////////////////////////// + + public long getAccountAge(AccountAgeWitness accountAgeWitness, Date now) { + log.debug("getAccountAge now={}, accountAgeWitness.getDate()={}", now.getTime(), accountAgeWitness.getDate()); + return now.getTime() - accountAgeWitness.getDate(); + } + + // Return -1 if no witness found + public long getAccountAge(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { + return findWitness(paymentAccountPayload, pubKeyRing) + .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) + .orElse(-1L); + } + + public long getAccountAge(Offer offer) { + return findWitness(offer) + .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) + .orElse(-1L); + } + + public long getAccountAge(Trade trade) { + return findTradePeerWitness(trade) + .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) + .orElse(-1L); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Signed age + /////////////////////////////////////////////////////////////////////////////////////////// + + // Return -1 if not signed + public long getWitnessSignAge(AccountAgeWitness accountAgeWitness, Date now) { + List dates = signedWitnessService.getVerifiedWitnessDateList(accountAgeWitness); + if (dates.isEmpty()) { + return -1L; + } else { + return now.getTime() - dates.get(0); + } + } + + // Return -1 if not signed + public long getWitnessSignAge(Offer offer, Date now) { + return findWitness(offer) + .map(witness -> getWitnessSignAge(witness, now)) + .orElse(-1L); + } + + public long getWitnessSignAge(Trade trade, Date now) { + return findTradePeerWitness(trade) + .map(witness -> getWitnessSignAge(witness, now)) + .orElse(-1L); + } + + public AccountAge getPeersAccountAgeCategory(long peersAccountAge) { + return getAccountAgeCategory(peersAccountAge); + } + + private AccountAge getAccountAgeCategory(long accountAge) { + if (accountAge < 0) { + return AccountAge.UNVERIFIED; + } else if (accountAge < TimeUnit.DAYS.toMillis(30)) { + return AccountAge.LESS_ONE_MONTH; + } else if (accountAge < TimeUnit.DAYS.toMillis(60)) { + return AccountAge.ONE_TO_TWO_MONTHS; + } else { + return AccountAge.TWO_MONTHS_OR_MORE; + } + } + + // Get trade limit based on a time schedule + // Buying of BTC with a payment method that has chargeback risk will use a low trade limit schedule + // All selling and all other fiat payment methods use the normal trade limit schedule + // Non fiat always has max limit + // Account types that can get signed will use time since signing, other methods use time since account age creation + // when measuring account age + private long getTradeLimit(Coin maxTradeLimit, + String currencyCode, + AccountAgeWitness accountAgeWitness, + AccountAge accountAgeCategory, + OfferPayload.Direction direction, + PaymentMethod paymentMethod) { + if (CurrencyUtil.isCryptoCurrency(currencyCode) || + !PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode) || + direction == OfferPayload.Direction.SELL) { + return maxTradeLimit.value; + } + + long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; + var factor = signedBuyFactor(accountAgeCategory); + if (factor > 0) { + limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor); + } + + log.debug("limit={}, factor={}, accountAgeWitnessHash={}", + Coin.valueOf(limit).toFriendlyString(), + factor, + Utilities.bytesAsHexString(accountAgeWitness.getHash())); + return limit; + } + + private double signedBuyFactor(AccountAge accountAgeCategory) { + switch (accountAgeCategory) { + case TWO_MONTHS_OR_MORE: + return 1; + case ONE_TO_TWO_MONTHS: + return 0.5; + case LESS_ONE_MONTH: + case UNVERIFIED: + default: + return 0; + } + } + + private double normalFactor() { + return 1; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trade limit exceptions + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isImmature(AccountAgeWitness accountAgeWitness) { + return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE; + } + + public boolean myHasTradeLimitException(PaymentAccount myPaymentAccount) { + return hasTradeLimitException(getMyWitness(myPaymentAccount.getPaymentAccountPayload())); + } + + // There are no trade limits on accounts that + // - are mature + // - were signed by an arbitrator + private boolean hasTradeLimitException(AccountAgeWitness accountAgeWitness) { + return !isImmature(accountAgeWitness) || signedWitnessService.isSignedByArbitrator(accountAgeWitness); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // My witness + /////////////////////////////////////////////////////////////////////////////////////////// + + public AccountAgeWitness getMyWitness(PaymentAccountPayload paymentAccountPayload) { + final Optional accountAgeWitnessOptional = + findWitness(paymentAccountPayload, keyRing.getPubKeyRing()); + return accountAgeWitnessOptional.orElseGet(() -> getNewWitness(paymentAccountPayload, keyRing.getPubKeyRing())); + } + + private byte[] getMyWitnessHash(PaymentAccountPayload paymentAccountPayload) { + return getMyWitness(paymentAccountPayload).getHash(); + } + + public String getMyWitnessHashAsHex(PaymentAccountPayload paymentAccountPayload) { + return Utilities.bytesAsHexString(getMyWitnessHash(paymentAccountPayload)); + } + + public long getMyAccountAge(PaymentAccountPayload paymentAccountPayload) { + return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); + } + + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferPayload.Direction direction) { + if (paymentAccount == null) + return 0; + + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); + Coin maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(currencyCode); + if (hasTradeLimitException(accountAgeWitness)) { + return maxTradeLimit.value; + } + final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); + AccountAge accountAgeCategory = getAccountAgeCategory(accountSignAge); + + return getTradeLimit(maxTradeLimit, + currencyCode, + accountAgeWitness, + accountAgeCategory, + direction, + paymentAccount.getPaymentMethod()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Verification + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean verifyAccountAgeWitness(Trade trade, + PaymentAccountPayload peersPaymentAccountPayload, + Date peersCurrentDate, + PubKeyRing peersPubKeyRing, + byte[] nonce, + byte[] signature, + ErrorMessageHandler errorMessageHandler) { + final Optional accountAgeWitnessOptional = + findWitness(peersPaymentAccountPayload, peersPubKeyRing); + // If we don't find a stored witness data we create a new dummy object which makes is easier to reuse the + // below validation methods. This peersWitness object is not used beside for validation. Some of the + // validation calls are pointless in the case we create a new Witness ourselves but the verifyPeersTradeLimit + // need still be called, so we leave also the rest for sake of simplicity. + AccountAgeWitness peersWitness; + if (accountAgeWitnessOptional.isPresent()) { + peersWitness = accountAgeWitnessOptional.get(); + } else { + peersWitness = getNewWitness(peersPaymentAccountPayload, peersPubKeyRing); + log.warn("We did not find the peers witness data. That is expected with peers using an older version."); + } + + // Check if date in witness is not older than the release date of that feature (was added in v0.6) + if (!isDateAfterReleaseDate(peersWitness.getDate(), RELEASE, errorMessageHandler)) + return false; + + // Check if peer current date is in tolerance range + if (!verifyPeersCurrentDate(peersCurrentDate, errorMessageHandler)) + return false; + + final byte[] peersAccountInputDataWithSalt = Utilities.concatenateByteArrays( + peersPaymentAccountPayload.getAgeWitnessInputData(), peersPaymentAccountPayload.getSalt()); + byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(peersAccountInputDataWithSalt, + peersPubKeyRing.getSignaturePubKeyBytes())); + + // Check if the hash in the witness data matches the hash derived from the data provided by the peer + final byte[] peersWitnessHash = peersWitness.getHash(); + if (!verifyWitnessHash(peersWitnessHash, hash, errorMessageHandler)) + return false; + + // Check if the peers trade limit is not less than the trade amount + if (!verifyPeersTradeLimit(trade.getOffer(), trade.getTradeAmount(), peersWitness, peersCurrentDate, + errorMessageHandler)) { + log.error("verifyPeersTradeLimit failed: peersPaymentAccountPayload {}", peersPaymentAccountPayload); + return false; + } + // Check if the signature is correct + return verifySignature(peersPubKeyRing.getSignaturePubKey(), nonce, signature, errorMessageHandler); + } + + public boolean verifyPeersTradeAmount(Offer offer, + Coin tradeAmount, + ErrorMessageHandler errorMessageHandler) { + checkNotNull(offer); + + // In case we don't find the witness we check if the trade amount is above the + // TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false. + return findWitness(offer) + .map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler)) + .orElse(isToleratedSmalleAmount(tradeAmount)); + } + + private boolean isToleratedSmalleAmount(Coin tradeAmount) { + return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Package scope verification subroutines + /////////////////////////////////////////////////////////////////////////////////////////// + + boolean isDateAfterReleaseDate(long witnessDateAsLong, + Date ageWitnessReleaseDate, + ErrorMessageHandler errorMessageHandler) { + // Release date minus 1 day as tolerance for not synced clocks + Date releaseDateWithTolerance = new Date(ageWitnessReleaseDate.getTime() - TimeUnit.DAYS.toMillis(1)); + final Date witnessDate = new Date(witnessDateAsLong); + final boolean result = witnessDate.after(releaseDateWithTolerance); + if (!result) { + final String msg = "Witness date is set earlier than release date of ageWitness feature. " + + "ageWitnessReleaseDate=" + ageWitnessReleaseDate + ", witnessDate=" + witnessDate; + log.warn(msg); + errorMessageHandler.handleErrorMessage(msg); + } + return result; + } + + private boolean verifyPeersCurrentDate(Date peersCurrentDate, ErrorMessageHandler errorMessageHandler) { + boolean result = Math.abs(peersCurrentDate.getTime() - new Date().getTime()) <= TimeUnit.DAYS.toMillis(1); + if (!result) { + String msg = "Peers current date is further than 1 day off to our current date. " + + "PeersCurrentDate=" + peersCurrentDate + "; myCurrentDate=" + new Date(); + log.warn(msg); + errorMessageHandler.handleErrorMessage(msg); + } + return result; + } + + private boolean verifyWitnessHash(byte[] witnessHash, + byte[] hash, + ErrorMessageHandler errorMessageHandler) { + final boolean result = Arrays.equals(witnessHash, hash); + if (!result) { + final String msg = "witnessHash is not matching peers hash. " + + "witnessHash=" + Utilities.bytesAsHexString(witnessHash) + ", hash=" + Utilities.bytesAsHexString(hash); + log.warn(msg); + errorMessageHandler.handleErrorMessage(msg); + } + return result; + } + + private boolean verifyPeersTradeLimit(Offer offer, + Coin tradeAmount, + AccountAgeWitness peersWitness, + Date peersCurrentDate, + ErrorMessageHandler errorMessageHandler) { + checkNotNull(offer); + final String currencyCode = offer.getCurrencyCode(); + final Coin defaultMaxTradeLimit = PaymentMethod.getPaymentMethodById( + offer.getOfferPayload().getPaymentMethodId()).getMaxTradeLimitAsCoin(currencyCode); + long peersCurrentTradeLimit = defaultMaxTradeLimit.value; + if (!hasTradeLimitException(peersWitness)) { + final long accountSignAge = getWitnessSignAge(peersWitness, peersCurrentDate); + AccountAge accountAgeCategory = getPeersAccountAgeCategory(accountSignAge); + OfferPayload.Direction direction = offer.isMyOffer(keyRing) ? + offer.getMirroredDirection() : offer.getDirection(); + peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness, + accountAgeCategory, direction, offer.getPaymentMethod()); + } + // Makers current trade limit cannot be smaller than that in the offer + boolean result = tradeAmount.value <= peersCurrentTradeLimit; + if (!result) { + String msg = "The peers trade limit is less than the traded amount.\n" + + "tradeAmount=" + tradeAmount.toFriendlyString() + + "\nPeers trade limit=" + Coin.valueOf(peersCurrentTradeLimit).toFriendlyString() + + "\nOffer ID=" + offer.getShortId() + + "\nPaymentMethod=" + offer.getPaymentMethod().getId() + + "\nCurrencyCode=" + offer.getCurrencyCode(); + log.warn(msg); + errorMessageHandler.handleErrorMessage(msg); + } + return result; + } + + boolean verifySignature(PublicKey peersPublicKey, + byte[] nonce, + byte[] signature, + ErrorMessageHandler errorMessageHandler) { + boolean result; + try { + result = Sig.verify(peersPublicKey, nonce, signature); + } catch (CryptoException e) { + log.warn(e.toString()); + result = false; + } + if (!result) { + final String msg = "Signature of nonce is not correct. " + + "peersPublicKey=" + peersPublicKey + ", nonce(hex)=" + Utilities.bytesAsHexString(nonce) + + ", signature=" + Utilities.bytesAsHexString(signature); + log.warn(msg); + errorMessageHandler.handleErrorMessage(msg); + } + return result; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Witness signing + /////////////////////////////////////////////////////////////////////////////////////////// + + public void arbitratorSignAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + ECKey key, + PublicKey peersPubKey) { + signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey); + } + + public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness, + ECKey ecKey, + long time) { + // TODO Is not found signedWitness considered an error case? + // Previous code version was throwing an exception in case no signedWitness was found... + + // signAndPublishAccountAgeWitness returns an empty string in success case and error otherwise + return signedWitnessService.getSignedWitnessSet(accountAgeWitness).stream() + .findAny() + .map(SignedWitness::getWitnessOwnerPubKey) + .map(witnessOwnerPubKey -> + signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, ecKey, + witnessOwnerPubKey, time) + ) + .orElse("No signedWitness found"); + } + + public String arbitratorSignOrphanPubKey(ECKey key, + byte[] peersPubKey, + long childSignTime) { + return signedWitnessService.signTraderPubKey(key, peersPubKey, childSignTime); + } + + public void arbitratorSignAccountAgeWitness(AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] tradersPubKey, + long time) { + signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key, tradersPubKey, time); + } + + public Optional traderSignAndPublishPeersAccountAgeWitness(Trade trade) { + AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); + Coin tradeAmount = trade.getTradeAmount(); + checkNotNull(trade.getProcessModel().getTradingPeer().getPubKeyRing(), "Peer must have a keyring"); + PublicKey peersPubKey = trade.getProcessModel().getTradingPeer().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"); + + try { + return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); + } catch (CryptoException e) { + log.warn("Trader failed to sign witness, exception {}", e.toString()); + } + return Optional.empty(); + } + + public boolean publishOwnSignedWitness(SignedWitness signedWitness) { + return signedWitnessService.publishOwnSignedWitness(signedWitness); + } + + // Arbitrator signing + public List getTraderPaymentAccounts(long safeDate, + PaymentMethod paymentMethod, + List disputes) { + return disputes.stream() + .filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId())) + .filter(this::isNotFiltered) + .filter(this::hasChargebackRisk) + .filter(this::isBuyerWinner) + .flatMap(this::getTraderData) + .filter(Objects::nonNull) + .filter(traderDataItem -> + !signedWitnessService.isSignedAccountAgeWitness(traderDataItem.getAccountAgeWitness())) + .filter(traderDataItem -> traderDataItem.getAccountAgeWitness().getDate() < safeDate) + .distinct() + .collect(Collectors.toList()); + } + + private boolean isNotFiltered(Dispute dispute) { + boolean isFiltered = filterManager.isNodeAddressBanned(dispute.getContract().getBuyerNodeAddress()) || + filterManager.isNodeAddressBanned(dispute.getContract().getSellerNodeAddress()) || + filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || + filterManager.isPaymentMethodBanned( + PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload()) || + filterManager.arePeersPaymentAccountDataBanned( + dispute.getContract().getSellerPaymentAccountPayload()) || + filterManager.isWitnessSignerPubKeyBanned( + Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || + filterManager.isWitnessSignerPubKeyBanned( + Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes())); + return !isFiltered; + } + + @VisibleForTesting + public boolean hasChargebackRisk(Dispute dispute) { + return chargeBackRisk.hasChargebackRisk(dispute.getContract().getPaymentMethodId(), + dispute.getContract().getOfferPayload().getCurrencyCode()); + } + + private boolean isBuyerWinner(Dispute dispute) { + if (!dispute.isClosed() || dispute.getDisputeResultProperty() == null) + return false; + return dispute.getDisputeResultProperty().get().getWinner() == DisputeResult.Winner.BUYER; + } + + private Stream getTraderData(Dispute dispute) { + Coin tradeAmount = dispute.getContract().getTradeAmount(); + + PubKeyRing buyerPubKeyRing = dispute.getContract().getBuyerPubKeyRing(); + PubKeyRing sellerPubKeyRing = dispute.getContract().getSellerPubKeyRing(); + + PaymentAccountPayload buyerPaymentAccountPaload = dispute.getContract().getBuyerPaymentAccountPayload(); + PaymentAccountPayload sellerPaymentAccountPaload = dispute.getContract().getSellerPaymentAccountPayload(); + + TraderDataItem buyerData = findWitness(buyerPaymentAccountPaload, buyerPubKeyRing) + .map(witness -> new TraderDataItem( + buyerPaymentAccountPaload, + witness, + tradeAmount, + buyerPubKeyRing.getSignaturePubKey())) + .orElse(null); + TraderDataItem sellerData = findWitness(sellerPaymentAccountPaload, sellerPubKeyRing) + .map(witness -> new TraderDataItem( + sellerPaymentAccountPaload, + witness, + tradeAmount, + sellerPubKeyRing.getSignaturePubKey())) + .orElse(null); + return Stream.of(buyerData, sellerData); + } + + public boolean hasSignedWitness(Offer offer) { + return findWitness(offer) + .map(signedWitnessService::isSignedAccountAgeWitness) + .orElse(false); + } + + public boolean peerHasSignedWitness(Trade trade) { + return findTradePeerWitness(trade) + .map(signedWitnessService::isSignedAccountAgeWitness) + .orElse(false); + } + + public boolean accountIsSigner(AccountAgeWitness accountAgeWitness) { + return signedWitnessService.isSignerAccountAgeWitness(accountAgeWitness); + } + + public boolean tradeAmountIsSufficient(Coin tradeAmount) { + return signedWitnessService.isSufficientTradeAmountForSigning(tradeAmount); + } + + public SignState getSignState(Offer offer) { + return findWitness(offer) + .map(this::getSignState) + .orElse(SignState.UNSIGNED); + } + + public SignState getSignState(Trade trade) { + return findTradePeerWitness(trade) + .map(this::getSignState) + .orElse(SignState.UNSIGNED); + } + + public SignState getSignState(AccountAgeWitness accountAgeWitness) { + // Add hash to sign state info when running in debug mode + String hash = log.isDebugEnabled() ? Utilities.bytesAsHexString(accountAgeWitness.getHash()) + "\n" + + signedWitnessService.ownerPubKeyAsString(accountAgeWitness) : ""; + if (signedWitnessService.isFilteredWitness(accountAgeWitness)) { + return SignState.BANNED.addHash(hash); + } + if (signedWitnessService.isSignedByArbitrator(accountAgeWitness)) { + return SignState.ARBITRATOR.addHash(hash); + } else { + final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); + switch (getAccountAgeCategory(accountSignAge)) { + case TWO_MONTHS_OR_MORE: + case ONE_TO_TWO_MONTHS: + return SignState.PEER_SIGNER.addHash(hash); + case LESS_ONE_MONTH: + return SignState.PEER_INITIAL.addHash(hash) + .setDaysUntilLimitLifted(30 - TimeUnit.MILLISECONDS.toDays(accountSignAge)); + case UNVERIFIED: + default: + return SignState.UNSIGNED.addHash(hash); + } + } + } + + public Set getOrphanSignedWitnesses() { + return signedWitnessService.getRootSignedWitnessSet(false).stream() + .map(signedWitness -> getWitnessByHash(signedWitness.getAccountAgeWitnessHash()).orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + public void signAndPublishSameNameAccounts() { + // Collect accounts that have ownerId to sign unsigned accounts with the same ownderId + var signerAccounts = Objects.requireNonNull(user.getPaymentAccounts()).stream() + .filter(account -> account.getOwnerId() != null && + accountIsSigner(getMyWitness(account.getPaymentAccountPayload()))) + .collect(Collectors.toSet()); + var unsignedAccounts = user.getPaymentAccounts().stream() + .filter(account -> account.getOwnerId() != null && + !signedWitnessService.isSignedAccountAgeWitness( + getMyWitness(account.getPaymentAccountPayload()))) + .collect(Collectors.toSet()); + + signerAccounts.forEach(signer -> unsignedAccounts.forEach(unsigned -> { + if (signer.getOwnerId().equals(unsigned.getOwnerId())) { + try { + signedWitnessService.selfSignAndPublishAccountAgeWitness( + getMyWitness(unsigned.getPaymentAccountPayload())); + } catch (CryptoException e) { + log.warn("Self signing failed, exception {}", e.toString()); + } + } + })); + } + + public Set getUnsignedSignerPubKeys() { + return signedWitnessService.getUnsignedSignerPubKeys(); + } + + public boolean isSignWitnessTrade(Trade trade) { + checkNotNull(trade, "trade must not be null"); + checkNotNull(trade.getOffer(), "offer must not be null"); + Contract contract = checkNotNull(trade.getContract()); + PaymentAccountPayload sellerPaymentAccountPayload = contract.getSellerPaymentAccountPayload(); + AccountAgeWitness myWitness = getMyWitness(sellerPaymentAccountPayload); + + getAccountAgeWitnessUtils().witnessDebugLog(trade, myWitness); + + return accountIsSigner(myWitness) && + !peerHasSignedWitness(trade) && + tradeAmountIsSufficient(trade.getTradeAmount()); + } + + public String getSignInfoFromAccount(PaymentAccount paymentAccount) { + var pubKey = keyRing.getSignatureKeyPair().getPublic(); + var witness = getMyWitness(paymentAccount.getPaymentAccountPayload()); + return Utilities.bytesAsHexString(witness.getHash()) + "," + Utilities.bytesAsHexString(pubKey.getEncoded()); + } + + public Tuple2 getSignInfoFromString(String signInfo) { + var parts = signInfo.split(","); + if (parts.length != 2) { + return null; + } + byte[] pubKeyHash; + Optional accountAgeWitness; + try { + var accountAgeWitnessHash = Utilities.decodeFromHex(parts[0]); + pubKeyHash = Utilities.decodeFromHex(parts[1]); + accountAgeWitness = getWitnessByHash(accountAgeWitnessHash); + return accountAgeWitness + .map(ageWitness -> new Tuple2<>(ageWitness, pubKeyHash)) + .orElse(null); + } catch (Exception e) { + return null; + } + } +} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java new file mode 100644 index 0000000000..ad5d7449d1 --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.account.witness; + +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.HistoricalDataStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AccountAgeWitnessStorageService extends HistoricalDataStoreService { + private static final String FILE_NAME = "AccountAgeWitnessStore"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AccountAgeWitnessStorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof AccountAgeWitness; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected AccountAgeWitnessStore createStore() { + return new AccountAgeWitnessStore(); + } +} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java new file mode 100644 index 0000000000..349d18e80d --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.account.witness; + +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class AccountAgeWitnessStore extends PersistableNetworkPayloadStore { + + public AccountAgeWitnessStore() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AccountAgeWitnessStore(List list) { + super(list); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setAccountAgeWitnessStore(getBuilder()) + .build(); + } + + private protobuf.AccountAgeWitnessStore.Builder getBuilder() { + final List protoList = map.values().stream() + .map(payload -> (AccountAgeWitness) payload) + .map(AccountAgeWitness::toProtoAccountAgeWitness) + .collect(Collectors.toList()); + return protobuf.AccountAgeWitnessStore.newBuilder().addAllItems(protoList); + } + + public static AccountAgeWitnessStore fromProto(protobuf.AccountAgeWitnessStore proto) { + List list = proto.getItemsList().stream() + .map(AccountAgeWitness::fromProto).collect(Collectors.toList()); + return new AccountAgeWitnessStore(list); + } +} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java new file mode 100644 index 0000000000..628f0a8186 --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -0,0 +1,171 @@ +/* + * 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 bisq.core.account.witness; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; + +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Utilities; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.Stack; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AccountAgeWitnessUtils { + private final AccountAgeWitnessService accountAgeWitnessService; + private final SignedWitnessService signedWitnessService; + private final KeyRing keyRing; + + AccountAgeWitnessUtils(AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService, + KeyRing keyRing) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.signedWitnessService = signedWitnessService; + this.keyRing = keyRing; + } + + // Log tree of signed witnesses + public void logSignedWitnesses() { + var orphanSigners = signedWitnessService.getRootSignedWitnessSet(true); + log.info("Orphaned signed account age witnesses:"); + orphanSigners.forEach(w -> { + log.info("{}: Signer PKH: {} Owner PKH: {} time: {}", w.getVerificationMethod().toString(), + Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getSignerPubKey())).substring(0, 7), + Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getWitnessOwnerPubKey())).substring(0, 7), + w.getDate()); + logChild(w, " ", new Stack<>()); + }); + } + + private void logChild(SignedWitness sigWit, String initString, Stack excluded) { + log.info("{}AEW: {} PKH: {} time: {}", initString, + Utilities.bytesAsHexString(sigWit.getAccountAgeWitnessHash()).substring(0, 7), + Utilities.bytesAsHexString(Hash.getRipemd160hash(sigWit.getWitnessOwnerPubKey())).substring(0, 7), + sigWit.getDate()); + signedWitnessService.getSignedWitnessMapValues().forEach(w -> { + if (!excluded.contains(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())) && + Arrays.equals(w.getSignerPubKey(), sigWit.getWitnessOwnerPubKey())) { + excluded.push(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())); + logChild(w, initString + " ", excluded); + excluded.pop(); + } + }); + } + + // Log signers per + public void logSigners() { + log.info("Signers per AEW"); + Collection signedWitnessMapValues = signedWitnessService.getSignedWitnessMapValues(); + signedWitnessMapValues.forEach(w -> { + log.info("AEW {}", Utilities.bytesAsHexString(w.getAccountAgeWitnessHash())); + signedWitnessMapValues.forEach(ww -> { + if (Arrays.equals(w.getSignerPubKey(), ww.getWitnessOwnerPubKey())) { + log.info(" {}", Utilities.bytesAsHexString(ww.getAccountAgeWitnessHash())); + } + }); + } + ); + } + + public void logUnsignedSignerPubKeys() { + log.info("Unsigned signer pubkeys"); + signedWitnessService.getUnsignedSignerPubKeys().forEach(signedWitness -> + log.info("PK hash {} date {}", + Utilities.bytesAsHexString(Hash.getRipemd160hash(signedWitness.getSignerPubKey())), + signedWitness.getDate())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Debug logs + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { + Optional accountAgeWitness = + accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing); + if (!accountAgeWitness.isPresent()) { + byte[] accountInputDataWithSalt = + accountAgeWitnessService.getAccountInputDataWithSalt(paymentAccountPayload); + byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, + pubKeyRing.getSignaturePubKeyBytes())); + return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash); + } + + AccountAgeWitnessService.SignState signState = + accountAgeWitnessService.getSignState(accountAgeWitness.get()); + return signState.name() + " " + signState.getDisplayString() + + "\n" + accountAgeWitness.toString(); + } + + public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) { + // Log to find why accounts sometimes don't get signed as expected + // TODO: Demote to debug or remove once account signing is working ok + checkNotNull(trade.getContract()); + checkNotNull(trade.getContract().getBuyerPaymentAccountPayload()); + boolean checkingSignTrade = true; + boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing()); + AccountAgeWitness witness = myWitness; + if (witness == null) { + witness = isBuyer ? + accountAgeWitnessService.getMyWitness(trade.getContract().getBuyerPaymentAccountPayload()) : + accountAgeWitnessService.getMyWitness(trade.getContract().getSellerPaymentAccountPayload()); + checkingSignTrade = false; + } + boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && + !accountAgeWitnessService.peerHasSignedWitness(trade) && + accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()); + log.info("AccountSigning debug log: " + + "\ntradeId: {}" + + "\nis buyer: {}" + + "\nbuyer account age witness info: {}" + + "\nseller account age witness info: {}" + + "\nchecking for sign trade: {}" + + "\nis myWitness signer: {}" + + "\npeer has signed witness: {}" + + "\ntrade amount: {}" + + "\ntrade amount is sufficient: {}" + + "\nisSignWitnessTrade: {}", + trade.getId(), + isBuyer, + getWitnessDebugLog(trade.getContract().getBuyerPaymentAccountPayload(), + trade.getContract().getBuyerPubKeyRing()), + getWitnessDebugLog(trade.getContract().getSellerPaymentAccountPayload(), + trade.getContract().getSellerPubKeyRing()), + checkingSignTrade, // Following cases added to use same logic as in seller signing check + accountAgeWitnessService.accountIsSigner(witness), + accountAgeWitnessService.peerHasSignedWitness(trade), + trade.getTradeAmount(), + accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()), + isSignWitnessTrade); + } +} diff --git a/core/src/main/java/bisq/core/alert/Alert.java b/core/src/main/java/bisq/core/alert/Alert.java new file mode 100644 index 0000000000..d9830b38ee --- /dev/null +++ b/core/src/main/java/bisq/core/alert/Alert.java @@ -0,0 +1,179 @@ +/* + * 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 bisq.core.alert; + +import bisq.core.user.Preferences; + +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.app.Version; +import bisq.common.crypto.Sig; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; + +import com.google.protobuf.ByteString; + +import java.security.PublicKey; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@EqualsAndHashCode +@Getter +@ToString +@Slf4j +public final class Alert implements ProtectedStoragePayload, ExpirablePayload { + public static final long TTL = TimeUnit.DAYS.toMillis(90); + + private final String message; + private final boolean isUpdateInfo; + private final boolean isPreReleaseInfo; + private final String version; + + @Nullable + private byte[] ownerPubKeyBytes; + @Nullable + private String signatureAsBase64; + @Nullable + private PublicKey ownerPubKey; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + private Map extraDataMap; + + public Alert(String message, + boolean isUpdateInfo, + boolean isPreReleaseInfo, + String version) { + this.message = message; + this.isUpdateInfo = isUpdateInfo; + this.isPreReleaseInfo = isPreReleaseInfo; + this.version = version; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("NullableProblems") + public Alert(String message, + boolean isUpdateInfo, + boolean isPreReleaseInfo, + String version, + byte[] ownerPubKeyBytes, + String signatureAsBase64, + Map extraDataMap) { + this.message = message; + this.isUpdateInfo = isUpdateInfo; + this.isPreReleaseInfo = isPreReleaseInfo; + this.version = version; + this.ownerPubKeyBytes = ownerPubKeyBytes; + this.signatureAsBase64 = signatureAsBase64; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + + ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + } + + @Override + public protobuf.StoragePayload toProtoMessage() { + checkNotNull(ownerPubKeyBytes, "storagePublicKeyBytes must not be null"); + checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); + protobuf.Alert.Builder builder = protobuf.Alert.newBuilder() + .setMessage(message) + .setIsUpdateInfo(isUpdateInfo) + .setIsPreReleaseInfo(isPreReleaseInfo) + .setVersion(version) + .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .setSignatureAsBase64(signatureAsBase64); + Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData); + return protobuf.StoragePayload.newBuilder().setAlert(builder).build(); + } + + @Nullable + public static Alert fromProto(protobuf.Alert proto) { + // We got in dev testing sometimes an empty protobuf Alert. Not clear why that happened but as it causes an + // exception and corrupted user db file we prefer to set it to null. + if (proto.getSignatureAsBase64().isEmpty()) + return null; + + return new Alert(proto.getMessage(), + proto.getIsUpdateInfo(), + proto.getIsPreReleaseInfo(), + proto.getVersion(), + proto.getOwnerPubKeyBytes().toByteArray(), + proto.getSignatureAsBase64(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + public void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { + this.signatureAsBase64 = signatureAsBase64; + this.ownerPubKey = ownerPubKey; + + ownerPubKeyBytes = Sig.getPublicKeyBytes(ownerPubKey); + } + + public boolean isNewVersion(Preferences preferences) { + // regular release: always notify user + // pre-release: if user has set preference to receive pre-release notification + if (isUpdateInfo || + (isPreReleaseInfo && preferences.isNotifyOnPreRelease())) { + return Version.isNewVersion(version); + } + return false; + } + + public boolean isSoftwareUpdateNotification() { + return (isUpdateInfo || isPreReleaseInfo); + } + + public boolean canShowPopup(Preferences preferences) { + // only show popup if its version is newer than current + // and only if user has not checked "don't show again" + return isNewVersion(preferences) && preferences.showAgain(showAgainKey()); + } + + public String showAgainKey() { + return "Update_" + version; + } + +} diff --git a/core/src/main/java/bisq/core/alert/AlertManager.java b/core/src/main/java/bisq/core/alert/AlertManager.java new file mode 100644 index 0000000000..b9ce837a7f --- /dev/null +++ b/core/src/main/java/bisq/core/alert/AlertManager.java @@ -0,0 +1,177 @@ +/* + * 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 bisq.core.alert; + +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.base.Charsets; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.security.SignatureException; + +import java.math.BigInteger; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.bitcoinj.core.Utils.HEX; + +public class AlertManager { + private static final Logger log = LoggerFactory.getLogger(AlertManager.class); + + private final P2PService p2PService; + private final KeyRing keyRing; + private final User user; + private final ObjectProperty alertMessageProperty = new SimpleObjectProperty<>(); + + // Pub key for developer global alert message + private final String pubKeyAsHex; + private ECKey alertSigningKey; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AlertManager(P2PService p2PService, + KeyRing keyRing, + User user, + @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + this.p2PService = p2PService; + this.keyRing = keyRing; + this.user = user; + + if (!ignoreDevMsg) { + p2PService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> { + final ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); + if (protectedStoragePayload instanceof Alert) { + Alert alert = (Alert) protectedStoragePayload; + if (verifySignature(alert)) + alertMessageProperty.set(alert); + } + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> { + final ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); + if (protectedStoragePayload instanceof Alert) { + if (verifySignature((Alert) protectedStoragePayload)) + alertMessageProperty.set(null); + } + }); + } + }); + } + pubKeyAsHex = useDevPrivilegeKeys ? + DevEnv.DEV_PRIVILEGE_PUB_KEY : + "036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f"; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyObjectProperty alertMessageProperty() { + return alertMessageProperty; + } + + public boolean addAlertMessageIfKeyIsValid(Alert alert, String privKeyString) { + // if there is a previous message we remove that first + if (user.getDevelopersAlert() != null) + removeAlertMessageIfKeyIsValid(privKeyString); + + boolean isKeyValid = isKeyValid(privKeyString); + if (isKeyValid) { + signAndAddSignatureToAlertMessage(alert); + user.setDevelopersAlert(alert); + boolean result = p2PService.addProtectedStorageEntry(alert); + if (result) { + log.trace("Add alertMessage to network was successful. AlertMessage={}", alert); + } + + } + return isKeyValid; + } + + public boolean removeAlertMessageIfKeyIsValid(String privKeyString) { + Alert alert = user.getDevelopersAlert(); + if (isKeyValid(privKeyString) && alert != null) { + if (p2PService.removeData(alert)) + log.trace("Remove alertMessage from network was successful. AlertMessage={}", alert); + + user.setDevelopersAlert(null); + return true; + } else { + return false; + } + } + + private boolean isKeyValid(String privKeyString) { + try { + alertSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + return pubKeyAsHex.equals(Utils.HEX.encode(alertSigningKey.getPubKey())); + } catch (Throwable t) { + return false; + } + } + + private void signAndAddSignatureToAlertMessage(Alert alert) { + String alertMessageAsHex = Utils.HEX.encode(alert.getMessage().getBytes(Charsets.UTF_8)); + String signatureAsBase64 = alertSigningKey.signMessage(alertMessageAsHex); + alert.setSigAndPubKey(signatureAsBase64, keyRing.getSignatureKeyPair().getPublic()); + } + + private boolean verifySignature(Alert alert) { + String alertMessageAsHex = Utils.HEX.encode(alert.getMessage().getBytes(Charsets.UTF_8)); + try { + ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(alertMessageAsHex, alert.getSignatureAsBase64()); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } +} diff --git a/core/src/main/java/bisq/core/alert/AlertModule.java b/core/src/main/java/bisq/core/alert/AlertModule.java new file mode 100644 index 0000000000..3f2e11b7eb --- /dev/null +++ b/core/src/main/java/bisq/core/alert/AlertModule.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.core.alert; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +import static bisq.common.config.Config.IGNORE_DEV_MSG; +import static com.google.inject.name.Names.named; + +public class AlertModule extends AppModule { + + public AlertModule(Config config) { + super(config); + } + + @Override + protected final void configure() { + bind(AlertManager.class).in(Singleton.class); + bind(PrivateNotificationManager.class).in(Singleton.class); + bindConstant().annotatedWith(named(IGNORE_DEV_MSG)).to(config.ignoreDevMsg); + } +} diff --git a/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java b/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java new file mode 100644 index 0000000000..17bdf47859 --- /dev/null +++ b/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java @@ -0,0 +1,177 @@ +/* + * 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 bisq.core.alert; + +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; +import bisq.network.p2p.mailbox.MailboxMessageService; + +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.base.Charsets; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.security.SignatureException; + +import java.math.BigInteger; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +import static org.bitcoinj.core.Utils.HEX; + +public class PrivateNotificationManager { + private static final Logger log = LoggerFactory.getLogger(PrivateNotificationManager.class); + + private final P2PService p2PService; + private final MailboxMessageService mailboxMessageService; + private final KeyRing keyRing; + private final ObjectProperty privateNotificationMessageProperty = new SimpleObjectProperty<>(); + + // Pub key for developer global privateNotification message + private final String pubKeyAsHex; + + private ECKey privateNotificationSigningKey; + @Nullable + private PrivateNotificationMessage privateNotificationMessage; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PrivateNotificationManager(P2PService p2PService, + MailboxMessageService mailboxMessageService, + KeyRing keyRing, + @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + this.p2PService = p2PService; + this.mailboxMessageService = mailboxMessageService; + this.keyRing = keyRing; + + if (!ignoreDevMsg) { + this.p2PService.addDecryptedDirectMessageListener(this::handleMessage); + this.mailboxMessageService.addDecryptedMailboxListener(this::handleMessage); + } + pubKeyAsHex = useDevPrivilegeKeys ? + DevEnv.DEV_PRIVILEGE_PUB_KEY : + "02ba7c5de295adfe57b60029f3637a2c6b1d0e969a8aaefb9e0ddc3a7963f26925"; + } + + private void handleMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress senderNodeAddress) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof PrivateNotificationMessage) { + privateNotificationMessage = (PrivateNotificationMessage) networkEnvelope; + log.info("Received PrivateNotificationMessage from {} with uid={}", + senderNodeAddress, privateNotificationMessage.getUid()); + if (privateNotificationMessage.getSenderNodeAddress().equals(senderNodeAddress)) { + final PrivateNotificationPayload privateNotification = privateNotificationMessage.getPrivateNotificationPayload(); + if (verifySignature(privateNotification)) + privateNotificationMessageProperty.set(privateNotification); + } else { + log.warn("Peer address not matching for privateNotificationMessage"); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyObjectProperty privateNotificationProperty() { + return privateNotificationMessageProperty; + } + + public boolean sendPrivateNotificationMessageIfKeyIsValid(PrivateNotificationPayload privateNotification, + PubKeyRing pubKeyRing, + NodeAddress peersNodeAddress, + String privKeyString, + SendMailboxMessageListener sendMailboxMessageListener) { + boolean isKeyValid = isKeyValid(privKeyString); + if (isKeyValid) { + signAndAddSignatureToPrivateNotificationMessage(privateNotification); + + PrivateNotificationMessage message = new PrivateNotificationMessage(privateNotification, + p2PService.getNetworkNode().getNodeAddress(), + UUID.randomUUID().toString()); + log.info("Send {} to peer {}. uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + pubKeyRing, + message, + sendMailboxMessageListener); + } + + return isKeyValid; + } + + public void removePrivateNotification() { + if (privateNotificationMessage != null) { + mailboxMessageService.removeMailboxMsg(privateNotificationMessage); + } + } + + private boolean isKeyValid(String privKeyString) { + try { + privateNotificationSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + return pubKeyAsHex.equals(Utils.HEX.encode(privateNotificationSigningKey.getPubKey())); + } catch (Throwable t) { + return false; + } + } + + private void signAndAddSignatureToPrivateNotificationMessage(PrivateNotificationPayload privateNotification) { + String privateNotificationMessageAsHex = Utils.HEX.encode(privateNotification.getMessage().getBytes(Charsets.UTF_8)); + String signatureAsBase64 = privateNotificationSigningKey.signMessage(privateNotificationMessageAsHex); + privateNotification.setSigAndPubKey(signatureAsBase64, keyRing.getSignatureKeyPair().getPublic()); + } + + private boolean verifySignature(PrivateNotificationPayload privateNotification) { + String privateNotificationMessageAsHex = Utils.HEX.encode(privateNotification.getMessage().getBytes(Charsets.UTF_8)); + try { + ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(privateNotificationMessageAsHex, privateNotification.getSignatureAsBase64()); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } + + +} diff --git a/core/src/main/java/bisq/core/alert/PrivateNotificationMessage.java b/core/src/main/java/bisq/core/alert/PrivateNotificationMessage.java new file mode 100644 index 0000000000..9a14324e68 --- /dev/null +++ b/core/src/main/java/bisq/core/alert/PrivateNotificationMessage.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.alert; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.mailbox.MailboxMessage; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public class PrivateNotificationMessage extends NetworkEnvelope implements MailboxMessage { + public static final long TTL = TimeUnit.DAYS.toMillis(30); + + private final PrivateNotificationPayload privateNotificationPayload; + private final NodeAddress senderNodeAddress; + private final String uid; + + public PrivateNotificationMessage(PrivateNotificationPayload privateNotificationPayload, + NodeAddress senderNodeAddress, + String uid) { + this(privateNotificationPayload, senderNodeAddress, uid, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PrivateNotificationMessage(PrivateNotificationPayload privateNotificationPayload, + NodeAddress senderNodeAddress, + String uid, + int messageVersion) { + super(messageVersion); + this.privateNotificationPayload = privateNotificationPayload; + this.senderNodeAddress = senderNodeAddress; + this.uid = uid; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setPrivateNotificationMessage(protobuf.PrivateNotificationMessage.newBuilder() + .setPrivateNotificationPayload(privateNotificationPayload.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid)) + .build(); + } + + public static PrivateNotificationMessage fromProto(protobuf.PrivateNotificationMessage proto, int messageVersion) { + return new PrivateNotificationMessage(PrivateNotificationPayload.fromProto(proto.getPrivateNotificationPayload()), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion); + } + + @Override + public long getTTL() { + return TTL; + } +} diff --git a/core/src/main/java/bisq/core/alert/PrivateNotificationPayload.java b/core/src/main/java/bisq/core/alert/PrivateNotificationPayload.java new file mode 100644 index 0000000000..2c29647878 --- /dev/null +++ b/core/src/main/java/bisq/core/alert/PrivateNotificationPayload.java @@ -0,0 +1,101 @@ +/* + * 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 bisq.core.alert; + +import bisq.common.crypto.Sig; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.security.PublicKey; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + + +@EqualsAndHashCode +@Getter +public final class PrivateNotificationPayload implements NetworkPayload { + private final String message; + @Nullable + private String signatureAsBase64; + @Nullable + private byte[] sigPublicKeyBytes; + @Nullable + private PublicKey sigPublicKey; + + public PrivateNotificationPayload(String message) { + this.message = message; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("NullableProblems") + private PrivateNotificationPayload(String message, String signatureAsBase64, byte[] sigPublicKeyBytes) { + this(message); + this.signatureAsBase64 = signatureAsBase64; + this.sigPublicKeyBytes = sigPublicKeyBytes; + sigPublicKey = Sig.getPublicKeyFromBytes(sigPublicKeyBytes); + } + + public static PrivateNotificationPayload fromProto(protobuf.PrivateNotificationPayload proto) { + return new PrivateNotificationPayload(proto.getMessage(), + proto.getSignatureAsBase64(), + proto.getSigPublicKeyBytes().toByteArray()); + } + + @Override + public protobuf.PrivateNotificationPayload toProtoMessage() { + checkNotNull(sigPublicKeyBytes, "sigPublicKeyBytes must not be null"); + checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); + return protobuf.PrivateNotificationPayload.newBuilder() + .setMessage(message) + .setSignatureAsBase64(signatureAsBase64) + .setSigPublicKeyBytes(ByteString.copyFrom(sigPublicKeyBytes)) + .build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setSigAndPubKey(String signatureAsBase64, PublicKey sigPublicKey) { + this.signatureAsBase64 = signatureAsBase64; + this.sigPublicKey = sigPublicKey; + sigPublicKeyBytes = Sig.getPublicKeyBytes(sigPublicKey); + } + + // Hex + @Override + public String toString() { + return "PrivateNotification{" + + "message='" + message + '\'' + + ", signatureAsBase64='" + signatureAsBase64 + '\'' + + ", publicKeyBytes=" + Utilities.bytesAsHexString(sigPublicKeyBytes) + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java new file mode 100644 index 0000000000..fe5bbceba5 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -0,0 +1,363 @@ +/* + * 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 bisq.core.api; + +import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.TxFeeRateInfo; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.monetary.Price; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOffer; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.trade.Trade; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.util.concurrent.FutureCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Provides high level interface to functionality of core Bisq features. + * E.g. useful for different APIs to access data of different domains of Bisq. + */ +@Singleton +@Slf4j +public class CoreApi { + + @Getter + private final Config config; + private final CoreDisputeAgentsService coreDisputeAgentsService; + private final CoreHelpService coreHelpService; + private final CoreOffersService coreOffersService; + private final CorePaymentAccountsService paymentAccountsService; + private final CorePriceService corePriceService; + private final CoreTradesService coreTradesService; + private final CoreWalletsService walletsService; + private final TradeStatisticsManager tradeStatisticsManager; + + @Inject + public CoreApi(Config config, + CoreDisputeAgentsService coreDisputeAgentsService, + CoreHelpService coreHelpService, + CoreOffersService coreOffersService, + CorePaymentAccountsService paymentAccountsService, + CorePriceService corePriceService, + CoreTradesService coreTradesService, + CoreWalletsService walletsService, + TradeStatisticsManager tradeStatisticsManager) { + this.config = config; + this.coreDisputeAgentsService = coreDisputeAgentsService; + this.coreHelpService = coreHelpService; + this.coreOffersService = coreOffersService; + this.paymentAccountsService = paymentAccountsService; + this.coreTradesService = coreTradesService; + this.corePriceService = corePriceService; + this.walletsService = walletsService; + this.tradeStatisticsManager = tradeStatisticsManager; + } + + @SuppressWarnings("SameReturnValue") + public String getVersion() { + return Version.VERSION; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dispute Agents + /////////////////////////////////////////////////////////////////////////////////////////// + + public void registerDisputeAgent(String disputeAgentType, String registrationKey) { + coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Help + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getMethodHelp(String methodName) { + return coreHelpService.getMethodHelp(methodName); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Offers + /////////////////////////////////////////////////////////////////////////////////////////// + + public Offer getOffer(String id) { + return coreOffersService.getOffer(id); + } + + public Offer getMyOffer(String id) { + return coreOffersService.getMyOffer(id); + } + + public List getOffers(String direction, String currencyCode) { + return coreOffersService.getOffers(direction, currencyCode); + } + + public List getMyOffers(String direction, String currencyCode) { + return coreOffersService.getMyOffers(direction, currencyCode); + } + + public OpenOffer getMyOpenOffer(String id) { + return coreOffersService.getMyOpenOffer(id); + } + + public void createAnPlaceOffer(String currencyCode, + String directionAsString, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + long triggerPrice, + String paymentAccountId, + String makerFeeCurrencyCode, + Consumer resultHandler) { + coreOffersService.createAndPlaceOffer(currencyCode, + directionAsString, + priceAsString, + useMarketBasedPrice, + marketPriceMargin, + amountAsLong, + minAmountAsLong, + buyerSecurityDeposit, + triggerPrice, + paymentAccountId, + makerFeeCurrencyCode, + resultHandler); + } + + public Offer editOffer(String offerId, + String currencyCode, + OfferPayload.Direction direction, + Price price, + boolean useMarketBasedPrice, + double marketPriceMargin, + Coin amount, + Coin minAmount, + double buyerSecurityDeposit, + PaymentAccount paymentAccount) { + return coreOffersService.editOffer(offerId, + currencyCode, + direction, + price, + useMarketBasedPrice, + marketPriceMargin, + amount, + minAmount, + buyerSecurityDeposit, + paymentAccount); + } + + public void cancelOffer(String id) { + coreOffersService.cancelOffer(id); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PaymentAccounts + /////////////////////////////////////////////////////////////////////////////////////////// + + public PaymentAccount createPaymentAccount(String jsonString) { + return paymentAccountsService.createPaymentAccount(jsonString); + } + + public Set getPaymentAccounts() { + return paymentAccountsService.getPaymentAccounts(); + } + + public List getFiatPaymentMethods() { + return paymentAccountsService.getFiatPaymentMethods(); + } + + public String getPaymentAccountForm(String paymentMethodId) { + return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId); + } + + public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address, + boolean tradeInstant) { + return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName, + currencyCode, + address, + tradeInstant); + } + + public List getCryptoCurrencyPaymentMethods() { + return paymentAccountsService.getCryptoCurrencyPaymentMethods(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Prices + /////////////////////////////////////////////////////////////////////////////////////////// + + public void getMarketPrice(String currencyCode, Consumer resultHandler) { + corePriceService.getMarketPrice(currencyCode, resultHandler); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trades + /////////////////////////////////////////////////////////////////////////////////////////// + + public void takeOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { + Offer offer = coreOffersService.getOffer(offerId); + coreTradesService.takeOffer(offer, + paymentAccountId, + takerFeeCurrencyCode, + resultHandler, + errorMessageHandler); + } + + public void confirmPaymentStarted(String tradeId) { + coreTradesService.confirmPaymentStarted(tradeId); + } + + public void confirmPaymentReceived(String tradeId) { + coreTradesService.confirmPaymentReceived(tradeId); + } + + public void keepFunds(String tradeId) { + coreTradesService.keepFunds(tradeId); + } + + public void withdrawFunds(String tradeId, String address, String memo) { + coreTradesService.withdrawFunds(tradeId, address, memo); + } + + public Trade getTrade(String tradeId) { + return coreTradesService.getTrade(tradeId); + } + + public String getTradeRole(String tradeId) { + return coreTradesService.getTradeRole(tradeId); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Wallets + /////////////////////////////////////////////////////////////////////////////////////////// + + public BalancesInfo getBalances(String currencyCode) { + return walletsService.getBalances(currencyCode); + } + + public long getAddressBalance(String addressString) { + return walletsService.getAddressBalance(addressString); + } + + public AddressBalanceInfo getAddressBalanceInfo(String addressString) { + return walletsService.getAddressBalanceInfo(addressString); + } + + public List getFundingAddresses() { + return walletsService.getFundingAddresses(); + } + + public String getUnusedBsqAddress() { + return walletsService.getUnusedBsqAddress(); + } + + public void sendBsq(String address, + String amount, + String txFeeRate, + TxBroadcaster.Callback callback) { + walletsService.sendBsq(address, amount, txFeeRate, callback); + } + + public void sendBtc(String address, + String amount, + String txFeeRate, + String memo, + FutureCallback callback) { + walletsService.sendBtc(address, amount, txFeeRate, memo, callback); + } + + public boolean verifyBsqSentToAddress(String address, String amount) { + return walletsService.verifyBsqSentToAddress(address, amount); + } + + public void getTxFeeRate(ResultHandler resultHandler) { + walletsService.getTxFeeRate(resultHandler); + } + + public void setTxFeeRatePreference(long txFeeRate, + ResultHandler resultHandler) { + walletsService.setTxFeeRatePreference(txFeeRate, resultHandler); + } + + public void unsetTxFeeRatePreference(ResultHandler resultHandler) { + walletsService.unsetTxFeeRatePreference(resultHandler); + } + + public TxFeeRateInfo getMostRecentTxFeeRateInfo() { + return walletsService.getMostRecentTxFeeRateInfo(); + } + + public Transaction getTransaction(String txId) { + return walletsService.getTransaction(txId); + } + + public void setWalletPassword(String password, String newPassword) { + walletsService.setWalletPassword(password, newPassword); + } + + public void lockWallet() { + walletsService.lockWallet(); + } + + public void unlockWallet(String password, long timeout) { + walletsService.unlockWallet(password, timeout); + } + + public void removeWalletPassword(String password) { + walletsService.removeWalletPassword(password); + } + + public List getTradeStatistics() { + return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); + } + + public int getNumConfirmationsForMostRecentTransaction(String addressString) { + return walletsService.getNumConfirmationsForMostRecentTransaction(addressString); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreContext.java b/core/src/main/java/bisq/core/api/CoreContext.java new file mode 100644 index 0000000000..dece76bc06 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreContext.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.core.api; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Singleton +@Slf4j +public class CoreContext { + + @Getter + @Setter + private boolean isApiUser; + + @Inject + public CoreContext() { + } +} diff --git a/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java b/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java new file mode 100644 index 0000000000..7b060834a9 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java @@ -0,0 +1,175 @@ +/* + * 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 bisq.core.api; + +import bisq.core.support.SupportType; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import org.bitcoinj.core.ECKey; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; +import static bisq.core.support.SupportType.ARBITRATION; +import static bisq.core.support.SupportType.MEDIATION; +import static bisq.core.support.SupportType.REFUND; +import static bisq.core.support.SupportType.TRADE; +import static java.lang.String.format; +import static java.net.InetAddress.getLoopbackAddress; +import static java.util.Arrays.asList; + +@Singleton +@Slf4j +class CoreDisputeAgentsService { + + private final Config config; + private final KeyRing keyRing; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final P2PService p2PService; + private final NodeAddress nodeAddress; + private final List languageCodes; + + @Inject + public CoreDisputeAgentsService(Config config, + KeyRing keyRing, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + P2PService p2PService) { + this.config = config; + this.keyRing = keyRing; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.p2PService = p2PService; + this.nodeAddress = new NodeAddress(getLoopbackAddress().getHostAddress(), config.nodePort); + this.languageCodes = asList("de", "en", "es", "fr"); + } + + void registerDisputeAgent(String disputeAgentType, String registrationKey) { + if (!p2PService.isBootstrapped()) + throw new IllegalStateException("p2p service is not bootstrapped yet"); + + if (config.baseCurrencyNetwork.isMainnet() + || config.baseCurrencyNetwork.isDaoBetaNet() + || !config.useLocalhostForP2P) + throw new IllegalStateException("dispute agents must be registered in a Bisq UI"); + + if (!registrationKey.equals(DEV_PRIVILEGE_PRIV_KEY)) + throw new IllegalArgumentException("invalid registration key"); + + Optional supportType = getSupportType(disputeAgentType); + if (supportType.isPresent()) { + ECKey ecKey; + String signature; + switch (supportType.get()) { + case ARBITRATION: + throw new IllegalArgumentException("arbitrators must be registered in a Bisq UI"); + case MEDIATION: + ecKey = mediatorManager.getRegistrationKey(registrationKey); + signature = mediatorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); + registerMediator(nodeAddress, languageCodes, ecKey, signature); + return; + case REFUND: + ecKey = refundAgentManager.getRegistrationKey(registrationKey); + signature = refundAgentManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); + registerRefundAgent(nodeAddress, languageCodes, ecKey, signature); + return; + case TRADE: + throw new IllegalArgumentException("trade agent registration not supported"); + } + } else { + throw new IllegalArgumentException(format("unknown dispute agent type '%s'", disputeAgentType)); + } + } + + private void registerMediator(NodeAddress nodeAddress, + List languageCodes, + ECKey ecKey, + String signature) { + Mediator mediator = new Mediator(nodeAddress, + keyRing.getPubKeyRing(), + languageCodes, + new Date().getTime(), + ecKey.getPubKey(), + signature, + null, + null, + null + ); + mediatorManager.addDisputeAgent(mediator, () -> { + }, errorMessage -> { + }); + mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> + new IllegalStateException("could not register mediator")); + } + + private void registerRefundAgent(NodeAddress nodeAddress, + List languageCodes, + ECKey ecKey, + String signature) { + RefundAgent refundAgent = new RefundAgent(nodeAddress, + keyRing.getPubKeyRing(), + languageCodes, + new Date().getTime(), + ecKey.getPubKey(), + signature, + null, + null, + null + ); + refundAgentManager.addDisputeAgent(refundAgent, () -> { + }, errorMessage -> { + }); + refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> + new IllegalStateException("could not register refund agent")); + } + + private Optional getSupportType(String disputeAgentType) { + switch (disputeAgentType.toLowerCase()) { + case "arbitrator": + return Optional.of(ARBITRATION); + case "mediator": + return Optional.of(MEDIATION); + case "refundagent": + case "refund_agent": + return Optional.of(REFUND); + case "tradeagent": + case "trade_agent": + return Optional.of(TRADE); + default: + return Optional.empty(); + } + } +} diff --git a/core/src/main/java/bisq/core/api/CoreHelpService.java b/core/src/main/java/bisq/core/api/CoreHelpService.java new file mode 100644 index 0000000000..6a4605f748 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreHelpService.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.core.api; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import lombok.extern.slf4j.Slf4j; + +import static java.io.File.separator; +import static java.lang.String.format; +import static java.lang.System.out; + +@Singleton +@Slf4j +class CoreHelpService { + + @Inject + public CoreHelpService() { + } + + public String getMethodHelp(String methodName) { + String resourceFile = "/help" + separator + methodName + "-" + "help.txt"; + try { + return readHelpFile(resourceFile); + } catch (NullPointerException ex) { + log.error("", ex); + throw new IllegalStateException(format("no help found for api method %s", methodName)); + } catch (IOException ex) { + log.error("", ex); + throw new IllegalStateException(format("could not read %s help doc", methodName)); + } + } + + private String readHelpFile(String resourceFile) throws NullPointerException, IOException { + // The deployed text file is in the core.jar file, so use + // Class.getResourceAsStream to read it. + InputStream is = getClass().getResourceAsStream(resourceFile); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String line; + StringBuilder builder = new StringBuilder(); + while ((line = br.readLine()) != null) + builder.append(line).append("\n"); + + return builder.toString(); + } + + // Main method for devs to view help text without running the server. + @SuppressWarnings("CommentedOutCode") + public static void main(String[] args) { + CoreHelpService coreHelpService = new CoreHelpService(); + out.println(coreHelpService.getMethodHelp("getversion")); + // out.println(coreHelpService.getMethodHelp("getfundingaddresses")); + // out.println(coreHelpService.getMethodHelp("getfundingaddresses")); + // out.println(coreHelpService.getMethodHelp("getunusedbsqaddress")); + // out.println(coreHelpService.getMethodHelp("unsettxfeerate")); + // out.println(coreHelpService.getMethodHelp("getpaymentmethods")); + // out.println(coreHelpService.getMethodHelp("getpaymentaccts")); + // out.println(coreHelpService.getMethodHelp("lockwallet")); + // out.println(coreHelpService.getMethodHelp("gettxfeerate")); + // out.println(coreHelpService.getMethodHelp("createoffer")); + // out.println(coreHelpService.getMethodHelp("takeoffer")); + // out.println(coreHelpService.getMethodHelp("garbage")); + // out.println(coreHelpService.getMethodHelp("")); + // out.println(coreHelpService.getMethodHelp(null)); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java new file mode 100644 index 0000000000..e18a760c07 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -0,0 +1,278 @@ +/* + * 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 bisq.core.api; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.CreateOfferService; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferFilter; +import bisq.core.offer.OfferUtil; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccount; +import bisq.core.user.User; + +import bisq.common.crypto.KeyRing; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.math.BigDecimal; + +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.MathUtils.exactMultiply; +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; +import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; +import static bisq.core.offer.OfferPayload.Direction; +import static bisq.core.offer.OfferPayload.Direction.BUY; +import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; +import static java.lang.String.format; +import static java.util.Comparator.comparing; + +@Singleton +@Slf4j +class CoreOffersService { + + private final Supplier> priceComparator = () -> comparing(Offer::getPrice); + private final Supplier> reversePriceComparator = () -> comparing(Offer::getPrice).reversed(); + + private final CoreContext coreContext; + private final KeyRing keyRing; + // Dependencies on core api services in this package must be kept to an absolute + // minimum, but some trading functions require an unlocked wallet's key, so an + // exception is made in this case. + private final CoreWalletsService coreWalletsService; + private final CreateOfferService createOfferService; + private final OfferBookService offerBookService; + private final OfferFilter offerFilter; + private final OpenOfferManager openOfferManager; + private final OfferUtil offerUtil; + private final User user; + + @Inject + public CoreOffersService(CoreContext coreContext, + KeyRing keyRing, + CoreWalletsService coreWalletsService, + CreateOfferService createOfferService, + OfferBookService offerBookService, + OfferFilter offerFilter, + OpenOfferManager openOfferManager, + OfferUtil offerUtil, + User user) { + this.coreContext = coreContext; + this.keyRing = keyRing; + this.coreWalletsService = coreWalletsService; + this.createOfferService = createOfferService; + this.offerBookService = offerBookService; + this.offerFilter = offerFilter; + this.openOfferManager = openOfferManager; + this.offerUtil = offerUtil; + this.user = user; + } + + Offer getOffer(String id) { + return offerBookService.getOffers().stream() + .filter(o -> o.getId().equals(id)) + .filter(o -> !o.isMyOffer(keyRing)) + .filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid()) + .findAny().orElseThrow(() -> + new IllegalStateException(format("offer with id '%s' not found", id))); + } + + Offer getMyOffer(String id) { + return offerBookService.getOffers().stream() + .filter(o -> o.getId().equals(id)) + .filter(o -> o.isMyOffer(keyRing)) + .findAny().orElseThrow(() -> + new IllegalStateException(format("offer with id '%s' not found", id))); + } + + List getOffers(String direction, String currencyCode) { + return offerBookService.getOffers().stream() + .filter(o -> !o.isMyOffer(keyRing)) + .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) + .filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid()) + .sorted(priceComparator(direction)) + .collect(Collectors.toList()); + } + + List getMyOffers(String direction, String currencyCode) { + return offerBookService.getOffers().stream() + .filter(o -> o.isMyOffer(keyRing)) + .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) + .sorted(priceComparator(direction)) + .collect(Collectors.toList()); + } + + OpenOffer getMyOpenOffer(String id) { + return openOfferManager.getOpenOfferById(id) + .filter(open -> open.getOffer().isMyOffer(keyRing)) + .orElseThrow(() -> + new IllegalStateException(format("openoffer with id '%s' not found", id))); + } + + // Create and place new offer. + void createAndPlaceOffer(String currencyCode, + String directionAsString, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + long triggerPrice, + String paymentAccountId, + String makerFeeCurrencyCode, + Consumer resultHandler) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + offerUtil.maybeSetFeePaymentCurrencyPreference(makerFeeCurrencyCode); + + PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); + if (paymentAccount == null) + throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); + + String upperCaseCurrencyCode = currencyCode.toUpperCase(); + String offerId = createOfferService.getRandomOfferId(); + Direction direction = Direction.valueOf(directionAsString.toUpperCase()); + Price price = Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode)); + Coin amount = Coin.valueOf(amountAsLong); + Coin minAmount = Coin.valueOf(minAmountAsLong); + Coin useDefaultTxFee = Coin.ZERO; + Offer offer = createOfferService.createAndGetOffer(offerId, + direction, + upperCaseCurrencyCode, + amount, + minAmount, + price, + useDefaultTxFee, + useMarketBasedPrice, + exactMultiply(marketPriceMargin, 0.01), + buyerSecurityDeposit, + paymentAccount); + + verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); + + // We don't support atm funding from external wallet to keep it simple. + boolean useSavingsWallet = true; + //noinspection ConstantConditions + placeOffer(offer, + buyerSecurityDeposit, + triggerPrice, + useSavingsWallet, + transaction -> resultHandler.accept(offer)); + } + + // Edit a placed offer. + Offer editOffer(String offerId, + String currencyCode, + Direction direction, + Price price, + boolean useMarketBasedPrice, + double marketPriceMargin, + Coin amount, + Coin minAmount, + double buyerSecurityDeposit, + PaymentAccount paymentAccount) { + Coin useDefaultTxFee = Coin.ZERO; + return createOfferService.createAndGetOffer(offerId, + direction, + currencyCode.toUpperCase(), + amount, + minAmount, + price, + useDefaultTxFee, + useMarketBasedPrice, + exactMultiply(marketPriceMargin, 0.01), + buyerSecurityDeposit, + paymentAccount); + } + + void cancelOffer(String id) { + Offer offer = getMyOffer(id); + openOfferManager.removeOffer(offer, + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + }); + } + + private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { + if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { + String error = format("cannot create %s offer with payment account %s", + offer.getOfferPayload().getCounterCurrencyCode(), + paymentAccount.getId()); + throw new IllegalStateException(error); + } + } + + private void placeOffer(Offer offer, + double buyerSecurityDeposit, + long triggerPrice, + boolean useSavingsWallet, + Consumer resultHandler) { + openOfferManager.placeOffer(offer, + buyerSecurityDeposit, + useSavingsWallet, + triggerPrice, + resultHandler::accept, + log::error); + + if (offer.getErrorMessage() != null) + throw new IllegalStateException(offer.getErrorMessage()); + } + + private boolean offerMatchesDirectionAndCurrency(Offer offer, + String direction, + String currencyCode) { + var offerOfWantedDirection = offer.getDirection().name().equalsIgnoreCase(direction); + var offerInWantedCurrency = offer.getOfferPayload().getCounterCurrencyCode() + .equalsIgnoreCase(currencyCode); + return offerOfWantedDirection && offerInWantedCurrency; + } + + private Comparator priceComparator(String direction) { + // A buyer probably wants to see sell orders in price ascending order. + // A seller probably wants to see buy orders in price descending order. + return direction.equalsIgnoreCase(BUY.name()) + ? reversePriceComparator.get() + : priceComparator.get(); + } + + private long priceStringToLong(String priceAsString, String currencyCode) { + int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + double priceAsDouble = new BigDecimal(priceAsString).doubleValue(); + double scaled = scaleUpByPowerOf10(priceAsDouble, precision); + return roundDoubleToLong(scaled); + } +} diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java new file mode 100644 index 0000000000..0843e20ab7 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -0,0 +1,146 @@ +/* + * 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 bisq.core.api; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.model.PaymentAccountForm; +import bisq.core.locale.CryptoCurrency; +import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.user.User; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.File; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.locale.CurrencyUtil.getCryptoCurrency; +import static java.lang.String.format; + +@Singleton +@Slf4j +class CorePaymentAccountsService { + + private final CoreWalletsService coreWalletsService; + private final AccountAgeWitnessService accountAgeWitnessService; + private final PaymentAccountForm paymentAccountForm; + private final User user; + + @Inject + public CorePaymentAccountsService(CoreWalletsService coreWalletsService, + AccountAgeWitnessService accountAgeWitnessService, + PaymentAccountForm paymentAccountForm, + User user) { + this.coreWalletsService = coreWalletsService; + this.accountAgeWitnessService = accountAgeWitnessService; + this.paymentAccountForm = paymentAccountForm; + this.user = user; + } + + // Fiat Currency Accounts + + PaymentAccount createPaymentAccount(String jsonString) { + PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString); + verifyPaymentAccountHasRequiredFields(paymentAccount); + user.addPaymentAccountIfNotExists(paymentAccount); + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + log.info("Saved payment account with id {} and payment method {}.", + paymentAccount.getId(), + paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); + return paymentAccount; + } + + Set getPaymentAccounts() { + return user.getPaymentAccounts(); + } + + List getFiatPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() + .filter(paymentMethod -> !paymentMethod.isAsset()) + .sorted(Comparator.comparing(PaymentMethod::getId)) + .collect(Collectors.toList()); + } + + String getPaymentAccountFormAsString(String paymentMethodId) { + File jsonForm = getPaymentAccountForm(paymentMethodId); + jsonForm.deleteOnExit(); // If just asking for a string, delete the form file. + return paymentAccountForm.toJsonString(jsonForm); + } + + File getPaymentAccountForm(String paymentMethodId) { + return paymentAccountForm.getPaymentAccountForm(paymentMethodId); + } + + // Crypto Currency Accounts + + PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address, + boolean tradeInstant) { + String bsqCode = currencyCode.toUpperCase(); + if (!bsqCode.equals("BSQ")) + throw new IllegalArgumentException("api does not currently support " + currencyCode + " accounts"); + + // Validate the BSQ address string but ignore the return value. + coreWalletsService.getValidBsqLegacyAddress(address); + + var cryptoCurrencyAccount = tradeInstant + ? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT) + : (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS); + cryptoCurrencyAccount.init(); + cryptoCurrencyAccount.setAccountName(accountName); + cryptoCurrencyAccount.setAddress(address); + Optional cryptoCurrency = getCryptoCurrency(bsqCode); + cryptoCurrency.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); + user.addPaymentAccount(cryptoCurrencyAccount); + accountAgeWitnessService.publishMyAccountAgeWitness(cryptoCurrencyAccount.getPaymentAccountPayload()); + log.info("Saved crypto payment account with id {} and payment method {}.", + cryptoCurrencyAccount.getId(), + cryptoCurrencyAccount.getPaymentAccountPayload().getPaymentMethodId()); + return cryptoCurrencyAccount; + } + + // TODO Support all alt coin payment methods supported by UI. + // The getCryptoCurrencyPaymentMethods method below will be + // callable from the CLI when more are supported. + + List getCryptoCurrencyPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() + .filter(PaymentMethod::isAsset) + .sorted(Comparator.comparing(PaymentMethod::getId)) + .collect(Collectors.toList()); + } + + private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) { + // Do checks here to make sure required fields are populated. + if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty()) + throw new IllegalArgumentException(format("no trade currencies defined for %s payment account", + paymentAccount.getPaymentMethod().getDisplayString().toLowerCase())); + } +} diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java new file mode 100644 index 0000000000..4553689e98 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.api; + +import bisq.core.provider.price.PriceFeedService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.MathUtils.roundDouble; +import static bisq.core.locale.CurrencyUtil.isFiatCurrency; +import static java.lang.String.format; + +@Singleton +@Slf4j +class CorePriceService { + + private final PriceFeedService priceFeedService; + + @Inject + public CorePriceService(PriceFeedService priceFeedService) { + this.priceFeedService = priceFeedService; + } + + public void getMarketPrice(String currencyCode, Consumer resultHandler) { + String upperCaseCurrencyCode = currencyCode.toUpperCase(); + + if (!isFiatCurrency(upperCaseCurrencyCode)) + throw new IllegalStateException(format("%s is not a valid currency code", upperCaseCurrencyCode)); + + if (!priceFeedService.hasPrices()) + throw new IllegalStateException("price feed service has no prices"); + + try { + priceFeedService.setCurrencyCode(upperCaseCurrencyCode); + } catch (Throwable throwable) { + log.warn("Could not set currency code in PriceFeedService", throwable); + } + + priceFeedService.requestPriceFeed(price -> { + if (price > 0) { + log.info("{} price feed request returned {}", upperCaseCurrencyCode, price); + resultHandler.accept(roundDouble(price, 4)); + } else { + throw new IllegalStateException(format("%s price is not available", upperCaseCurrencyCode)); + } + }, + (errorMessage, throwable) -> log.warn(errorMessage, throwable)); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java new file mode 100644 index 0000000000..2f5683108a --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -0,0 +1,272 @@ +/* + * 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 bisq.core.api; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferUtil; +import bisq.core.offer.takeoffer.TakeOfferModel; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.TradeUtil; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.protocol.BuyerProtocol; +import bisq.core.trade.protocol.SellerProtocol; +import bisq.core.user.User; +import bisq.core.util.validation.BtcAddressValidator; + +import bisq.common.handlers.ErrorMessageHandler; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT; +import static java.lang.String.format; + +@Singleton +@Slf4j +class CoreTradesService { + + private final CoreContext coreContext; + // Dependencies on core api services in this package must be kept to an absolute + // minimum, but some trading functions require an unlocked wallet's key, so an + // exception is made in this case. + private final CoreWalletsService coreWalletsService; + private final BtcWalletService btcWalletService; + private final OfferUtil offerUtil; + private final ClosedTradableManager closedTradableManager; + private final TakeOfferModel takeOfferModel; + private final TradeManager tradeManager; + private final TradeUtil tradeUtil; + private final User user; + + @Inject + public CoreTradesService(CoreContext coreContext, + CoreWalletsService coreWalletsService, + BtcWalletService btcWalletService, + OfferUtil offerUtil, + ClosedTradableManager closedTradableManager, + TakeOfferModel takeOfferModel, + TradeManager tradeManager, + TradeUtil tradeUtil, + User user) { + this.coreContext = coreContext; + this.coreWalletsService = coreWalletsService; + this.btcWalletService = btcWalletService; + this.offerUtil = offerUtil; + this.closedTradableManager = closedTradableManager; + this.takeOfferModel = takeOfferModel; + this.tradeManager = tradeManager; + this.tradeUtil = tradeUtil; + this.user = user; + } + + void takeOffer(Offer offer, + String paymentAccountId, + String takerFeeCurrencyCode, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + + offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode); + + var paymentAccount = user.getPaymentAccount(paymentAccountId); + if (paymentAccount == null) + throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId)); + + var useSavingsWallet = true; + //noinspection ConstantConditions + takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet); + log.info("Initiating take {} offer, {}", + offer.isBuyOffer() ? "buy" : "sell", + takeOfferModel); + //noinspection ConstantConditions + tradeManager.onTakeOffer(offer.getAmount(), + takeOfferModel.getTxFeeFromFeeService(), + takeOfferModel.getTakerFee(), + takeOfferModel.isCurrencyForTakerFeeBtc(), + offer.getPrice().getValue(), + takeOfferModel.getFundsNeededForTrade(), + offer, + paymentAccountId, + useSavingsWallet, + coreContext.isApiUser(), + resultHandler::accept, + errorMessageHandler + ); + } + + void confirmPaymentStarted(String tradeId) { + var trade = getTrade(tradeId); + if (isFollowingBuyerProtocol(trade)) { + var tradeProtocol = tradeManager.getTradeProtocol(trade); + ((BuyerProtocol) tradeProtocol).onPaymentStarted( + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + } + ); + } else { + throw new IllegalStateException("you are the seller and not sending payment"); + } + } + + void confirmPaymentReceived(String tradeId) { + var trade = getTrade(tradeId); + if (isFollowingBuyerProtocol(trade)) { + throw new IllegalStateException("you are the buyer, and not receiving payment"); + } else { + var tradeProtocol = tradeManager.getTradeProtocol(trade); + ((SellerProtocol) tradeProtocol).onPaymentReceived( + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + } + ); + } + } + + void keepFunds(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + + verifyTradeIsNotClosed(tradeId); + var trade = getOpenTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + log.info("Keeping funds received from trade {}", tradeId); + tradeManager.onTradeCompleted(trade); + } + + void withdrawFunds(String tradeId, String toAddress, String memo) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + + verifyTradeIsNotClosed(tradeId); + var trade = getOpenTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + + verifyIsValidBTCAddress(toAddress); + + var fromAddressEntry = btcWalletService.getOrCreateAddressEntry(trade.getId(), TRADE_PAYOUT); + verifyFundsNotWithdrawn(fromAddressEntry); + + var amount = trade.getPayoutAmount(); + var fee = getEstimatedTxFee(fromAddressEntry.getAddressString(), toAddress, amount); + var receiverAmount = amount.subtract(fee); + + log.info(format("Withdrawing funds received from trade %s:" + + "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s%n Memo %s%n", + tradeId, + fromAddressEntry.getAddressString(), + toAddress, + amount.toFriendlyString(), + fee.toFriendlyString(), + receiverAmount.toFriendlyString(), + memo)); + tradeManager.onWithdrawRequest( + toAddress, + amount, + fee, + coreWalletsService.getKey(), + trade, + memo.isEmpty() ? null : memo, + () -> { + }, + (errorMessage, throwable) -> { + log.error(errorMessage, throwable); + throw new IllegalStateException(errorMessage, throwable); + }); + } + + String getTradeRole(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + return tradeUtil.getRole(getTrade(tradeId)); + } + + Trade getTrade(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + return getOpenTrade(tradeId).orElseGet(() -> + getClosedTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId)) + )); + } + + private Optional getOpenTrade(String tradeId) { + return tradeManager.getTradeById(tradeId); + } + + private Optional getClosedTrade(String tradeId) { + Optional tradable = closedTradableManager.getTradableById(tradeId); + return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value); + } + + private boolean isFollowingBuyerProtocol(Trade trade) { + 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/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java new file mode 100644 index 0000000000..b9ea896ef7 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -0,0 +1,676 @@ +/* + * 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 bisq.core.api; + +import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; +import bisq.core.api.model.TxFeeRateInfo; +import bisq.core.app.AppStartupState; +import bisq.core.btc.Balances; +import bisq.core.btc.exceptions.AddressEntryException; +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.InsufficientFundsException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.BsqTransferModel; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqTransferService; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.KeyCrypterScrypt; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.common.config.BaseCurrencyNetwork.BTC_DAO_REGTEST; +import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput; +import static bisq.core.util.ParsingUtils.parseToCoin; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +@Singleton +@Slf4j +class CoreWalletsService { + + private final AppStartupState appStartupState; + private final CoreContext coreContext; + private final Balances balances; + private final WalletsManager walletsManager; + private final WalletsSetup walletsSetup; + private final BsqWalletService bsqWalletService; + private final BsqTransferService bsqTransferService; + private final BsqFormatter bsqFormatter; + private final BtcWalletService btcWalletService; + private final CoinFormatter btcFormatter; + private final FeeService feeService; + private final Preferences preferences; + + @Nullable + private Timer lockTimer; + + @Nullable + private KeyParameter tempAesKey; + + private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("CoreWalletsService"); + + @Inject + public CoreWalletsService(AppStartupState appStartupState, + CoreContext coreContext, + Balances balances, + WalletsManager walletsManager, + WalletsSetup walletsSetup, + BsqWalletService bsqWalletService, + BsqTransferService bsqTransferService, + BsqFormatter bsqFormatter, + BtcWalletService btcWalletService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + FeeService feeService, + Preferences preferences) { + this.appStartupState = appStartupState; + this.coreContext = coreContext; + this.balances = balances; + this.walletsManager = walletsManager; + this.walletsSetup = walletsSetup; + this.bsqWalletService = bsqWalletService; + this.bsqTransferService = bsqTransferService; + this.bsqFormatter = bsqFormatter; + this.btcWalletService = btcWalletService; + this.btcFormatter = btcFormatter; + this.feeService = feeService; + this.preferences = preferences; + } + + @Nullable + KeyParameter getKey() { + verifyEncryptedWalletIsUnlocked(); + return tempAesKey; + } + + NetworkParameters getNetworkParameters() { + return btcWalletService.getWallet().getContext().getParams(); + } + + BalancesInfo getBalances(String currencyCode) { + verifyWalletCurrencyCodeIsValid(currencyCode); + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + if (balances.getAvailableBalance().get() == null) + throw new IllegalStateException("balance is not yet available"); + + switch (currencyCode.trim().toUpperCase()) { + case "BSQ": + return new BalancesInfo(getBsqBalances(), BtcBalanceInfo.EMPTY); + case "BTC": + return new BalancesInfo(BsqBalanceInfo.EMPTY, getBtcBalances()); + default: + return new BalancesInfo(getBsqBalances(), getBtcBalances()); + } + } + + long getAddressBalance(String addressString) { + Address address = getAddressEntry(addressString).getAddress(); + return btcWalletService.getBalanceForAddress(address).value; + } + + AddressBalanceInfo getAddressBalanceInfo(String addressString) { + var satoshiBalance = getAddressBalance(addressString); + var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString); + Address address = getAddressEntry(addressString).getAddress(); + return new AddressBalanceInfo(addressString, + satoshiBalance, + numConfirmations, + btcWalletService.isAddressUnused(address)); + } + + List getFundingAddresses() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + // Create a new unused funding address if none exists. + boolean unusedAddressExists = btcWalletService.getAvailableAddressEntries() + .stream() + .anyMatch(a -> btcWalletService.isAddressUnused(a.getAddress())); + if (!unusedAddressExists) + btcWalletService.getFreshAddressEntry(); + + List addressStrings = btcWalletService + .getAvailableAddressEntries() + .stream() + .map(AddressEntry::getAddressString) + .collect(Collectors.toList()); + + // getAddressBalance is memoized, because we'll map it over addresses twice. + // To get the balances, we'll be using .getUnchecked, because we know that + // this::getAddressBalance cannot return null. + var balances = memoize(this::getAddressBalance); + + boolean noAddressHasZeroBalance = addressStrings.stream() + .allMatch(addressString -> balances.getUnchecked(addressString) != 0); + + if (noAddressHasZeroBalance) { + var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry(); + addressStrings.add(newZeroBalanceAddress.getAddressString()); + } + + return addressStrings.stream().map(address -> + new AddressBalanceInfo(address, + balances.getUnchecked(address), + getNumConfirmationsForMostRecentTransaction(address), + btcWalletService.isAddressUnused(getAddressEntry(address).getAddress()))) + .collect(Collectors.toList()); + } + + String getUnusedBsqAddress() { + return bsqWalletService.getUnusedBsqAddressAsString(); + } + + void sendBsq(String address, + String amount, + String txFeeRate, + TxBroadcaster.Callback callback) { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + try { + LegacyAddress legacyAddress = getValidBsqLegacyAddress(address); + Coin receiverAmount = getValidTransferAmount(amount, bsqFormatter); + Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate); + BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, + receiverAmount, + txFeePerVbyte); + log.info("Sending {} BSQ to {} with tx fee rate {} sats/byte.", + amount, + address, + txFeePerVbyte.value); + bsqTransferService.sendFunds(model, callback); + } catch (InsufficientMoneyException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send bsq due to insufficient funds", ex); + } catch (NumberFormatException + | BsqChangeBelowDustException + | TransactionVerificationException + | WalletException ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + } + + void sendBtc(String address, + String amount, + String txFeeRate, + String memo, + FutureCallback callback) { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + try { + Set fromAddresses = btcWalletService.getAddressEntriesForAvailableBalanceStream() + .map(AddressEntry::getAddressString) + .collect(Collectors.toSet()); + Coin receiverAmount = getValidTransferAmount(amount, btcFormatter); + Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate); + + // TODO Support feeExcluded (or included), default is fee included. + // See WithdrawalView # onWithdraw (and refactor). + Transaction feeEstimationTransaction = + btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, + receiverAmount, + txFeePerVbyte); + if (feeEstimationTransaction == null) + throw new IllegalStateException("could not estimate the transaction fee"); + + Coin dust = btcWalletService.getDust(feeEstimationTransaction); + Coin fee = feeEstimationTransaction.getFee().add(dust); + if (dust.isPositive()) { + fee = feeEstimationTransaction.getFee().add(dust); + log.info("Dust txo ({} sats) was detected, the dust amount has been added to the fee (was {}, now {})", + dust.value, + feeEstimationTransaction.getFee(), + fee.value); + } + log.info("Sending {} BTC to {} with tx fee of {} sats (fee rate {} sats/byte).", + amount, + address, + fee.value, + txFeePerVbyte.value); + btcWalletService.sendFundsForMultipleAddresses(fromAddresses, + address, + receiverAmount, + fee, + null, + tempAesKey, + memo.isEmpty() ? null : memo, + callback); + } catch (AddressEntryException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send btc from any addresses in wallet", ex); + } catch (InsufficientFundsException | InsufficientMoneyException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send btc due to insufficient funds", ex); + } + } + + boolean verifyBsqSentToAddress(String address, String amount) { + Address receiverAddress = getValidBsqLegacyAddress(address); + NetworkParameters networkParameters = getNetworkParameters(); + Predicate isTxOutputAddressMatch = (txOut) -> + txOut.getScriptPubKey().getToAddress(networkParameters).equals(receiverAddress); + Coin coinValue = parseToCoin(amount, bsqFormatter); + Predicate isTxOutputValueMatch = (txOut) -> + txOut.getValue().longValue() == coinValue.longValue(); + List spendableBsqTxOutputs = bsqWalletService.getSpendableBsqTransactionOutputs(); + + log.info("Searching {} spendable tx outputs for matching address {} and value {}:", + spendableBsqTxOutputs.size(), + address, + coinValue.toPlainString()); + long numMatches = 0; + for (TransactionOutput txOut : spendableBsqTxOutputs) { + if (isTxOutputAddressMatch.test(txOut) && isTxOutputValueMatch.test(txOut)) { + log.info("\t\tTx {} output has matching address {} and value {}.", + txOut.getParentTransaction().getTxId(), + address, + txOut.getValue().toPlainString()); + numMatches++; + } + } + if (numMatches > 1) { + log.warn("{} tx outputs matched address {} and value {}, could be a" + + " false positive BSQ payment verification result.", + numMatches, + address, + coinValue.toPlainString()); + + } + return numMatches > 0; + } + + void getTxFeeRate(ResultHandler resultHandler) { + try { + @SuppressWarnings({"unchecked", "Convert2MethodRef"}) + ListenableFuture future = + (ListenableFuture) executor.submit(() -> feeService.requestFees()); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void ignored) { + resultHandler.handleResult(); + } + + @Override + public void onFailure(Throwable t) { + log.error("", t); + throw new IllegalStateException("could not request fees from fee service", t); + } + }, MoreExecutors.directExecutor()); + + } catch (Exception ex) { + log.error("", ex); + throw new IllegalStateException("could not request fees from fee service", ex); + } + } + + void setTxFeeRatePreference(long txFeeRate, + ResultHandler resultHandler) { + long minFeePerVbyte = feeService.getMinFeePerVByte(); + if (txFeeRate < minFeePerVbyte) + throw new IllegalStateException( + format("tx fee rate preference must be >= %d sats/byte", minFeePerVbyte)); + + preferences.setUseCustomWithdrawalTxFee(true); + Coin satsPerByte = Coin.valueOf(txFeeRate); + preferences.setWithdrawalTxFeeInVbytes(satsPerByte.value); + getTxFeeRate(resultHandler); + } + + void unsetTxFeeRatePreference(ResultHandler resultHandler) { + preferences.setUseCustomWithdrawalTxFee(false); + getTxFeeRate(resultHandler); + } + + TxFeeRateInfo getMostRecentTxFeeRateInfo() { + return new TxFeeRateInfo( + preferences.isUseCustomWithdrawalTxFee(), + preferences.getWithdrawalTxFeeInVbytes(), + feeService.getMinFeePerVByte(), + feeService.getTxFeePerVbyte().value, + feeService.getLastRequest()); + } + + Transaction getTransaction(String txId) { + if (txId.length() != 64) + throw new IllegalArgumentException(format("%s is not a transaction id", txId)); + + try { + Transaction tx = btcWalletService.getTransaction(txId); + if (tx == null) + throw new IllegalArgumentException(format("tx with id %s not found", txId)); + else + return tx; + + } catch (IllegalArgumentException ex) { + log.error("", ex); + throw new IllegalArgumentException( + format("could not get transaction with id %s%ncause: %s", + txId, + ex.getMessage().toLowerCase())); + } + } + + int getNumConfirmationsForMostRecentTransaction(String addressString) { + Address address = getAddressEntry(addressString).getAddress(); + TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); + return confidence == null ? 0 : confidence.getDepthInBlocks(); + } + + void setWalletPassword(String password, String newPassword) { + verifyWalletsAreAvailable(); + + KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); + + if (newPassword != null && !newPassword.isEmpty()) { + // TODO Validate new password before replacing old password. + if (!walletsManager.areWalletsEncrypted()) + throw new IllegalStateException("wallet is not encrypted with a password"); + + KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); + if (!walletsManager.checkAESKey(aesKey)) + throw new IllegalStateException("incorrect old password"); + + walletsManager.decryptWallets(aesKey); + aesKey = keyCrypterScrypt.deriveKey(newPassword); + walletsManager.encryptWallets(keyCrypterScrypt, aesKey); + walletsManager.backupWallets(); + return; + } + + if (walletsManager.areWalletsEncrypted()) + throw new IllegalStateException("wallet is encrypted with a password"); + + // TODO Validate new password. + KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); + walletsManager.encryptWallets(keyCrypterScrypt, aesKey); + walletsManager.backupWallets(); + } + + void lockWallet() { + if (!walletsManager.areWalletsEncrypted()) + throw new IllegalStateException("wallet is not encrypted with a password"); + + if (tempAesKey == null) + throw new IllegalStateException("wallet is already locked"); + + tempAesKey = null; + } + + void unlockWallet(String password, long timeout) { + verifyWalletIsAvailableAndEncrypted(); + + KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); + // The aesKey is also cached for timeout (secs) after being used to decrypt the + // wallet, in case the user wants to manually lock the wallet before the timeout. + tempAesKey = keyCrypterScrypt.deriveKey(password); + + if (!walletsManager.checkAESKey(tempAesKey)) + throw new IllegalStateException("incorrect password"); + + if (lockTimer != null) { + // The user has called unlockwallet again, before the prior unlockwallet + // timeout has expired. He's overriding it with a new timeout value. + // Remove the existing lock timer to prevent it from calling lockwallet + // before or after the new one does. + lockTimer.stop(); + lockTimer = null; + } + + if (coreContext.isApiUser()) + maybeSetWalletsManagerKey(); + + lockTimer = UserThread.runAfter(() -> { + if (tempAesKey != null) { + // The unlockwallet timeout has expired; re-lock the wallet. + log.info("Locking wallet after {} second timeout expired.", timeout); + tempAesKey = null; + } + }, timeout, SECONDS); + } + + // Provided for automated wallet protection method testing, despite the + // security risks exposed by providing users the ability to decrypt their wallets. + void removeWalletPassword(String password) { + verifyWalletIsAvailableAndEncrypted(); + KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); + + KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); + if (!walletsManager.checkAESKey(aesKey)) + throw new IllegalStateException("incorrect password"); + + walletsManager.decryptWallets(aesKey); + walletsManager.backupWallets(); + } + + // Throws a RuntimeException if wallets are not available (encrypted or not). + void verifyWalletsAreAvailable() { + verifyWalletAndNetworkIsReady(); + + // TODO This check may be redundant, but the AppStartupState is new and unused + // prior to commit 838595cb03886c3980c40df9cfe5f19e9f8a0e39. I would prefer + // to leave this check in place until certain AppStartupState will always work + // as expected. + if (!walletsManager.areWalletsAvailable()) + throw new IllegalStateException("wallet is not yet available"); + } + + // Throws a RuntimeException if wallets are not available or not encrypted. + void verifyWalletIsAvailableAndEncrypted() { + verifyWalletAndNetworkIsReady(); + + if (!walletsManager.areWalletsAvailable()) + throw new IllegalStateException("wallet is not yet available"); + + if (!walletsManager.areWalletsEncrypted()) + throw new IllegalStateException("wallet is not encrypted with a password"); + } + + // Throws a RuntimeException if wallets are encrypted and locked. + void verifyEncryptedWalletIsUnlocked() { + if (walletsManager.areWalletsEncrypted() && tempAesKey == null) + throw new IllegalStateException("wallet is locked"); + } + + // Throws a RuntimeException if wallets and network are not ready. + void verifyWalletAndNetworkIsReady() { + if (!appStartupState.isWalletAndNetworkReady()) + throw new IllegalStateException("wallet and network is not yet initialized"); + } + + // Throws a RuntimeException if application is not fully initialized. + void verifyApplicationIsFullyInitialized() { + if (!appStartupState.isApplicationFullyInitialized()) + throw new IllegalStateException("server is not fully initialized"); + } + + // Returns a LegacyAddress for the string, or a RuntimeException if invalid. + LegacyAddress getValidBsqLegacyAddress(String address) { + try { + return bsqFormatter.getAddressFromBsqAddress(address); + } catch (Throwable t) { + log.error("", t); + throw new IllegalStateException(format("%s is not a valid bsq address", address)); + } + } + + // Throws a RuntimeException if wallet currency code is not BSQ or BTC. + private void verifyWalletCurrencyCodeIsValid(String currencyCode) { + if (currencyCode == null || currencyCode.isEmpty()) + return; + + if (!currencyCode.equalsIgnoreCase("BSQ") + && !currencyCode.equalsIgnoreCase("BTC")) + throw new IllegalStateException(format("wallet does not support %s", currencyCode)); + } + + private void maybeSetWalletsManagerKey() { + // Unlike the UI, a daemon cannot capture the user's wallet encryption password + // during startup. This method will set the wallet service's aesKey if necessary. + if (tempAesKey == null) + throw new IllegalStateException("cannot use null key, unlockwallet timeout may have expired"); + + if (btcWalletService.getAesKey() == null || bsqWalletService.getAesKey() == null) { + KeyParameter aesKey = new KeyParameter(tempAesKey.getKey()); + walletsManager.setAesKey(aesKey); + walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(), aesKey); + } + } + + private BsqBalanceInfo getBsqBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance(); + var unverifiedBalance = bsqWalletService.getUnverifiedBalance(); + var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance(); + var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance(); + var lockupBondsBalance = bsqWalletService.getLockupBondsBalance(); + var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance(); + + return new BsqBalanceInfo(availableConfirmedBalance.value, + unverifiedBalance.value, + unconfirmedChangeBalance.value, + lockedForVotingBalance.value, + lockupBondsBalance.value, + unlockingBondsBalance.value); + } + + private BtcBalanceInfo getBtcBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableBalance = balances.getAvailableBalance().get(); + if (availableBalance == null) + throw new IllegalStateException("balance is not yet available"); + + var reservedBalance = balances.getReservedBalance().get(); + if (reservedBalance == null) + throw new IllegalStateException("reserved balance is not yet available"); + + var lockedBalance = balances.getLockedBalance().get(); + if (lockedBalance == null) + throw new IllegalStateException("locked balance is not yet available"); + + return new BtcBalanceInfo(availableBalance.value, + reservedBalance.value, + availableBalance.add(reservedBalance).value, + lockedBalance.value); + } + + // Returns a Coin for the transfer amount string, or a RuntimeException if invalid. + private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) { + Coin amountAsCoin = parseToCoin(amount, coinFormatter); + if (amountAsCoin.isLessThan(getMinNonDustOutput())) + throw new IllegalStateException(format("%s is an invalid transfer amount", amount)); + + return amountAsCoin; + } + + private Coin getTxFeeRateFromParamOrPreferenceOrFeeService(String txFeeRate) { + // A non txFeeRate String value overrides the fee service and custom fee. + return txFeeRate.isEmpty() + ? btcWalletService.getTxFeeForWithdrawalPerVbyte() + : Coin.valueOf(Long.parseLong(txFeeRate)); + } + + private KeyCrypterScrypt getKeyCrypterScrypt() { + KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); + if (keyCrypterScrypt == null) + throw new IllegalStateException("wallet encrypter is not available"); + return keyCrypterScrypt; + } + + private AddressEntry getAddressEntry(String addressString) { + Optional addressEntry = + btcWalletService.getAddressEntryListAsImmutableList().stream() + .filter(e -> addressString.equals(e.getAddressString())) + .findFirst(); + + if (!addressEntry.isPresent()) + throw new IllegalStateException(format("address %s not found in wallet", addressString)); + + return addressEntry.get(); + } + + /** + * Memoization stores the results of expensive function calls and returns + * the cached result when the same input occurs again. + * + * Resulting LoadingCache is used by calling `.get(input I)` or + * `.getUnchecked(input I)`, depending on whether or not `f` can return null. + * That's because CacheLoader throws an exception on null output from `f`. + */ + private static LoadingCache memoize(Function f) { + // f::apply is used, because Guava 20.0 Function doesn't yet extend + // Java Function. + return CacheBuilder.newBuilder().build(CacheLoader.from(f::apply)); + } +} diff --git a/core/src/main/java/bisq/core/api/model/AddressBalanceInfo.java b/core/src/main/java/bisq/core/api/model/AddressBalanceInfo.java new file mode 100644 index 0000000000..5921b0ad18 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/AddressBalanceInfo.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.api.model; + +import bisq.common.Payload; + +public class AddressBalanceInfo implements Payload { + + private final String address; + private final long balance; // address' balance in satoshis + private final long numConfirmations; // # confirmations for address' most recent tx + private final boolean isAddressUnused; + + public AddressBalanceInfo(String address, + long balance, + long numConfirmations, + boolean isAddressUnused) { + this.address = address; + this.balance = balance; + this.numConfirmations = numConfirmations; + this.isAddressUnused = isAddressUnused; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.AddressBalanceInfo toProtoMessage() { + return bisq.proto.grpc.AddressBalanceInfo.newBuilder() + .setAddress(address) + .setBalance(balance) + .setNumConfirmations(numConfirmations) + .setIsAddressUnused(isAddressUnused) + .build(); + } + + public static AddressBalanceInfo fromProto(bisq.proto.grpc.AddressBalanceInfo proto) { + return new AddressBalanceInfo(proto.getAddress(), + proto.getBalance(), + proto.getNumConfirmations(), + proto.getIsAddressUnused()); + } + + @Override + public String toString() { + return "AddressBalanceInfo{" + + "address='" + address + '\'' + + ", balance=" + balance + + ", numConfirmations=" + numConfirmations + + ", isAddressUnused=" + isAddressUnused + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java new file mode 100644 index 0000000000..3b063bc0d2 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -0,0 +1,45 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import lombok.Getter; + +@Getter +public class BalancesInfo implements Payload { + + // Getter names are shortened for readability's sake, i.e., + // balancesInfo.getBtc().getAvailableBalance() is cleaner than + // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). + private final BsqBalanceInfo bsq; + private final BtcBalanceInfo btc; + + public BalancesInfo(BsqBalanceInfo bsq, BtcBalanceInfo btc) { + this.bsq = bsq; + this.btc = btc; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BalancesInfo toProtoMessage() { + return bisq.proto.grpc.BalancesInfo.newBuilder() + .setBsq(bsq.toProtoMessage()) + .setBtc(btc.toProtoMessage()) + .build(); + } + + public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) { + return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsq()), + BtcBalanceInfo.fromProto(proto.getBtc())); + } + + @Override + public String toString() { + return "BalancesInfo{" + "\n" + + " " + bsq.toString() + "\n" + + ", " + btc.toString() + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java new file mode 100644 index 0000000000..23324e21f3 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java @@ -0,0 +1,94 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BsqBalanceInfo implements Payload { + + public static final BsqBalanceInfo EMPTY = new BsqBalanceInfo(-1, + -1, + -1, + -1, + -1, + -1); + + // All balances are in BSQ satoshis. + private final long availableConfirmedBalance; + private final long unverifiedBalance; + private final long unconfirmedChangeBalance; + private final long lockedForVotingBalance; + private final long lockupBondsBalance; + private final long unlockingBondsBalance; + + public BsqBalanceInfo(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + this.availableConfirmedBalance = availableConfirmedBalance; + this.unverifiedBalance = unverifiedBalance; + this.unconfirmedChangeBalance = unconfirmedChangeBalance; + this.lockedForVotingBalance = lockedForVotingBalance; + this.lockupBondsBalance = lockupBondsBalance; + this.unlockingBondsBalance = unlockingBondsBalance; + } + + @VisibleForTesting + public static BsqBalanceInfo valueOf(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + // Convenience for creating a model instance instead of a proto. + return new BsqBalanceInfo(availableConfirmedBalance, + unverifiedBalance, + unconfirmedChangeBalance, + lockedForVotingBalance, + lockupBondsBalance, + unlockingBondsBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BsqBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BsqBalanceInfo.newBuilder() + .setAvailableConfirmedBalance(availableConfirmedBalance) + .setUnverifiedBalance(unverifiedBalance) + .setUnconfirmedChangeBalance(unconfirmedChangeBalance) + .setLockedForVotingBalance(lockedForVotingBalance) + .setLockupBondsBalance(lockupBondsBalance) + .setUnlockingBondsBalance(unlockingBondsBalance) + .build(); + + } + + public static BsqBalanceInfo fromProto(bisq.proto.grpc.BsqBalanceInfo proto) { + return new BsqBalanceInfo(proto.getAvailableConfirmedBalance(), + proto.getUnverifiedBalance(), + proto.getUnconfirmedChangeBalance(), + proto.getLockedForVotingBalance(), + proto.getLockupBondsBalance(), + proto.getUnlockingBondsBalance()); + } + + @Override + public String toString() { + return "BsqBalanceInfo{" + + "availableConfirmedBalance=" + availableConfirmedBalance + + ", unverifiedBalance=" + unverifiedBalance + + ", unconfirmedChangeBalance=" + unconfirmedChangeBalance + + ", lockedForVotingBalance=" + lockedForVotingBalance + + ", lockupBondsBalance=" + lockupBondsBalance + + ", unlockingBondsBalance=" + unlockingBondsBalance + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java new file mode 100644 index 0000000000..e3803b0001 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java @@ -0,0 +1,75 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BtcBalanceInfo implements Payload { + + public static final BtcBalanceInfo EMPTY = new BtcBalanceInfo(-1, + -1, + -1, + -1); + + // All balances are in BTC satoshis. + private final long availableBalance; + private final long reservedBalance; + private final long totalAvailableBalance; // available + reserved + private final long lockedBalance; + + public BtcBalanceInfo(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + this.availableBalance = availableBalance; + this.reservedBalance = reservedBalance; + this.totalAvailableBalance = totalAvailableBalance; + this.lockedBalance = lockedBalance; + } + + @VisibleForTesting + public static BtcBalanceInfo valueOf(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + // Convenience for creating a model instance instead of a proto. + return new BtcBalanceInfo(availableBalance, + reservedBalance, + totalAvailableBalance, + lockedBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BtcBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BtcBalanceInfo.newBuilder() + .setAvailableBalance(availableBalance) + .setReservedBalance(reservedBalance) + .setTotalAvailableBalance(totalAvailableBalance) + .setLockedBalance(lockedBalance) + .build(); + } + + public static BtcBalanceInfo fromProto(bisq.proto.grpc.BtcBalanceInfo proto) { + return new BtcBalanceInfo(proto.getAvailableBalance(), + proto.getReservedBalance(), + proto.getTotalAvailableBalance(), + proto.getLockedBalance()); + } + + @Override + public String toString() { + return "BtcBalanceInfo{" + + "availableBalance=" + availableBalance + + ", reservedBalance=" + reservedBalance + + ", totalAvailableBalance=" + totalAvailableBalance + + ", lockedBalance=" + lockedBalance + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/ContractInfo.java b/core/src/main/java/bisq/core/api/model/ContractInfo.java new file mode 100644 index 0000000000..404335c9c7 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/ContractInfo.java @@ -0,0 +1,126 @@ +/* + * 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 bisq.core.api.model; + +import bisq.common.Payload; + +import java.util.function.Supplier; + +import lombok.Getter; + +import static bisq.core.api.model.PaymentAccountPayloadInfo.emptyPaymentAccountPayload; + +/** + * A lightweight Trade Contract constructed from a trade's json contract. + * Many fields in the core Contract are ignored, but can be added as needed. + */ +@Getter +public class ContractInfo implements Payload { + + private final String buyerNodeAddress; + private final String sellerNodeAddress; + private final String mediatorNodeAddress; + private final String refundAgentNodeAddress; + private final boolean isBuyerMakerAndSellerTaker; + private final String makerAccountId; + private final String takerAccountId; + private final PaymentAccountPayloadInfo makerPaymentAccountPayload; + private final PaymentAccountPayloadInfo takerPaymentAccountPayload; + private final String makerPayoutAddressString; + private final String takerPayoutAddressString; + private final long lockTime; + + public ContractInfo(String buyerNodeAddress, + String sellerNodeAddress, + String mediatorNodeAddress, + String refundAgentNodeAddress, + boolean isBuyerMakerAndSellerTaker, + String makerAccountId, + String takerAccountId, + PaymentAccountPayloadInfo makerPaymentAccountPayload, + PaymentAccountPayloadInfo takerPaymentAccountPayload, + String makerPayoutAddressString, + String takerPayoutAddressString, + long lockTime) { + this.buyerNodeAddress = buyerNodeAddress; + this.sellerNodeAddress = sellerNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; + this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker; + this.makerAccountId = makerAccountId; + this.takerAccountId = takerAccountId; + this.makerPaymentAccountPayload = makerPaymentAccountPayload; + this.takerPaymentAccountPayload = takerPaymentAccountPayload; + this.makerPayoutAddressString = makerPayoutAddressString; + this.takerPayoutAddressString = takerPayoutAddressString; + this.lockTime = lockTime; + } + + + // For transmitting TradeInfo messages when no contract is available. + public static Supplier emptyContract = () -> + new ContractInfo("", + "", + "", + "", + false, + "", + "", + emptyPaymentAccountPayload.get(), + emptyPaymentAccountPayload.get(), + "", + "", + 0); + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static ContractInfo fromProto(bisq.proto.grpc.ContractInfo proto) { + return new ContractInfo(proto.getBuyerNodeAddress(), + proto.getSellerNodeAddress(), + proto.getMediatorNodeAddress(), + proto.getRefundAgentNodeAddress(), + proto.getIsBuyerMakerAndSellerTaker(), + proto.getMakerAccountId(), + proto.getTakerAccountId(), + PaymentAccountPayloadInfo.fromProto(proto.getMakerPaymentAccountPayload()), + PaymentAccountPayloadInfo.fromProto(proto.getTakerPaymentAccountPayload()), + proto.getMakerPayoutAddressString(), + proto.getTakerPayoutAddressString(), + proto.getLockTime()); + } + + @Override + public bisq.proto.grpc.ContractInfo toProtoMessage() { + return bisq.proto.grpc.ContractInfo.newBuilder() + .setBuyerNodeAddress(buyerNodeAddress) + .setSellerNodeAddress(sellerNodeAddress) + .setMediatorNodeAddress(mediatorNodeAddress) + .setRefundAgentNodeAddress(refundAgentNodeAddress) + .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) + .setMakerAccountId(makerAccountId) + .setTakerAccountId(takerAccountId) + .setMakerPaymentAccountPayload(makerPaymentAccountPayload.toProtoMessage()) + .setTakerPaymentAccountPayload(takerPaymentAccountPayload.toProtoMessage()) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setLockTime(lockTime) + .build(); + } +} diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java new file mode 100644 index 0000000000..f8501f7df1 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -0,0 +1,341 @@ +/* + * 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 bisq.core.api.model; + +import bisq.core.offer.Offer; + +import bisq.common.Payload; + +import java.util.Objects; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode +@ToString +@Getter +public class OfferInfo implements Payload { + + // The client cannot see bisq.core.Offer or its fromProto method. We use the lighter + // weight OfferInfo proto wrapper instead, containing just enough fields to view, + // create and take offers. + + private final String id; + private final String direction; + private final long price; + private final boolean useMarketBasedPrice; + private final double marketPriceMargin; + private final long amount; + private final long minAmount; + private final long volume; + private final long minVolume; + private final long txFee; + private final long makerFee; + private final String offerFeePaymentTxId; + private final long buyerSecurityDeposit; + private final long sellerSecurityDeposit; + private final long triggerPrice; + private final boolean isCurrencyForMakerFeeBtc; + private final String paymentAccountId; + private final String paymentMethodId; + private final String paymentMethodShortName; + // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency + // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + private final String baseCurrencyCode; + private final String counterCurrencyCode; + private final long date; + private final String state; + + + public OfferInfo(OfferInfoBuilder builder) { + this.id = builder.id; + this.direction = builder.direction; + this.price = builder.price; + this.useMarketBasedPrice = builder.useMarketBasedPrice; + this.marketPriceMargin = builder.marketPriceMargin; + this.amount = builder.amount; + this.minAmount = builder.minAmount; + this.volume = builder.volume; + this.minVolume = builder.minVolume; + this.txFee = builder.txFee; + this.makerFee = builder.makerFee; + this.offerFeePaymentTxId = builder.offerFeePaymentTxId; + this.buyerSecurityDeposit = builder.buyerSecurityDeposit; + this.sellerSecurityDeposit = builder.sellerSecurityDeposit; + this.triggerPrice = builder.triggerPrice; + this.isCurrencyForMakerFeeBtc = builder.isCurrencyForMakerFeeBtc; + this.paymentAccountId = builder.paymentAccountId; + this.paymentMethodId = builder.paymentMethodId; + this.paymentMethodShortName = builder.paymentMethodShortName; + this.baseCurrencyCode = builder.baseCurrencyCode; + this.counterCurrencyCode = builder.counterCurrencyCode; + this.date = builder.date; + this.state = builder.state; + + } + + public static OfferInfo toOfferInfo(Offer offer) { + return getOfferInfoBuilder(offer).build(); + } + + public static OfferInfo toOfferInfo(Offer offer, long triggerPrice) { + // The Offer does not have a triggerPrice attribute, so we get + // the base OfferInfoBuilder, then add the OpenOffer's triggerPrice. + return getOfferInfoBuilder(offer).withTriggerPrice(triggerPrice).build(); + } + + private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { + return new OfferInfoBuilder() + .withId(offer.getId()) + .withDirection(offer.getDirection().name()) + .withPrice(Objects.requireNonNull(offer.getPrice()).getValue()) + .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) + .withMarketPriceMargin(offer.getMarketPriceMargin()) + .withAmount(offer.getAmount().value) + .withMinAmount(offer.getMinAmount().value) + .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) + .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) + .withMakerFee(offer.getMakerFee().value) + .withTxFee(offer.getTxFee().value) + .withOfferFeePaymentTxId(offer.getOfferFeePaymentTxId()) + .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) + .withSellerSecurityDeposit(offer.getSellerSecurityDeposit().value) + .withIsCurrencyForMakerFeeBtc(offer.isCurrencyForMakerFeeBtc()) + .withPaymentAccountId(offer.getMakerPaymentAccountId()) + .withPaymentMethodId(offer.getPaymentMethod().getId()) + .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) + .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) + .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) + .withDate(offer.getDate().getTime()) + .withState(offer.getState().name()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.OfferInfo toProtoMessage() { + return bisq.proto.grpc.OfferInfo.newBuilder() + .setId(id) + .setDirection(direction) + .setPrice(price) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setAmount(amount) + .setMinAmount(minAmount) + .setVolume(volume) + .setMinVolume(minVolume) + .setMakerFee(makerFee) + .setTxFee(txFee) + .setOfferFeePaymentTxId(offerFeePaymentTxId) + .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setSellerSecurityDeposit(sellerSecurityDeposit) + .setTriggerPrice(triggerPrice) + .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) + .setPaymentAccountId(paymentAccountId) + .setPaymentMethodId(paymentMethodId) + .setPaymentMethodShortName(paymentMethodShortName) + .setBaseCurrencyCode(baseCurrencyCode) + .setCounterCurrencyCode(counterCurrencyCode) + .setDate(date) + .setState(state) + .build(); + } + + @SuppressWarnings("unused") + public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { + return new OfferInfoBuilder() + .withId(proto.getId()) + .withDirection(proto.getDirection()) + .withPrice(proto.getPrice()) + .withUseMarketBasedPrice(proto.getUseMarketBasedPrice()) + .withMarketPriceMargin(proto.getMarketPriceMargin()) + .withAmount(proto.getAmount()) + .withMinAmount(proto.getMinAmount()) + .withVolume(proto.getVolume()) + .withMinVolume(proto.getMinVolume()) + .withMakerFee(proto.getMakerFee()) + .withTxFee(proto.getTxFee()) + .withOfferFeePaymentTxId(proto.getOfferFeePaymentTxId()) + .withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit()) + .withSellerSecurityDeposit(proto.getSellerSecurityDeposit()) + .withTriggerPrice(proto.getTriggerPrice()) + .withIsCurrencyForMakerFeeBtc(proto.getIsCurrencyForMakerFeeBtc()) + .withPaymentAccountId(proto.getPaymentAccountId()) + .withPaymentMethodId(proto.getPaymentMethodId()) + .withPaymentMethodShortName(proto.getPaymentMethodShortName()) + .withBaseCurrencyCode(proto.getBaseCurrencyCode()) + .withCounterCurrencyCode(proto.getCounterCurrencyCode()) + .withDate(proto.getDate()) + .withState(proto.getState()) + .build(); + } + + /* + * OfferInfoBuilder helps avoid bungling use of a large OfferInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. + */ + public static class OfferInfoBuilder { + private String id; + private String direction; + private long price; + private boolean useMarketBasedPrice; + private double marketPriceMargin; + private long amount; + private long minAmount; + private long volume; + private long minVolume; + private long txFee; + private long makerFee; + private String offerFeePaymentTxId; + private long buyerSecurityDeposit; + private long sellerSecurityDeposit; + private long triggerPrice; + private boolean isCurrencyForMakerFeeBtc; + private String paymentAccountId; + private String paymentMethodId; + private String paymentMethodShortName; + private String baseCurrencyCode; + private String counterCurrencyCode; + private long date; + private String state; + + public OfferInfoBuilder withId(String id) { + this.id = id; + return this; + } + + public OfferInfoBuilder withDirection(String direction) { + this.direction = direction; + return this; + } + + public OfferInfoBuilder withPrice(long price) { + this.price = price; + return this; + } + + public OfferInfoBuilder withUseMarketBasedPrice(boolean useMarketBasedPrice) { + this.useMarketBasedPrice = useMarketBasedPrice; + return this; + } + + public OfferInfoBuilder withMarketPriceMargin(double useMarketBasedPrice) { + this.marketPriceMargin = useMarketBasedPrice; + return this; + } + + public OfferInfoBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public OfferInfoBuilder withMinAmount(long minAmount) { + this.minAmount = minAmount; + return this; + } + + public OfferInfoBuilder withVolume(long volume) { + this.volume = volume; + return this; + } + + public OfferInfoBuilder withMinVolume(long minVolume) { + this.minVolume = minVolume; + return this; + } + + public OfferInfoBuilder withTxFee(long txFee) { + this.txFee = txFee; + return this; + } + + public OfferInfoBuilder withMakerFee(long makerFee) { + this.makerFee = makerFee; + return this; + } + + public OfferInfoBuilder withOfferFeePaymentTxId(String offerFeePaymentTxId) { + this.offerFeePaymentTxId = offerFeePaymentTxId; + return this; + } + + public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { + this.buyerSecurityDeposit = buyerSecurityDeposit; + return this; + } + + public OfferInfoBuilder withSellerSecurityDeposit(long sellerSecurityDeposit) { + this.sellerSecurityDeposit = sellerSecurityDeposit; + return this; + } + + public OfferInfoBuilder withTriggerPrice(long triggerPrice) { + this.triggerPrice = triggerPrice; + return this; + } + + public OfferInfoBuilder withIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { + this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; + return this; + } + + public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { + this.paymentAccountId = paymentAccountId; + return this; + } + + public OfferInfoBuilder withPaymentMethodId(String paymentMethodId) { + this.paymentMethodId = paymentMethodId; + return this; + } + + public OfferInfoBuilder withPaymentMethodShortName(String paymentMethodShortName) { + this.paymentMethodShortName = paymentMethodShortName; + return this; + } + + public OfferInfoBuilder withBaseCurrencyCode(String baseCurrencyCode) { + this.baseCurrencyCode = baseCurrencyCode; + return this; + } + + public OfferInfoBuilder withCounterCurrencyCode(String counterCurrencyCode) { + this.counterCurrencyCode = counterCurrencyCode; + return this; + } + + public OfferInfoBuilder withDate(long date) { + this.date = date; + return this; + } + + public OfferInfoBuilder withState(String state) { + this.state = state; + return this; + } + + public OfferInfo build() { + return new OfferInfo(this); + } + } +} diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java new file mode 100644 index 0000000000..eb3a6cd3ba --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -0,0 +1,250 @@ +/* + * 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 bisq.core.api.model; + +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.payload.PaymentMethod; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.inject.Singleton; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.commons.lang3.StringUtils; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import java.util.Map; + +import java.lang.reflect.Type; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; + + +/** + *

    + * An instance of this class can write new payment account forms (editable json files), + * and de-serialize edited json files into {@link PaymentAccount} + * instances. + *

    + *

    + * Example use case: (1) ask for a blank Hal Cash account form, (2) edit it, (3) derive a + * {@link bisq.core.payment.HalCashAccount} instance from the edited json file. + *

    + *
    + *

    + * (1) Ask for a hal cash account form: Pass a {@link PaymentMethod#HAL_CASH_ID} + * to {@link PaymentAccountForm#getPaymentAccountForm(String)} to + * get the json Hal Cash payment account form: + *

    + * {
    + *   "_COMMENTS_": [
    + *     "Do not manually edit the paymentMethodId field.",
    + *     "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
    + *   ],
    + *   "paymentMethodId": "HAL_CASH",
    + *   "accountName": "Your accountname",
    + *   "mobileNr": "Your mobilenr"
    + *   "salt": ""
    + * }
    + * 
    + *

    + *

    + * (2) Save the Hal Cash payment account form to disk, and edit it: + *

    + * {
    + *   "_COMMENTS_": [
    + *     "Do not manually edit the paymentMethodId field.",
    + *     "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
    + *   ],
    + *   "paymentMethodId": "HAL_CASH",
    + *   "accountName": "Hal Cash Acct",
    + *   "mobileNr": "798 123 456"
    + *   "salt": ""
    + * }
    + * 
    + *

    + * (3) De-serialize the edited json account form: Pass the edited json file to + * {@link PaymentAccountForm#toPaymentAccount(File)}, or + * a json string to {@link PaymentAccountForm#toPaymentAccount(String)} + * and get a {@link bisq.core.payment.HalCashAccount} instance. + *
    + * PaymentAccount(
    + * paymentMethod=PaymentMethod(id=HAL_CASH,
    + *                             maxTradePeriod=86400000,
    + *                             maxTradeLimit=50000000),
    + * id=e33c9d94-1a1a-43fd-aa11-fcaacbb46100,
    + * creationDate=Mon Nov 16 12:26:43 BRST 2020,
    + * paymentAccountPayload=HalCashAccountPayload(mobileNr=798 123 456),
    + * accountName=Hal Cash Acct,
    + * tradeCurrencies=[FiatCurrency(currency=EUR)],
    + * selectedTradeCurrency=FiatCurrency(currency=EUR)
    + * )
    + * 
    + */ +@Singleton +@Slf4j +public class PaymentAccountForm { + + private final GsonBuilder gsonBuilder = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls(); + + // A list of PaymentAccount fields to exclude from json forms. + private final String[] excludedFields = new String[]{ + "log", + "id", + "acceptedCountryCodes", + "countryCode", + "creationDate", + "excludeFromJsonDataMap", + "maxTradePeriod", + "paymentAccountPayload", + "paymentMethod", + "paymentMethodId", // This field will be included, but handled differently. + "selectedTradeCurrency", + "tradeCurrencies", // This field may be included, but handled differently. + "HOLDER_NAME", + "SALT" // This field will be included, but handled differently. + }; + + /** + * Returns a blank payment account form (json) for the given paymentMethodId. + * + * @param paymentMethodId Determines what kind of json form to return. + * @return A uniquely named tmp file used to define new payment account details. + */ + public File getPaymentAccountForm(String paymentMethodId) { + PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + File file = getTmpJsonFile(paymentMethodId); + try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { + PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); + Class clazz = paymentAccount.getClass(); + Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz, excludedFields)).create(); + String json = gson.toJson(paymentAccount); // serializes target to json + outputStreamWriter.write(json); + } catch (Exception ex) { + String errMsg = format("cannot create a payment account form for a %s payment method", paymentMethodId); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException(errMsg); + } + return file; + } + + /** + * De-serialize a PaymentAccount json form into a new PaymentAccount instance. + * + * @param jsonForm The file representing a new payment account form. + * @return A populated PaymentAccount subclass instance. + */ + @SuppressWarnings("unused") + @VisibleForTesting + public PaymentAccount toPaymentAccount(File jsonForm) { + String jsonString = toJsonString(jsonForm); + return toPaymentAccount(jsonString); + } + + /** + * De-serialize a PaymentAccount json string into a new PaymentAccount instance. + * + * @param jsonString The json data representing a new payment account form. + * @return A populated PaymentAccount subclass instance. + */ + public PaymentAccount toPaymentAccount(String jsonString) { + Class clazz = getPaymentAccountClassFromJson(jsonString); + Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz)).create(); + return gson.fromJson(jsonString, clazz); + } + + public String toJsonString(File jsonFile) { + try { + checkNotNull(jsonFile, "json file cannot be null"); + return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath()))); + } catch (IOException ex) { + String errMsg = format("cannot read json string from file '%s'", + jsonFile.getAbsolutePath()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException(errMsg); + } + } + + @VisibleForTesting + public URI getClickableURI(File jsonFile) { + try { + return new URI("file", + "", + jsonFile.toURI().getPath(), + null, + null); + } catch (URISyntaxException ex) { + String errMsg = format("cannot create clickable url to file '%s'", + jsonFile.getAbsolutePath()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException(errMsg); + } + } + + @VisibleForTesting + public static File getTmpJsonFile(String paymentMethodId) { + File file; + try { + // Creates a tmp file that includes a random number string between the + // prefix and suffix, i.e., sepa_form_13243546575879.json, so there is + // little chance this will fail because the tmp file already exists. + file = File.createTempFile(paymentMethodId.toLowerCase() + "_form_", + ".json", + Paths.get(getProperty("java.io.tmpdir")).toFile()); + } catch (IOException ex) { + String errMsg = format("cannot create json file for a %s payment method", + paymentMethodId); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException(errMsg); + } + return file; + } + + private Class getPaymentAccountClassFromJson(String json) { + Map jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class); + String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"), + format("cannot not find a paymentMethodId in json string: %s", json)); + return getPaymentAccountClass(paymentMethodId); + } + + private Class getPaymentAccountClass(String paymentMethodId) { + PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass(); + } +} diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java new file mode 100644 index 0000000000..8bce2e96db --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.core.api.model; + +import bisq.core.payment.payload.CryptoCurrencyAccountPayload; +import bisq.core.payment.payload.InstantCryptoCurrencyPayload; +import bisq.core.payment.payload.PaymentAccountPayload; + +import bisq.common.Payload; + +import java.util.Optional; +import java.util.function.Supplier; + +import lombok.Getter; + +import javax.annotation.Nullable; + +@Getter +public class PaymentAccountPayloadInfo implements Payload { + + private final String id; + private final String paymentMethodId; + @Nullable + private final String address; + + public PaymentAccountPayloadInfo(String id, + String paymentMethodId, + @Nullable String address) { + this.id = id; + this.paymentMethodId = paymentMethodId; + this.address = address; + } + + public static PaymentAccountPayloadInfo toPaymentAccountPayloadInfo(PaymentAccountPayload paymentAccountPayload) { + Optional address = Optional.empty(); + if (paymentAccountPayload instanceof CryptoCurrencyAccountPayload) + address = Optional.of(((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress()); + else if (paymentAccountPayload instanceof InstantCryptoCurrencyPayload) + address = Optional.of(((InstantCryptoCurrencyPayload) paymentAccountPayload).getAddress()); + + return new PaymentAccountPayloadInfo(paymentAccountPayload.getId(), + paymentAccountPayload.getPaymentMethodId(), + address.orElse("")); + } + + // For transmitting TradeInfo messages when no contract & payloads are available. + public static Supplier emptyPaymentAccountPayload = () -> + new PaymentAccountPayloadInfo("", "", ""); + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static PaymentAccountPayloadInfo fromProto(bisq.proto.grpc.PaymentAccountPayloadInfo proto) { + return new PaymentAccountPayloadInfo(proto.getId(), proto.getPaymentMethodId(), proto.getAddress()); + } + + @Override + public bisq.proto.grpc.PaymentAccountPayloadInfo toProtoMessage() { + return bisq.proto.grpc.PaymentAccountPayloadInfo.newBuilder() + .setId(id) + .setPaymentMethodId(paymentMethodId) + .setAddress(address != null ? address : "") + .build(); + } +} diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java new file mode 100644 index 0000000000..219c3db04c --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java @@ -0,0 +1,438 @@ +/* + * 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 bisq.core.api.model; + + +import bisq.core.locale.Country; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.ReflectionUtils.*; +import static bisq.common.util.Utilities.decodeFromHex; +import static bisq.core.locale.CountryUtil.findCountryByCode; +import static bisq.core.locale.CurrencyUtil.getAllTransferwiseCurrencies; +import static bisq.core.locale.CurrencyUtil.getCurrencyByCountryCode; +import static bisq.core.locale.CurrencyUtil.getTradeCurrencies; +import static bisq.core.locale.CurrencyUtil.getTradeCurrenciesInList; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +@Slf4j +class PaymentAccountTypeAdapter extends TypeAdapter { + + private static final String[] JSON_COMMENTS = new String[]{ + "Do not manually edit the paymentMethodId field.", + "Edit the salt field only if you are recreating a payment" + + " account on a new installation and wish to preserve the account age." + }; + + private final Class paymentAccountType; + private final Class paymentAccountPayloadType; + private final Map> fieldSettersMap; + private final Predicate isExcludedField; + + /** + * Constructor used when de-serializing a json payment account form into a + * PaymentAccount instance. + * + * @param paymentAccountType the PaymentAccount subclass being instantiated + */ + public PaymentAccountTypeAdapter(Class paymentAccountType) { + this(paymentAccountType, new String[]{}); + } + + /** + * Constructor used when serializing a PaymentAccount subclass instance into a json + * payment account json form. + * + * @param paymentAccountType the PaymentAccount subclass being serialized + * @param excludedFields a string array of field names to exclude from the serialized + * payment account json form. + */ + public PaymentAccountTypeAdapter(Class paymentAccountType, String[] excludedFields) { + this.paymentAccountType = paymentAccountType; + this.paymentAccountPayloadType = getPaymentAccountPayloadType(); + this.isExcludedField = (f) -> stream(excludedFields).anyMatch(e -> e.equals(f.getName())); + this.fieldSettersMap = getFieldSetterMap(); + } + + @Override + public void write(JsonWriter out, PaymentAccount account) throws IOException { + // We write a blank payment acct form for a payment method id. + // We're not serializing a real payment account instance here. + out.beginObject(); + + // All json forms start with immutable _COMMENTS_ and paymentMethodId fields. + out.name("_COMMENTS_"); + out.beginArray(); + for (String s : JSON_COMMENTS) { + out.value(s); + } + out.endArray(); + + out.name("paymentMethodId"); + out.value(account.getPaymentMethod().getId()); + + // Write the editable, PaymentAccount subclass specific fields. + writeInnerMutableFields(out, account); + + // The last field in all json forms is the empty, editable salt field. + out.name("salt"); + out.value(""); + + out.endObject(); + } + + private void writeInnerMutableFields(JsonWriter out, PaymentAccount account) { + if (account.isTransferwiseAccount()) + writeTradeCurrenciesField(out, account); + + fieldSettersMap.forEach((field, value) -> { + try { + // Write out a json element if there is a @Setter for this field. + if (value.isPresent()) { + log.debug("Append form with settable field: {} {} {} setter: {}", + getVisibilityModifierAsString(field), + field.getType().getSimpleName(), + field.getName(), + value); + + String fieldName = field.getName(); + out.name(fieldName); + if (fieldName.equals("country")) + out.value("your two letter country code"); + else + out.value("your " + fieldName.toLowerCase()); + } + } catch (Exception ex) { + String errMsg = format("cannot create a new %s json form", + account.getClass().getSimpleName()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + }); + } + + // In some cases (TransferwiseAccount), we need to include a 'tradeCurrencies' + // field in the json form, though the 'tradeCurrencies' field has no setter method in + // the PaymentAccount class hierarchy. At of time of this change, TransferwiseAccount + // is the only known exception to the rule. + private void writeTradeCurrenciesField(JsonWriter out, PaymentAccount account) { + try { + String fieldName = "tradeCurrencies"; + log.debug("Append form with non-settable field: {}", fieldName); + out.name(fieldName); + out.value("comma delimited currency code list, e.g., gbp,eur"); + } catch (Exception ex) { + String errMsg = format("cannot create a new %s json form", + account.getClass().getSimpleName()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + + @Override + public PaymentAccount read(JsonReader in) throws IOException { + PaymentAccount account = initNewPaymentAccount(); + in.beginObject(); + while (in.hasNext()) { + String currentFieldName = in.nextName(); + + // The tradeCurrency field is common to all payment account types, + // but has no setter. + if (didReadTradeCurrenciesField(in, account, currentFieldName)) + continue; + + // Some of the fields are common to all payment account types. + if (didReadCommonField(in, account, currentFieldName)) + continue; + + // If the account is a subclass of CountryBasedPaymentAccount, set the + // account's Country, and use the Country to derive and set the account's + // FiatCurrency. + if (didReadCountryField(in, account, currentFieldName)) + continue; + + Optional field = fieldSettersMap.keySet().stream() + .filter(k -> k.getName().equals(currentFieldName)).findFirst(); + + field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> { + throw new IllegalStateException( + format("programmer error: cannot de-serialize json to a '%s' " + + " because there is no %s field.", + account.getClass().getSimpleName(), + currentFieldName)); + }); + } + in.endObject(); + return account; + } + + private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader jsonReader) { + Optional setter = fieldSettersMap.get(field); + if (setter.isPresent()) { + try { + // The setter might be on the PaymentAccount instance, or its + // PaymentAccountPayload instance. + if (isSetterOnPaymentAccountClass(setter.get(), account)) { + setter.get().invoke(account, nextStringOrNull(jsonReader)); + } else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) { + setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader)); + } else { + String errMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection" + + " because the setter method's declaring class was not found.", + account.getClass().getSimpleName()); + throw new IllegalStateException(errMsg); + } + } catch (ReflectiveOperationException ex) { + handleSetFieldValueError(account, field, ex); + } + } else { + throw new IllegalStateException( + format("programmer error: cannot de-serialize json to a '%s' " + + " because field value cannot be set %s.", + account.getClass().getSimpleName(), + field.getName())); + } + } + + private boolean isSetterOnPaymentAccountClass(Method setter, PaymentAccount account) { + return isSetterOnClass(setter, account.getClass()); + } + + private boolean isSetterOnPaymentAccountPayloadClass(Method setter, PaymentAccount account) { + return isSetterOnClass(setter, account.getPaymentAccountPayload().getClass()) + || isSetterOnClass(setter, account.getPaymentAccountPayload().getClass().getSuperclass()); + } + + private Map> getFieldSetterMap() { + List orderedFields = getOrderedFields(); + Map> map = new LinkedHashMap<>(); + for (Field field : orderedFields) { + Optional setter = getSetterMethodForFieldInClassHierarchy(field, paymentAccountType) + .or(() -> getSetterMethodForFieldInClassHierarchy(field, paymentAccountPayloadType)); + map.put(field, setter); + } + return unmodifiableMap(map); + } + + private List getOrderedFields() { + List fields = new ArrayList<>(); + loadFieldListForClassHierarchy(fields, paymentAccountType, isExcludedField); + loadFieldListForClassHierarchy(fields, paymentAccountPayloadType, isExcludedField); + fields.sort(comparing(Field::getName)); + return fields; + } + + private String nextStringOrNull(JsonReader in) { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return in.nextString(); + } + } catch (IOException ex) { + String errMsg = "cannot see next string in json reader"; + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + @SuppressWarnings("unused") + private Long nextLongOrNull(JsonReader in) { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return in.nextLong(); + } + } catch (IOException ex) { + String errMsg = "cannot see next long in json reader"; + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + private final Predicate isCommaDelimitedCurrencyList = (s) -> s != null && s.contains(","); + private final Function> commaDelimitedCodesToList = (s) -> { + if (isCommaDelimitedCurrencyList.test(s)) + return stream(s.split(",")).map(a -> a.trim().toUpperCase()).collect(toList()); + else if (s != null && !s.isEmpty()) + return singletonList(s.trim().toUpperCase()); + else + return new ArrayList<>(); + }; + + private boolean didReadTradeCurrenciesField(JsonReader in, + PaymentAccount account, + String fieldName) { + // The PaymentAccount.tradeCurrencies field is a special case because it has + // no setter, and we add currencies to the List here. Normally, it is an + // excluded field, TransferwiseAccount excepted. + if (fieldName.equals("tradeCurrencies")) { + String fieldValue = nextStringOrNull(in); + List currencyCodes = commaDelimitedCodesToList.apply(fieldValue); + + Optional> tradeCurrencies; + if (account.isTransferwiseAccount()) + tradeCurrencies = getTradeCurrenciesInList(currencyCodes, getAllTransferwiseCurrencies()); + else + tradeCurrencies = getTradeCurrencies(currencyCodes); + + if (tradeCurrencies.isPresent()) { + for (TradeCurrency tradeCurrency : tradeCurrencies.get()) { + account.addCurrency(tradeCurrency); + } + // For api users, define a selected currency. + account.setSelectedTradeCurrency(account.getTradeCurrency().orElse(null)); + } else { + // Log a warning. We should not throw an exception here because the + // gson library will not pass it up to the calling Bisq class as it + // would be defined here. Do a check in a calling class to make sure + // the tradeCurrencies field is populated in the PaymentAccount + // object, if it is required for the payment account method. + log.warn("No trade currencies were found in the {} account form.", + account.getPaymentMethod().getDisplayString()); + } + return true; + } + return false; + } + + private boolean didReadCommonField(JsonReader in, + PaymentAccount account, + String fieldName) throws IOException { + switch (fieldName) { + case "_COMMENTS_": + case "paymentMethodId": + // Skip over the the comments and paymentMethodId, which is already + // set on the PaymentAccount instance. + in.skipValue(); + return true; + case "accountName": + // Set the acct name using the value read from json. + account.setAccountName(nextStringOrNull(in)); + return true; + case "salt": + // Set the acct salt using the value read from json. + String saltAsHex = nextStringOrNull(in); + if (saltAsHex != null && !saltAsHex.trim().isEmpty()) { + account.setSalt(decodeFromHex(saltAsHex)); + } + return true; + default: + return false; + } + } + + private boolean didReadCountryField(JsonReader in, PaymentAccount account, String fieldName) { + if (!fieldName.equals("country")) + return false; + + String countryCode = nextStringOrNull(in); + Optional country = findCountryByCode(countryCode); + if (country.isPresent()) { + + if (account.isCountryBasedPaymentAccount()) { + ((CountryBasedPaymentAccount) account).setCountry(country.get()); + FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); + account.setSingleTradeCurrency(fiatCurrency); + } else if (account.isMoneyGramAccount()) { + ((MoneyGramAccount) account).setCountry(country.get()); + } else { + String errMsg = format("cannot set the country on a %s", + paymentAccountType.getSimpleName()); + log.error(StringUtils.capitalize(errMsg) + "."); + throw new IllegalStateException("programmer error: " + errMsg); + } + + return true; + + } else { + throw new IllegalArgumentException( + format("'%s' is an invalid country code.", countryCode)); + } + } + + private Class getPaymentAccountPayloadType() { + try { + Package pkg = PaymentAccountPayload.class.getPackage(); + //noinspection unchecked + return (Class) Class.forName(pkg.getName() + + "." + paymentAccountType.getSimpleName() + "Payload"); + } catch (Exception ex) { + String errMsg = format("cannot get the payload class for %s", + paymentAccountType.getSimpleName()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + private PaymentAccount initNewPaymentAccount() { + try { + Constructor constructor = paymentAccountType.getDeclaredConstructor(); + PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance(); + paymentAccount.init(); + return paymentAccount; + } catch (NoSuchMethodException + | IllegalAccessException + | InstantiationException + | InvocationTargetException ex) { + String errMsg = format("cannot instantiate a new %s", + paymentAccountType.getSimpleName()); + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } +} diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java new file mode 100644 index 0000000000..078e5ee4d9 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -0,0 +1,408 @@ +/* + * 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 bisq.core.api.model; + +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; + +import bisq.common.Payload; + +import java.util.Objects; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import static bisq.core.api.model.OfferInfo.toOfferInfo; +import static bisq.core.api.model.PaymentAccountPayloadInfo.toPaymentAccountPayloadInfo; + +@EqualsAndHashCode +@Getter +public class TradeInfo implements Payload { + + // The client cannot see bisq.core.trade.Trade or its fromProto method. We use the + // lighter weight TradeInfo proto wrapper instead, containing just enough fields to + // view and interact with trades. + + private final OfferInfo offer; + private final String tradeId; + private final String shortId; + private final long date; + private final String role; + private final boolean isCurrencyForTakerFeeBtc; + private final long txFeeAsLong; + private final long takerFeeAsLong; + private final String takerFeeTxId; + private final String depositTxId; + private final String payoutTxId; + private final long tradeAmountAsLong; + private final long tradePrice; + private final String tradingPeerNodeAddress; + private final String state; + private final String phase; + private final String tradePeriodState; + private final boolean isDepositPublished; + private final boolean isDepositConfirmed; + private final boolean isFiatSent; + private final boolean isFiatReceived; + private final boolean isPayoutPublished; + private final boolean isWithdrawn; + private final String contractAsJson; + private final ContractInfo contract; + + public TradeInfo(TradeInfoBuilder builder) { + this.offer = builder.offer; + this.tradeId = builder.tradeId; + this.shortId = builder.shortId; + this.date = builder.date; + this.role = builder.role; + this.isCurrencyForTakerFeeBtc = builder.isCurrencyForTakerFeeBtc; + this.txFeeAsLong = builder.txFeeAsLong; + this.takerFeeAsLong = builder.takerFeeAsLong; + this.takerFeeTxId = builder.takerFeeTxId; + this.depositTxId = builder.depositTxId; + this.payoutTxId = builder.payoutTxId; + this.tradeAmountAsLong = builder.tradeAmountAsLong; + this.tradePrice = builder.tradePrice; + this.tradingPeerNodeAddress = builder.tradingPeerNodeAddress; + this.state = builder.state; + this.phase = builder.phase; + this.tradePeriodState = builder.tradePeriodState; + this.isDepositPublished = builder.isDepositPublished; + this.isDepositConfirmed = builder.isDepositConfirmed; + this.isFiatSent = builder.isFiatSent; + this.isFiatReceived = builder.isFiatReceived; + this.isPayoutPublished = builder.isPayoutPublished; + this.isWithdrawn = builder.isWithdrawn; + this.contractAsJson = builder.contractAsJson; + this.contract = builder.contract; + } + + public static TradeInfo toTradeInfo(Trade trade) { + return toTradeInfo(trade, null); + } + + public static TradeInfo toTradeInfo(Trade trade, String role) { + ContractInfo contractInfo; + if (trade.getContract() != null) { + Contract contract = trade.getContract(); + contractInfo = new ContractInfo(contract.getBuyerPayoutAddressString(), + contract.getSellerPayoutAddressString(), + contract.getMediatorNodeAddress().getFullAddress(), + contract.getRefundAgentNodeAddress().getFullAddress(), + contract.isBuyerMakerAndSellerTaker(), + contract.getMakerAccountId(), + contract.getTakerAccountId(), + toPaymentAccountPayloadInfo(contract.getMakerPaymentAccountPayload()), + toPaymentAccountPayloadInfo(contract.getTakerPaymentAccountPayload()), + contract.getMakerPayoutAddressString(), + contract.getTakerPayoutAddressString(), + contract.getLockTime()); + } else { + contractInfo = ContractInfo.emptyContract.get(); + } + + return new TradeInfoBuilder() + .withOffer(toOfferInfo(trade.getOffer())) + .withTradeId(trade.getId()) + .withShortId(trade.getShortId()) + .withDate(trade.getDate().getTime()) + .withRole(role == null ? "" : role) + .withIsCurrencyForTakerFeeBtc(trade.isCurrencyForTakerFeeBtc()) + .withTxFeeAsLong(trade.getTxFeeAsLong()) + .withTakerFeeAsLong(trade.getTakerFeeAsLong()) + .withTakerFeeAsLong(trade.getTakerFeeAsLong()) + .withTakerFeeTxId(trade.getTakerFeeTxId()) + .withDepositTxId(trade.getDepositTxId()) + .withPayoutTxId(trade.getPayoutTxId()) + .withTradeAmountAsLong(trade.getTradeAmountAsLong()) + .withTradePrice(trade.getTradePrice().getValue()) + .withTradingPeerNodeAddress(Objects.requireNonNull( + trade.getTradingPeerNodeAddress()).getHostNameWithoutPostFix()) + .withState(trade.getState().name()) + .withPhase(trade.getPhase().name()) + .withTradePeriodState(trade.getTradePeriodState().name()) + .withIsDepositPublished(trade.isDepositPublished()) + .withIsDepositConfirmed(trade.isDepositConfirmed()) + .withIsFiatSent(trade.isFiatSent()) + .withIsFiatReceived(trade.isFiatReceived()) + .withIsPayoutPublished(trade.isPayoutPublished()) + .withIsWithdrawn(trade.isWithdrawn()) + .withContractAsJson(trade.getContractAsJson()) + .withContract(contractInfo) + .build(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.TradeInfo toProtoMessage() { + return bisq.proto.grpc.TradeInfo.newBuilder() + .setOffer(offer.toProtoMessage()) + .setTradeId(tradeId) + .setShortId(shortId) + .setDate(date) + .setRole(role) + .setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc) + .setTxFeeAsLong(txFeeAsLong) + .setTakerFeeAsLong(takerFeeAsLong) + .setTakerFeeTxId(takerFeeTxId == null ? "" : takerFeeTxId) + .setDepositTxId(depositTxId == null ? "" : depositTxId) + .setPayoutTxId(payoutTxId == null ? "" : payoutTxId) + .setTradeAmountAsLong(tradeAmountAsLong) + .setTradePrice(tradePrice) + .setTradingPeerNodeAddress(tradingPeerNodeAddress) + .setState(state) + .setPhase(phase) + .setTradePeriodState(tradePeriodState) + .setIsDepositPublished(isDepositPublished) + .setIsDepositConfirmed(isDepositConfirmed) + .setIsFiatSent(isFiatSent) + .setIsFiatReceived(isFiatReceived) + .setIsPayoutPublished(isPayoutPublished) + .setIsWithdrawn(isWithdrawn) + .setContractAsJson(contractAsJson == null ? "" : contractAsJson) + .setContract(contract.toProtoMessage()) + .build(); + } + + public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) { + return new TradeInfoBuilder() + .withOffer(OfferInfo.fromProto(proto.getOffer())) + .withTradeId(proto.getTradeId()) + .withShortId(proto.getShortId()) + .withDate(proto.getDate()) + .withRole(proto.getRole()) + .withIsCurrencyForTakerFeeBtc(proto.getIsCurrencyForTakerFeeBtc()) + .withTxFeeAsLong(proto.getTxFeeAsLong()) + .withTakerFeeAsLong(proto.getTakerFeeAsLong()) + .withTakerFeeTxId(proto.getTakerFeeTxId()) + .withDepositTxId(proto.getDepositTxId()) + .withPayoutTxId(proto.getPayoutTxId()) + .withTradeAmountAsLong(proto.getTradeAmountAsLong()) + .withTradePrice(proto.getTradePrice()) + .withTradePeriodState(proto.getTradePeriodState()) + .withState(proto.getState()) + .withPhase(proto.getPhase()) + .withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress()) + .withIsDepositPublished(proto.getIsDepositPublished()) + .withIsDepositConfirmed(proto.getIsDepositConfirmed()) + .withIsFiatSent(proto.getIsFiatSent()) + .withIsFiatReceived(proto.getIsFiatReceived()) + .withIsPayoutPublished(proto.getIsPayoutPublished()) + .withIsWithdrawn(proto.getIsWithdrawn()) + .withContractAsJson(proto.getContractAsJson()) + .withContract((ContractInfo.fromProto(proto.getContract()))) + .build(); + } + + /* + * TradeInfoBuilder helps avoid bungling use of a large TradeInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. + */ + public static class TradeInfoBuilder { + private OfferInfo offer; + private String tradeId; + private String shortId; + private long date; + private String role; + private boolean isCurrencyForTakerFeeBtc; + private long txFeeAsLong; + private long takerFeeAsLong; + private String takerFeeTxId; + private String depositTxId; + private String payoutTxId; + private long tradeAmountAsLong; + private long tradePrice; + private String tradingPeerNodeAddress; + private String state; + private String phase; + private String tradePeriodState; + private boolean isDepositPublished; + private boolean isDepositConfirmed; + private boolean isFiatSent; + private boolean isFiatReceived; + private boolean isPayoutPublished; + private boolean isWithdrawn; + private String contractAsJson; + private ContractInfo contract; + + public TradeInfoBuilder withOffer(OfferInfo offer) { + this.offer = offer; + return this; + } + + public TradeInfoBuilder withTradeId(String tradeId) { + this.tradeId = tradeId; + return this; + } + + public TradeInfoBuilder withShortId(String shortId) { + this.shortId = shortId; + return this; + } + + public TradeInfoBuilder withDate(long date) { + this.date = date; + return this; + } + + public TradeInfoBuilder withRole(String role) { + this.role = role; + return this; + } + + public TradeInfoBuilder withIsCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { + this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; + return this; + } + + public TradeInfoBuilder withTxFeeAsLong(long txFeeAsLong) { + this.txFeeAsLong = txFeeAsLong; + return this; + } + + public TradeInfoBuilder withTakerFeeAsLong(long takerFeeAsLong) { + this.takerFeeAsLong = takerFeeAsLong; + return this; + } + + public TradeInfoBuilder withTakerFeeTxId(String takerFeeTxId) { + this.takerFeeTxId = takerFeeTxId; + return this; + } + + public TradeInfoBuilder withDepositTxId(String depositTxId) { + this.depositTxId = depositTxId; + return this; + } + + public TradeInfoBuilder withPayoutTxId(String payoutTxId) { + this.payoutTxId = payoutTxId; + return this; + } + + public TradeInfoBuilder withTradeAmountAsLong(long tradeAmountAsLong) { + this.tradeAmountAsLong = tradeAmountAsLong; + return this; + } + + public TradeInfoBuilder withTradePrice(long tradePrice) { + this.tradePrice = tradePrice; + return this; + } + + public TradeInfoBuilder withTradePeriodState(String tradePeriodState) { + this.tradePeriodState = tradePeriodState; + return this; + } + + public TradeInfoBuilder withState(String state) { + this.state = state; + return this; + } + + public TradeInfoBuilder withPhase(String phase) { + this.phase = phase; + return this; + } + + public TradeInfoBuilder withTradingPeerNodeAddress(String tradingPeerNodeAddress) { + this.tradingPeerNodeAddress = tradingPeerNodeAddress; + return this; + } + + public TradeInfoBuilder withIsDepositPublished(boolean isDepositPublished) { + this.isDepositPublished = isDepositPublished; + return this; + } + + public TradeInfoBuilder withIsDepositConfirmed(boolean isDepositConfirmed) { + this.isDepositConfirmed = isDepositConfirmed; + return this; + } + + public TradeInfoBuilder withIsFiatSent(boolean isFiatSent) { + this.isFiatSent = isFiatSent; + return this; + } + + public TradeInfoBuilder withIsFiatReceived(boolean isFiatReceived) { + this.isFiatReceived = isFiatReceived; + return this; + } + + public TradeInfoBuilder withIsPayoutPublished(boolean isPayoutPublished) { + this.isPayoutPublished = isPayoutPublished; + return this; + } + + public TradeInfoBuilder withIsWithdrawn(boolean isWithdrawn) { + this.isWithdrawn = isWithdrawn; + return this; + } + + public TradeInfoBuilder withContractAsJson(String contractAsJson) { + this.contractAsJson = contractAsJson; + return this; + } + + public TradeInfoBuilder withContract(ContractInfo contract) { + this.contract = contract; + return this; + } + + public TradeInfo build() { + return new TradeInfo(this); + } + } + + @Override + public String toString() { + return "TradeInfo{" + + " tradeId='" + tradeId + '\'' + "\n" + + ", shortId='" + shortId + '\'' + "\n" + + ", date='" + date + '\'' + "\n" + + ", role='" + role + '\'' + "\n" + + ", isCurrencyForTakerFeeBtc='" + isCurrencyForTakerFeeBtc + '\'' + "\n" + + ", txFeeAsLong='" + txFeeAsLong + '\'' + "\n" + + ", takerFeeAsLong='" + takerFeeAsLong + '\'' + "\n" + + ", takerFeeTxId='" + takerFeeTxId + '\'' + "\n" + + ", depositTxId='" + depositTxId + '\'' + "\n" + + ", payoutTxId='" + payoutTxId + '\'' + "\n" + + ", tradeAmountAsLong='" + tradeAmountAsLong + '\'' + "\n" + + ", tradePrice='" + tradePrice + '\'' + "\n" + + ", tradingPeerNodeAddress='" + tradingPeerNodeAddress + '\'' + "\n" + + ", state='" + state + '\'' + "\n" + + ", phase='" + phase + '\'' + "\n" + + ", tradePeriodState='" + tradePeriodState + '\'' + "\n" + + ", isDepositPublished=" + isDepositPublished + "\n" + + ", isDepositConfirmed=" + isDepositConfirmed + "\n" + + ", isFiatSent=" + isFiatSent + "\n" + + ", isFiatReceived=" + isFiatReceived + "\n" + + ", isPayoutPublished=" + isPayoutPublished + "\n" + + ", isWithdrawn=" + isWithdrawn + "\n" + + ", offer=" + offer + "\n" + + ", contractAsJson=" + contractAsJson + "\n" + + ", contract=" + contract + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/TxFeeRateInfo.java b/core/src/main/java/bisq/core/api/model/TxFeeRateInfo.java new file mode 100644 index 0000000000..8b246e4b70 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TxFeeRateInfo.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.core.api.model; + +import bisq.common.Payload; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public class TxFeeRateInfo implements Payload { + + private final boolean useCustomTxFeeRate; + private final long customTxFeeRate; + private final long minFeeServiceRate; + private final long feeServiceRate; + private final long lastFeeServiceRequestTs; + + public TxFeeRateInfo(boolean useCustomTxFeeRate, + long customTxFeeRate, + long minFeeServiceRate, + long feeServiceRate, + long lastFeeServiceRequestTs) { + this.useCustomTxFeeRate = useCustomTxFeeRate; + this.customTxFeeRate = customTxFeeRate; + this.minFeeServiceRate = minFeeServiceRate; + this.feeServiceRate = feeServiceRate; + this.lastFeeServiceRequestTs = lastFeeServiceRequestTs; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + ////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.TxFeeRateInfo toProtoMessage() { + return bisq.proto.grpc.TxFeeRateInfo.newBuilder() + .setUseCustomTxFeeRate(useCustomTxFeeRate) + .setCustomTxFeeRate(customTxFeeRate) + .setMinFeeServiceRate(minFeeServiceRate) + .setFeeServiceRate(feeServiceRate) + .setLastFeeServiceRequestTs(lastFeeServiceRequestTs) + .build(); + } + + @SuppressWarnings("unused") + public static TxFeeRateInfo fromProto(bisq.proto.grpc.TxFeeRateInfo proto) { + return new TxFeeRateInfo(proto.getUseCustomTxFeeRate(), + proto.getCustomTxFeeRate(), + proto.getMinFeeServiceRate(), + proto.getFeeServiceRate(), + proto.getLastFeeServiceRequestTs()); + } + + @Override + public String toString() { + return "TxFeeRateInfo{" + "\n" + + " useCustomTxFeeRate=" + useCustomTxFeeRate + "\n" + + ", customTxFeeRate=" + customTxFeeRate + " sats/byte" + "\n" + + ", minFeeServiceRate=" + minFeeServiceRate + " sats/byte" + "\n" + + ", feeServiceRate=" + feeServiceRate + " sats/byte" + "\n" + + ", lastFeeServiceRequestTs=" + lastFeeServiceRequestTs + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/TxInfo.java b/core/src/main/java/bisq/core/api/model/TxInfo.java new file mode 100644 index 0000000000..3bd937c96e --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TxInfo.java @@ -0,0 +1,212 @@ +/* + * 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 bisq.core.api.model; + +import bisq.common.Payload; + +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; + +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import static java.util.Objects.requireNonNull; + +@EqualsAndHashCode +@Getter +public class TxInfo implements Payload { + + // The client cannot see an instance of an org.bitcoinj.core.Transaction. We use the + // lighter weight TxInfo proto wrapper instead, containing just enough fields to + // view some transaction details. A block explorer or bitcoin-core client can be + // used to see more detail. + + private final String txId; + private final long inputSum; + private final long outputSum; + private final long fee; + private final int size; + private final boolean isPending; + private final String memo; + + public TxInfo(TxInfoBuilder builder) { + this.txId = builder.txId; + this.inputSum = builder.inputSum; + this.outputSum = builder.outputSum; + this.fee = builder.fee; + this.size = builder.size; + this.isPending = builder.isPending; + this.memo = builder.memo; + } + + public static TxInfo toTxInfo(Transaction transaction) { + if (transaction == null) + throw new IllegalStateException("server created a null transaction"); + + if (transaction.getFee() != null) + return new TxInfoBuilder() + .withTxId(transaction.getTxId().toString()) + .withInputSum(transaction.getInputSum().value) + .withOutputSum(transaction.getOutputSum().value) + .withFee(transaction.getFee().value) + .withSize(transaction.getMessageSize()) + .withIsPending(transaction.isPending()) + .withMemo(transaction.getMemo()) + .build(); + else + return new TxInfoBuilder() + .withTxId(transaction.getTxId().toString()) + .withInputSum(transaction.getInputSum().value) + .withOutputSum(transaction.getOutputSum().value) + // Do not set fee == null. + .withSize(transaction.getMessageSize()) + .withIsPending(transaction.isPending()) + .withMemo(transaction.getMemo()) + .build(); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + ////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.TxInfo toProtoMessage() { + return bisq.proto.grpc.TxInfo.newBuilder() + .setTxId(txId) + .setInputSum(inputSum) + .setOutputSum(outputSum) + .setFee(fee) + .setSize(size) + .setIsPending(isPending) + .setMemo(memo == null ? "" : memo) + .build(); + } + + @SuppressWarnings("unused") + public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) { + return new TxInfoBuilder() + .withTxId(proto.getTxId()) + .withInputSum(proto.getInputSum()) + .withOutputSum(proto.getOutputSum()) + .withFee(proto.getFee()) + .withSize(proto.getSize()) + .withIsPending(proto.getIsPending()) + .withMemo(proto.getMemo()) + .build(); + } + + public static class TxInfoBuilder { + private String txId; + private long inputSum; + private long outputSum; + private long fee; + private int size; + private boolean isPending; + private String memo; + + public TxInfoBuilder withTxId(String txId) { + this.txId = txId; + return this; + } + + public TxInfoBuilder withInputSum(long inputSum) { + this.inputSum = inputSum; + return this; + } + + public TxInfoBuilder withOutputSum(long outputSum) { + this.outputSum = outputSum; + return this; + } + + public TxInfoBuilder withFee(long fee) { + this.fee = fee; + return this; + } + + public TxInfoBuilder withSize(int size) { + this.size = size; + return this; + } + + public TxInfoBuilder withIsPending(boolean isPending) { + this.isPending = isPending; + return this; + } + + public TxInfoBuilder withMemo(String memo) { + this.memo = memo; + return this; + } + + public TxInfo build() { + return new TxInfo(this); + } + } + + @Override + public String toString() { + return "TxInfo{" + "\n" + + " txId='" + txId + '\'' + "\n" + + ", inputSum=" + inputSum + "\n" + + ", outputSum=" + outputSum + "\n" + + ", fee=" + fee + "\n" + + ", size=" + size + "\n" + + ", isPending=" + isPending + "\n" + + ", memo='" + memo + '\'' + "\n" + + '}'; + } + + public static String getTransactionDetailString(Transaction tx) { + if (tx == null) + throw new IllegalArgumentException("Cannot print details for null transaction"); + + StringBuilder builder = new StringBuilder("Transaction " + requireNonNull(tx).getTxId() + ":").append("\n"); + + builder.append("\tisPending: ").append(tx.isPending()).append("\n"); + builder.append("\tfee: ").append(tx.getFee()).append("\n"); + builder.append("\tweight: ").append(tx.getWeight()).append("\n"); + builder.append("\tVsize: ").append(tx.getVsize()).append("\n"); + builder.append("\tinputSum: ").append(tx.getInputSum()).append("\n"); + builder.append("\toutputSum: ").append(tx.getOutputSum()).append("\n"); + + Map appearsInHashes = tx.getAppearsInHashes(); + if (appearsInHashes != null) + builder.append("\tappearsInHashes: yes, count: ").append(appearsInHashes.size()).append("\n"); + else + builder.append("\tappearsInHashes: ").append("no").append("\n"); + + builder.append("\tanyOutputSpent: ").append(tx.isAnyOutputSpent()).append("\n"); + builder.append("\tupdateTime: ").append(tx.getUpdateTime()).append("\n"); + builder.append("\tincludedInBestChainAt: ").append(tx.getIncludedInBestChainAt()).append("\n"); + builder.append("\thasWitnesses: ").append(tx.hasWitnesses()).append("\n"); + builder.append("\tlockTime: ").append(tx.getLockTime()).append("\n"); + builder.append("\tversion: ").append(tx.getVersion()).append("\n"); + builder.append("\thasConfidence: ").append(tx.hasConfidence()).append("\n"); + builder.append("\tsigOpCount: ").append(tx.getSigOpCount()).append("\n"); + builder.append("\tisTimeLocked: ").append(tx.isTimeLocked()).append("\n"); + builder.append("\thasRelativeLockTime: ").append(tx.hasRelativeLockTime()).append("\n"); + builder.append("\tisOptInFullRBF: ").append(tx.isOptInFullRBF()).append("\n"); + builder.append("\tpurpose: ").append(tx.getPurpose()).append("\n"); + builder.append("\texchangeRate: ").append(tx.getExchangeRate()).append("\n"); + builder.append("\tmemo: ").append(tx.getMemo()).append("\n"); + return builder.toString(); + } +} diff --git a/core/src/main/java/bisq/core/app/AppStartupState.java b/core/src/main/java/bisq/core/app/AppStartupState.java new file mode 100644 index 0000000000..6163b1a3cb --- /dev/null +++ b/core/src/main/java/bisq/core/app/AppStartupState.java @@ -0,0 +1,149 @@ +/* + * 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 bisq.core.app; + +import bisq.core.btc.setup.WalletsSetup; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import lombok.extern.slf4j.Slf4j; + +/** + * We often need to wait until network and wallet is ready or other combination of startup states. + * To avoid those repeated checks for the state or setting of listeners on different domains we provide here a + * collection of useful states. + */ +@Slf4j +@Singleton +public class AppStartupState { + // Do not convert to local field as there have been issues observed that the object got GC'ed. + private final MonadicBinding p2pNetworkAndWalletInitialized; + + private final BooleanProperty walletAndNetworkReady = new SimpleBooleanProperty(); + private final BooleanProperty allDomainServicesInitialized = new SimpleBooleanProperty(); + private final BooleanProperty applicationFullyInitialized = new SimpleBooleanProperty(); + private final BooleanProperty updatedDataReceived = new SimpleBooleanProperty(); + private final BooleanProperty isBlockDownloadComplete = new SimpleBooleanProperty(); + private final BooleanProperty hasSufficientPeersForBroadcast = new SimpleBooleanProperty(); + + @Inject + public AppStartupState(WalletsSetup walletsSetup, P2PService p2PService) { + + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + updatedDataReceived.set(true); + } + }); + + walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.isDownloadComplete()) + isBlockDownloadComplete.set(true); + }); + + walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.hasSufficientPeersForBroadcast()) + hasSufficientPeersForBroadcast.set(true); + }); + + p2pNetworkAndWalletInitialized = EasyBind.combine(updatedDataReceived, + isBlockDownloadComplete, + hasSufficientPeersForBroadcast, + allDomainServicesInitialized, + (a, b, c, d) -> { + if (a && b && c) { + walletAndNetworkReady.set(true); + } + return a && b && c && d; + }); + p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> { + if (newValue) { + applicationFullyInitialized.set(true); + } + }); + } + + public void onDomainServicesInitialized() { + allDomainServicesInitialized.set(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isWalletAndNetworkReady() { + return walletAndNetworkReady.get(); + } + + public ReadOnlyBooleanProperty walletAndNetworkReadyProperty() { + return walletAndNetworkReady; + } + + public boolean isAllDomainServicesInitialized() { + return allDomainServicesInitialized.get(); + } + + public ReadOnlyBooleanProperty allDomainServicesInitializedProperty() { + return allDomainServicesInitialized; + } + + public boolean isApplicationFullyInitialized() { + return applicationFullyInitialized.get(); + } + + public ReadOnlyBooleanProperty applicationFullyInitializedProperty() { + return applicationFullyInitialized; + } + + public boolean isUpdatedDataReceived() { + return updatedDataReceived.get(); + } + + public ReadOnlyBooleanProperty updatedDataReceivedProperty() { + return updatedDataReceived; + } + + public boolean isBlockDownloadComplete() { + return isBlockDownloadComplete.get(); + } + + public ReadOnlyBooleanProperty isBlockDownloadCompleteProperty() { + return isBlockDownloadComplete; + } + + public boolean isHasSufficientPeersForBroadcast() { + return hasSufficientPeersForBroadcast.get(); + } + + public ReadOnlyBooleanProperty hasSufficientPeersForBroadcastProperty() { + return hasSufficientPeersForBroadcast; + } + +} diff --git a/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java b/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java new file mode 100644 index 0000000000..4415b4acfa --- /dev/null +++ b/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java @@ -0,0 +1,253 @@ +/* + * 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 bisq.core.app; + +import bisq.core.user.Preferences; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.file.ResourceNotFoundException; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; + + + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +/** + * Prevents that Bisq gets hibernated from the OS. On OSX there is a tool called caffeinate but it seems it does not + * provide the behaviour we need, thus we use the trick to play a almost silent sound file in a loop. This keeps the + * application active even if the OS has moved to hibernate. Hibernating Bisq would cause network degradations and other + * resource limitations which would lead to offers not published or if a taker takes an offer that the trade process is + * at risk to fail due too slow response time. + */ +@Slf4j +@Singleton +public class AvoidStandbyModeService { + + private final Preferences preferences; + private final Config config; + private final Optional inhibitorPathSpec; + private CountDownLatch stopLinuxInhibitorCountdownLatch; + + private volatile boolean isStopped; + + @Inject + public AvoidStandbyModeService(Preferences preferences, Config config) { + this.preferences = preferences; + this.config = config; + this.inhibitorPathSpec = inhibitorPath(); + preferences.getUseStandbyModeProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + if (Utilities.isLinux() && runningInhibitorProcess().isPresent()) { + Objects.requireNonNull(stopLinuxInhibitorCountdownLatch).countDown(); + } + } else { + start(); + } + }); + } + + public void init() { + isStopped = preferences.isUseStandbyMode(); + if (!isStopped) { + start(); + } + } + + private void start() { + isStopped = false; + if (Utilities.isLinux()) { + startInhibitor(); + } else { + new Thread(this::playSilentAudioFile, "AvoidStandbyModeService-thread").start(); + } + } + + public void shutDown() { + isStopped = true; + stopInhibitor(); + } + + private void startInhibitor() { + try { + if (runningInhibitorProcess().isPresent()) { + log.info("Inhibitor already started"); + return; + } + inhibitCommand().ifPresent(cmd -> { + try { + new ProcessBuilder(cmd).start(); + log.info("Started -- disabled power management via {}", String.join(" ", cmd)); + if (Utilities.isLinux()) { + stopLinuxInhibitorCountdownLatch = new CountDownLatch(1); + new Thread(this::stopInhibitor, "StopAvoidStandbyModeService-thread").start(); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } catch (Exception e) { + log.error("Cannot avoid standby mode", e); + } + } + + private void stopInhibitor() { + try { + if (Utilities.isLinux()) { + if (!isStopped) { + Objects.requireNonNull(stopLinuxInhibitorCountdownLatch).await(); + } + Optional runningInhibitor = runningInhibitorProcess(); + runningInhibitor.ifPresent(processHandle -> { + processHandle.destroy(); + log.info("Stopped"); + }); + } + } catch (Exception e) { + log.error("Stop inhibitor thread interrupted", e); + } + } + + private void playSilentAudioFile() { + try { + log.info("Started"); + while (!isStopped) { + try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(getSoundFile()); + SourceDataLine sourceDataLine = getSourceDataLine(audioInputStream.getFormat())) { + byte[] tempBuffer = new byte[10000]; + sourceDataLine.open(audioInputStream.getFormat()); + sourceDataLine.start(); + int cnt; + while ((cnt = audioInputStream.read(tempBuffer, 0, tempBuffer.length)) != -1 && !isStopped) { + if (cnt > 0) { + sourceDataLine.write(tempBuffer, 0, cnt); + } + } + sourceDataLine.drain(); + } + } + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + + private File getSoundFile() throws IOException, ResourceNotFoundException { + File soundFile = new File(config.appDataDir, "prevent-app-nap-silent-sound.aiff"); + // We replaced the old file which was 42 MB with a smaller file of 0.8 MB. To enforce replacement we check for + // the size... + if (!soundFile.exists() || soundFile.length() > 42000000) { + FileUtil.resourceToFile("prevent-app-nap-silent-sound.aiff", soundFile); + } + return soundFile; + } + + private SourceDataLine getSourceDataLine(AudioFormat audioFormat) throws LineUnavailableException { + DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat); + return (SourceDataLine) AudioSystem.getLine(dataLineInfo); + } + + private Optional inhibitorPath() { + for (Optional installedInhibitor : installedInhibitors.get()) { + if (installedInhibitor.isPresent()) { + return installedInhibitor; + } + } + return Optional.empty(); // falling back to silent audio file player + } + + private Optional inhibitCommand() { + final String[] params; + if (inhibitorPathSpec.isPresent()) { + String cmd = inhibitorPathSpec.get(); + if (Utilities.isLinux()) { + params = cmd.contains("gnome-session-inhibit") + ? new String[]{cmd, "--app-id", "Bisq", "--inhibit", "suspend", "--reason", "Avoid Standby", "--inhibit-only"} + : new String[]{cmd, "--who", "Bisq", "--what", "sleep", "--why", "Avoid Standby", "--mode", "block", "tail", "-f", "/dev/null"}; + } else { + params = null; + } + } else { + params = null; // fall back to silent audio file player + } + return params == null ? Optional.empty() : Optional.of(params); + } + + private Optional runningInhibitorProcess() { + final ProcessHandle[] inhibitorProc = new ProcessHandle[1]; + inhibitorPathSpec.ifPresent(cmd -> { + Optional jvmProc = ProcessHandle.of(ProcessHandle.current().pid()); + jvmProc.ifPresent(proc -> proc.children().forEach(childProc -> childProc.info().command().ifPresent(command -> { + if (command.equals(cmd) && childProc.isAlive()) { + inhibitorProc[0] = childProc; + } + }))); + }); + return inhibitorProc[0] == null ? Optional.empty() : Optional.of(inhibitorProc[0]); + } + + private final Predicate isCmdInstalled = (p) -> { + File executable = Paths.get(p).toFile(); + return executable.exists() && executable.canExecute(); + }; + + private final Function> cmdPath = (possiblePaths) -> { + for (String path : possiblePaths) { + if (isCmdInstalled.test(path)) { + return Optional.of(path); + } + } + return Optional.empty(); + }; + + private final Supplier>> installedInhibitors = () -> + new ArrayList<>() {{ + add(gnomeSessionInhibitPathSpec.get()); // On linux, preferred inhibitor is gnome-session-inhibit, + add(systemdInhibitPathSpec.get()); // then fall back to systemd-inhibit if it is installed. + }}; + + private final Supplier> gnomeSessionInhibitPathSpec = () -> + cmdPath.apply(new String[]{"/usr/bin/gnome-session-inhibit", "/bin/gnome-session-inhibit"}); + + private final Supplier> systemdInhibitPathSpec = () -> + cmdPath.apply(new String[]{"/usr/bin/systemd-inhibit", "/bin/systemd-inhibit"}); +} diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java new file mode 100644 index 0000000000..980875cd0a --- /dev/null +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -0,0 +1,312 @@ +/* + * 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 bisq.core.app; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoSetup; +import bisq.core.dao.node.full.RpcService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.setup.CorePersistedDataHost; +import bisq.core.setup.CoreSetup; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.trade.txproof.xmr.XmrTxProofService; + +import bisq.network.p2p.P2PService; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.config.BisqHelpFormatter; +import bisq.common.config.Config; +import bisq.common.config.ConfigException; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.setup.CommonSetup; +import bisq.common.setup.GracefulShutDownHandler; +import bisq.common.setup.UncaughtExceptionHandler; +import bisq.common.util.Utilities; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSetup.BisqSetupListener, UncaughtExceptionHandler { + + public static final int EXIT_SUCCESS = 0; + public static final int EXIT_FAILURE = 1; + + private final String fullName; + private final String scriptName; + private final String appName; + private final String version; + + protected Injector injector; + protected AppModule module; + protected Config config; + private boolean isShutdownInProgress; + private boolean hasDowngraded; + + public BisqExecutable(String fullName, String scriptName, String appName, String version) { + this.fullName = fullName; + this.scriptName = scriptName; + this.appName = appName; + this.version = version; + } + + public void execute(String[] args) { + try { + config = new Config(appName, Utilities.getUserDataDir(), args); + if (config.helpRequested) { + config.printHelp(System.out, new BisqHelpFormatter(fullName, scriptName, version)); + System.exit(EXIT_SUCCESS); + } + } catch (ConfigException ex) { + System.err.println("error: " + ex.getMessage()); + System.exit(EXIT_FAILURE); + } catch (Throwable ex) { + System.err.println("fault: An unexpected error occurred. " + + "Please file a report at https://bisq.network/issues"); + ex.printStackTrace(System.err); + System.exit(EXIT_FAILURE); + } + + doExecute(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // First synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void doExecute() { + CommonSetup.setup(config, this); + CoreSetup.setup(config); + + addCapabilities(); + + // If application is JavaFX application we need to wait until it is initialized + launchApplication(); + } + + protected abstract void configUserThread(); + + protected void addCapabilities() { + } + + // The onApplicationLaunched call must map to UserThread, so that all following methods are running in the + // thread the application is running and we don't run into thread interference. + protected abstract void launchApplication(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // If application is a JavaFX application we need wait for onApplicationLaunched + /////////////////////////////////////////////////////////////////////////////////////////// + + // Headless versions can call inside launchApplication the onApplicationLaunched() manually + protected void onApplicationLaunched() { + configUserThread(); + CommonSetup.printSystemLoadPeriodically(10); + // As the handler method might be overwritten by subclasses and they use the application as handler + // we need to setup the handler after the application is created. + CommonSetup.setupUncaughtExceptionHandler(this); + setupGuice(); + setupAvoidStandbyMode(); + + hasDowngraded = BisqSetup.hasDowngraded(); + if (hasDowngraded) { + // If user tried to downgrade we do not read the persisted data to avoid data corruption + // We call startApplication to enable UI to show popup. We prevent in BisqSetup to go further + // in the process and require a shut down. + startApplication(); + } else { + readAllPersisted(this::startApplication); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // We continue with a series of synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void setupGuice() { + module = getModule(); + injector = getInjector(); + applyInjector(); + } + + protected abstract AppModule getModule(); + + protected Injector getInjector() { + return Guice.createInjector(module); + } + + protected void applyInjector() { + // Subclasses might configure classes with the injector here + } + + protected void readAllPersisted(Runnable completeHandler) { + readAllPersisted(null, completeHandler); + } + + protected void readAllPersisted(@Nullable List additionalHosts, Runnable completeHandler) { + List hosts = CorePersistedDataHost.getPersistedDataHosts(injector); + if (additionalHosts != null) { + hosts.addAll(additionalHosts); + } + + AtomicInteger remaining = new AtomicInteger(hosts.size()); + hosts.forEach(host -> { + host.readPersisted(() -> { + if (remaining.decrementAndGet() == 0) { + UserThread.execute(completeHandler); + } + }); + }); + } + + protected void setupAvoidStandbyMode() { + } + + protected abstract void startApplication(); + + // Once the application is ready we get that callback and we start the setup + protected void onApplicationStarted() { + runBisqSetup(); + } + + protected void runBisqSetup() { + BisqSetup bisqSetup = injector.getInstance(BisqSetup.class); + bisqSetup.addBisqSetupListener(this); + bisqSetup.start(); + } + + public abstract void onSetupComplete(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // GracefulShutDownHandler implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + // This might need to be overwritten in case the application is not using all modules + @Override + public void gracefulShutDown(ResultHandler resultHandler) { + log.info("Start graceful shutDown"); + if (isShutdownInProgress) { + return; + } + + isShutdownInProgress = true; + + if (injector == null) { + log.info("Shut down called before injector was created"); + resultHandler.handleResult(); + System.exit(EXIT_SUCCESS); + } + + try { + injector.getInstance(PriceFeedService.class).shutDown(); + injector.getInstance(ArbitratorManager.class).shutDown(); + injector.getInstance(TradeStatisticsManager.class).shutDown(); + injector.getInstance(XmrTxProofService.class).shutDown(); + injector.getInstance(RpcService.class).shutDown(); + injector.getInstance(DaoSetup.class).shutDown(); + injector.getInstance(AvoidStandbyModeService.class).shutDown(); + log.info("OpenOfferManager shutdown started"); + injector.getInstance(OpenOfferManager.class).shutDown(() -> { + log.info("OpenOfferManager shutdown completed"); + + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(BsqWalletService.class).shutDown(); + + // We need to shutdown BitcoinJ before the P2PService as it uses Tor. + WalletsSetup walletsSetup = injector.getInstance(WalletsSetup.class); + walletsSetup.shutDownComplete.addListener((ov, o, n) -> { + log.info("WalletsSetup shutdown completed"); + + injector.getInstance(P2PService.class).shutDown(() -> { + log.info("P2PService shutdown completed"); + module.close(injector); + if (!hasDowngraded) { + // If user tried to downgrade we do not write the persistable data to avoid data corruption + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + log.info("Graceful shutdown completed. Exiting now."); + resultHandler.handleResult(); + UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1); + }); + } else { + UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1); + } + }); + }); + walletsSetup.shutDown(); + + }); + + // Wait max 20 sec. + UserThread.runAfter(() -> { + log.warn("Graceful shut down not completed in 20 sec. We trigger our timeout handler."); + if (!hasDowngraded) { + // If user tried to downgrade we do not write the persistable data to avoid data corruption + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + log.info("Graceful shutdown resulted in a timeout. Exiting now."); + resultHandler.handleResult(); + UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1); + }); + } else { + UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1); + } + + }, 20); + } catch (Throwable t) { + log.error("App shutdown failed with exception {}", t.toString()); + t.printStackTrace(); + if (!hasDowngraded) { + // If user tried to downgrade we do not write the persistable data to avoid data corruption + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + log.info("Graceful shutdown resulted in an error. Exiting now."); + resultHandler.handleResult(); + UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1); + }); + } else { + UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UncaughtExceptionHandler implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + log.error(throwable.toString()); + + if (doShutDown) + gracefulShutDown(() -> log.info("gracefulShutDown complete")); + } +} diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java new file mode 100644 index 0000000000..ae9bd25693 --- /dev/null +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -0,0 +1,147 @@ +/* + * 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 bisq.core.app; + +import bisq.core.trade.TradeManager; + +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.setup.GracefulShutDownHandler; + +import com.google.inject.Injector; + +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BisqHeadlessApp implements HeadlessApp { + @Getter + private static Runnable shutDownHandler; + + @Setter + protected Injector injector; + @Setter + private GracefulShutDownHandler gracefulShutDownHandler; + private boolean shutDownRequested; + protected BisqSetup bisqSetup; + private CorruptedStorageFileHandler corruptedStorageFileHandler; + private TradeManager tradeManager; + + public BisqHeadlessApp() { + shutDownHandler = this::stop; + } + + public void startApplication() { + try { + bisqSetup = injector.getInstance(BisqSetup.class); + bisqSetup.addBisqSetupListener(this); + + corruptedStorageFileHandler = injector.getInstance(CorruptedStorageFileHandler.class); + tradeManager = injector.getInstance(TradeManager.class); + + setupHandlers(); + } catch (Throwable throwable) { + log.error("Error during app init", throwable); + handleUncaughtException(throwable, false); + } + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } + + protected void setupHandlers() { + bisqSetup.setDisplayTacHandler(acceptedHandler -> { + log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); + acceptedHandler.run(); + }); + bisqSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); + bisqSetup.setSpvFileCorruptedHandler(msg -> log.error("onSpvFileCorruptedHandler: msg={}", msg)); + bisqSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); + bisqSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); + bisqSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler")); + bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> log.info("onRequestWalletPasswordHandler")); + bisqSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); + bisqSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); + bisqSetup.setDisplayPrivateNotificationHandler(privateNotification -> log.info("onDisplayPrivateNotificationHandler. privateNotification={}", privateNotification)); + bisqSetup.setDaoErrorMessageHandler(errorMessage -> log.error("onDaoErrorMessageHandler. errorMessage={}", errorMessage)); + bisqSetup.setDaoWarnMessageHandler(warnMessage -> log.warn("onDaoWarnMessageHandler. warnMessage={}", warnMessage)); + bisqSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); + bisqSetup.setDisplayLocalhostHandler(key -> log.info("onDisplayLocalhostHandler")); + bisqSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); + bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString())); + bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); + bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); + bisqSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); + bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); + bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported", + lastVersion, Version.VERSION)); + bisqSetup.setDaoRequiresRestartHandler(() -> { + log.info("There was a problem with synchronizing the DAO state. " + + "A restart of the application is required to fix the issue."); + gracefulShutDownHandler.gracefulShutDown(() -> { + }); + }); + + corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); + tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("onTakeOfferRequestErrorMessageHandler")); + } + + public void stop() { + if (!shutDownRequested) { + UserThread.runAfter(() -> { + gracefulShutDownHandler.gracefulShutDown(() -> { + log.debug("App shutdown complete"); + }); + }, 200, TimeUnit.MILLISECONDS); + shutDownRequested = true; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UncaughtExceptionHandler implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + if (!shutDownRequested) { + try { + try { + log.error(throwable.getMessage()); + } catch (Throwable throwable3) { + log.error("Error at displaying Throwable."); + throwable3.printStackTrace(); + } + if (doShutDown) + stop(); + } catch (Throwable throwable2) { + // If printStackTrace cause a further exception we don't pass the throwable to the Popup. + log.error(throwable2.toString()); + if (doShutDown) + stop(); + } + } + } +} diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java new file mode 100644 index 0000000000..6a867d875b --- /dev/null +++ b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java @@ -0,0 +1,126 @@ +/* + * 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 bisq.core.app; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.app.Version; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BisqHeadlessAppMain extends BisqExecutable { + protected HeadlessApp headlessApp; + + public BisqHeadlessAppMain() { + super("Bisq Daemon", "bisqd", "Bisq", Version.VERSION); + } + + public static void main(String[] args) throws Exception { + // For some reason the JavaFX launch process results in us losing the thread + // context class loader: reset it. In order to work around a bug in JavaFX 8u25 + // and below, you must include the following code as the first line of your + // realMain method: + Thread.currentThread().setContextClassLoader(BisqHeadlessAppMain.class.getClassLoader()); + + new BisqHeadlessAppMain().execute(args); + } + + @Override + protected void doExecute() { + super.doExecute(); + + keepRunning(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // First synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void configUserThread() { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(this.getClass().getSimpleName()) + .setDaemon(true) + .build(); + UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); + } + + @Override + protected void launchApplication() { + headlessApp = new BisqHeadlessApp(); + + UserThread.execute(this::onApplicationLaunched); + } + + @Override + protected void onApplicationLaunched() { + super.onApplicationLaunched(); + headlessApp.setGracefulShutDownHandler(this); + } + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + headlessApp.handleUncaughtException(throwable, doShutDown); + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // We continue with a series of synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected AppModule getModule() { + return new CoreModule(config); + } + + @Override + protected void applyInjector() { + super.applyInjector(); + + headlessApp.setInjector(injector); + } + + @Override + protected void startApplication() { + // We need to be in user thread! We mapped at launchApplication already... + headlessApp.startApplication(); + + // In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted + onApplicationStarted(); + } + + private void keepRunning() { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException ignore) { + } + } + } +} diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java new file mode 100644 index 0000000000..de6db2c2ba --- /dev/null +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -0,0 +1,776 @@ +/* + * 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 bisq.core.app; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessStorageService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.Alert; +import bisq.core.alert.AlertManager; +import bisq.core.alert.PrivateNotificationPayload; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; +import bisq.core.dao.governance.voteresult.VoteResultException; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.trade.TradeManager; +import bisq.core.trade.TradeTxException; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.app.Log; +import bisq.common.app.Version; +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; +import bisq.common.util.InvalidVersionException; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import javafx.collections.SetChangeListener; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import ch.qos.logback.classic.Level; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public class BisqSetup { + private static final String VERSION_FILE_NAME = "version"; + private static final String RESYNC_SPV_FILE_NAME = "resyncSpv"; + + public interface BisqSetupListener { + default void onInitP2pNetwork() { + } + + default void onInitWallet() { + } + + default void onRequestWalletPassword() { + } + + void onSetupComplete(); + } + + private static final long STARTUP_TIMEOUT_MINUTES = 4; + + private final DomainInitialisation domainInitialisation; + private final P2PNetworkSetup p2PNetworkSetup; + private final WalletAppSetup walletAppSetup; + private final WalletsManager walletsManager; + private final WalletsSetup walletsSetup; + private final BtcWalletService btcWalletService; + private final P2PService p2PService; + private final SignedWitnessStorageService signedWitnessStorageService; + private final TradeManager tradeManager; + private final OpenOfferManager openOfferManager; + private final Preferences preferences; + private final User user; + private final AlertManager alertManager; + private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService; + private final Config config; + private final AccountAgeWitnessService accountAgeWitnessService; + private final TorSetup torSetup; + private final CoinFormatter formatter; + private final LocalBitcoinNode localBitcoinNode; + private final AppStartupState appStartupState; + + @Setter + @Nullable + private Consumer displayTacHandler; + @Setter + @Nullable + private Consumer chainFileLockedExceptionHandler, + spvFileCorruptedHandler, lockedUpFundsHandler, daoErrorMessageHandler, daoWarnMessageHandler, + filterWarningHandler, displaySecurityRecommendationHandler, displayLocalhostHandler, + wrongOSArchitectureHandler, displaySignedByArbitratorHandler, + displaySignedByPeerHandler, displayPeerLimitLiftedHandler, displayPeerSignerHandler, + rejectedTxErrorMessageHandler; + @Setter + @Nullable + private Consumer displayTorNetworkSettingsHandler; + @Setter + @Nullable + private Runnable showFirstPopupIfResyncSPVRequestedHandler; + @Setter + @Nullable + private Consumer> requestWalletPasswordHandler; + @Setter + @Nullable + private Consumer displayAlertHandler; + @Setter + @Nullable + private BiConsumer displayUpdateHandler; + @Setter + @Nullable + private Consumer voteResultExceptionHandler; + @Setter + @Nullable + private Consumer displayPrivateNotificationHandler; + @Setter + @Nullable + private Runnable showPopupIfInvalidBtcConfigHandler; + @Setter + @Nullable + private Consumer> revolutAccountsUpdateHandler; + @Setter + @Nullable + private Consumer> amazonGiftCardAccountsUpdateHandler; + @Setter + @Nullable + private Runnable osxKeyLoggerWarningHandler; + @Setter + @Nullable + private Runnable qubesOSInfoHandler; + @Setter + @Nullable + private Runnable daoRequiresRestartHandler; + @Setter + @Nullable + private Consumer downGradePreventionHandler; + + @Getter + final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); + private BooleanProperty p2pNetworkReady; + private final BooleanProperty walletInitialized = new SimpleBooleanProperty(); + private boolean allBasicServicesInitialized; + @SuppressWarnings("FieldCanBeLocal") + private MonadicBinding p2pNetworkAndWalletInitialized; + private final List bisqSetupListeners = new ArrayList<>(); + + @Inject + public BisqSetup(DomainInitialisation domainInitialisation, + P2PNetworkSetup p2PNetworkSetup, + WalletAppSetup walletAppSetup, + WalletsManager walletsManager, + WalletsSetup walletsSetup, + BtcWalletService btcWalletService, + P2PService p2PService, + SignedWitnessStorageService signedWitnessStorageService, + TradeManager tradeManager, + OpenOfferManager openOfferManager, + Preferences preferences, + User user, + AlertManager alertManager, + UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService, + Config config, + AccountAgeWitnessService accountAgeWitnessService, + TorSetup torSetup, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + LocalBitcoinNode localBitcoinNode, + AppStartupState appStartupState, + Socks5ProxyProvider socks5ProxyProvider) { + this.domainInitialisation = domainInitialisation; + this.p2PNetworkSetup = p2PNetworkSetup; + this.walletAppSetup = walletAppSetup; + this.walletsManager = walletsManager; + this.walletsSetup = walletsSetup; + this.btcWalletService = btcWalletService; + this.p2PService = p2PService; + this.signedWitnessStorageService = signedWitnessStorageService; + this.tradeManager = tradeManager; + this.openOfferManager = openOfferManager; + this.preferences = preferences; + this.user = user; + this.alertManager = alertManager; + this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService; + this.config = config; + this.accountAgeWitnessService = accountAgeWitnessService; + this.torSetup = torSetup; + this.formatter = formatter; + this.localBitcoinNode = localBitcoinNode; + this.appStartupState = appStartupState; + + MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void displayAlertIfPresent(Alert alert, boolean openNewVersionPopup) { + if (alert == null) + return; + + if (alert.isSoftwareUpdateNotification()) { + // only process if the alert version is "newer" than ours + if (alert.isNewVersion(preferences)) { + user.setDisplayedAlert(alert); // save context to compare later + newVersionAvailableProperty.set(true); // shows link in footer bar + if ((alert.canShowPopup(preferences) || openNewVersionPopup) && displayUpdateHandler != null) { + displayUpdateHandler.accept(alert, alert.showAgainKey()); + } + } + } else { + // it is a normal message alert + final Alert displayedAlert = user.getDisplayedAlert(); + if ((displayedAlert == null || !displayedAlert.equals(alert)) && displayAlertHandler != null) + displayAlertHandler.accept(alert); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Main startup tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addBisqSetupListener(BisqSetupListener listener) { + bisqSetupListeners.add(listener); + } + + public void start() { + // If user tried to downgrade we require a shutdown + if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_MAINNET && + hasDowngraded(downGradePreventionHandler)) { + return; + } + + persistBisqVersion(); + maybeReSyncSPVChain(); + maybeShowTac(this::step2); + } + + private void step2() { + readMapsFromResources(this::step3); + checkForCorrectOSArchitecture(); + checkOSXVersion(); + checkIfRunningOnQubesOS(); + } + + private void step3() { + startP2pNetworkAndWallet(this::step4); + } + + private void step4() { + initDomainServices(); + + bisqSetupListeners.forEach(BisqSetupListener::onSetupComplete); + + // We set that after calling the setupCompleteHandler to not trigger a popup from the dev dummy accounts + // in MainViewModel + maybeShowSecurityRecommendation(); + maybeShowLocalhostRunningInfo(); + maybeShowAccountSigningStateInfo(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Sub tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeReSyncSPVChain() { + // We do the delete of the spv file at startup before BitcoinJ is initialized to avoid issues with locked files under Windows. + if (getResyncSpvSemaphore()) { + try { + walletsSetup.reSyncSPVChain(); + + // In case we had an unconfirmed change output we reset the unconfirmedBsqChangeOutputList so that + // after a SPV resync we do not have any dangling BSQ utxos in that list which would cause an incorrect + // BSQ balance state after the SPV resync. + unconfirmedBsqChangeOutputListService.onSpvResync(); + } catch (IOException e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + } + + private void maybeShowTac(Runnable nextStep) { + if (!preferences.isTacAcceptedV120() && !DevEnv.isDevMode()) { + if (displayTacHandler != null) + displayTacHandler.accept(() -> { + preferences.setTacAcceptedV120(true); + nextStep.run(); + }); + } else { + nextStep.run(); + } + } + + private void readMapsFromResources(Runnable completeHandler) { + String postFix = "_" + config.baseCurrencyNetwork.name(); + p2PService.getP2PDataStorage().readFromResources(postFix, completeHandler); + } + + private void startP2pNetworkAndWallet(Runnable nextStep) { + ChangeListener walletInitializedListener = (observable, oldValue, newValue) -> { + // TODO that seems to be called too often if Tor takes longer to start up... + if (newValue && !p2pNetworkReady.get() && displayTorNetworkSettingsHandler != null) + displayTorNetworkSettingsHandler.accept(true); + }; + + Timer startupTimeout = UserThread.runAfter(() -> { + if (p2PNetworkSetup.p2pNetworkFailed.get() || walletsSetup.walletsSetupFailed.get()) { + // Skip this timeout action if the p2p network or wallet setup failed + // since an error prompt will be shown containing the error message + return; + } + log.warn("startupTimeout called"); + if (walletsManager.areWalletsEncrypted()) + walletInitialized.addListener(walletInitializedListener); + else if (displayTorNetworkSettingsHandler != null) + displayTorNetworkSettingsHandler.accept(true); + + log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG to show more details for " + + "Tor network connection issues"); + Log.setCustomLogLevel("org.berndpruenster.netlayer", Level.DEBUG); + + }, STARTUP_TIMEOUT_MINUTES, TimeUnit.MINUTES); + + log.info("Init P2P network"); + bisqSetupListeners.forEach(BisqSetupListener::onInitP2pNetwork); + p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); + + // We only init wallet service here if not using Tor for bitcoinj. + // When using Tor, wallet init must be deferred until Tor is ready. + // TODO encapsulate below conditional inside getUseTorForBitcoinJ + if (!preferences.getUseTorForBitcoinJ() || localBitcoinNode.shouldBeUsed()) { + initWallet(); + } + + // need to store it to not get garbage collected + p2pNetworkAndWalletInitialized = EasyBind.combine(walletInitialized, p2pNetworkReady, + (a, b) -> { + log.info("walletInitialized={}, p2pNetWorkReady={}", a, b); + return a && b; + }); + p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> { + if (newValue) { + startupTimeout.stop(); + walletInitialized.removeListener(walletInitializedListener); + if (displayTorNetworkSettingsHandler != null) + displayTorNetworkSettingsHandler.accept(false); + nextStep.run(); + } + }); + } + + private void initWallet() { + log.info("Init wallet"); + bisqSetupListeners.forEach(BisqSetupListener::onInitWallet); + Runnable walletPasswordHandler = () -> { + log.info("Wallet password required"); + bisqSetupListeners.forEach(BisqSetupListener::onRequestWalletPassword); + if (p2pNetworkReady.get()) + p2PNetworkSetup.setSplashP2PNetworkAnimationVisible(true); + + if (requestWalletPasswordHandler != null) { + requestWalletPasswordHandler.accept(aesKey -> { + walletsManager.setAesKey(aesKey); + walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(), + aesKey); + if (getResyncSpvSemaphore()) { + if (showFirstPopupIfResyncSPVRequestedHandler != null) + showFirstPopupIfResyncSPVRequestedHandler.run(); + } else { + // TODO no guarantee here that the wallet is really fully initialized + // We would need a new walletInitializedButNotEncrypted state to track + // Usually init is fast and we have our wallet initialized at that state though. + walletInitialized.set(true); + } + }); + } + }; + walletAppSetup.init(chainFileLockedExceptionHandler, + spvFileCorruptedHandler, + getResyncSpvSemaphore(), + showFirstPopupIfResyncSPVRequestedHandler, + showPopupIfInvalidBtcConfigHandler, + walletPasswordHandler, + () -> { + if (allBasicServicesInitialized) { + checkForLockedUpFunds(); + checkForInvalidMakerFeeTxs(); + } + }, + () -> walletInitialized.set(true)); + } + + private void initDomainServices() { + log.info("initDomainServices"); + + domainInitialisation.initDomainServices(rejectedTxErrorMessageHandler, + displayPrivateNotificationHandler, + daoErrorMessageHandler, + daoWarnMessageHandler, + filterWarningHandler, + voteResultExceptionHandler, + revolutAccountsUpdateHandler, + amazonGiftCardAccountsUpdateHandler, + daoRequiresRestartHandler); + + if (walletsSetup.downloadPercentageProperty().get() == 1) { + checkForLockedUpFunds(); + checkForInvalidMakerFeeTxs(); + } + + alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> + displayAlertIfPresent(newValue, false)); + displayAlertIfPresent(alertManager.alertMessageProperty().get(), false); + + allBasicServicesInitialized = true; + + appStartupState.onDomainServicesInitialized(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void checkForLockedUpFunds() { + // We check if there are locked up funds in failed or closed trades + try { + Set setOfAllTradeIds = tradeManager.getSetOfFailedOrClosedTradeIdsFromLockedInFunds(); + btcWalletService.getAddressEntriesForTrade().stream() + .filter(e -> setOfAllTradeIds.contains(e.getOfferId()) && + e.getContext() == AddressEntry.Context.MULTI_SIG) + .forEach(e -> { + Coin balance = e.getCoinLockedInMultiSigAsCoin(); + if (balance.isPositive()) { + String message = Res.get("popup.warning.lockedUpFunds", + formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId()); + log.warn(message); + if (lockedUpFundsHandler != null) { + lockedUpFundsHandler.accept(message); + } + } + }); + } catch (TradeTxException e) { + log.warn(e.getMessage()); + if (lockedUpFundsHandler != null) { + lockedUpFundsHandler.accept(e.getMessage()); + } + } + } + + private void checkForInvalidMakerFeeTxs() { + // We check if we have open offers with no confidence object at the maker fee tx. That can happen if the + // miner fee was too low and the transaction got removed from mempool and got out from our wallet after a + // resync. + openOfferManager.getObservableList().forEach(e -> { + String offerFeePaymentTxId = e.getOffer().getOfferFeePaymentTxId(); + if (btcWalletService.getConfidenceForTxId(offerFeePaymentTxId) == null) { + String message = Res.get("popup.warning.openOfferWithInvalidMakerFeeTx", + e.getOffer().getShortId(), offerFeePaymentTxId); + log.warn(message); + if (lockedUpFundsHandler != null) { + lockedUpFundsHandler.accept(message); + } + } + }); + } + + @Nullable + public static String getLastBisqVersion() { + File versionFile = getVersionFile(); + if (!versionFile.exists()) { + return null; + } + try (Scanner scanner = new Scanner(versionFile)) { + // We only expect 1 line + if (scanner.hasNextLine()) { + return scanner.nextLine(); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + @Nullable + public static boolean getResyncSpvSemaphore() { + File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME); + return resyncSpvSemaphore.exists(); + } + + public static void setResyncSpvSemaphore(boolean isResyncSpvRequested) { + File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME); + if (isResyncSpvRequested) { + if (!resyncSpvSemaphore.exists()) { + try { + if (!resyncSpvSemaphore.createNewFile()) { + log.error("ResyncSpv file could not be created"); + } + } catch (IOException e) { + e.printStackTrace(); + log.error("ResyncSpv file could not be created. {}", e.toString()); + } + } + } else { + resyncSpvSemaphore.delete(); + } + } + + + + + private static File getVersionFile() { + return new File(Config.appDataDir(), VERSION_FILE_NAME); + } + + public static boolean hasDowngraded() { + return hasDowngraded(getLastBisqVersion()); + } + + public static boolean hasDowngraded(String lastVersion) { + return lastVersion != null && Version.isNewVersion(lastVersion, Version.VERSION); + } + + public static boolean hasDowngraded(@Nullable Consumer downGradePreventionHandler) { + String lastVersion = getLastBisqVersion(); + boolean hasDowngraded = hasDowngraded(lastVersion); + if (hasDowngraded) { + log.error("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION); + if (downGradePreventionHandler != null) { + downGradePreventionHandler.accept(lastVersion); + } + } + return hasDowngraded; + } + + public static void persistBisqVersion() { + File versionFile = getVersionFile(); + if (!versionFile.exists()) { + try { + if (!versionFile.createNewFile()) { + log.error("Version file could not be created"); + } + } catch (IOException e) { + e.printStackTrace(); + log.error("Version file could not be created. {}", e.toString()); + } + } + + try (FileWriter fileWriter = new FileWriter(versionFile, false)) { + fileWriter.write(Version.VERSION); + } catch (IOException e) { + e.printStackTrace(); + log.error("Writing Version failed. {}", e.toString()); + } + } + + private void checkForCorrectOSArchitecture() { + if (!Utilities.isCorrectOSArchitecture() && wrongOSArchitectureHandler != null) { + String osArchitecture = Utilities.getOSArchitecture(); + // We don't force a shutdown as the osArchitecture might in strange cases return a wrong value. + // Needs at least more testing on different machines... + wrongOSArchitectureHandler.accept(Res.get("popup.warning.wrongVersion", + osArchitecture, + Utilities.getJVMArchitecture(), + osArchitecture)); + } + } + + private void checkOSXVersion() { + if (Utilities.isOSX() && osxKeyLoggerWarningHandler != null) { + try { + // Seems it was introduced at 10.14: https://github.com/wesnoth/wesnoth/issues/4109 + if (Utilities.getMajorVersion() >= 10 && Utilities.getMinorVersion() >= 14) { + osxKeyLoggerWarningHandler.run(); + } + } catch (InvalidVersionException | NumberFormatException e) { + log.warn(e.getMessage()); + } + } + } + + /** + * If Bisq is running on an OS that is virtualized under Qubes, show info popup with + * link to the Setup Guide. The guide documents what other steps are needed, in + * addition to installing the Linux package (qube sizing, etc) + */ + private void checkIfRunningOnQubesOS() { + if (Utilities.isQubesOS() && qubesOSInfoHandler != null) { + qubesOSInfoHandler.run(); + } + } + + private void maybeShowSecurityRecommendation() { + String key = "remindPasswordAndBackup"; + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) change -> { + if (!walletsManager.areWalletsEncrypted() && !user.isPaymentAccountImport() && preferences.showAgain(key) && change.wasAdded() && + displaySecurityRecommendationHandler != null) + displaySecurityRecommendationHandler.accept(key); + }); + } + + private void maybeShowLocalhostRunningInfo() { + maybeTriggerDisplayHandler("bitcoinLocalhostNode", displayLocalhostHandler, localBitcoinNode.shouldBeUsed()); + } + + private void maybeShowAccountSigningStateInfo() { + String keySignedByArbitrator = "accountSignedByArbitrator"; + String keySignedByPeer = "accountSignedByPeer"; + String keyPeerLimitedLifted = "accountLimitLifted"; + String keyPeerSigner = "accountPeerSigner"; + + // check signed witness on startup + checkSigningState(AccountAgeWitnessService.SignState.ARBITRATOR, keySignedByArbitrator, displaySignedByArbitratorHandler); + checkSigningState(AccountAgeWitnessService.SignState.PEER_INITIAL, keySignedByPeer, displaySignedByPeerHandler); + checkSigningState(AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED, keyPeerLimitedLifted, displayPeerLimitLiftedHandler); + checkSigningState(AccountAgeWitnessService.SignState.PEER_SIGNER, keyPeerSigner, displayPeerSignerHandler); + + // check signed witness during runtime + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener( + payload -> { + maybeTriggerDisplayHandler(keySignedByArbitrator, displaySignedByArbitratorHandler, + isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.ARBITRATOR)); + maybeTriggerDisplayHandler(keySignedByPeer, displaySignedByPeerHandler, + isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_INITIAL)); + maybeTriggerDisplayHandler(keyPeerLimitedLifted, displayPeerLimitLiftedHandler, + isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED)); + maybeTriggerDisplayHandler(keyPeerSigner, displayPeerSignerHandler, + isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_SIGNER)); + }); + } + + private void checkSigningState(AccountAgeWitnessService.SignState state, + String key, Consumer displayHandler) { + boolean signingStateFound = signedWitnessStorageService.getMap().values().stream() + .anyMatch(payload -> isSignedWitnessOfMineWithState(payload, state)); + + maybeTriggerDisplayHandler(key, displayHandler, signingStateFound); + } + + private boolean isSignedWitnessOfMineWithState(PersistableNetworkPayload payload, + AccountAgeWitnessService.SignState state) { + if (payload instanceof SignedWitness && user.getPaymentAccounts() != null) { + // We know at this point that it is already added to the signed witness list + // Check if new signed witness is for one of my own accounts + return user.getPaymentAccounts().stream() + .filter(a -> PaymentMethod.hasChargebackRisk(a.getPaymentMethod(), a.getTradeCurrencies())) + .filter(a -> Arrays.equals(((SignedWitness) payload).getAccountAgeWitnessHash(), + accountAgeWitnessService.getMyWitness(a.getPaymentAccountPayload()).getHash())) + .anyMatch(a -> accountAgeWitnessService.getSignState(accountAgeWitnessService.getMyWitness( + a.getPaymentAccountPayload())).equals(state)); + } + return false; + } + + private void maybeTriggerDisplayHandler(String key, Consumer displayHandler, boolean signingStateFound) { + if (signingStateFound && preferences.showAgain(key) && + displayHandler != null) { + displayHandler.accept(key); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + // Wallet + public StringProperty getBtcInfo() { + return walletAppSetup.getBtcInfo(); + } + + public DoubleProperty getBtcSyncProgress() { + return walletAppSetup.getBtcSyncProgress(); + } + + public StringProperty getWalletServiceErrorMsg() { + return walletAppSetup.getWalletServiceErrorMsg(); + } + + public StringProperty getBtcSplashSyncIconId() { + return walletAppSetup.getBtcSplashSyncIconId(); + } + + public BooleanProperty getUseTorForBTC() { + return walletAppSetup.getUseTorForBTC(); + } + + // P2P + public StringProperty getP2PNetworkInfo() { + return p2PNetworkSetup.getP2PNetworkInfo(); + } + + public BooleanProperty getSplashP2PNetworkAnimationVisible() { + return p2PNetworkSetup.getSplashP2PNetworkAnimationVisible(); + } + + public StringProperty getP2pNetworkWarnMsg() { + return p2PNetworkSetup.getP2pNetworkWarnMsg(); + } + + public StringProperty getP2PNetworkIconId() { + return p2PNetworkSetup.getP2PNetworkIconId(); + } + + public BooleanProperty getUpdatedDataReceived() { + return p2PNetworkSetup.getUpdatedDataReceived(); + } + + public StringProperty getP2pNetworkLabelId() { + return p2PNetworkSetup.getP2pNetworkLabelId(); + } + + +} diff --git a/core/src/main/java/bisq/core/app/CoreModule.java b/core/src/main/java/bisq/core/app/CoreModule.java new file mode 100644 index 0000000000..4984a84aae --- /dev/null +++ b/core/src/main/java/bisq/core/app/CoreModule.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.app; + +import bisq.core.alert.AlertModule; +import bisq.core.btc.BitcoinModule; +import bisq.core.dao.DaoModule; +import bisq.core.filter.FilterModule; +import bisq.core.network.CoreNetworkFilter; +import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; +import bisq.core.offer.OfferModule; +import bisq.core.presentation.CorePresentationModule; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; +import bisq.core.trade.TradeModule; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.ImmutableCoinFormatter; + +import bisq.network.crypto.EncryptionServiceModule; +import bisq.network.p2p.P2PModule; +import bisq.network.p2p.network.BridgeAddressProvider; +import bisq.network.p2p.network.NetworkFilter; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.PubKeyRingProvider; +import bisq.common.proto.network.NetworkProtoResolver; +import bisq.common.proto.persistable.PersistenceProtoResolver; + +import com.google.inject.Singleton; + +import java.io.File; + +import static bisq.common.config.Config.*; +import static com.google.inject.name.Names.named; + +public class CoreModule extends AppModule { + + public CoreModule(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(Config.class).toInstance(config); + + bind(BridgeAddressProvider.class).to(Preferences.class); + + bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class); + bind(NetworkFilter.class).to(CoreNetworkFilter.class).in(Singleton.class); + + bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); + + CoinFormatter btcFormatter = new ImmutableCoinFormatter(config.networkParameters.getMonetaryFormat()); + bind(CoinFormatter.class).annotatedWith(named(FormattingUtils.BTC_FORMATTER_KEY)).toInstance(btcFormatter); + + bind(File.class).annotatedWith(named(KEY_STORAGE_DIR)).toInstance(config.keyStorageDir); + + bind(NetworkProtoResolver.class).to(CoreNetworkProtoResolver.class); + bind(PersistenceProtoResolver.class).to(CorePersistenceProtoResolver.class); + + bindConstant().annotatedWith(named(USE_DEV_PRIVILEGE_KEYS)).to(config.useDevPrivilegeKeys); + bindConstant().annotatedWith(named(USE_DEV_MODE)).to(config.useDevMode); + bindConstant().annotatedWith(named(USE_DEV_MODE_HEADER)).to(config.useDevModeHeader); + bindConstant().annotatedWith(named(REFERRAL_ID)).to(config.referralId); + + // ordering is used for shut down sequence + install(new TradeModule(config)); + install(new EncryptionServiceModule(config)); + install(new OfferModule(config)); + install(new P2PModule(config)); + install(new BitcoinModule(config)); + install(new DaoModule(config)); + install(new AlertModule(config)); + install(new FilterModule(config)); + install(new CorePresentationModule(config)); + bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class); + } +} diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java new file mode 100644 index 0000000000..b0dbd16b77 --- /dev/null +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -0,0 +1,288 @@ +/* + * 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 bisq.core.app; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.alert.PrivateNotificationPayload; +import bisq.core.btc.Balances; +import bisq.core.dao.DaoSetup; +import bisq.core.dao.governance.voteresult.VoteResultException; +import bisq.core.dao.governance.voteresult.VoteResultService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.filter.FilterManager; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.notifications.alerts.DisputeMsgEvents; +import bisq.core.notifications.alerts.MyOfferTakenEvents; +import bisq.core.notifications.alerts.TradeEvents; +import bisq.core.notifications.alerts.market.MarketAlerts; +import bisq.core.notifications.alerts.price.PriceAlert; +import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.TriggerPriceService; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.TradeLimits; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.mempool.MempoolService; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.support.traderchat.TraderChatManager; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.trade.txproof.xmr.XmrTxProofService; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.ClockWatcher; +import bisq.common.app.DevEnv; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; + +import javafx.collections.ListChangeListener; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Handles the initialisation of domain classes. We should refactor to the model that the domain classes listen on the + * relevant start up state from AppStartupState instead to get called. Only for initialisation which has a required + * order we will still need this class. For now it helps to keep BisqSetup more focussed on the process and not getting + * overloaded with domain initialisation code. + */ +public class DomainInitialisation { + private final ClockWatcher clockWatcher; + private final TradeLimits tradeLimits; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; + private final RefundManager refundManager; + private final TraderChatManager traderChatManager; + private final TradeManager tradeManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; + private final XmrTxProofService xmrTxProofService; + private final OpenOfferManager openOfferManager; + private final Balances balances; + private final WalletAppSetup walletAppSetup; + private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final PrivateNotificationManager privateNotificationManager; + private final P2PService p2PService; + private final FeeService feeService; + private final DaoSetup daoSetup; + private final TradeStatisticsManager tradeStatisticsManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final SignedWitnessService signedWitnessService; + private final PriceFeedService priceFeedService; + private final FilterManager filterManager; + private final VoteResultService voteResultService; + private final MobileNotificationService mobileNotificationService; + private final MyOfferTakenEvents myOfferTakenEvents; + private final TradeEvents tradeEvents; + private final DisputeMsgEvents disputeMsgEvents; + private final PriceAlert priceAlert; + private final MarketAlerts marketAlerts; + private final User user; + private final DaoStateSnapshotService daoStateSnapshotService; + private final TriggerPriceService triggerPriceService; + private final MempoolService mempoolService; + + @Inject + public DomainInitialisation(ClockWatcher clockWatcher, + TradeLimits tradeLimits, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + RefundManager refundManager, + TraderChatManager traderChatManager, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, + XmrTxProofService xmrTxProofService, + OpenOfferManager openOfferManager, + Balances balances, + WalletAppSetup walletAppSetup, + ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + PrivateNotificationManager privateNotificationManager, + P2PService p2PService, + FeeService feeService, + DaoSetup daoSetup, + TradeStatisticsManager tradeStatisticsManager, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService, + PriceFeedService priceFeedService, + FilterManager filterManager, + VoteResultService voteResultService, + MobileNotificationService mobileNotificationService, + MyOfferTakenEvents myOfferTakenEvents, + TradeEvents tradeEvents, + DisputeMsgEvents disputeMsgEvents, + PriceAlert priceAlert, + MarketAlerts marketAlerts, + User user, + DaoStateSnapshotService daoStateSnapshotService, + TriggerPriceService triggerPriceService, + MempoolService mempoolService) { + this.clockWatcher = clockWatcher; + this.tradeLimits = tradeLimits; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + this.refundManager = refundManager; + this.traderChatManager = traderChatManager; + this.tradeManager = tradeManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; + this.xmrTxProofService = xmrTxProofService; + this.openOfferManager = openOfferManager; + this.balances = balances; + this.walletAppSetup = walletAppSetup; + this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.privateNotificationManager = privateNotificationManager; + this.p2PService = p2PService; + this.feeService = feeService; + this.daoSetup = daoSetup; + this.tradeStatisticsManager = tradeStatisticsManager; + this.accountAgeWitnessService = accountAgeWitnessService; + this.signedWitnessService = signedWitnessService; + this.priceFeedService = priceFeedService; + this.filterManager = filterManager; + this.voteResultService = voteResultService; + this.mobileNotificationService = mobileNotificationService; + this.myOfferTakenEvents = myOfferTakenEvents; + this.tradeEvents = tradeEvents; + this.disputeMsgEvents = disputeMsgEvents; + this.priceAlert = priceAlert; + this.marketAlerts = marketAlerts; + this.user = user; + this.daoStateSnapshotService = daoStateSnapshotService; + this.triggerPriceService = triggerPriceService; + this.mempoolService = mempoolService; + } + + public void initDomainServices(Consumer rejectedTxErrorMessageHandler, + Consumer displayPrivateNotificationHandler, + Consumer daoErrorMessageHandler, + Consumer daoWarnMessageHandler, + Consumer filterWarningHandler, + Consumer voteResultExceptionHandler, + Consumer> revolutAccountsUpdateHandler, + Consumer> amazonGiftCardAccountsUpdateHandler, + Runnable daoRequiresRestartHandler) { + clockWatcher.start(); + + PersistenceManager.onAllServicesInitialized(); + + tradeLimits.onAllServicesInitialized(); + + tradeManager.onAllServicesInitialized(); + arbitrationManager.onAllServicesInitialized(); + mediationManager.onAllServicesInitialized(); + refundManager.onAllServicesInitialized(); + traderChatManager.onAllServicesInitialized(); + + closedTradableManager.onAllServicesInitialized(); + failedTradesManager.onAllServicesInitialized(); + xmrTxProofService.onAllServicesInitialized(); + + openOfferManager.onAllServicesInitialized(); + + balances.onAllServicesInitialized(); + + walletAppSetup.setRejectedTxErrorMessageHandler(rejectedTxErrorMessageHandler, openOfferManager, tradeManager); + + arbitratorManager.onAllServicesInitialized(); + mediatorManager.onAllServicesInitialized(); + refundAgentManager.onAllServicesInitialized(); + + privateNotificationManager.privateNotificationProperty().addListener((observable, oldValue, newValue) -> { + if (displayPrivateNotificationHandler != null) + displayPrivateNotificationHandler.accept(newValue); + }); + + p2PService.onAllServicesInitialized(); + + feeService.onAllServicesInitialized(); + + if (DevEnv.isDaoActivated()) { + daoSetup.onAllServicesInitialized(errorMessage -> { + if (daoErrorMessageHandler != null) + daoErrorMessageHandler.accept(errorMessage); + }, warningMessage -> { + if (daoWarnMessageHandler != null) + daoWarnMessageHandler.accept(warningMessage); + }); + + daoStateSnapshotService.setDaoRequiresRestartHandler(daoRequiresRestartHandler); + } + + tradeStatisticsManager.onAllServicesInitialized(); + + accountAgeWitnessService.onAllServicesInitialized(); + signedWitnessService.onAllServicesInitialized(); + + priceFeedService.setCurrencyCodeOnInit(); + + filterManager.setFilterWarningHandler(filterWarningHandler); + filterManager.onAllServicesInitialized(); + + voteResultService.getVoteResultExceptions().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded() && voteResultExceptionHandler != null) { + c.getAddedSubList().forEach(voteResultExceptionHandler); + } + }); + + mobileNotificationService.onAllServicesInitialized(); + myOfferTakenEvents.onAllServicesInitialized(); + tradeEvents.onAllServicesInitialized(); + disputeMsgEvents.onAllServicesInitialized(); + priceAlert.onAllServicesInitialized(); + marketAlerts.onAllServicesInitialized(); + triggerPriceService.onAllServicesInitialized(); + mempoolService.onAllServicesInitialized(); + + if (revolutAccountsUpdateHandler != null) { + revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() + .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) + .map(paymentAccount -> (RevolutAccount) paymentAccount) + .filter(RevolutAccount::userNameNotSet) + .collect(Collectors.toList())); + } + if (amazonGiftCardAccountsUpdateHandler != null) { + amazonGiftCardAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() + .filter(paymentAccount -> paymentAccount instanceof AmazonGiftCardAccount) + .map(paymentAccount -> (AmazonGiftCardAccount) paymentAccount) + .filter(AmazonGiftCardAccount::countryNotSet) + .collect(Collectors.toList())); + } + } +} diff --git a/core/src/main/java/bisq/core/app/HeadlessApp.java b/core/src/main/java/bisq/core/app/HeadlessApp.java new file mode 100644 index 0000000000..3808b89b1a --- /dev/null +++ b/core/src/main/java/bisq/core/app/HeadlessApp.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.core.app; + +import bisq.common.setup.GracefulShutDownHandler; +import bisq.common.setup.UncaughtExceptionHandler; + +import com.google.inject.Injector; + +public interface HeadlessApp extends UncaughtExceptionHandler, BisqSetup.BisqSetupListener { + void setGracefulShutDownHandler(GracefulShutDownHandler gracefulShutDownHandler); + + void setInjector(Injector injector); + + void startApplication(); +} diff --git a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java new file mode 100644 index 0000000000..2643a389dc --- /dev/null +++ b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java @@ -0,0 +1,228 @@ +/* + * 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 bisq.core.app; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.P2PServiceListener; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.ConnectionListener; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Singleton +@Slf4j +public class P2PNetworkSetup { + private final PriceFeedService priceFeedService; + private final P2PService p2PService; + private final WalletsSetup walletsSetup; + private final Preferences preferences; + + @SuppressWarnings("FieldCanBeLocal") + private MonadicBinding p2PNetworkInfoBinding; + + @Getter + final StringProperty p2PNetworkInfo = new SimpleStringProperty(); + @Getter + final StringProperty p2PNetworkIconId = new SimpleStringProperty(); + @Getter + final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true); + @Getter + final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); + @Getter + final StringProperty p2pNetworkWarnMsg = new SimpleStringProperty(); + @Getter + final BooleanProperty updatedDataReceived = new SimpleBooleanProperty(); + @Getter + final BooleanProperty p2pNetworkFailed = new SimpleBooleanProperty(); + + @Inject + public P2PNetworkSetup(PriceFeedService priceFeedService, + P2PService p2PService, + WalletsSetup walletsSetup, + Preferences preferences) { + + this.priceFeedService = priceFeedService; + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + this.preferences = preferences; + } + + BooleanProperty init(Runnable initWalletServiceHandler, @Nullable Consumer displayTorNetworkSettingsHandler) { + StringProperty bootstrapState = new SimpleStringProperty(); + StringProperty bootstrapWarning = new SimpleStringProperty(); + BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); + BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); + + p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), + walletsSetup.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, + (state, warning, numP2pPeers, numBtcPeers, hiddenService, dataReceived) -> { + String result; + String daoFullNode = preferences.isDaoFullNode() ? Res.get("mainView.footer.daoFullNode") + " / " : ""; + int p2pPeers = (int) numP2pPeers; + if (warning != null && p2pPeers == 0) { + result = warning; + } else { + String p2pInfo = Res.get("mainView.footer.p2pInfo", numBtcPeers, numP2pPeers); + if (dataReceived && hiddenService) { + result = p2pInfo; + } else if (p2pPeers == 0) + result = state; + else + result = state + " / " + p2pInfo; + } + return daoFullNode + result; + }); + p2PNetworkInfoBinding.subscribe((observable, oldValue, newValue) -> { + p2PNetworkInfo.set(newValue); + }); + + bootstrapState.set(Res.get("mainView.bootstrapState.connectionToTorNetwork")); + + p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + // We only check at seed nodes as they are running the latest version + // Other disconnects might be caused by peers running an older version + if (connection.getConnectionState().isSeedNode() && + closeConnectionReason == CloseConnectionReason.RULE_VIOLATION) { + log.warn("RULE_VIOLATION onDisconnect closeConnectionReason={}, connection={}", + closeConnectionReason, connection); + } + } + + @Override + public void onError(Throwable throwable) { + } + }); + + final BooleanProperty p2pNetworkInitialized = new SimpleBooleanProperty(); + p2PService.start(new P2PServiceListener() { + @Override + public void onTorNodeReady() { + log.debug("onTorNodeReady"); + bootstrapState.set(Res.get("mainView.bootstrapState.torNodeCreated")); + p2PNetworkIconId.set("image-connection-tor"); + + if (preferences.getUseTorForBitcoinJ()) + initWalletServiceHandler.run(); + + // We want to get early connected to the price relay so we call it already now + priceFeedService.setCurrencyCodeOnInit(); + priceFeedService.initialRequestPriceFeed(); + } + + @Override + public void onHiddenServicePublished() { + log.debug("onHiddenServicePublished"); + hiddenServicePublished.set(true); + bootstrapState.set(Res.get("mainView.bootstrapState.hiddenServicePublished")); + } + + @Override + public void onDataReceived() { + log.debug("onRequestingDataCompleted"); + initialP2PNetworkDataReceived.set(true); + bootstrapState.set(Res.get("mainView.bootstrapState.initialDataReceived")); + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoSeedNodeAvailable() { + log.warn("onNoSeedNodeAvailable"); + if (p2PService.getNumConnectedPeers().get() == 0) + bootstrapWarning.set(Res.get("mainView.bootstrapWarning.noSeedNodesAvailable")); + else + bootstrapWarning.set(null); + + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoPeersAvailable() { + log.warn("onNoPeersAvailable"); + if (p2PService.getNumConnectedPeers().get() == 0) { + p2pNetworkWarnMsg.set(Res.get("mainView.p2pNetworkWarnMsg.noNodesAvailable")); + bootstrapWarning.set(Res.get("mainView.bootstrapWarning.noNodesAvailable")); + p2pNetworkLabelId.set("splash-error-state-msg"); + } else { + bootstrapWarning.set(null); + p2pNetworkLabelId.set("footer-pane"); + } + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onUpdatedDataReceived() { + log.debug("onUpdatedDataReceived"); + splashP2PNetworkAnimationVisible.set(false); + updatedDataReceived.set(true); + } + + @Override + public void onSetupFailed(Throwable throwable) { + log.error("onSetupFailed"); + p2pNetworkWarnMsg.set(Res.get("mainView.p2pNetworkWarnMsg.connectionToP2PFailed", throwable.getMessage())); + splashP2PNetworkAnimationVisible.set(false); + bootstrapWarning.set(Res.get("mainView.bootstrapWarning.bootstrappingToP2PFailed")); + p2pNetworkLabelId.set("splash-error-state-msg"); + p2pNetworkFailed.set(true); + } + + @Override + public void onRequestCustomBridges() { + if (displayTorNetworkSettingsHandler != null) + displayTorNetworkSettingsHandler.accept(true); + } + }); + + return p2pNetworkInitialized; + } + + public void setSplashP2PNetworkAnimationVisible(boolean value) { + splashP2PNetworkAnimationVisible.set(value); + } +} diff --git a/core/src/main/java/bisq/core/app/TorSetup.java b/core/src/main/java/bisq/core/app/TorSetup.java new file mode 100644 index 0000000000..b855e51462 --- /dev/null +++ b/core/src/main/java/bisq/core/app/TorSetup.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.app; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.handlers.ErrorMessageHandler; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.common.util.Preconditions.checkDir; + +@Slf4j +@Singleton +public class TorSetup { + private final File torDir; + + @Inject + public TorSetup(@Named(Config.TOR_DIR) File torDir) { + this.torDir = checkDir(torDir); + } + + // Should only be called if needed. Slows down Tor startup from about 5 sec. to 30 sec. if it gets deleted. + public void cleanupTorFiles(@Nullable Runnable resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()); + try { + FileUtil.deleteDirectory(torDir, hiddenservice, true); + if (resultHandler != null) + resultHandler.run(); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.toString()); + if (errorMessageHandler != null) + errorMessageHandler.handleErrorMessage(e.toString()); + } + } +} diff --git a/core/src/main/java/bisq/core/app/WalletAppSetup.java b/core/src/main/java/bisq/core/app/WalletAppSetup.java new file mode 100644 index 0000000000..b54e6706ad --- /dev/null +++ b/core/src/main/java/bisq/core/app/WalletAppSetup.java @@ -0,0 +1,287 @@ +/* + * 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 bisq.core.app; + +import bisq.core.api.CoreContext; +import bisq.core.btc.exceptions.InvalidHostException; +import bisq.core.btc.exceptions.RejectedTxException; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.fee.FeeService; +import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; + +import bisq.common.UserThread; +import bisq.common.config.Config; + +import org.bitcoinj.core.RejectMessage; +import org.bitcoinj.core.VersionMessage; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.ChainFileLockedException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public class WalletAppSetup { + + private final CoreContext coreContext; + private final WalletsManager walletsManager; + private final WalletsSetup walletsSetup; + private final FeeService feeService; + private final Config config; + private final Preferences preferences; + + @SuppressWarnings("FieldCanBeLocal") + private MonadicBinding btcInfoBinding; + + @Getter + private final DoubleProperty btcSyncProgress = new SimpleDoubleProperty(-1); + @Getter + private final StringProperty walletServiceErrorMsg = new SimpleStringProperty(); + @Getter + private final StringProperty btcSplashSyncIconId = new SimpleStringProperty(); + @Getter + private final StringProperty btcInfo = new SimpleStringProperty(Res.get("mainView.footer.btcInfo.initializing")); + @Getter + private final ObjectProperty rejectedTxException = new SimpleObjectProperty<>(); + @Getter + private final BooleanProperty useTorForBTC = new SimpleBooleanProperty(); + + @Inject + public WalletAppSetup(CoreContext coreContext, + WalletsManager walletsManager, + WalletsSetup walletsSetup, + FeeService feeService, + Config config, + Preferences preferences) { + this.coreContext = coreContext; + this.walletsManager = walletsManager; + this.walletsSetup = walletsSetup; + this.feeService = feeService; + this.config = config; + this.preferences = preferences; + this.useTorForBTC.set(preferences.getUseTorForBitcoinJ()); + } + + void init(@Nullable Consumer chainFileLockedExceptionHandler, + @Nullable Consumer spvFileCorruptedHandler, + boolean isSpvResyncRequested, + @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, + @Nullable Runnable showPopupIfInvalidBtcConfigHandler, + Runnable walletPasswordHandler, + Runnable downloadCompleteHandler, + Runnable walletInitializedHandler) { + log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}", + VersionMessage.BITCOINJ_VERSION, "2a80db4"); + + ObjectProperty walletServiceException = new SimpleObjectProperty<>(); + btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(), + walletsSetup.chainHeightProperty(), + feeService.feeUpdateCounterProperty(), + walletServiceException, + (downloadPercentage, chainHeight, feeUpdate, exception) -> { + String result; + if (exception == null) { + double percentage = (double) downloadPercentage; + btcSyncProgress.set(percentage); + int bestChainHeight = walletsSetup.getChain() != null ? + walletsSetup.getChain().getBestChainHeight() : + 0; + String chainHeightAsString = bestChainHeight > 0 ? + String.valueOf(bestChainHeight) : + ""; + if (percentage == 1) { + String synchronizedWith = Res.get("mainView.footer.btcInfo.synchronizedWith", + getBtcNetworkAsString(), chainHeightAsString); + String info = feeService.isFeeAvailable() ? + Res.get("mainView.footer.btcFeeRate", feeService.getTxFeePerVbyte().value) : + ""; + result = Res.get("mainView.footer.btcInfo", synchronizedWith, info); + getBtcSplashSyncIconId().set("image-connection-synced"); + downloadCompleteHandler.run(); + } else if (percentage > 0.0) { + String synchronizingWith = Res.get("mainView.footer.btcInfo.synchronizingWith", + getBtcNetworkAsString(), chainHeightAsString, + FormattingUtils.formatToPercentWithSymbol(percentage)); + result = Res.get("mainView.footer.btcInfo", synchronizingWith, ""); + } else { + result = Res.get("mainView.footer.btcInfo", + Res.get("mainView.footer.btcInfo.connectingTo"), + getBtcNetworkAsString()); + } + } else { + result = Res.get("mainView.footer.btcInfo", + Res.get("mainView.footer.btcInfo.connectionFailed"), + getBtcNetworkAsString()); + log.error(exception.toString()); + if (exception instanceof TimeoutException) { + getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.timeout")); + } else if (exception.getCause() instanceof BlockStoreException) { + if (exception.getCause().getCause() instanceof ChainFileLockedException && chainFileLockedExceptionHandler != null) { + chainFileLockedExceptionHandler.accept(Res.get("popup.warning.startupFailed.twoInstances")); + } else if (spvFileCorruptedHandler != null) { + spvFileCorruptedHandler.accept(Res.get("error.spvFileCorrupted", exception.getMessage())); + } + } else if (exception instanceof RejectedTxException) { + rejectedTxException.set((RejectedTxException) exception); + getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.rejectedTxException", exception.getMessage())); + } else { + getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.toString())); + } + } + return result; + + }); + btcInfoBinding.subscribe((observable, oldValue, newValue) -> getBtcInfo().set(newValue)); + + walletsSetup.initialize(null, + () -> { + // We only check one wallet as we apply encryption to all or none + if (walletsManager.areWalletsEncrypted() && !coreContext.isApiUser()) { + walletPasswordHandler.run(); + } else { + if (isSpvResyncRequested && !coreContext.isApiUser()) { + if (showFirstPopupIfResyncSPVRequestedHandler != null) + showFirstPopupIfResyncSPVRequestedHandler.run(); + } else { + walletInitializedHandler.run(); + } + } + }, + exception -> { + if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { + showPopupIfInvalidBtcConfigHandler.run(); + } else { + walletServiceException.set(exception); + } + }); + } + + void setRejectedTxErrorMessageHandler(Consumer rejectedTxErrorMessageHandler, + OpenOfferManager openOfferManager, + TradeManager tradeManager) { + getRejectedTxException().addListener((observable, oldValue, newValue) -> { + if (newValue == null || newValue.getTxId() == null) { + return; + } + + RejectMessage rejectMessage = newValue.getRejectMessage(); + log.warn("We received reject message: {}", rejectMessage); + + // TODO: Find out which reject messages are critical and which not. + // We got a report where a "tx already known" message caused a failed trade but the deposit tx was valid. + // To avoid such false positives we only handle reject messages which we consider clearly critical. + + switch (rejectMessage.getReasonCode()) { + case OBSOLETE: + case DUPLICATE: + case NONSTANDARD: + case CHECKPOINT: + case OTHER: + // We ignore those cases to avoid that not critical reject messages trigger a failed trade. + log.warn("We ignore that reject message as it is likely not critical."); + break; + case MALFORMED: + case INVALID: + case DUST: + case INSUFFICIENTFEE: + // We delay as we might get the rejected tx error before we have completed the create offer protocol + log.warn("We handle that reject message as it is likely critical."); + UserThread.runAfter(() -> { + String txId = newValue.getTxId(); + openOfferManager.getObservableList().stream() + .filter(openOffer -> txId.equals(openOffer.getOffer().getOfferFeePaymentTxId())) + .forEach(openOffer -> { + // We delay to avoid concurrent modification exceptions + UserThread.runAfter(() -> { + openOffer.getOffer().setErrorMessage(newValue.getMessage()); + if (rejectedTxErrorMessageHandler != null) { + rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.openOffer.makerFeeTxRejected", openOffer.getId(), txId)); + } + openOfferManager.removeOpenOffer(openOffer, () -> { + log.warn("We removed an open offer because the maker fee was rejected by the Bitcoin " + + "network. OfferId={}, txId={}", openOffer.getShortId(), txId); + }, log::warn); + }, 1); + }); + + tradeManager.getObservableList().stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> { + String details = null; + if (txId.equals(trade.getDepositTxId())) { + details = Res.get("popup.warning.trade.txRejected.deposit"); + } + if (txId.equals(trade.getOffer().getOfferFeePaymentTxId()) || txId.equals(trade.getTakerFeeTxId())) { + details = Res.get("popup.warning.trade.txRejected.tradeFee"); + } + + if (details != null) { + // We delay to avoid concurrent modification exceptions + String finalDetails = details; + UserThread.runAfter(() -> { + trade.setErrorMessage(newValue.getMessage()); + tradeManager.requestPersistence(); + if (rejectedTxErrorMessageHandler != null) { + rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.trade.txRejected", + finalDetails, trade.getShortId(), txId)); + } + }, 1); + } + }); + }, 3); + } + }); + } + + private String getBtcNetworkAsString() { + String postFix; + if (config.ignoreLocalBtcNode) + postFix = " " + Res.get("mainView.footer.localhostBitcoinNode"); + else if (preferences.getUseTorForBitcoinJ()) + postFix = " " + Res.get("mainView.footer.usingTor"); + else + postFix = ""; + return Res.get(config.baseCurrencyNetwork.name()) + postFix; + } +} diff --git a/core/src/main/java/bisq/core/app/misc/AppSetup.java b/core/src/main/java/bisq/core/app/misc/AppSetup.java new file mode 100644 index 0000000000..88e8b12f15 --- /dev/null +++ b/core/src/main/java/bisq/core/app/misc/AppSetup.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.app.misc; + +import bisq.common.app.Version; +import bisq.common.config.Config; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AppSetup { + protected final Config config; + + @Inject + public AppSetup(Config config) { + // we need to reference it so the seed node stores tradeStatistics + this.config = config; + + Version.setBaseCryptoNetworkId(this.config.baseCurrencyNetwork.ordinal()); + Version.printVersion(); + } + + public void start() { + initPersistedDataHosts(); + initBasicServices(); + } + + abstract void initPersistedDataHosts(); + + abstract void initBasicServices(); +} diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java new file mode 100644 index 0000000000..932cb5ec22 --- /dev/null +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java @@ -0,0 +1,197 @@ +/* + * 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 bisq.core.app.misc; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.P2PServiceListener; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.ConnectionListener; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import javax.inject.Inject; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import java.util.ArrayList; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AppSetupWithP2P extends AppSetup { + protected final P2PService p2PService; + protected final AccountAgeWitnessService accountAgeWitnessService; + private final SignedWitnessService signedWitnessService; + protected final FilterManager filterManager; + private final P2PDataStorage p2PDataStorage; + private final PeerManager peerManager; + protected final TradeStatisticsManager tradeStatisticsManager; + protected ArrayList persistedDataHosts; + protected BooleanProperty p2pNetWorkReady; + + @Inject + public AppSetupWithP2P(P2PService p2PService, + P2PDataStorage p2PDataStorage, + PeerManager peerManager, + TradeStatisticsManager tradeStatisticsManager, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService, + FilterManager filterManager, + Config config) { + super(config); + this.p2PService = p2PService; + this.p2PDataStorage = p2PDataStorage; + this.peerManager = peerManager; + this.tradeStatisticsManager = tradeStatisticsManager; + this.accountAgeWitnessService = accountAgeWitnessService; + this.signedWitnessService = signedWitnessService; + this.filterManager = filterManager; + this.persistedDataHosts = new ArrayList<>(); + } + + @Override + public void initPersistedDataHosts() { + persistedDataHosts.add(p2PDataStorage); + persistedDataHosts.add(peerManager); + + // we apply at startup the reading of persisted data but don't want to get it triggered in the constructor + persistedDataHosts.forEach(e -> { + try { + e.readPersisted(() -> { + }); + } catch (Throwable e1) { + log.error("readPersisted error", e1); + } + }); + } + + @Override + protected void initBasicServices() { + String postFix = "_" + config.baseCurrencyNetwork.name(); + p2PDataStorage.readFromResources(postFix, this::startInitP2PNetwork); + } + + private void startInitP2PNetwork() { + p2pNetWorkReady = initP2PNetwork(); + p2pNetWorkReady.addListener((observable, oldValue, newValue) -> { + if (newValue) + onBasicServicesInitialized(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Initialisation + /////////////////////////////////////////////////////////////////////////////////////////// + + private BooleanProperty initP2PNetwork() { + log.info("initP2PNetwork"); + p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + // We only check at seed nodes as they are running the latest version + // Other disconnects might be caused by peers running an older version + if (connection.getConnectionState().isSeedNode() && + closeConnectionReason == CloseConnectionReason.RULE_VIOLATION) { + log.warn("RULE_VIOLATION onDisconnect closeConnectionReason={}. connection={}", + closeConnectionReason, connection); + } + } + + @Override + public void onError(Throwable throwable) { + } + }); + + final BooleanProperty p2pNetworkInitialized = new SimpleBooleanProperty(); + p2PService.start(new P2PServiceListener() { + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + log.info("onHiddenServicePublished"); + } + + @Override + public void onDataReceived() { + log.info("onRequestingDataCompleted"); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoSeedNodeAvailable() { + log.info("onNoSeedNodeAvailable"); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoPeersAvailable() { + log.info("onNoPeersAvailable"); + p2pNetworkInitialized.set(true); + } + + @Override + public void onUpdatedDataReceived() { + log.info("onUpdatedDataReceived"); + } + + @Override + public void onSetupFailed(Throwable throwable) { + log.error(throwable.toString()); + } + + @Override + public void onRequestCustomBridges() { + + } + }); + + return p2pNetworkInitialized; + } + + protected void onBasicServicesInitialized() { + log.info("onBasicServicesInitialized"); + PersistenceManager.onAllServicesInitialized(); + + p2PService.onAllServicesInitialized(); + + tradeStatisticsManager.onAllServicesInitialized(); + + accountAgeWitnessService.onAllServicesInitialized(); + signedWitnessService.onAllServicesInitialized(); + + filterManager.onAllServicesInitialized(); + } +} diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java new file mode 100644 index 0000000000..95751832c9 --- /dev/null +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.app.misc; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.dao.DaoSetup; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.bond.reputation.MyReputationListService; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proposal.MyProposalListService; +import bisq.core.filter.FilterManager; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.config.Config; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AppSetupWithP2PAndDAO extends AppSetupWithP2P { + private final DaoSetup daoSetup; + + @Inject + public AppSetupWithP2PAndDAO(P2PService p2PService, + P2PDataStorage p2PDataStorage, + PeerManager peerManager, + TradeStatisticsManager tradeStatisticsManager, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService, + FilterManager filterManager, + DaoSetup daoSetup, + MyVoteListService myVoteListService, + BallotListService ballotListService, + MyBlindVoteListService myBlindVoteListService, + MyProposalListService myProposalListService, + MyReputationListService myReputationListService, + MyProofOfBurnListService myProofOfBurnListService, + Config config) { + super(p2PService, + p2PDataStorage, + peerManager, + tradeStatisticsManager, + accountAgeWitnessService, + signedWitnessService, + filterManager, + config); + + this.daoSetup = daoSetup; + + // TODO Should be refactored/removed. In the meantime keep in sync with CorePersistedDataHost + if (config.daoActivated) { + persistedDataHosts.add(myVoteListService); + persistedDataHosts.add(ballotListService); + persistedDataHosts.add(myBlindVoteListService); + persistedDataHosts.add(myProposalListService); + persistedDataHosts.add(myReputationListService); + persistedDataHosts.add(myProofOfBurnListService); + } + } + + @Override + protected void onBasicServicesInitialized() { + super.onBasicServicesInitialized(); + + daoSetup.onAllServicesInitialized(log::error, log::warn); + } +} diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java new file mode 100644 index 0000000000..ccca2a6a0c --- /dev/null +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -0,0 +1,246 @@ +/* + * 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 bisq.core.app.misc; + +import bisq.core.app.BisqExecutable; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoSetup; +import bisq.core.dao.node.full.RpcService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.file.JsonFileManager; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.setup.GracefulShutDownHandler; +import bisq.common.util.Profiler; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class ExecutableForAppWithP2p extends BisqExecutable { + private static final long CHECK_MEMORY_PERIOD_SEC = 300; + private static final long CHECK_SHUTDOWN_SEC = TimeUnit.HOURS.toSeconds(1); + private static final long SHUTDOWN_INTERVAL = TimeUnit.HOURS.toMillis(24); + private volatile boolean stopped; + private final long startTime = System.currentTimeMillis(); + + public ExecutableForAppWithP2p(String fullName, String scriptName, String appName, String version) { + super(fullName, scriptName, appName, version); + } + + @Override + protected void configUserThread() { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(this.getClass().getSimpleName()) + .setDaemon(true) + .build(); + UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } + + // We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules + @Override + public void gracefulShutDown(ResultHandler resultHandler) { + log.info("gracefulShutDown"); + try { + if (injector != null) { + JsonFileManager.shutDownAllInstances(); + injector.getInstance(RpcService.class).shutDown(); + injector.getInstance(DaoSetup.class).shutDown(); + injector.getInstance(ArbitratorManager.class).shutDown(); + injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> { + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { + module.close(injector); + + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + resultHandler.handleResult(); + log.info("Graceful shutdown completed. Exiting now."); + UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1); + }); + }); + injector.getInstance(WalletsSetup.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(BsqWalletService.class).shutDown(); + })); + // we wait max 5 sec. + UserThread.runAfter(() -> { + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + resultHandler.handleResult(); + log.info("Graceful shutdown caused a timeout. Exiting now."); + UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1); + }); + }, 5); + } else { + UserThread.runAfter(() -> { + resultHandler.handleResult(); + System.exit(BisqExecutable.EXIT_SUCCESS); + }, 1); + } + } catch (Throwable t) { + log.debug("App shutdown failed with exception"); + t.printStackTrace(); + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + resultHandler.handleResult(); + log.info("Graceful shutdown resulted in an error. Exiting now."); + UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_FAILURE), 1); + }); + + } + } + + public void startShutDownInterval(GracefulShutDownHandler gracefulShutDownHandler) { + if (DevEnv.isDevMode() || injector.getInstance(Config.class).useLocalhostForP2P) { + return; + } + + List seedNodeAddresses = new ArrayList<>(injector.getInstance(SeedNodeRepository.class).getSeedNodeAddresses()); + seedNodeAddresses.sort(Comparator.comparing(NodeAddress::getFullAddress)); + + NodeAddress myAddress = injector.getInstance(P2PService.class).getNetworkNode().getNodeAddress(); + int myIndex = -1; + for (int i = 0; i < seedNodeAddresses.size(); i++) { + if (seedNodeAddresses.get(i).equals(myAddress)) { + myIndex = i; + break; + } + } + + if (myIndex == -1) { + log.warn("We did not find our node address in the seed nodes repository. " + + "We use a 24 hour delay after startup as shut down strategy." + + "myAddress={}, seedNodeAddresses={}", + myAddress, seedNodeAddresses); + + UserThread.runPeriodically(() -> { + if (System.currentTimeMillis() - startTime > SHUTDOWN_INTERVAL) { + log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + + "Shut down as node was running longer as {} hours" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + SHUTDOWN_INTERVAL / 3600000); + + shutDown(gracefulShutDownHandler); + } + + }, CHECK_SHUTDOWN_SEC); + return; + } + + // We interpret the value of myIndex as hour of day (0-23). That way we avoid the risk of a restart of + // multiple nodes around the same time in case it would be not deterministic. + + // We wrap our periodic check in a delay of 2 hours to avoid that we get + // triggered multiple times after a restart while being in the same hour. It can be that we miss our target + // hour during that delay but that is not considered problematic, the seed would just restart a bit longer than + // 24 hours. + int target = myIndex; + UserThread.runAfter(() -> { + // We check every hour if we are in the target hour. + UserThread.runPeriodically(() -> { + int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).getHour(); + if (currentHour == target) { + log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + + "Shut down node at hour {} (UTC time is {})" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + target, + ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toString()); + shutDown(gracefulShutDownHandler); + } + }, TimeUnit.MINUTES.toSeconds(10)); + }, TimeUnit.HOURS.toSeconds(2)); + } + + @SuppressWarnings("InfiniteLoopStatement") + protected void keepRunning() { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException ignore) { + } + } + } + + protected void checkMemory(Config config, GracefulShutDownHandler gracefulShutDownHandler) { + int maxMemory = config.maxMemory; + UserThread.runPeriodically(() -> { + Profiler.printSystemLoad(); + if (!stopped) { + long usedMemoryInMB = Profiler.getUsedMemoryInMB(); + double warningTrigger = maxMemory * 0.8; + if (usedMemoryInMB > warningTrigger) { + log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + + "We are over 80% of our memory limit ({}) and call the GC. usedMemory: {} MB. freeMemory: {} MB" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + (int) warningTrigger, usedMemoryInMB, Profiler.getFreeMemoryInMB()); + System.gc(); + Profiler.printSystemLoad(); + } + + UserThread.runAfter(() -> { + log.info("Memory 2 sec. after calling the GC. usedMemory: {} MB. freeMemory: {} MB", + Profiler.getUsedMemoryInMB(), Profiler.getFreeMemoryInMB()); + }, 2); + + UserThread.runAfter(() -> { + long usedMemory = Profiler.getUsedMemoryInMB(); + if (usedMemory > maxMemory) { + log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + + "We are over our memory limit ({}) and trigger a shutdown. usedMemory: {} MB. freeMemory: {} MB" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + (int) maxMemory, usedMemory, Profiler.getFreeMemoryInMB()); + shutDown(gracefulShutDownHandler); + } + }, 5); + } + }, CHECK_MEMORY_PERIOD_SEC); + } + + protected void shutDown(GracefulShutDownHandler gracefulShutDownHandler) { + stopped = true; + gracefulShutDownHandler.gracefulShutDown(() -> { + log.info("Shutdown complete"); + System.exit(1); + }); + } +} diff --git a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java new file mode 100644 index 0000000000..879c29ab69 --- /dev/null +++ b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.app.misc; + +import bisq.core.alert.AlertModule; +import bisq.core.app.TorSetup; +import bisq.core.btc.BitcoinModule; +import bisq.core.dao.DaoModule; +import bisq.core.filter.FilterModule; +import bisq.core.network.CoreNetworkFilter; +import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; +import bisq.core.offer.OfferModule; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; +import bisq.core.trade.TradeModule; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.crypto.EncryptionServiceModule; +import bisq.network.p2p.P2PModule; +import bisq.network.p2p.network.BridgeAddressProvider; +import bisq.network.p2p.network.NetworkFilter; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.ClockWatcher; +import bisq.common.app.AppModule; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.KeyStorage; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.PubKeyRingProvider; +import bisq.common.proto.network.NetworkProtoResolver; +import bisq.common.proto.persistable.PersistenceProtoResolver; + +import com.google.inject.Singleton; + +import java.io.File; + +import static bisq.common.config.Config.*; +import static com.google.inject.name.Names.named; + +public class ModuleForAppWithP2p extends AppModule { + + public ModuleForAppWithP2p(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(Config.class).toInstance(config); + + bind(KeyStorage.class).in(Singleton.class); + bind(KeyRing.class).in(Singleton.class); + bind(User.class).in(Singleton.class); + bind(ClockWatcher.class).in(Singleton.class); + bind(NetworkProtoResolver.class).to(CoreNetworkProtoResolver.class).in(Singleton.class); + bind(PersistenceProtoResolver.class).to(CorePersistenceProtoResolver.class).in(Singleton.class); + bind(Preferences.class).in(Singleton.class); + bind(BridgeAddressProvider.class).to(Preferences.class).in(Singleton.class); + bind(TorSetup.class).in(Singleton.class); + + bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class).in(Singleton.class); + bind(NetworkFilter.class).to(CoreNetworkFilter.class).in(Singleton.class); + + bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); + bind(File.class).annotatedWith(named(KEY_STORAGE_DIR)).toInstance(config.keyStorageDir); + + bindConstant().annotatedWith(named(USE_DEV_PRIVILEGE_KEYS)).to(config.useDevPrivilegeKeys); + bindConstant().annotatedWith(named(USE_DEV_MODE)).to(config.useDevMode); + bindConstant().annotatedWith(named(USE_DEV_MODE_HEADER)).to(config.useDevModeHeader); + bindConstant().annotatedWith(named(REFERRAL_ID)).to(config.referralId); + bindConstant().annotatedWith(named(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE)).to(config.preventPeriodicShutdownAtSeedNode); + + // ordering is used for shut down sequence + install(new TradeModule(config)); + install(new EncryptionServiceModule(config)); + install(new OfferModule(config)); + install(new P2PModule(config)); + install(new BitcoinModule(config)); + install(new DaoModule(config)); + install(new AlertModule(config)); + install(new FilterModule(config)); + bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class); + } +} diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java new file mode 100644 index 0000000000..5d3ddd4e43 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -0,0 +1,131 @@ +/* + * 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 bisq.core.btc; + +import bisq.core.btc.listeners.BalanceListener; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; + +import bisq.common.UserThread; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import javafx.collections.ListChangeListener; + +import java.util.Objects; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Balances { + private final TradeManager tradeManager; + private final BtcWalletService btcWalletService; + private final OpenOfferManager openOfferManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; + private final RefundManager refundManager; + + @Getter + private final ObjectProperty availableBalance = new SimpleObjectProperty<>(); + @Getter + private final ObjectProperty reservedBalance = new SimpleObjectProperty<>(); + @Getter + private final ObjectProperty lockedBalance = new SimpleObjectProperty<>(); + + @Inject + public Balances(TradeManager tradeManager, + BtcWalletService btcWalletService, + OpenOfferManager openOfferManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, + RefundManager refundManager) { + this.tradeManager = tradeManager; + this.btcWalletService = btcWalletService; + this.openOfferManager = openOfferManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; + this.refundManager = refundManager; + } + + public void onAllServicesInitialized() { + openOfferManager.getObservableList().addListener((ListChangeListener) c -> updateBalance()); + tradeManager.getObservableList().addListener((ListChangeListener) change -> updateBalance()); + refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> updateBalance()); + btcWalletService.addBalanceListener(new BalanceListener() { + @Override + public void onBalanceChanged(Coin balance, Transaction tx) { + updateBalance(); + } + }); + + updateBalance(); + } + + private void updateBalance() { + // Need to delay a bit to get the balances correct + UserThread.execute(() -> { + updateAvailableBalance(); + updateReservedBalance(); + updateLockedBalance(); + }); + } + + private void updateAvailableBalance() { + long sum = btcWalletService.getAddressEntriesForAvailableBalanceStream() + .mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value) + .sum(); + availableBalance.set(Coin.valueOf(sum)); + } + + private void updateReservedBalance() { + long sum = openOfferManager.getObservableList().stream() + .map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE) + .orElse(null)) + .filter(Objects::nonNull) + .mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value) + .sum(); + reservedBalance.set(Coin.valueOf(sum)); + } + + private void updateLockedBalance() { + Stream lockedTrades = Stream.concat(closedTradableManager.getTradesStreamWithFundsLockedIn(), failedTradesManager.getTradesStreamWithFundsLockedIn()); + lockedTrades = Stream.concat(lockedTrades, tradeManager.getTradesStreamWithFundsLockedIn()); + long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG) + .orElse(null)) + .filter(Objects::nonNull) + .mapToLong(AddressEntry::getCoinLockedInMultiSig) + .sum(); + lockedBalance.set(Coin.valueOf(sum)); + } +} diff --git a/core/src/main/java/bisq/core/btc/BitcoinModule.java b/core/src/main/java/bisq/core/btc/BitcoinModule.java new file mode 100644 index 0000000000..3f733fb570 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/BitcoinModule.java @@ -0,0 +1,104 @@ +/* + * 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 bisq.core.btc; + +import bisq.core.btc.model.AddressEntryList; +import bisq.core.btc.nodes.BtcNodes; +import bisq.core.btc.setup.RegTestHost; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqCoinSelector; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.NonBsqCoinSelector; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.provider.ProvidersRepository; +import bisq.core.provider.fee.FeeProvider; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.PriceFeedService; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; + +import java.io.File; + +import java.util.Arrays; +import java.util.List; + +import static bisq.common.config.Config.PROVIDERS; +import static bisq.common.config.Config.WALLET_DIR; +import static com.google.inject.name.Names.named; + +public class BitcoinModule extends AppModule { + + public BitcoinModule(Config config) { + super(config); + } + + @Override + protected void configure() { + // If we have selected BTC_DAO_REGTEST or BTC_DAO_TESTNET we use our master regtest node, + // otherwise the specified host or default (localhost) + String regTestHost = config.bitcoinRegtestHost; + if (regTestHost.isEmpty()) { + regTestHost = config.baseCurrencyNetwork.isDaoTestNet() ? + "104.248.31.39" : + config.baseCurrencyNetwork.isDaoRegTest() ? + "134.209.242.206" : + Config.DEFAULT_REGTEST_HOST; + } + + RegTestHost.HOST = regTestHost; + if (Arrays.asList("localhost", "127.0.0.1").contains(regTestHost)) { + bind(RegTestHost.class).toInstance(RegTestHost.LOCALHOST); + } else if ("none".equals(regTestHost)) { + bind(RegTestHost.class).toInstance(RegTestHost.NONE); + } else { + bind(RegTestHost.class).toInstance(RegTestHost.REMOTE_HOST); + } + + bind(File.class).annotatedWith(named(WALLET_DIR)).toInstance(config.walletDir); + + bindConstant().annotatedWith(named(Config.BTC_NODES)).to(config.btcNodes); + bindConstant().annotatedWith(named(Config.USER_AGENT)).to(config.userAgent); + bindConstant().annotatedWith(named(Config.NUM_CONNECTIONS_FOR_BTC)).to(config.numConnectionsForBtc); + bindConstant().annotatedWith(named(Config.USE_ALL_PROVIDED_NODES)).to(config.useAllProvidedNodes); + bindConstant().annotatedWith(named(Config.IGNORE_LOCAL_BTC_NODE)).to(config.ignoreLocalBtcNode); + bindConstant().annotatedWith(named(Config.SOCKS5_DISCOVER_MODE)).to(config.socks5DiscoverMode); + bind(new TypeLiteral>(){}).annotatedWith(named(PROVIDERS)).toInstance(config.providers); + + bind(AddressEntryList.class).in(Singleton.class); + bind(WalletsSetup.class).in(Singleton.class); + bind(BtcWalletService.class).in(Singleton.class); + bind(BsqWalletService.class).in(Singleton.class); + bind(TradeWalletService.class).in(Singleton.class); + bind(BsqCoinSelector.class).in(Singleton.class); + bind(NonBsqCoinSelector.class).in(Singleton.class); + bind(BtcNodes.class).in(Singleton.class); + bind(Balances.class).in(Singleton.class); + + bind(ProvidersRepository.class).in(Singleton.class); + bind(FeeProvider.class).in(Singleton.class); + bind(PriceFeedService.class).in(Singleton.class); + bind(FeeService.class).in(Singleton.class); + bind(TxFeeEstimationService.class).in(Singleton.class); + } +} + diff --git a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java new file mode 100644 index 0000000000..ecc3652838 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java @@ -0,0 +1,212 @@ +/* + * 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 bisq.core.btc; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Util class for getting the estimated tx fee for maker or taker fee tx. + */ +@Slf4j +public class TxFeeEstimationService { + +// Size/vsize of typical trade txs +// Real txs size/vsize may vary in 1 or 2 bytes from the estimated values. +// Values calculated with https://gist.github.com/oscarguindzberg/3d1349cb65d9fd9af9de0feaa3fd27ac +// legacy fee tx with 1 input, maker/taker fee paid in btc size/vsize = 258 +// legacy deposit tx without change size/vsize = 381 +// legacy deposit tx with change size/vsize = 414 +// legacy payout tx size/vsize = 337 +// legacy delayed payout tx size/vsize = 302 +// segwit fee tx with 1 input, maker/taker fee paid in btc vsize = 173 +// segwit deposit tx without change vsize = 232 +// segwit deposit tx with change vsize = 263 +// segwit payout tx vsize = 169 +// segwit delayed payout tx vsize = 139 + public static int TYPICAL_TX_WITH_1_INPUT_VSIZE = 175; + private static int DEPOSIT_TX_VSIZE = 233; + + private static int BSQ_INPUT_INCREASE = 150; + private static int MAX_ITERATIONS = 10; + + private final FeeService feeService; + private final BtcWalletService btcWalletService; + private final Preferences preferences; + + @Inject + public TxFeeEstimationService(FeeService feeService, + BtcWalletService btcWalletService, + Preferences preferences) { + + this.feeService = feeService; + this.btcWalletService = btcWalletService; + this.preferences = preferences; + } + + public Tuple2 getEstimatedFeeAndTxVsizeForTaker(Coin fundsNeededForTrade, Coin tradeFee) { + return getEstimatedFeeAndTxVsize(true, + fundsNeededForTrade, + tradeFee, + feeService, + btcWalletService, + preferences); + } + + public Tuple2 getEstimatedFeeAndTxVsizeForMaker(Coin reservedFundsForOffer, + Coin tradeFee) { + return getEstimatedFeeAndTxVsize(false, + reservedFundsForOffer, + tradeFee, + feeService, + btcWalletService, + preferences); + } + + private Tuple2 getEstimatedFeeAndTxVsize(boolean isTaker, + Coin amount, + Coin tradeFee, + FeeService feeService, + BtcWalletService btcWalletService, + Preferences preferences) { + Coin txFeePerVbyte = feeService.getTxFeePerVbyte(); + // We start with min taker fee vsize of 175 + int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE; + try { + estimatedTxVsize = getEstimatedTxVsize(List.of(tradeFee, amount), estimatedTxVsize, txFeePerVbyte, btcWalletService); + } catch (InsufficientMoneyException e) { + if (isTaker) { + // If we cannot do the estimation, we use the vsize o the largest of our txs which is the deposit tx. + estimatedTxVsize = DEPOSIT_TX_VSIZE; + } + log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + + "if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize); + } + + if (!preferences.isPayFeeInBtc()) { + // If we pay the fee in BSQ we have one input more which adds about 150 bytes + // TODO: Clarify if there is always just one additional input or if there can be more. + estimatedTxVsize += BSQ_INPUT_INCREASE; + } + + Coin txFee; + int vsize; + if (isTaker) { + int averageVsize = (estimatedTxVsize + DEPOSIT_TX_VSIZE) / 2; // deposit tx has about 233 vbytes + // We use at least the vsize of the deposit tx to not underpay it. + vsize = Math.max(DEPOSIT_TX_VSIZE, averageVsize); + txFee = txFeePerVbyte.multiply(vsize); + log.info("Fee estimation resulted in a tx vsize of {} vbytes.\n" + + "We use an average between the taker fee tx and the deposit tx (233 vbytes) which results in {} vbytes.\n" + + "The deposit tx has 233 vbytes, we use that as our min value. Vsize for fee calculation is {} vbytes.\n" + + "The tx fee of {} Sat", estimatedTxVsize, averageVsize, vsize, txFee.value); + } else { + vsize = estimatedTxVsize; + txFee = txFeePerVbyte.multiply(vsize); + log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", vsize, txFee.value); + } + + return new Tuple2<>(txFee, vsize); + } + + public Tuple2 getEstimatedFeeAndTxVsize(Coin amount, + BtcWalletService btcWalletService) { + Coin txFeePerVbyte = btcWalletService.getTxFeeForWithdrawalPerVbyte(); + // We start with min taker fee vsize of 175 + int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE; + try { + estimatedTxVsize = getEstimatedTxVsize(List.of(amount), estimatedTxVsize, txFeePerVbyte, btcWalletService); + } catch (InsufficientMoneyException e) { + log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + + "if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize); + } + + Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize); + log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", estimatedTxVsize, txFee.value); + + return new Tuple2<>(txFee, estimatedTxVsize); + } + + // We start with the initialEstimatedTxVsize for a tx with 1 input (175) vbytes and get from BitcoinJ a tx back which + // contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on + // the assumption that we only need 1 input. Once we receive back the real tx vsize from the tx BitcoinJ has created + // with the required inputs we compare if the vsize is not more then 20% different to our assumed tx vsize. If we are inside + // that tolerance we use that tx vsize for our fee estimation, if not (if there has been more then 1 inputs) we + // apply the new fee based on the reported tx vsize and request again from BitcoinJ to fill that tx with the inputs + // to be sufficiently funded. The algorithm how BitcoinJ selects utxos is complex and contains several aspects + // (minimize fee, don't create too many tiny utxos,...). We treat that algorithm as an unknown and it is not + // guaranteed that there are more inputs required if we increase the fee (it could be that there is a better + // selection of inputs chosen if we have increased the fee and therefore less inputs and smaller tx vsize). As the increased fee might + // change the number of inputs we need to repeat that process until we are inside of a certain tolerance. To avoid + // potential endless loops we add a counter (we use 10, usually it takes just very few iterations). + // Worst case would be that the last vsize we got reported is > 20% off to + // the real tx vsize but as fee estimation is anyway a educated guess in the best case we don't worry too much. + // If we have underpaid the tx might take longer to get confirmed. + @VisibleForTesting + static int getEstimatedTxVsize(List outputValues, + int initialEstimatedTxVsize, + Coin txFeePerVbyte, + BtcWalletService btcWalletService) + throws InsufficientMoneyException { + boolean isInTolerance; + int estimatedTxVsize = initialEstimatedTxVsize; + int realTxVsize; + int counter = 0; + do { + Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize); + realTxVsize = btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee); + isInTolerance = isInTolerance(estimatedTxVsize, realTxVsize, 0.2); + if (!isInTolerance) { + estimatedTxVsize = realTxVsize; + } + counter++; + } + while (!isInTolerance && counter < MAX_ITERATIONS); + if (!isInTolerance) { + log.warn("We could not find a tx which satisfies our tolerance requirement of 20%. " + + "realTxVsize={}, estimatedTxVsize={}", + realTxVsize, estimatedTxVsize); + } + return estimatedTxVsize; + } + + @VisibleForTesting + static boolean isInTolerance(int estimatedVsize, int txVsize, double tolerance) { + checkArgument(estimatedVsize > 0, "estimatedVsize must be positive"); + checkArgument(txVsize > 0, "txVsize must be positive"); + checkArgument(tolerance > 0, "tolerance must be positive"); + double deviation = Math.abs(1 - ((double) estimatedVsize / (double) txVsize)); + return deviation <= tolerance; + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/AddressEntryException.java b/core/src/main/java/bisq/core/btc/exceptions/AddressEntryException.java new file mode 100644 index 0000000000..4dbb43eced --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/AddressEntryException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class AddressEntryException extends Exception { + public AddressEntryException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/BsqChangeBelowDustException.java b/core/src/main/java/bisq/core/btc/exceptions/BsqChangeBelowDustException.java new file mode 100644 index 0000000000..d0bf325151 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/BsqChangeBelowDustException.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.btc.exceptions; + +import org.bitcoinj.core.Coin; + +import lombok.Getter; + +public class BsqChangeBelowDustException extends Exception { + @Getter + private final Coin outputValue; + + public BsqChangeBelowDustException(String message, Coin outputValue) { + super(message); + + this.outputValue = outputValue; + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/InsufficientBsqException.java b/core/src/main/java/bisq/core/btc/exceptions/InsufficientBsqException.java new file mode 100644 index 0000000000..3b3749baf4 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/InsufficientBsqException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.btc.exceptions; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +public class InsufficientBsqException extends InsufficientMoneyException { + + public InsufficientBsqException(Coin missing) { + super(missing, "Insufficient BSQ, missing " + missing.value / 100D + " BSQ"); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/InsufficientFundsException.java b/core/src/main/java/bisq/core/btc/exceptions/InsufficientFundsException.java new file mode 100644 index 0000000000..d5a2af9c29 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/InsufficientFundsException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class InsufficientFundsException extends Exception { + public InsufficientFundsException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/InvalidHostException.java b/core/src/main/java/bisq/core/btc/exceptions/InvalidHostException.java new file mode 100644 index 0000000000..27009f9313 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/InvalidHostException.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class InvalidHostException extends IllegalArgumentException { + + public InvalidHostException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/RejectedTxException.java b/core/src/main/java/bisq/core/btc/exceptions/RejectedTxException.java new file mode 100644 index 0000000000..a45c62c9dc --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/RejectedTxException.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.btc.exceptions; + +import org.bitcoinj.core.RejectMessage; + +import lombok.Getter; + +import javax.annotation.Nullable; + +public class RejectedTxException extends RuntimeException { + @Getter + private final RejectMessage rejectMessage; + @Getter + @Nullable + private final String txId; + + public RejectedTxException(String message, RejectMessage rejectMessage) { + super(message); + this.rejectMessage = rejectMessage; + txId = rejectMessage.getRejectedObjectHash() != null ? rejectMessage.getRejectedObjectHash().toString() : null; + } + + @Override + public String toString() { + return "RejectedTxException{" + + "\n rejectMessage=" + rejectMessage + + ",\n txId='" + txId + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/SigningException.java b/core/src/main/java/bisq/core/btc/exceptions/SigningException.java new file mode 100644 index 0000000000..912bbd09a1 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/SigningException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class SigningException extends Exception { + public SigningException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/TransactionVerificationException.java b/core/src/main/java/bisq/core/btc/exceptions/TransactionVerificationException.java new file mode 100644 index 0000000000..d1121f8253 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/TransactionVerificationException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class TransactionVerificationException extends Exception { + public TransactionVerificationException(Throwable t) { + super(t); + } + + public TransactionVerificationException(String errorMessage) { + super(errorMessage); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastException.java b/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastException.java new file mode 100644 index 0000000000..e2eace17cc --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastException.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.btc.exceptions; + +import lombok.Getter; + +import javax.annotation.Nullable; + +/** + * Used in case the broadcasting of a tx did not succeed in the expected time. + * The broadcast can still succeed at a later moment though. + */ +public class TxBroadcastException extends Exception { + @Getter + @Nullable + private String txId; + + public TxBroadcastException(String message) { + super(message); + } + + public TxBroadcastException(String message, Throwable cause) { + super(message, cause); + } + + public TxBroadcastException(String message, String txId) { + super(message); + this.txId = txId; + } + + @Override + public String toString() { + return "TxBroadcastException{" + + "\n txId='" + txId + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastTimeoutException.java b/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastTimeoutException.java new file mode 100644 index 0000000000..1c97f63d82 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/TxBroadcastTimeoutException.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.core.btc.exceptions; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.wallet.Wallet; + +import lombok.Getter; + +import javax.annotation.Nullable; + + +public class TxBroadcastTimeoutException extends TxBroadcastException { + @Getter + @Nullable + private final Transaction localTx; + @Getter + private final int delay; + @Getter + private final Wallet wallet; + + /** + * @param localTx The tx we sent out + * @param delay The timeout delay + * @param wallet Wallet is needed if a client is calling wallet.commitTx(tx) + */ + public TxBroadcastTimeoutException(Transaction localTx, int delay, Wallet wallet) { + super("The transaction was not broadcasted in " + delay + + " seconds. txId=" + localTx.getTxId().toString()); + this.localTx = localTx; + this.delay = delay; + this.wallet = wallet; + } + + @Override + public String toString() { + return "TxBroadcastTimeoutException{" + + "\n localTx=" + localTx + + ",\n delay=" + delay + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/btc/exceptions/WalletException.java b/core/src/main/java/bisq/core/btc/exceptions/WalletException.java new file mode 100644 index 0000000000..4ec6db5f38 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/exceptions/WalletException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.btc.exceptions; + +public class WalletException extends Exception { + public WalletException(Throwable t) { + super(t); + } +} diff --git a/core/src/main/java/bisq/core/btc/listeners/AddressConfidenceListener.java b/core/src/main/java/bisq/core/btc/listeners/AddressConfidenceListener.java new file mode 100644 index 0000000000..ebbff874e8 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/listeners/AddressConfidenceListener.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.btc.listeners; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.TransactionConfidence; + +public class AddressConfidenceListener { + private final Address address; + + public AddressConfidenceListener(Address address) { + this.address = address; + } + + public Address getAddress() { + return address; + } + + @SuppressWarnings("UnusedParameters") + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + } +} diff --git a/core/src/main/java/bisq/core/btc/listeners/BalanceListener.java b/core/src/main/java/bisq/core/btc/listeners/BalanceListener.java new file mode 100644 index 0000000000..3257191d5c --- /dev/null +++ b/core/src/main/java/bisq/core/btc/listeners/BalanceListener.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.btc.listeners; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +public class BalanceListener { + private Address address; + + public BalanceListener() { + } + + public BalanceListener(Address address) { + this.address = address; + } + + public Address getAddress() { + return address; + } + + @SuppressWarnings("UnusedParameters") + public void onBalanceChanged(Coin balance, Transaction tx) { + } +} diff --git a/core/src/main/java/bisq/core/btc/listeners/BsqBalanceListener.java b/core/src/main/java/bisq/core/btc/listeners/BsqBalanceListener.java new file mode 100644 index 0000000000..7923db370c --- /dev/null +++ b/core/src/main/java/bisq/core/btc/listeners/BsqBalanceListener.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.core.btc.listeners; + +import org.bitcoinj.core.Coin; + +public interface BsqBalanceListener { + void onUpdateBalances(Coin availableConfirmedBalance, + Coin availableNonBsqBalance, + Coin unverifiedBalance, + Coin unconfirmedChangeBalance, + Coin lockedForVotingBalance, + Coin lockedInBondsBalance, + Coin unlockingBondsBalance); +} diff --git a/core/src/main/java/bisq/core/btc/listeners/TxConfidenceListener.java b/core/src/main/java/bisq/core/btc/listeners/TxConfidenceListener.java new file mode 100644 index 0000000000..6677555e81 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/listeners/TxConfidenceListener.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.btc.listeners; + +import org.bitcoinj.core.TransactionConfidence; + +public class TxConfidenceListener { + private final String txID; + + public TxConfidenceListener(String txID) { + this.txID = txID; + } + + public String getTxID() { + return txID; + } + + @SuppressWarnings("UnusedParameters") + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + } +} diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntry.java b/core/src/main/java/bisq/core/btc/model/AddressEntry.java new file mode 100644 index 0000000000..18bb07fcc2 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/AddressEntry.java @@ -0,0 +1,233 @@ +/* + * 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 bisq.core.btc.model; + +import bisq.common.config.Config; +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.script.Script; + +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Every trade uses a addressEntry with a dedicated address for all transactions related to the trade. + * That way we have a kind of separated trade wallet, isolated from other transactions and avoiding coin merge. + * If we would not avoid coin merge the user would lose privacy between trades. + */ +@EqualsAndHashCode +@Slf4j +public final class AddressEntry implements PersistablePayload { + public enum Context { + ARBITRATOR, + AVAILABLE, + OFFER_FUNDING, + RESERVED_FOR_TRADE, + MULTI_SIG, + TRADE_PAYOUT + } + + // keyPair can be null in case the object is created from deserialization as it is transient. + // It will be restored when the wallet is ready at setDeterministicKey + // So after startup it must never be null + + @Nullable + @Getter + private final String offerId; + @Getter + private final Context context; + @Getter + private final byte[] pubKey; + @Getter + private final byte[] pubKeyHash; + @Getter + private final long coinLockedInMultiSig; + @Getter + private final boolean segwit; + + // Not an immutable field. Set at startup once wallet is ready and at encrypting/decrypting wallet. + @Nullable + transient private DeterministicKey keyPair; + + // Only used as cache + @Nullable + transient private Address address; + // Only used as cache + @Nullable + transient private String addressString; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + public AddressEntry(DeterministicKey keyPair, Context context, boolean segwit) { + this(keyPair, context, null, segwit); + } + + public AddressEntry(DeterministicKey keyPair, + Context context, + @Nullable String offerId, + boolean segwit) { + this(keyPair, + context, + offerId, + 0, + segwit); + } + + public AddressEntry(DeterministicKey keyPair, + Context context, + @Nullable String offerId, + long coinLockedInMultiSig, + boolean segwit) { + this(keyPair.getPubKey(), + keyPair.getPubKeyHash(), + context, + offerId, + coinLockedInMultiSig, + segwit); + this.keyPair = keyPair; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AddressEntry(byte[] pubKey, + byte[] pubKeyHash, + Context context, + @Nullable String offerId, + long coinLockedInMultiSig, + boolean segwit) { + this.pubKey = pubKey; + this.pubKeyHash = pubKeyHash; + this.context = context; + this.offerId = offerId; + this.coinLockedInMultiSig = coinLockedInMultiSig; + this.segwit = segwit; + } + + public static AddressEntry fromProto(protobuf.AddressEntry proto) { + return new AddressEntry(proto.getPubKey().toByteArray(), + proto.getPubKeyHash().toByteArray(), + ProtoUtil.enumFromProto(AddressEntry.Context.class, proto.getContext().name()), + ProtoUtil.stringOrNullFromProto(proto.getOfferId()), + proto.getCoinLockedInMultiSig(), + proto.getSegwit()); + } + + @Override + public protobuf.AddressEntry toProtoMessage() { + protobuf.AddressEntry.Builder builder = protobuf.AddressEntry.newBuilder() + .setPubKey(ByteString.copyFrom(pubKey)) + .setPubKeyHash(ByteString.copyFrom(pubKeyHash)) + .setContext(protobuf.AddressEntry.Context.valueOf(context.name())) + .setCoinLockedInMultiSig(coinLockedInMultiSig) + .setSegwit(segwit); + Optional.ofNullable(offerId).ifPresent(builder::setOfferId); + return builder.build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // Set after wallet is ready + public void setDeterministicKey(DeterministicKey deterministicKey) { + this.keyPair = deterministicKey; + } + + // getKeyPair must not be called before wallet is ready (in case we get the object recreated from disk deserialization) + // If the object is created at runtime it must be always constructed after wallet is ready. + @NotNull + public DeterministicKey getKeyPair() { + checkNotNull(keyPair, "keyPair must not be null. If we got the addressEntry created from PB we need to have " + + "setDeterministicKey got called before any access with getKeyPair()."); + return keyPair; + } + + // For display we usually only display the first 8 characters. + @Nullable + public String getShortOfferId() { + return offerId != null ? Utilities.getShortId(offerId) : null; + } + + @Nullable + public String getAddressString() { + if (addressString == null && getAddress() != null) + addressString = getAddress().toString(); + return addressString; + } + + @Nullable + public Address getAddress() { + if (address == null && keyPair != null) { + address = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyPair, segwit ? Script.ScriptType.P2WPKH : Script.ScriptType.P2PKH); + } + if (address == null) { + log.warn("Address is null at getAddress(). keyPair={}", keyPair); + } + return address; + } + + public boolean isAddressNull() { + return address == null; + } + + public boolean isOpenOffer() { + return context == Context.OFFER_FUNDING || context == Context.RESERVED_FOR_TRADE; + } + + public boolean isTrade() { + return context == Context.MULTI_SIG || context == Context.TRADE_PAYOUT; + } + + public Coin getCoinLockedInMultiSigAsCoin() { + return Coin.valueOf(coinLockedInMultiSig); + } + + @Override + public String toString() { + return "AddressEntry{" + + "address=" + getAddress() + + ", context=" + context + + ", offerId='" + offerId + '\'' + + ", coinLockedInMultiSig=" + coinLockedInMultiSig + + ", segwit=" + segwit + + "}"; + } +} diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java new file mode 100644 index 0000000000..be9dd78fb4 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -0,0 +1,285 @@ +/* + * 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 bisq.core.btc.model; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistableEnvelope; +import bisq.common.proto.persistable.PersistedDataHost; + +import com.google.protobuf.Message; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.SegwitAddress; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.Wallet; + +import com.google.inject.Inject; + +import com.google.common.collect.ImmutableList; + +import org.apache.commons.lang3.tuple.Pair; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the + * associated protobuf message. + */ +@Slf4j +public final class AddressEntryList implements PersistableEnvelope, PersistedDataHost { + transient private PersistenceManager persistenceManager; + transient private Wallet wallet; + private final Set entrySet = new CopyOnWriteArraySet<>(); + + @Inject + public AddressEntryList(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(this, PersistenceManager.Source.PRIVATE); + } + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + entrySet.clear(); + entrySet.addAll(persisted.entrySet); + completeHandler.run(); + }, + completeHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AddressEntryList(Set entrySet) { + this.entrySet.addAll(entrySet); + } + + public static AddressEntryList fromProto(protobuf.AddressEntryList proto) { + Set entrySet = proto.getAddressEntryList().stream() + .map(AddressEntry::fromProto) + .collect(Collectors.toSet()); + return new AddressEntryList(entrySet); + } + + @Override + public Message toProtoMessage() { + Set addressEntries = entrySet.stream() + .map(AddressEntry::toProtoMessage) + .collect(Collectors.toSet()); + return protobuf.PersistableEnvelope.newBuilder() + .setAddressEntryList(protobuf.AddressEntryList.newBuilder() + .addAllAddressEntry(addressEntries)) + .build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onWalletReady(Wallet wallet) { + this.wallet = wallet; + + if (!entrySet.isEmpty()) { + Set toBeRemoved = new HashSet<>(); + entrySet.forEach(addressEntry -> { + Script.ScriptType scriptType = addressEntry.isSegwit() ? Script.ScriptType.P2WPKH + : Script.ScriptType.P2PKH; + DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash( + addressEntry.getPubKeyHash(), scriptType); + if (keyFromPubHash != null) { + Address addressFromKey = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash, + scriptType); + // We want to ensure key and address matches in case we have address in entry available already + if (addressEntry.isAddressNull() || addressFromKey.equals(addressEntry.getAddress())) { + addressEntry.setDeterministicKey(keyFromPubHash); + } else { + log.error("We found an address entry without key but cannot apply the key as the address " + + "is not matching. " + + "We remove that entry as it seems it is not compatible with our wallet. " + + "addressFromKey={}, addressEntry.getAddress()={}", + addressFromKey, addressEntry.getAddress()); + toBeRemoved.add(addressEntry); + } + } else { + log.error("Key from addressEntry {} not found in that wallet. We remove that entry. " + + "This is expected at restore from seeds.", addressEntry.toString()); + toBeRemoved.add(addressEntry); + } + }); + + toBeRemoved.forEach(entrySet::remove); + } else { + // As long the old arbitration domain is not removed from the code base we still support it here. + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); + entrySet.add(new AddressEntry(key, AddressEntry.Context.ARBITRATOR, false)); + } + + // In case we restore from seed words and have balance we need to add the relevant addresses to our list. + // IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to + // incoming txs at blockchain sync to add the rest. + if (wallet.getBalance().isPositive()) { + wallet.getIssuedReceiveAddresses().stream() + .filter(this::isAddressNotInEntries) + .forEach(address -> { + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address); + if (key != null) { + // Address will be derived from key in getAddress method + log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString()); + entrySet.add(new AddressEntry(key, AddressEntry.Context.AVAILABLE, address instanceof SegwitAddress)); + } else { + log.warn("DeterministicKey for address {} is null", address); + } + }); + } + + // We add those listeners to get notified about potential new transactions and + // add an address entry list in case it does not exist yet. This is mainly needed for restore from seed words + // but can help as well in case the addressEntry list would miss an address where the wallet was received + // funds (e.g. if the user sends funds to an address which has not been provided in the main UI - like from the + // wallet details window). + wallet.addCoinsReceivedEventListener((wallet1, tx, prevBalance, newBalance) -> { + maybeAddNewAddressEntry(tx); + }); + wallet.addCoinsSentEventListener((wallet1, tx, prevBalance, newBalance) -> { + maybeAddNewAddressEntry(tx); + }); + + requestPersistence(); + } + + public ImmutableList getAddressEntriesAsListImmutable() { + return ImmutableList.copyOf(entrySet); + } + + public void addAddressEntry(AddressEntry addressEntry) { + boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> { + if (addressEntry.getOfferId() != null) { + return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext(); + } + return false; + }); + if (entryWithSameOfferIdAndContextAlreadyExist) { + log.error("We have an address entry with the same offer ID and context. We do not add the new one. " + + "addressEntry={}, entrySet={}", addressEntry, entrySet); + return; + } + + log.info("addAddressEntry: add new AddressEntry {}", addressEntry); + boolean setChangedByAdd = entrySet.add(addressEntry); + if (setChangedByAdd) + requestPersistence(); + } + + public void swapToAvailable(AddressEntry addressEntry) { + if (addressEntry.getContext() == AddressEntry.Context.MULTI_SIG) { + log.error("swapToAvailable called with an addressEntry with MULTI_SIG context. " + + "This in not permitted as we must not reuse those address entries and there are " + + "no redeemable funds on those addresses. " + + "Only the keys are used for creating the Multisig address. " + + "addressEntry={}", addressEntry); + return; + } + + log.info("swapToAvailable addressEntry to swap={}", addressEntry); + boolean setChangedByRemove = entrySet.remove(addressEntry); + boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), + AddressEntry.Context.AVAILABLE, + addressEntry.isSegwit())); + if (setChangedByRemove || setChangedByAdd) { + requestPersistence(); + } + } + + public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry, + AddressEntry.Context context, + String offerId) { + boolean setChangedByRemove = entrySet.remove(addressEntry); + AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId, addressEntry.isSegwit()); + log.info("swapAvailableToAddressEntryWithOfferId newAddressEntry={}", newAddressEntry); + boolean setChangedByAdd = entrySet.add(newAddressEntry); + if (setChangedByRemove || setChangedByAdd) + requestPersistence(); + + return newAddressEntry; + } + + public void setCoinLockedInMultiSigAddressEntry(AddressEntry addressEntry, long value) { + if (addressEntry.getContext() != AddressEntry.Context.MULTI_SIG) { + log.error("setCoinLockedInMultiSigAddressEntry must be called only on MULTI_SIG entries"); + return; + } + + log.info("setCoinLockedInMultiSigAddressEntry addressEntry={}, value={}", addressEntry, value); + boolean setChangedByRemove = entrySet.remove(addressEntry); + AddressEntry entry = new AddressEntry(addressEntry.getKeyPair(), + addressEntry.getContext(), + addressEntry.getOfferId(), + value, + addressEntry.isSegwit()); + boolean setChangedByAdd = entrySet.add(entry); + if (setChangedByRemove || setChangedByAdd) { + requestPersistence(); + } + } + + public void requestPersistence() { + persistenceManager.requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeAddNewAddressEntry(Transaction tx) { + tx.getOutputs().stream() + .filter(output -> output.isMine(wallet)) + .map(output -> output.getScriptPubKey().getToAddress(wallet.getNetworkParameters())) + .filter(Objects::nonNull) + .filter(this::isAddressNotInEntries) + .map(address -> Pair.of(address, (DeterministicKey) wallet.findKeyFromAddress(address))) + .filter(pair -> pair.getRight() != null) + .map(pair -> new AddressEntry(pair.getRight(), AddressEntry.Context.AVAILABLE, + pair.getLeft() instanceof SegwitAddress)) + .forEach(this::addAddressEntry); + } + + private boolean isAddressNotInEntries(Address address) { + return entrySet.stream().noneMatch(e -> address.equals(e.getAddress())); + } + + @Override + public String toString() { + return "AddressEntryList{" + + ",\n entrySet=" + entrySet + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java new file mode 100644 index 0000000000..4ee77f24af --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java @@ -0,0 +1,70 @@ +package bisq.core.btc.model; + +import bisq.core.dao.state.model.blockchain.TxType; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import lombok.Getter; + +@Getter +public final class BsqTransferModel { + + private final LegacyAddress receiverAddress; + private final Coin receiverAmount; + private final Transaction preparedSendTx; + private final Transaction txWithBtcFee; + private final Transaction signedTx; + private final Coin miningFee; + private final int txSize; + private final TxType txType; + + public BsqTransferModel(LegacyAddress receiverAddress, + Coin receiverAmount, + Transaction preparedSendTx, + Transaction txWithBtcFee, + Transaction signedTx) { + this.receiverAddress = receiverAddress; + this.receiverAmount = receiverAmount; + this.preparedSendTx = preparedSendTx; + this.txWithBtcFee = txWithBtcFee; + this.signedTx = signedTx; + this.miningFee = signedTx.getFee(); + this.txSize = signedTx.bitcoinSerialize().length; + this.txType = TxType.TRANSFER_BSQ; + } + + public String getReceiverAddressAsString() { + return receiverAddress.toString(); + } + + public double getTxSizeInKb() { + return txSize / 1000d; + } + + public String toShortString() { + return "{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", txWithBtcFee.txId=" + txWithBtcFee.getTxId() + "\n" + + ", miningFee=" + miningFee + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + '}'; + } + + @Override + public String toString() { + return "BsqTransferModel{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", preparedSendTx=" + preparedSendTx + "\n" + + ", txWithBtcFee=" + txWithBtcFee + "\n" + + ", signedTx=" + signedTx + "\n" + + ", miningFee=" + miningFee + "\n" + + ", txSize=" + txSize + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + ", txType=" + txType + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/btc/model/InputsAndChangeOutput.java b/core/src/main/java/bisq/core/btc/model/InputsAndChangeOutput.java new file mode 100644 index 0000000000..aad74bd838 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/InputsAndChangeOutput.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.btc.model; + +import java.util.List; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +public class InputsAndChangeOutput { + public final List rawTransactionInputs; + + // Is set to 0L in case we don't have an output + public final long changeOutputValue; + @Nullable + public final String changeOutputAddress; + + public InputsAndChangeOutput(List rawTransactionInputs, long changeOutputValue, @Nullable String changeOutputAddress) { + checkArgument(!rawTransactionInputs.isEmpty(), "rawInputs.isEmpty()"); + + this.rawTransactionInputs = rawTransactionInputs; + this.changeOutputValue = changeOutputValue; + this.changeOutputAddress = changeOutputAddress; + } +} diff --git a/core/src/main/java/bisq/core/btc/model/PreparedDepositTxAndMakerInputs.java b/core/src/main/java/bisq/core/btc/model/PreparedDepositTxAndMakerInputs.java new file mode 100644 index 0000000000..6fcf8bfe7a --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/PreparedDepositTxAndMakerInputs.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.core.btc.model; + +import java.util.ArrayList; + +public class PreparedDepositTxAndMakerInputs { + public final ArrayList rawMakerInputs; + public final byte[] depositTransaction; + + public PreparedDepositTxAndMakerInputs(ArrayList rawMakerInputs, byte[] depositTransaction) { + this.rawMakerInputs = rawMakerInputs; + this.depositTransaction = depositTransaction; + } +} diff --git a/core/src/main/java/bisq/core/btc/model/RawTransactionInput.java b/core/src/main/java/bisq/core/btc/model/RawTransactionInput.java new file mode 100644 index 0000000000..3a59ab07f9 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/RawTransactionInput.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.core.btc.model; + +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; + +import javax.annotation.concurrent.Immutable; + +@EqualsAndHashCode +@Immutable +public final class RawTransactionInput implements NetworkPayload, PersistablePayload { + public final long index; // Index of spending txo + public final byte[] parentTransaction; // Spending tx (fromTx) + public final long value; + + /** + * Holds the relevant data for the connected output for a tx input. + * @param index the index of the parentTransaction + * @param parentTransaction the spending output tx, not the parent tx of the input + * @param value the number of satoshis being spent + */ + public RawTransactionInput(long index, byte[] parentTransaction, long value) { + this.index = index; + this.parentTransaction = parentTransaction; + this.value = value; + } + + @Override + public protobuf.RawTransactionInput toProtoMessage() { + return protobuf.RawTransactionInput.newBuilder() + .setIndex(index) + .setParentTransaction(ByteString.copyFrom(parentTransaction)) + .setValue(value) + .build(); + } + + public static RawTransactionInput fromProto(protobuf.RawTransactionInput proto) { + return new RawTransactionInput(proto.getIndex(), proto.getParentTransaction().toByteArray(), proto.getValue()); + } + + @Override + public String toString() { + return "RawTransactionInput{" + + "index=" + index + + ", parentTransaction as HEX " + Utilities.bytesAsHexString(parentTransaction) + + ", value=" + value + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNetworkConfig.java b/core/src/main/java/bisq/core/btc/nodes/BtcNetworkConfig.java new file mode 100644 index 0000000000..c202558cea --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNetworkConfig.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.setup.WalletConfig; + +import bisq.network.Socks5MultiDiscovery; + +import bisq.common.config.Config; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.params.MainNetParams; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +public class BtcNetworkConfig { + private static final Logger log = LoggerFactory.getLogger(BtcNetworkConfig.class); + + @Nullable + private final Socks5Proxy proxy; + private final WalletConfig delegate; + private final NetworkParameters parameters; + private final int socks5DiscoverMode; + + public BtcNetworkConfig(WalletConfig delegate, NetworkParameters parameters, int socks5DiscoverMode, + @Nullable Socks5Proxy proxy) { + this.delegate = delegate; + this.parameters = parameters; + this.socks5DiscoverMode = socks5DiscoverMode; + this.proxy = proxy; + } + + public void proposePeers(List peers) { + if (!peers.isEmpty()) { + log.info("You connect with peerAddresses: {}", peers); + PeerAddress[] peerAddresses = peers.toArray(new PeerAddress[peers.size()]); + delegate.setPeerNodes(peerAddresses); + } else if (proxy != null) { + if (log.isWarnEnabled()) { + MainNetParams mainNetParams = MainNetParams.get(); + if (parameters.equals(mainNetParams)) { + log.warn("You use the public Bitcoin network and are exposed to privacy issues " + + "caused by the broken bloom filters. See https://bisq.network/blog/privacy-in-bitsquare/ " + + "for more info. It is recommended to use the provided nodes."); + } + } + // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time. + delegate.setDiscovery(new Socks5MultiDiscovery(proxy, parameters, socks5DiscoverMode)); + } else if (Config.baseCurrencyNetwork().isMainnet()) { + log.warn("You don't use tor and use the public Bitcoin network and are exposed to privacy issues " + + "caused by the broken bloom filters. See https://bisq.network/blog/privacy-in-bitsquare/ " + + "for more info. It is recommended to use Tor and the provided nodes."); + } + } +} diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodeConverter.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodeConverter.java new file mode 100644 index 0000000000..ce94e16d34 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodeConverter.java @@ -0,0 +1,128 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.nodes.BtcNodes.BtcNode; + +import bisq.network.DnsLookupException; +import bisq.network.DnsLookupTor; + +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.net.OnionCatConverter; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +class BtcNodeConverter { + private static final Logger log = LoggerFactory.getLogger(BtcNodeConverter.class); + + private final Facade facade; + + BtcNodeConverter() { + this.facade = new Facade(); + } + + BtcNodeConverter(Facade facade) { + this.facade = facade; + } + + @Nullable + PeerAddress convertOnionHost(BtcNode node) { + // no DNS lookup for onion addresses + String onionAddress = Objects.requireNonNull(node.getOnionAddress()); + return new PeerAddress(onionAddress, node.getPort()); + } + + @Nullable + PeerAddress convertClearNode(BtcNode node) { + int port = node.getPort(); + + PeerAddress result = create(node.getHostNameOrAddress(), port); + if (result == null) { + String address = node.getAddress(); + if (address != null) { + result = create(address, port); + } else { + log.warn("Lookup failed, no address for node {}", node); + } + } + return result; + } + + @Nullable + PeerAddress convertWithTor(BtcNode node, Socks5Proxy proxy) { + int port = node.getPort(); + + PeerAddress result = create(proxy, node.getHostNameOrAddress(), port); + if (result == null) { + String address = node.getAddress(); + if (address != null) { + result = create(proxy, address, port); + } else { + log.warn("Lookup failed, no address for node {}", node); + } + } + return result; + } + + @Nullable + private PeerAddress create(Socks5Proxy proxy, String host, int port) { + try { + // We use DnsLookupTor to not leak with DNS lookup + // Blocking call. takes about 600 ms ;-( + InetAddress lookupAddress = facade.torLookup(proxy, host); + InetSocketAddress address = new InetSocketAddress(lookupAddress, port); + return new PeerAddress(address); + } catch (Exception e) { + log.error("Failed to create peer address", e); + return null; + } + } + + @Nullable + private static PeerAddress create(String hostName, int port) { + try { + InetSocketAddress address = new InetSocketAddress(hostName, port); + return new PeerAddress(address); + } catch (Exception e) { + log.error("Failed to create peer address", e); + return null; + } + } + + static class Facade { + InetAddress onionHostToInetAddress(String onionAddress) throws UnknownHostException { + return OnionCatConverter.onionHostToInetAddress(onionAddress); + } + + InetAddress torLookup(Socks5Proxy proxy, String host) throws DnsLookupException { + return DnsLookupTor.lookup(proxy, host); + } + } +} + diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java new file mode 100644 index 0000000000..e2ff3702eb --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java @@ -0,0 +1,182 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.common.config.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +// Managed here: https://github.com/bisq-network/roles/issues/39 +@Slf4j +public class BtcNodes { + + public enum BitcoinNodesOption { + PROVIDED, + CUSTOM, + PUBLIC + } + + // For other base currencies or testnet we ignore provided nodes + public List getProvidedBtcNodes() { + return useProvidedBtcNodes() ? + Arrays.asList( + // emzy + new BtcNode("kirsche.emzy.de", "fz6nsij6jiyuwlsc.onion", "78.47.61.83", BtcNode.DEFAULT_PORT, "@emzy"), + new BtcNode("node2.emzy.de", "c6ac4jdfyeiakex2.onion", "62.171.129.32", BtcNode.DEFAULT_PORT, "@emzy"), + new BtcNode("node1.emzy.de", "sjyzmwwu6diiit3r.onion", "167.86.90.239", BtcNode.DEFAULT_PORT, "@emzy"), + new BtcNode(null, "3xucqntxp5ddoaz5.onion", null, BtcNode.DEFAULT_PORT, "@emzy"), // cannot provide IP because no static IP + + // ripcurlx + new BtcNode("bitcoin.christophatteneder.com", "lgkvbvro67jomosw.onion", "174.138.35.229", BtcNode.DEFAULT_PORT, "@Christoph"), + + // mrosseel + new BtcNode("btc.vante.me", "4jyh6llqj264oggs.onion", "94.23.21.80", BtcNode.DEFAULT_PORT, "@miker"), + new BtcNode("btc2.vante.me", "mxdtrjhe2yfsx3pg.onion", "94.23.205.110", BtcNode.DEFAULT_PORT, "@miker"), + + // sqrrm + new BtcNode("btc1.sqrrm.net", "3r44ddzjitznyahw.onion", "185.25.48.184", BtcNode.DEFAULT_PORT, "@sqrrm"), + new BtcNode("btc2.sqrrm.net", "i3a5xtzfm4xwtybd.onion", "81.171.22.143", BtcNode.DEFAULT_PORT, "@sqrrm"), + + // KanoczTomas + new BtcNode("btc.ispol.sk", "mbm6ffx6j5ygi2ck.onion", "193.58.196.212", BtcNode.DEFAULT_PORT, "@KanoczTomas"), + + // Devin Bileck + new BtcNode("btc1.bisq.services", "lva54pnbq2nsmjyr.onion", "172.105.21.216", BtcNode.DEFAULT_PORT, "@devinbileck"), + new BtcNode("btc2.bisq.services", "qxjrxmhyqp5vy5hj.onion", "173.255.240.205", BtcNode.DEFAULT_PORT, "@devinbileck"), + new BtcNode(null, "wubwzaadboxwiffa.onion", null, BtcNode.DEFAULT_PORT, "@devinbileck"), + + // m52go + new BtcNode(null, "4nnuyxm5k5tlyjq3.onion", null, BtcNode.DEFAULT_PORT, "@m52go"), + + // wiz + new BtcNode("node100.hnl.wiz.biz", "m3yqzythryowgedc.onion", "103.99.168.100", BtcNode.DEFAULT_PORT, "@wiz"), + new BtcNode("node140.hnl.wiz.biz", "jiuuuislm7ooesic.onion", "103.99.168.140", BtcNode.DEFAULT_PORT, "@wiz"), + new BtcNode("node210.fmt.wiz.biz", "orsy2v63ecrmdj55.onion", "103.99.170.210", BtcNode.DEFAULT_PORT, "@wiz"), + new BtcNode("node220.fmt.wiz.biz", "z6mbqq7llxlrn4kq.onion", "103.99.170.220", BtcNode.DEFAULT_PORT, "@wiz"), + + // Rob Kaandorp + new BtcNode(null, "2pj2o2mrawj7yotg.onion", null, BtcNode.DEFAULT_PORT, "@robkaandorp") // cannot provide IP because no static IP + ) : + new ArrayList<>(); + } + + public boolean useProvidedBtcNodes() { + return Config.baseCurrencyNetwork().isMainnet(); + } + + public static List toBtcNodesList(Collection nodes) { + return nodes.stream() + .filter(e -> !e.isEmpty()) + .map(BtcNodes.BtcNode::fromFullAddress) + .collect(Collectors.toList()); + } + + @EqualsAndHashCode + @Getter + public static class BtcNode { + private static final int DEFAULT_PORT = Config.baseCurrencyNetworkParameters().getPort(); //8333 + + @Nullable + private final String onionAddress; + @Nullable + private final String hostName; + @Nullable + private final String operator; // null in case the user provides a list of custom btc nodes + @Nullable + private final String address; // IPv4 address + private int port = DEFAULT_PORT; + + /** + * @param fullAddress [IPv4 address:port or onion:port] + * @return BtcNode instance + */ + public static BtcNode fromFullAddress(String fullAddress) { + String[] parts = fullAddress.split("]"); + checkArgument(parts.length > 0); + String host = ""; + int port = DEFAULT_PORT; + if (parts[0].contains("[") && parts[0].contains(":")) { + // IPv6 address and optional port number + // address part delimited by square brackets e.g. [2a01:123:456:789::2]:8333 + host = parts[0].replace("[", "").replace("]", ""); + if (parts.length == 2) + port = Integer.parseInt(parts[1].replace(":", "")); + } + else if (parts[0].contains(":") && !parts[0].contains(".")) { + // IPv6 address only; not delimited by square brackets + host = parts[0]; + } + else if (parts[0].contains(".")) { + // address and an optional port number + // e.g. 127.0.0.1:8333 or abcdef123xyz.onion:9999 + parts = fullAddress.split(":"); + checkArgument(parts.length > 0); + host = parts[0]; + if (parts.length == 2) + port = Integer.parseInt(parts[1]); + } + + checkArgument(host.length()>0, "BtcNode address format not recognised"); + return host.contains(".onion") ? new BtcNode(null, host, null, port, null) : new BtcNode(null, null, host, port, null); + } + + public BtcNode(@Nullable String hostName, @Nullable String onionAddress, @Nullable String address, int port, @Nullable String operator) { + this.hostName = hostName; + this.onionAddress = onionAddress; + this.address = address; + this.port = port; + this.operator = operator; + } + + public boolean hasOnionAddress() { + return onionAddress != null; + } + + public String getHostNameOrAddress() { + if (hostName != null) + return hostName; + else + return address; + } + + public boolean hasClearNetAddress() { + return hostName != null || address != null; + } + + @Override + public String toString() { + return "onionAddress='" + onionAddress + '\'' + + ", hostName='" + hostName + '\'' + + ", address='" + address + '\'' + + ", port='" + port + '\'' + + ", operator='" + operator; + } + } +} diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodesRepository.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodesRepository.java new file mode 100644 index 0000000000..4e0013b8fb --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodesRepository.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.core.btc.nodes; + +import org.bitcoinj.core.PeerAddress; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +public class BtcNodesRepository { + private final BtcNodeConverter converter; + private final List nodes; + + public BtcNodesRepository(List nodes) { + this(new BtcNodeConverter(), nodes); + } + + public BtcNodesRepository(BtcNodeConverter converter, List nodes) { + this.converter = converter; + this.nodes = nodes; + } + + public List getPeerAddresses(@Nullable Socks5Proxy proxy, boolean isUseClearNodesWithProxies) { + List result; + // We connect to onion nodes only in case we use Tor for BitcoinJ (default) to avoid privacy leaks at + // exit nodes with bloom filters. + if (proxy != null) { + List onionHosts = getOnionHosts(); + result = new ArrayList<>(onionHosts); + + if (isUseClearNodesWithProxies) { + // We also use the clear net nodes (used for monitor) + List torAddresses = getClearNodesBehindProxy(proxy); + result.addAll(torAddresses); + } + } else { + result = getClearNodes(); + } + return result; + } + + private List getClearNodes() { + return nodes.stream() + .filter(BtcNodes.BtcNode::hasClearNetAddress) + .flatMap(node -> nullableAsStream(converter.convertClearNode(node))) + .collect(Collectors.toList()); + } + + private List getOnionHosts() { + return nodes.stream() + .filter(BtcNodes.BtcNode::hasOnionAddress) + .flatMap(node -> nullableAsStream(converter.convertOnionHost(node))) + .collect(Collectors.toList()); + } + + private List getClearNodesBehindProxy(Socks5Proxy proxy) { + return nodes.stream() + .filter(BtcNodes.BtcNode::hasClearNetAddress) + .flatMap(node -> nullableAsStream(converter.convertWithTor(node, proxy))) + .collect(Collectors.toList()); + } + + private static Stream nullableAsStream(@Nullable T item) { + return Optional.ofNullable(item).stream(); + } +} diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodesSetupPreferences.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodesSetupPreferences.java new file mode 100644 index 0000000000..7f5bfe27b4 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodesSetupPreferences.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.user.Preferences; + +import bisq.common.config.Config; +import bisq.common.util.Utilities; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class BtcNodesSetupPreferences { + private static final Logger log = LoggerFactory.getLogger(BtcNodesSetupPreferences.class); + + private final Preferences preferences; + + public BtcNodesSetupPreferences(Preferences preferences) { + this.preferences = preferences; + } + + public List selectPreferredNodes(BtcNodes nodes) { + List result; + + BtcNodes.BitcoinNodesOption nodesOption = BtcNodes.BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()]; + switch (nodesOption) { + case CUSTOM: + String bitcoinNodes = preferences.getBitcoinNodes(); + Set distinctNodes = Utilities.commaSeparatedListToSet(bitcoinNodes, false); + result = BtcNodes.toBtcNodesList(distinctNodes); + if (result.isEmpty()) { + log.warn("Custom nodes is set but no valid nodes are provided. " + + "We fall back to provided nodes option."); + preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); + result = nodes.getProvidedBtcNodes(); + } + break; + case PUBLIC: + result = Collections.emptyList(); + break; + case PROVIDED: + default: + result = nodes.getProvidedBtcNodes(); + break; + } + + return result; + } + + public boolean isUseCustomNodes() { + return BtcNodes.BitcoinNodesOption.CUSTOM.ordinal() == preferences.getBitcoinNodesOptionOrdinal(); + } + + public int calculateMinBroadcastConnections(List nodes) { + BtcNodes.BitcoinNodesOption nodesOption = BtcNodes.BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()]; + int result; + switch (nodesOption) { + case CUSTOM: + // We have set the nodes already above + result = (int) Math.ceil(nodes.size() * 0.5); + // If Tor is set we usually only use onion nodes, + // but if user provides mixed clear net and onion nodes we want to use both + break; + case PUBLIC: + // We keep the empty nodes + result = (int) Math.floor(Config.DEFAULT_NUM_CONNECTIONS_FOR_BTC * 0.8); + break; + case PROVIDED: + default: + // We require only 4 nodes instead of 7 (for 9 max connections) because our provided nodes + // are more reliable than random public nodes. + result = 4; + break; + } + return result; + } + +} diff --git a/core/src/main/java/bisq/core/btc/nodes/LocalBitcoinNode.java b/core/src/main/java/bisq/core/btc/nodes/LocalBitcoinNode.java new file mode 100644 index 0000000000..358de89a12 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/LocalBitcoinNode.java @@ -0,0 +1,94 @@ +package bisq.core.btc.nodes; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Detects whether a Bitcoin node is running on localhost and contains logic for when to + * ignore it. The query methods lazily trigger the needed checks and cache the results. + * @see bisq.common.config.Config#ignoreLocalBtcNode + */ +@Singleton +public class LocalBitcoinNode { + + private static final Logger log = LoggerFactory.getLogger(LocalBitcoinNode.class); + private static final int CONNECTION_TIMEOUT = 5000; + + private final Config config; + private final int port; + + private Boolean detected; + + @Inject + public LocalBitcoinNode(Config config) { + this.config = config; + this.port = config.networkParameters.getPort(); + } + + /** + * Returns whether Bisq should use a local Bitcoin node, meaning that a node was + * detected and conditions under which it should be ignored have not been met. If + * the local node should be ignored, a call to this method will not trigger an + * unnecessary detection attempt. + */ + public boolean shouldBeUsed() { + return !shouldBeIgnored() && isDetected(); + } + + /** + * Returns whether Bisq should ignore a local Bitcoin node even if it is usable. + */ + public boolean shouldBeIgnored() { + BaseCurrencyNetwork baseCurrencyNetwork = config.baseCurrencyNetwork; + + // For dao testnet (server side regtest) we disable the use of local bitcoin node + // to avoid confusion if local btc node is not synced with our dao testnet master + // node. Note: above comment was previously in WalletConfig::createPeerGroup. + return config.ignoreLocalBtcNode || + baseCurrencyNetwork.isDaoRegTest() || + baseCurrencyNetwork.isDaoTestNet(); + } + + /** + * Returns whether a local Bitcoin node was detected. The check is triggered in case + * it has not been performed. No further monitoring is performed, so if the node + * goes up or down in the meantime, this method will continue to return its original + * value. See {@code MainViewModel#setupBtcNumPeersWatcher} to understand how + * disconnection and reconnection of the local Bitcoin node is actually handled. + */ + private boolean isDetected() { + if (detected == null) { + detected = detect(port); + } + return detected; + } + + /** + * Detect whether a Bitcoin node is running on localhost by attempting to connect + * to the node's port. + */ + private static boolean detect(int port) { + try (Socket socket = new Socket()) { + var address = new InetSocketAddress(InetAddress.getLoopbackAddress(), port); + socket.connect(address, CONNECTION_TIMEOUT); + log.info("Local Bitcoin node detected on port {}", port); + return true; + } catch (IOException ex) { + log.info("No local Bitcoin node detected on port {}.", port); + return false; + } + } + +} diff --git a/core/src/main/java/bisq/core/btc/nodes/ProxySocketFactory.java b/core/src/main/java/bisq/core/btc/nodes/ProxySocketFactory.java new file mode 100644 index 0000000000..97d05e9f4d --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/ProxySocketFactory.java @@ -0,0 +1,112 @@ +/* + * 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 . + */ + +/** + * Copyright (C) 2010-2014 Leon Blakey + *

    + * This file is part of PircBotX. + *

    + * PircBotX is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + *

    + * PircBotX 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 General Public License for more details. + *

    + * You should have received a copy of the GNU General Public License along with + * PircBotX. If not, see . + */ + +package bisq.core.btc.nodes; + +import javax.net.SocketFactory; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +import java.io.IOException; + +/** + * A basic SocketFactory for creating sockets that connect through the specified + * proxy. + * + * @author Leon Blakey + */ +public class ProxySocketFactory extends SocketFactory { + protected final Proxy proxy; + + /** + * Create all sockets with the specified proxy. + * + * @param proxy An existing proxy + */ + public ProxySocketFactory(Proxy proxy) { + this.proxy = proxy; + } + + /** + * A convenience constructor for creating a proxy with the specified host + * and port. + * + * @param proxyType The type of proxy were connecting to + * @param hostname The hostname of the proxy server + * @param port The port of the proxy server + */ + public ProxySocketFactory(Proxy.Type proxyType, String hostname, int port) { + this.proxy = new Proxy(proxyType, new InetSocketAddress(hostname, port)); + } + + @Override + public Socket createSocket() throws IOException { + Socket socket = new Socket(proxy); + return socket; + } + + @Override + public Socket createSocket(String string, int i) throws IOException { + Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(string, i)); + return socket; + } + + @Override + public Socket createSocket(String string, int i, InetAddress localAddress, int localPort) throws IOException { + Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(string, i)); + return socket; + } + + @Override + public Socket createSocket(InetAddress ia, int i) throws IOException { + Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(ia, i)); + return socket; + } + + @Override + public Socket createSocket(InetAddress ia, int i, InetAddress localAddress, int localPort) throws IOException { + Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(ia, i)); + return socket; + } +} diff --git a/core/src/main/java/bisq/core/btc/nodes/SeedPeersSocks5Dns.java b/core/src/main/java/bisq/core/btc/nodes/SeedPeersSocks5Dns.java new file mode 100644 index 0000000000..20f835de7b --- /dev/null +++ b/core/src/main/java/bisq/core/btc/nodes/SeedPeersSocks5Dns.java @@ -0,0 +1,192 @@ +/* + * 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 . + */ + +/** + * Copyright 2011 Micheal Swiggs + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package bisq.core.btc.nodes; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.net.discovery.PeerDiscovery; +import org.bitcoinj.net.discovery.PeerDiscoveryException; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import com.runjva.sourceforge.jsocks.protocol.SocksSocket; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +// TODO not used anymore. Not sure if it was replaced by something else or removed by accident. +/** + * SeedPeersSocks5Dns resolves peers via Proxy (Socks5) remote DNS. + */ +public class SeedPeersSocks5Dns implements PeerDiscovery { + private final Socks5Proxy proxy; + private final NetworkParameters params; + private final InetSocketAddress[] seedAddrs; + @SuppressWarnings("MismatchedReadAndWriteOfArray") + private InetSocketAddress[] seedAddrsIP; + private int pnseedIndex; + + private final InetSocketAddress[] seedAddrsResolved; + + private static final Logger log = LoggerFactory.getLogger(SeedPeersSocks5Dns.class); + + /** + * Supports finding peers by hostname over a socks5 proxy. + */ + public SeedPeersSocks5Dns(Socks5Proxy proxy, NetworkParameters params) { + + this.proxy = proxy; + this.params = params; + this.seedAddrs = convertAddrsString(params.getDnsSeeds(), params.getPort()); + + /* + // This is an example of how .onion servers could be used. Unfortunately there is presently no way + // to hand the onion address (or a connected socket) back to bitcoinj without it crashing in PeerAddress. + // note: the onion addresses should be added into bitcoinj NetworkParameters classes, eg for mainnet, testnet + // not here! + this.seedAddrs = new InetSocketAddress[]{InetSocketAddress.createUnresolved("cajrifqkvalh2ooa.onion", 8333), + InetSocketAddress.createUnresolved("bk7yp6epnmcllq72.onion", 8333) + }; + */ + + //TODO seedAddrsIP is never written; not used method... + seedAddrsResolved = new InetSocketAddress[seedAddrs.length]; + System.arraycopy(seedAddrsIP, seedAddrs.length, seedAddrsResolved, + seedAddrs.length, seedAddrsResolved.length - seedAddrs.length); + } + + /** + * Acts as an iterator, returning the address of each node in the list sequentially. + * Once all the list has been iterated, null will be returned for each subsequent query. + * + * @return InetSocketAddress - The address/port of the next node. + * @throws PeerDiscoveryException + */ + @Nullable + public InetSocketAddress getPeer() throws PeerDiscoveryException { + try { + return nextPeer(); + } catch (PeerDiscoveryException e) { + throw new PeerDiscoveryException(e); + } + } + + /** + * worker for getPeer() + */ + @Nullable + private InetSocketAddress nextPeer() throws PeerDiscoveryException { + if (seedAddrs == null || seedAddrs.length == 0) { + throw new PeerDiscoveryException("No IP address seeds configured; unable to find any peers"); + } + + if (pnseedIndex >= seedAddrsResolved.length) { + return null; + } + if (seedAddrsResolved[pnseedIndex] == null) { + seedAddrsResolved[pnseedIndex] = lookup(proxy, seedAddrs[pnseedIndex]); + } + log.error("SeedPeersSocks5Dns::nextPeer: " + seedAddrsResolved[pnseedIndex]); + + return seedAddrsResolved[pnseedIndex++]; + } + + /** + * Returns an array containing all the Bitcoin nodes within the list. + */ + @Override + public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { + if (services != 0) + throw new PeerDiscoveryException("DNS seeds cannot filter by services: " + services); + return allPeers(); + } + + /** + * returns all seed peers, performs hostname lookups if necessary. + */ + private InetSocketAddress[] allPeers() { + for (int i = 0; i < seedAddrsResolved.length; ++i) { + if (seedAddrsResolved[i] == null) { + seedAddrsResolved[i] = lookup(proxy, seedAddrs[i]); + } + } + return seedAddrsResolved; + } + + /** + * Resolves a hostname via remote DNS over socks5 proxy. + */ + @Nullable + public static InetSocketAddress lookup(Socks5Proxy proxy, InetSocketAddress addr) { + if (!addr.isUnresolved()) { + return addr; + } + try { + SocksSocket proxySocket = new SocksSocket(proxy, addr.getHostString(), addr.getPort()); + InetAddress addrResolved = proxySocket.getInetAddress(); + proxySocket.close(); + if (addrResolved != null) { + //log.debug("Resolved " + addr.getHostString() + " to " + addrResolved.getHostAddress()); + return new InetSocketAddress(addrResolved, addr.getPort()); + } else { + // note: .onion nodes fall in here when proxy is Tor. But they have no IP address. + // Unfortunately bitcoinj crashes in PeerAddress if it finds an unresolved address. + log.error("Connected to " + addr.getHostString() + ". But did not resolve to address."); + } + } catch (Exception e) { + log.warn("Error resolving " + addr.getHostString() + ". Exception:\n" + e.toString()); + } + return null; + } + + /** + * Converts an array of hostnames to array of unresolved InetSocketAddress + */ + private InetSocketAddress[] convertAddrsString(String[] addrs, int port) { + InetSocketAddress[] list = new InetSocketAddress[addrs.length]; + for (int i = 0; i < addrs.length; i++) { + list[i] = InetSocketAddress.createUnresolved(addrs[i], port); + } + return list; + } + + @Override + public void shutdown() { + } +} diff --git a/core/src/main/java/bisq/core/btc/setup/BisqKeyChainFactory.java b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainFactory.java new file mode 100644 index 0000000000..7c576a3c84 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainFactory.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.btc.setup; + +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.DefaultKeyChainFactory; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.KeyChainGroupStructure; +import org.bitcoinj.wallet.Protos; + +import com.google.common.collect.ImmutableList; + +/** + * Hack to convert bitcoinj 0.14 wallets to bitcoinj 0.15 format. + * + * This code is required to be executed only once per user (actually twice, for btc and bsq wallets). + * Once all users using bitcoinj 0.14 wallets have executed this code, this class will be no longer needed. + * + * Since that is almost impossible to guarantee, this hack will stay until we decide to don't be + * backwards compatible with pre bitcoinj 0.15 wallets. + * In that scenario, users will have to migrate using this procedure: + * 1) Run pre bitcoinj 0.15 bisq and copy their seed words on a piece of paper. + * 2) Run post bitcoinj 0.15 bisq and use recover from seed. + * */ +public class BisqKeyChainFactory extends DefaultKeyChainFactory { + + private boolean isBsqWallet; + + public BisqKeyChainFactory(boolean isBsqWallet) { + this.isBsqWallet = isBsqWallet; + } + + @Override + public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, KeyCrypter crypter, boolean isMarried, Script.ScriptType outputScriptType, ImmutableList accountPath) { + ImmutableList maybeUpdatedAccountPath = accountPath; + if (DeterministicKeyChain.ACCOUNT_ZERO_PATH.equals(accountPath)) { + // This is a bitcoinj 0.14 wallet that has no account path in the serialized mnemonic + KeyChainGroupStructure structure = new BisqKeyChainGroupStructure(isBsqWallet); + maybeUpdatedAccountPath = structure.accountPathFor(outputScriptType); + } + + return super.makeKeyChain(key, firstSubKey, seed, crypter, isMarried, outputScriptType, maybeUpdatedAccountPath); + } + + @Override + public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried, Script.ScriptType outputScriptType) { + throw new UnsupportedOperationException("Bisq is not supposed to use this"); + } + + @Override + public DeterministicKeyChain makeSpendingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isMarried, Script.ScriptType outputScriptType) { + throw new UnsupportedOperationException("Bisq is not supposed to use this"); + } +} diff --git a/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java new file mode 100644 index 0000000000..66ec8b7cea --- /dev/null +++ b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.core.btc.setup; + +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.KeyChainGroupStructure; + +import com.google.common.collect.ImmutableList; + +public class BisqKeyChainGroupStructure implements KeyChainGroupStructure { + + // See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + // https://github.com/satoshilabs/slips/blob/master/slip-0044.md + // We use 0 (0x80000000) as coin_type for BTC + // m / purpose' / coin_type' / account' / change / address_index + public static final ImmutableList BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH = ImmutableList.of( + new ChildNumber(44, true), + new ChildNumber(0, true), + ChildNumber.ZERO_HARDENED); + + public static final ImmutableList BIP44_BTC_SEGWIT_ACCOUNT_PATH = ImmutableList.of( + new ChildNumber(44, true), + new ChildNumber(0, true), + ChildNumber.ONE_HARDENED); + + // See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + // https://github.com/satoshilabs/slips/blob/master/slip-0044.md + // We have registered 142 (0x8000008E) as coin_type for BSQ + public static final ImmutableList BIP44_BSQ_NON_SEGWIT_ACCOUNT_PATH = ImmutableList.of( + new ChildNumber(44, true), + new ChildNumber(142, true), + ChildNumber.ZERO_HARDENED); + + // We don't use segwit for BSQ + // public static final ImmutableList BIP44_BSQ_SEGWIT_ACCOUNT_PATH = ImmutableList.of( + // new ChildNumber(44, true), + // new ChildNumber(142, true), + // ChildNumber.ONE_HARDENED); + + private boolean isBsqWallet; + + public BisqKeyChainGroupStructure (boolean isBsqWallet) { + this.isBsqWallet = isBsqWallet; + } + + @Override + public ImmutableList accountPathFor(Script.ScriptType outputScriptType) { + if (!isBsqWallet) { + if (outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH) + return BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH; + else if (outputScriptType == Script.ScriptType.P2WPKH) + return BIP44_BTC_SEGWIT_ACCOUNT_PATH; + else + throw new IllegalArgumentException(outputScriptType.toString()); + } else { + if (outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH) + return BIP44_BSQ_NON_SEGWIT_ACCOUNT_PATH; + else if (outputScriptType == Script.ScriptType.P2WPKH) + //return BIP44_BSQ_SEGWIT_ACCOUNT_PATH; + throw new IllegalArgumentException(outputScriptType.toString()); + else + throw new IllegalArgumentException(outputScriptType.toString()); + } + } +} diff --git a/core/src/main/java/bisq/core/btc/setup/RegTestHost.java b/core/src/main/java/bisq/core/btc/setup/RegTestHost.java new file mode 100644 index 0000000000..754cffee98 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/setup/RegTestHost.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.core.btc.setup; + +import bisq.common.config.Config; + +public enum RegTestHost { + + NONE, + LOCALHOST, + REMOTE_HOST; + + public static String HOST = Config.DEFAULT_REGTEST_HOST; + +} diff --git a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java new file mode 100644 index 0000000000..bd260c13d8 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java @@ -0,0 +1,585 @@ +/* + * 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 bisq.core.btc.setup; + +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.btc.nodes.ProxySocketFactory; +import bisq.core.btc.wallet.BisqRiskAnalysis; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; + +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.CheckpointManager; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.listeners.DownloadProgressTracker; +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.net.BlockingClientManager; +import org.bitcoinj.net.discovery.DnsDiscovery; +import org.bitcoinj.net.discovery.PeerDiscovery; +import org.bitcoinj.script.Script; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.SPVBlockStore; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.KeyChainGroup; +import org.bitcoinj.wallet.KeyChainGroupStructure; +import org.bitcoinj.wallet.Protos; +import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.wallet.WalletExtension; +import org.bitcoinj.wallet.WalletProtobufSerializer; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import com.google.common.io.Closeables; +import com.google.common.util.concurrent.AbstractIdleService; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +import static bisq.common.util.Preconditions.checkDir; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + *

    Utility class that wraps the boilerplate needed to set up a new SPV bitcoinj app. Instantiate it with a directory + * and file prefix, optionally configure a few things, then use startAsync and optionally awaitRunning. The object will + * construct and configure a {@link BlockChain}, {@link SPVBlockStore}, {@link Wallet} and {@link PeerGroup}.

    + * + *

    To add listeners and modify the objects that are constructed, you can either do that by overriding the + * {@link #onSetupCompleted()} method (which will run on a background thread) and make your changes there, + * or by waiting for the service to start and then accessing the objects from wherever you want. However, you cannot + * access the objects this class creates until startup is complete.

    + * + *

    The asynchronous design of this class may seem puzzling (just use {@link #awaitRunning()} if you don't want that). + * It is to make it easier to fit bitcoinj into GUI apps, which require a high degree of responsiveness on their main + * thread which handles all the animation and user interaction. Even when blockingStart is false, initializing bitcoinj + * means doing potentially blocking file IO, generating keys and other potentially intensive operations. By running it + * on a background thread, there's no risk of accidentally causing UI lag.

    + * + *

    Note that {@link #awaitRunning()} can throw an unchecked {@link IllegalStateException} + * if anything goes wrong during startup - you should probably handle it and use {@link Exception#getCause()} to figure + * out what went wrong more precisely. Same thing if you just use the {@link #startAsync()} method.

    + */ +public class WalletConfig extends AbstractIdleService { + + private static final int TOR_SOCKET_TIMEOUT = 120 * 1000; // 1 sec used in bitcoinj, but since bisq uses Tor we allow more. + private static final int TOR_VERSION_EXCHANGE_TIMEOUT = 125 * 1000; // 5 sec used in bitcoinj, but since bisq uses Tor we allow more. + + protected static final Logger log = LoggerFactory.getLogger(WalletConfig.class); + + protected final NetworkParameters params; + protected final String filePrefix; + protected volatile BlockChain vChain; + protected volatile SPVBlockStore vStore; + protected volatile Wallet vBtcWallet; + protected volatile Wallet vBsqWallet; + protected volatile PeerGroup vPeerGroup; + + protected final File directory; + protected volatile File vBtcWalletFile; + protected volatile File vBsqWalletFile; + + protected PeerAddress[] peerAddresses; + protected DownloadProgressTracker downloadListener; + protected InputStream checkpoints; + protected String userAgent, version; + @Nullable + protected DeterministicSeed restoreFromSeed; + @Nullable + protected PeerDiscovery discovery; + + protected volatile Context context; + + protected Config config; + protected LocalBitcoinNode localBitcoinNode; + protected Socks5Proxy socks5Proxy; + protected int numConnectionsForBtc; + @Getter + @Setter + private int minBroadcastConnections; + @Getter + private BooleanProperty migratedWalletToSegwit = new SimpleBooleanProperty(false); + + /** + * Creates a new WalletConfig, with a newly created {@link Context}. Files will be stored in the given directory. + */ + public WalletConfig(NetworkParameters params, File directory, String filePrefix) { + this(new Context(params), directory, filePrefix); + } + + /** + * Creates a new WalletConfig, with the given {@link Context}. Files will be stored in the given directory. + */ + private WalletConfig(Context context, File directory, String filePrefix) { + this.context = context; + this.params = checkNotNull(context.getParams()); + this.directory = checkDir(directory); + this.filePrefix = checkNotNull(filePrefix); + } + + public WalletConfig setSocks5Proxy(Socks5Proxy socks5Proxy) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.socks5Proxy = socks5Proxy; + return this; + } + + public WalletConfig setConfig(Config config) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.config = config; + return this; + } + + public WalletConfig setLocalBitcoinNode(LocalBitcoinNode localBitcoinNode) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.localBitcoinNode = localBitcoinNode; + return this; + } + + public WalletConfig setNumConnectionsForBtc(int numConnectionsForBtc) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.numConnectionsForBtc = numConnectionsForBtc; + return this; + } + + + /** Will only connect to the given addresses. Cannot be called after startup. */ + public WalletConfig setPeerNodes(PeerAddress... addresses) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.peerAddresses = addresses; + return this; + } + + /** Will only connect to localhost. Cannot be called after startup. */ + public WalletConfig connectToLocalHost() { + final InetAddress localHost = InetAddress.getLoopbackAddress(); + return setPeerNodes(new PeerAddress(params, localHost, params.getPort())); + } + + /** + * If you want to learn about the sync process, you can provide a listener here. For instance, a + * {@link DownloadProgressTracker} is a good choice. + */ + public WalletConfig setDownloadListener(DownloadProgressTracker listener) { + this.downloadListener = listener; + return this; + } + + /** + * If set, the file is expected to contain a checkpoints file calculated with BuildCheckpoints. It makes initial + * block sync faster for new users - please refer to the documentation on the bitcoinj website + * (https://bitcoinj.github.io/speeding-up-chain-sync) for further details. + */ + public WalletConfig setCheckpoints(InputStream checkpoints) { + if (this.checkpoints != null) + Closeables.closeQuietly(checkpoints); + this.checkpoints = checkNotNull(checkpoints); + return this; + } + + /** + * Sets the string that will appear in the subver field of the version message. + * @param userAgent A short string that should be the name of your app, e.g. "My Wallet" + * @param version A short string that contains the version number, e.g. "1.0-BETA" + */ + public WalletConfig setUserAgent(String userAgent, String version) { + this.userAgent = checkNotNull(userAgent); + this.version = checkNotNull(version); + return this; + } + + /** + * If a seed is set here then any existing wallet that matches the file name will be renamed to a backup name, + * the chain file will be deleted, and the wallet object will be instantiated with the given seed instead of + * a fresh one being created. This is intended for restoring a wallet from the original seed. To implement restore + * you would shut down the existing appkit, if any, then recreate it with the seed given by the user, then start + * up the new kit. The next time your app starts it should work as normal (that is, don't keep calling this each + * time). + */ + public WalletConfig restoreWalletFromSeed(DeterministicSeed seed) { + this.restoreFromSeed = seed; + return this; + } + + /** + * Sets the peer discovery class to use. If none is provided then DNS is used, which is a reasonable default. + */ + public WalletConfig setDiscovery(@Nullable PeerDiscovery discovery) { + this.discovery = discovery; + return this; + } + + /** + * This method is invoked on a background thread after all objects are initialised, but before the peer group + * or block chain download is started. You can tweak the objects configuration here. + */ + protected void onSetupCompleted() { + // Meant to be overridden by subclasses + } + + @Override + protected void startUp() throws Exception { + // Runs in a separate thread. + Context.propagate(context); + try { + File chainFile = new File(directory, filePrefix + ".spvchain"); + boolean chainFileExists = chainFile.exists(); + String btcPrefix = "_BTC"; + vBtcWalletFile = new File(directory, filePrefix + btcPrefix + ".wallet"); + boolean shouldReplayWallet = (vBtcWalletFile.exists() && !chainFileExists) || restoreFromSeed != null; + vBtcWallet = createOrLoadWallet(shouldReplayWallet, vBtcWalletFile, false); + vBtcWallet.allowSpendingUnconfirmedTransactions(); + vBtcWallet.setRiskAnalyzer(new BisqRiskAnalysis.Analyzer()); + + String bsqPrefix = "_BSQ"; + vBsqWalletFile = new File(directory, filePrefix + bsqPrefix + ".wallet"); + vBsqWallet = createOrLoadWallet(shouldReplayWallet, vBsqWalletFile, true); + vBsqWallet.setRiskAnalyzer(new BisqRiskAnalysis.Analyzer()); + + // Initiate Bitcoin network objects (block store, blockchain and peer group) + vStore = new SPVBlockStore(params, chainFile); + if (!chainFileExists || restoreFromSeed != null) { + if (checkpoints == null) { + checkpoints = CheckpointManager.openStream(params); + } + + if (checkpoints != null) { + // Initialize the chain file with a checkpoint to speed up first-run sync. + long time; + if (restoreFromSeed != null) { + time = restoreFromSeed.getCreationTimeSeconds(); + if (chainFileExists) { + log.info("Clearing the chain file in preparation for restore."); + vStore.clear(); + } + } else { + time = vBtcWallet.getEarliestKeyCreationTime(); + } + if (time > 0) + CheckpointManager.checkpoint(params, checkpoints, vStore, time); + else + log.warn("Creating a new uncheckpointed block store due to a wallet with a creation time of zero: this will result in a very slow chain sync"); + } else if (chainFileExists) { + log.info("Clearing the chain file in preparation for restore."); + vStore.clear(); + } + } + vChain = new BlockChain(params, vStore); + vPeerGroup = createPeerGroup(); + if (minBroadcastConnections > 0) + vPeerGroup.setMinBroadcastConnections(minBroadcastConnections); + if (this.userAgent != null) + vPeerGroup.setUserAgent(userAgent, version); + + // Set up peer addresses or discovery first, so if wallet extensions try to broadcast a transaction + // before we're actually connected the broadcast waits for an appropriate number of connections. + if (peerAddresses != null) { + for (PeerAddress addr : peerAddresses) vPeerGroup.addAddress(addr); + int maxConnections = Math.min(numConnectionsForBtc, peerAddresses.length); + log.info("We try to connect to {} btc nodes", maxConnections); + vPeerGroup.setMaxConnections(maxConnections); + vPeerGroup.setAddPeersFromAddressMessage(false); + peerAddresses = null; + } else if (!params.getId().equals(NetworkParameters.ID_REGTEST)) { + vPeerGroup.addPeerDiscovery(discovery != null ? discovery : new DnsDiscovery(params)); + } + vChain.addWallet(vBtcWallet); + vPeerGroup.addWallet(vBtcWallet); + vChain.addWallet(vBsqWallet); + vPeerGroup.addWallet(vBsqWallet); + onSetupCompleted(); + + if (migratedWalletToSegwit.get()) { + startPeerGroup(); + } else { + migratedWalletToSegwit.addListener((observable, oldValue, newValue) -> { + if (newValue) { + startPeerGroup(); + } + }); + } + + } catch (BlockStoreException e) { + throw new IOException(e); + } + } + + private void startPeerGroup() { + Futures.addCallback((ListenableFuture) vPeerGroup.startAsync(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Object result) { + //completeExtensionInitiations(vPeerGroup); + DownloadProgressTracker tracker = downloadListener == null ? new DownloadProgressTracker() : downloadListener; + vPeerGroup.startBlockChainDownload(tracker); + } + + @Override + public void onFailure(Throwable t) { + throw new RuntimeException(t); + + } + }, MoreExecutors.directExecutor()); + } + + private Wallet createOrLoadWallet(boolean shouldReplayWallet, + File walletFile, + boolean isBsqWallet) throws Exception { + Wallet wallet; + + maybeMoveOldWalletOutOfTheWay(walletFile); + + if (walletFile.exists()) { + wallet = loadWallet(shouldReplayWallet, walletFile, isBsqWallet); + } else { + wallet = createWallet(isBsqWallet); + //wallet.freshReceiveKey(); + + // Currently the only way we can be sure that an extension is aware of its containing wallet is by + // deserializing the extension (see WalletExtension#deserializeWalletExtension(Wallet, byte[])) + // Hence, we first save and then load wallet to ensure any extensions are correctly initialized. + wallet.saveToFile(walletFile); + wallet = loadWallet(false, walletFile, isBsqWallet); + } + + this.setupAutoSave(wallet, walletFile); + + return wallet; + } + + protected void setupAutoSave(Wallet wallet, File walletFile) { + wallet.autosaveToFile(walletFile, 5, TimeUnit.SECONDS, null); + } + + private Wallet loadWallet(boolean shouldReplayWallet, File walletFile, boolean isBsqWallet) throws Exception { + Wallet wallet; + try (FileInputStream walletStream = new FileInputStream(walletFile)) { + WalletExtension[] extArray = new WalletExtension[]{}; + Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream); + final WalletProtobufSerializer serializer; + serializer = new WalletProtobufSerializer(); + // Hack to convert bitcoinj 0.14 wallets to bitcoinj 0.15 format + serializer.setKeyChainFactory(new BisqKeyChainFactory(isBsqWallet)); + wallet = serializer.readWallet(params, extArray, proto); + if (shouldReplayWallet) + wallet.reset(); + if (!isBsqWallet) { + maybeAddSegwitKeychain(wallet, null); + } + } + return wallet; + } + + protected Wallet createWallet(boolean isBsqWallet) { + Script.ScriptType preferredOutputScriptType = isBsqWallet ? Script.ScriptType.P2PKH : Script.ScriptType.P2WPKH; + KeyChainGroupStructure structure = new BisqKeyChainGroupStructure(isBsqWallet); + KeyChainGroup.Builder kcgBuilder = KeyChainGroup.builder(params, structure); + if (restoreFromSeed != null) { + kcgBuilder.fromSeed(restoreFromSeed, preferredOutputScriptType); + } else { + // new wallet + if (!isBsqWallet) { + // btc wallet uses a new random seed. + kcgBuilder.fromRandom(preferredOutputScriptType); + } else { + // bsq wallet uses btc wallet's seed created a few milliseconds ago. + kcgBuilder.fromSeed(vBtcWallet.getKeyChainSeed(), preferredOutputScriptType); + } + } + return new Wallet(params, kcgBuilder.build()); + } + + private void maybeMoveOldWalletOutOfTheWay(File walletFile) { + if (restoreFromSeed == null) return; + if (!walletFile.exists()) return; + int counter = 1; + File newName; + do { + newName = new File(walletFile.getParent(), "Backup " + counter + " for " + walletFile.getName()); + counter++; + } while (newName.exists()); + log.info("Renaming old wallet file {} to {}", walletFile, newName); + if (!walletFile.renameTo(newName)) { + // This should not happen unless something is really messed up. + throw new RuntimeException("Failed to rename wallet for restore"); + } + } + + private PeerGroup createPeerGroup() { + PeerGroup peerGroup; + // no proxy case. + if (socks5Proxy == null) { + peerGroup = new PeerGroup(params, vChain); + } else { + // proxy case (tor). + Proxy proxy = new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress(socks5Proxy.getInetAddress(), socks5Proxy.getPort())); + + ProxySocketFactory proxySocketFactory = new ProxySocketFactory(proxy); + // We don't use tor mode if we have a local node running + BlockingClientManager blockingClientManager = localBitcoinNode.shouldBeUsed() ? + new BlockingClientManager() : + new BlockingClientManager(proxySocketFactory); + + peerGroup = new PeerGroup(params, vChain, blockingClientManager); + + blockingClientManager.setConnectTimeoutMillis(TOR_SOCKET_TIMEOUT); + peerGroup.setConnectTimeoutMillis(TOR_VERSION_EXCHANGE_TIMEOUT); + } + + if (!localBitcoinNode.shouldBeUsed()) + peerGroup.setUseLocalhostPeerWhenPossible(false); + + return peerGroup; + } + + @Override + protected void shutDown() throws Exception { + // Runs in a separate thread. + try { + Context.propagate(context); + + vBtcWallet.saveToFile(vBtcWalletFile); + vBtcWallet = null; + log.info("BtcWallet saved to file"); + + if (vBsqWallet != null && vBsqWalletFile != null) { + vBsqWallet.saveToFile(vBsqWalletFile); + vBsqWallet = null; + log.info("BsqWallet saved to file"); + } + + vStore.close(); + vStore = null; + log.info("SPV file closed"); + + vChain = null; + + // vPeerGroup.stop has no timeout and can take very long (10 sec. in my test). So we call it at the end. + // We might get likely interrupted by the parent call timeout. + if (vPeerGroup.isRunning()) { + vPeerGroup.stop(); + log.info("PeerGroup stopped"); + } else { + log.info("PeerGroup not stopped because it was not running"); + } + vPeerGroup = null; + } catch (BlockStoreException e) { + throw new IOException(e); + } + } + + public NetworkParameters params() { + return params; + } + + public BlockChain chain() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vChain; + } + + public BlockStore store() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vStore; + } + + public Wallet btcWallet() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vBtcWallet; + } + + public Wallet bsqWallet() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vBsqWallet; + } + + public PeerGroup peerGroup() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vPeerGroup; + } + + public File directory() { + return directory; + } + + public void maybeAddSegwitKeychain(Wallet wallet, KeyParameter aesKey) { + if (BisqKeyChainGroupStructure.BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH.equals(wallet.getActiveKeyChain().getAccountPath())) { + if (wallet.isEncrypted() && aesKey == null) { + // wait for the aesKey to be set and this method to be invoked again. + return; + } + // Do a backup of the wallet + File backup = new File(directory, WalletsSetup.PRE_SEGWIT_WALLET_BACKUP); + try { + FileUtil.copyFile(new File(directory, "bisq_BTC.wallet"), backup); + } catch (IOException e) { + log.error(e.toString(), e); + } + + // Btc wallet does not have a native segwit keychain, we should add one. + DeterministicSeed seed = wallet.getKeyChainSeed(); + if (aesKey != null) { + // If wallet is encrypted, decrypt the seed. + KeyCrypter keyCrypter = wallet.getKeyCrypter(); + seed = seed.decrypt(keyCrypter, DeterministicKeyChain.DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey); + } + DeterministicKeyChain nativeSegwitKeyChain = DeterministicKeyChain.builder().seed(seed) + .outputScriptType(Script.ScriptType.P2WPKH) + .accountPath(new BisqKeyChainGroupStructure(false).accountPathFor(Script.ScriptType.P2WPKH)).build(); + if (aesKey != null) { + // If wallet is encrypted, encrypt the new keychain. + KeyCrypter keyCrypter = wallet.getKeyCrypter(); + nativeSegwitKeyChain = nativeSegwitKeyChain.toEncrypted(keyCrypter, aesKey); + } + wallet.addAndActivateHDChain(nativeSegwitKeyChain); + } + migratedWalletToSegwit.set(true); + } + + public boolean stateStartingOrRunning() { + return state() == State.STARTING || state() == State.RUNNING; + } +} diff --git a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java new file mode 100644 index 0000000000..fad4ac3051 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java @@ -0,0 +1,583 @@ +/* + * 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 bisq.core.btc.setup; + +import bisq.core.btc.exceptions.InvalidHostException; +import bisq.core.btc.exceptions.RejectedTxException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.AddressEntryList; +import bisq.core.btc.nodes.BtcNetworkConfig; +import bisq.core.btc.nodes.BtcNodes; +import bisq.core.btc.nodes.BtcNodes.BtcNode; +import bisq.core.btc.nodes.BtcNodesRepository; +import bisq.core.btc.nodes.BtcNodesSetupPreferences; +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.user.Preferences; + +import bisq.network.Socks5MultiDiscovery; +import bisq.network.Socks5ProxyProvider; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.handlers.ExceptionHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Peer; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.RejectMessage; +import org.bitcoinj.core.listeners.DownloadProgressTracker; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.Wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.Service; + +import org.apache.commons.lang3.StringUtils; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +// Setup wallets and use WalletConfig for BitcoinJ wiring. +// Other like WalletConfig we are here always on the user thread. That is one reason why we do not +// merge WalletsSetup with WalletConfig to one class. +@Slf4j +public class WalletsSetup { + + public static final String PRE_SEGWIT_WALLET_BACKUP = "pre_segwit_bisq_BTC.wallet.backup"; + + @Getter + public final BooleanProperty walletsSetupFailed = new SimpleBooleanProperty(); + + private static final long STARTUP_TIMEOUT = 180; + private static final String BSQ_WALLET_FILE_NAME = "bisq_BSQ.wallet"; + private static final String SPV_CHAIN_FILE_NAME = "bisq.spvchain"; + + private final RegTestHost regTestHost; + private final AddressEntryList addressEntryList; + private final Preferences preferences; + private final Socks5ProxyProvider socks5ProxyProvider; + private final Config config; + private final LocalBitcoinNode localBitcoinNode; + private final BtcNodes btcNodes; + private final String btcWalletFileName; + private final int numConnectionsForBtc; + private final String userAgent; + private final NetworkParameters params; + private final File walletDir; + private final int socks5DiscoverMode; + private final IntegerProperty numPeers = new SimpleIntegerProperty(0); + private final IntegerProperty chainHeight = new SimpleIntegerProperty(0); + private final ObjectProperty blocksDownloadedFromPeer = new SimpleObjectProperty<>(); + private final ObjectProperty> connectedPeers = new SimpleObjectProperty<>(); + private final DownloadListener downloadListener = new DownloadListener(); + private final List setupCompletedHandlers = new ArrayList<>(); + public final BooleanProperty shutDownComplete = new SimpleBooleanProperty(); + private final boolean useAllProvidedNodes; + private WalletConfig walletConfig; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public WalletsSetup(RegTestHost regTestHost, + AddressEntryList addressEntryList, + Preferences preferences, + Socks5ProxyProvider socks5ProxyProvider, + Config config, + LocalBitcoinNode localBitcoinNode, + BtcNodes btcNodes, + @Named(Config.USER_AGENT) String userAgent, + @Named(Config.WALLET_DIR) File walletDir, + @Named(Config.USE_ALL_PROVIDED_NODES) boolean useAllProvidedNodes, + @Named(Config.NUM_CONNECTIONS_FOR_BTC) int numConnectionsForBtc, + @Named(Config.SOCKS5_DISCOVER_MODE) String socks5DiscoverModeString) { + this.regTestHost = regTestHost; + this.addressEntryList = addressEntryList; + this.preferences = preferences; + this.socks5ProxyProvider = socks5ProxyProvider; + this.config = config; + this.localBitcoinNode = localBitcoinNode; + this.btcNodes = btcNodes; + this.numConnectionsForBtc = numConnectionsForBtc; + this.useAllProvidedNodes = useAllProvidedNodes; + this.userAgent = userAgent; + this.socks5DiscoverMode = evaluateMode(socks5DiscoverModeString); + this.walletDir = walletDir; + + btcWalletFileName = "bisq_" + config.baseCurrencyNetwork.getCurrencyCode() + ".wallet"; + params = Config.baseCurrencyNetworkParameters(); + PeerGroup.setIgnoreHttpSeeds(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public void initialize(@Nullable DeterministicSeed seed, + ResultHandler resultHandler, + ExceptionHandler exceptionHandler) { + // Tell bitcoinj to execute event handlers on the JavaFX UI thread. This keeps things simple and means + // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener + // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in + // a future version. + + Threading.USER_THREAD = UserThread.getExecutor(); + + Timer timeoutTimer = UserThread.runAfter(() -> + exceptionHandler.handleException(new TimeoutException("Wallet did not initialize in " + + STARTUP_TIMEOUT + " seconds.")), STARTUP_TIMEOUT); + + backupWallets(); + + final Socks5Proxy socks5Proxy = preferences.getUseTorForBitcoinJ() ? socks5ProxyProvider.getSocks5Proxy() : null; + log.info("Socks5Proxy for bitcoinj: socks5Proxy=" + socks5Proxy); + + walletConfig = new WalletConfig(params, + walletDir, + "bisq") { + @Override + protected void onSetupCompleted() { + //We are here in the btcj thread Thread[ STARTING,5,main] + super.onSetupCompleted(); + + final PeerGroup peerGroup = walletConfig.peerGroup(); + final BlockChain chain = walletConfig.chain(); + + // We don't want to get our node white list polluted with nodes from AddressMessage calls. + if (preferences.getBitcoinNodes() != null && !preferences.getBitcoinNodes().isEmpty()) + peerGroup.setAddPeersFromAddressMessage(false); + + peerGroup.addConnectedEventListener((peer, peerCount) -> { + // We get called here on our user thread + numPeers.set(peerCount); + connectedPeers.set(peerGroup.getConnectedPeers()); + }); + peerGroup.addDisconnectedEventListener((peer, peerCount) -> { + // We get called here on our user thread + numPeers.set(peerCount); + connectedPeers.set(peerGroup.getConnectedPeers()); + }); + peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> { + blocksDownloadedFromPeer.set(peer); + }); + + // Need to be Threading.SAME_THREAD executor otherwise BitcoinJ will skip that listener + peerGroup.addPreMessageReceivedEventListener(Threading.SAME_THREAD, (peer, message) -> { + if (message instanceof RejectMessage) { + UserThread.execute(() -> { + RejectMessage rejectMessage = (RejectMessage) message; + String msg = rejectMessage.toString(); + log.warn(msg); + exceptionHandler.handleException(new RejectedTxException(msg, rejectMessage)); + }); + } + return message; + }); + + chain.addNewBestBlockListener(block -> { + UserThread.execute(() -> { + connectedPeers.set(peerGroup.getConnectedPeers()); + chainHeight.set(block.getHeight()); + }); + }); + + // Map to user thread + UserThread.execute(() -> { + chainHeight.set(chain.getBestChainHeight()); + addressEntryList.onWalletReady(walletConfig.btcWallet()); + timeoutTimer.stop(); + setupCompletedHandlers.forEach(Runnable::run); + }); + + // onSetupCompleted in walletAppKit is not the called on the last invocations, so we add a bit of delay + UserThread.runAfter(resultHandler::handleResult, 100, TimeUnit.MILLISECONDS); + } + }; + walletConfig.setSocks5Proxy(socks5Proxy); + walletConfig.setConfig(config); + walletConfig.setLocalBitcoinNode(localBitcoinNode); + walletConfig.setUserAgent(userAgent, Version.VERSION); + walletConfig.setNumConnectionsForBtc(numConnectionsForBtc); + + String checkpointsPath = null; + if (params.equals(MainNetParams.get())) { + // Checkpoints are block headers that ship inside our app: for a new user, we pick the last header + // in the checkpoints file and then download the rest from the network. It makes things much faster. + // Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the + // last months worth or more (takes a few seconds). + checkpointsPath = "/wallet/checkpoints.txt"; + } else if (params.equals(TestNet3Params.get())) { + checkpointsPath = "/wallet/checkpoints.testnet.txt"; + } + if (checkpointsPath != null) { + walletConfig.setCheckpoints(getClass().getResourceAsStream(checkpointsPath)); + } + + + if (params == RegTestParams.get()) { + walletConfig.setMinBroadcastConnections(1); + if (regTestHost == RegTestHost.LOCALHOST) { + walletConfig.connectToLocalHost(); + } else if (regTestHost == RegTestHost.REMOTE_HOST) { + configPeerNodesForRegTestServer(); + } else { + try { + configPeerNodes(socks5Proxy); + } catch (IllegalArgumentException e) { + timeoutTimer.stop(); + walletsSetupFailed.set(true); + exceptionHandler.handleException(new InvalidHostException(e.getMessage())); + return; + } + } + } else if (localBitcoinNode.shouldBeUsed()) { + walletConfig.setMinBroadcastConnections(1); + walletConfig.connectToLocalHost(); + } else { + try { + configPeerNodes(socks5Proxy); + } catch (IllegalArgumentException e) { + timeoutTimer.stop(); + walletsSetupFailed.set(true); + exceptionHandler.handleException(new InvalidHostException(e.getMessage())); + return; + } + } + + walletConfig.setDownloadListener(downloadListener); + + // If seed is non-null it means we are restoring from backup. + if (seed != null) { + walletConfig.restoreWalletFromSeed(seed); + } + + walletConfig.addListener(new Service.Listener() { + @Override + public void failed(@NotNull Service.State from, @NotNull Throwable failure) { + walletConfig = null; + log.error("Service failure from state: {}; failure={}", from, failure); + timeoutTimer.stop(); + UserThread.execute(() -> exceptionHandler.handleException(failure)); + } + }, Threading.USER_THREAD); + + walletConfig.startAsync(); + } + + public void shutDown() { + if (walletConfig != null) { + try { + log.info("walletConfig shutDown started"); + walletConfig.stopAsync(); + walletConfig.awaitTerminated(1, TimeUnit.SECONDS); + log.info("walletConfig shutDown completed"); + } catch (Throwable ignore) { + log.info("walletConfig shutDown interrupted by timeout"); + } + } + + shutDownComplete.set(true); + } + + public void reSyncSPVChain() throws IOException { + FileUtil.deleteFileIfExists(new File(walletDir, SPV_CHAIN_FILE_NAME)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Initialize methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + private int evaluateMode(String socks5DiscoverModeString) { + String[] socks5DiscoverModes = StringUtils.deleteWhitespace(socks5DiscoverModeString).split(","); + int mode = 0; + for (String socks5DiscoverMode : socks5DiscoverModes) { + switch (socks5DiscoverMode) { + case "ADDR": + mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ADDR; + break; + case "DNS": + mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_DNS; + break; + case "ONION": + mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ONION; + break; + case "ALL": + default: + mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ALL; + break; + } + } + return mode; + } + + private void configPeerNodesForRegTestServer() { + try { + if (RegTestHost.HOST.endsWith(".onion")) { + walletConfig.setPeerNodes(new PeerAddress(RegTestHost.HOST, params.getPort())); + } else { + walletConfig.setPeerNodes(new PeerAddress(params, InetAddress.getByName(RegTestHost.HOST), params.getPort())); + } + } catch (UnknownHostException e) { + log.error(e.toString()); + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private void configPeerNodes(@Nullable Socks5Proxy proxy) { + BtcNodesSetupPreferences btcNodesSetupPreferences = new BtcNodesSetupPreferences(preferences); + + List nodes = btcNodesSetupPreferences.selectPreferredNodes(btcNodes); + int minBroadcastConnections = btcNodesSetupPreferences.calculateMinBroadcastConnections(nodes); + walletConfig.setMinBroadcastConnections(minBroadcastConnections); + + BtcNodesRepository repository = new BtcNodesRepository(nodes); + boolean isUseClearNodesWithProxies = (useAllProvidedNodes || btcNodesSetupPreferences.isUseCustomNodes()); + List peers = repository.getPeerAddresses(proxy, isUseClearNodesWithProxies); + + BtcNetworkConfig networkConfig = new BtcNetworkConfig(walletConfig, params, socks5DiscoverMode, proxy); + networkConfig.proposePeers(peers); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Backup + /////////////////////////////////////////////////////////////////////////////////////////// + + public void backupWallets() { + FileUtil.rollingBackup(walletDir, btcWalletFileName, 20); + FileUtil.rollingBackup(walletDir, BSQ_WALLET_FILE_NAME, 20); + } + + public void clearBackups() { + try { + FileUtil.deleteDirectory(new File(Paths.get(walletDir.getAbsolutePath(), "backup").toString())); + } catch (IOException e) { + log.error("Could not delete directory " + e.getMessage()); + e.printStackTrace(); + } + + File segwitBackup = new File(walletDir, PRE_SEGWIT_WALLET_BACKUP); + try { + FileUtil.deleteFileIfExists(segwitBackup); + } catch (IOException e) { + log.error(e.toString(), e); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Restore + /////////////////////////////////////////////////////////////////////////////////////////// + + public void restoreSeedWords(@Nullable DeterministicSeed seed, + ResultHandler resultHandler, + ExceptionHandler exceptionHandler) { + checkNotNull(seed, "Seed must be not be null."); + + backupWallets(); + + Context ctx = Context.get(); + new Thread(() -> { + try { + Context.propagate(ctx); + walletConfig.stopAsync(); + walletConfig.awaitTerminated(); + initialize(seed, resultHandler, exceptionHandler); + } catch (Throwable t) { + t.printStackTrace(); + log.error("Executing task failed. " + t.getMessage()); + } + }, "RestoreBTCWallet-%d").start(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handlers + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addSetupCompletedHandler(Runnable handler) { + setupCompletedHandlers.add(handler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Wallet getBtcWallet() { + return walletConfig.btcWallet(); + } + + @Nullable + public Wallet getBsqWallet() { + return walletConfig.bsqWallet(); + } + + public NetworkParameters getParams() { + return params; + } + + @Nullable + public BlockChain getChain() { + return walletConfig != null && walletConfig.stateStartingOrRunning() ? walletConfig.chain() : null; + } + + public PeerGroup getPeerGroup() { + return walletConfig.peerGroup(); + } + + public WalletConfig getWalletConfig() { + return walletConfig; + } + + public ReadOnlyIntegerProperty numPeersProperty() { + return numPeers; + } + + public ReadOnlyObjectProperty> connectedPeersProperty() { + return connectedPeers; + } + + public ReadOnlyIntegerProperty chainHeightProperty() { + return chainHeight; + } + + public ReadOnlyObjectProperty blocksDownloadedFromPeerProperty() { + return blocksDownloadedFromPeer; + } + + public ReadOnlyDoubleProperty downloadPercentageProperty() { + return downloadListener.percentageProperty(); + } + + public boolean isDownloadComplete() { + return downloadPercentageProperty().get() == 1d; + } + + public boolean isChainHeightSyncedWithinTolerance() { + int peersChainHeight = PeerGroup.getMostCommonChainHeight(connectedPeers.get()); + int bestChainHeight = walletConfig.chain().getBestChainHeight(); + if (Math.abs(peersChainHeight - bestChainHeight) <= 3) { + return true; + } + log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), peersChainHeight); + return false; + } + + public Set
    getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) { + return addressEntryList.getAddressEntriesAsListImmutable().stream() + .filter(addressEntry -> addressEntry.getContext() == context) + .map(AddressEntry::getAddress) + .collect(Collectors.toSet()); + } + + public Set
    getAddressesFromAddressEntries(Set addressEntries) { + return addressEntries.stream() + .map(AddressEntry::getAddress) + .collect(Collectors.toSet()); + } + + public boolean hasSufficientPeersForBroadcast() { + return numPeers.get() >= getMinBroadcastConnections(); + } + + public int getMinBroadcastConnections() { + return walletConfig.getMinBroadcastConnections(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Inner classes + /////////////////////////////////////////////////////////////////////////////////////////// + + private static class DownloadListener extends DownloadProgressTracker { + private final DoubleProperty percentage = new SimpleDoubleProperty(-1); + + @Override + protected void progress(double percentage, int blocksLeft, Date date) { + super.progress(percentage, blocksLeft, date); + UserThread.execute(() -> this.percentage.set(percentage / 100d)); + } + + @Override + protected void doneDownload() { + super.doneDownload(); + UserThread.execute(() -> this.percentage.set(1d)); + } + + public ReadOnlyDoubleProperty percentageProperty() { + return percentage; + } + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java new file mode 100644 index 0000000000..c540cc5112 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java @@ -0,0 +1,166 @@ +/* + * 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 bisq.core.btc.wallet; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.wallet.CoinSelection; +import org.bitcoinj.wallet.CoinSelector; + +import java.math.BigInteger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Used from org.bitcoinj.wallet.DefaultCoinSelector but added selectOutput method and changed static methods to + * instance methods. + *

    + *

    + * This class implements a {@link CoinSelector} which attempts to get the highest priority + * possible. This means that the transaction is the most likely to get confirmed. Note that this means we may end up + * "spending" more priority than would be required to get the transaction we are creating confirmed. + */ +@Slf4j +public abstract class BisqDefaultCoinSelector implements CoinSelector { + + protected final boolean permitForeignPendingTx; + + // TransactionOutputs to be used as candidates in the select method. + // We reset the value to null just after we have applied it inside the select method. + @Nullable + @Setter + protected Set utxoCandidates; + + public CoinSelection select(Coin target, Set candidates) { + return select(target, new ArrayList<>(candidates)); + } + + public BisqDefaultCoinSelector(boolean permitForeignPendingTx) { + this.permitForeignPendingTx = permitForeignPendingTx; + } + + public BisqDefaultCoinSelector() { + permitForeignPendingTx = false; + } + + @Override + public CoinSelection select(Coin target, List candidates) { + ArrayList selected = new ArrayList<>(); + // Sort the inputs by age*value so we get the highest "coin days" spent. + + ArrayList sortedOutputs; + if (utxoCandidates != null) { + sortedOutputs = new ArrayList<>(utxoCandidates); + // We reuse the selectors. Reset the transactionOutputCandidates field + utxoCandidates = null; + } else { + sortedOutputs = new ArrayList<>(candidates); + } + + // If we spend all we don't need to sort + if (!target.equals(NetworkParameters.MAX_MONEY)) + sortOutputs(sortedOutputs); + + // Now iterate over the sorted outputs until we have got as close to the target as possible or a little + // bit over (excessive value will be change). + long total = 0; + long targetValue = target.value; + for (TransactionOutput output : sortedOutputs) { + if (!isDustAttackUtxo(output)) { + if (total >= targetValue) { + long change = total - targetValue; + if (change == 0 || change >= Restrictions.getMinNonDustOutput().value) + break; + } + + if (output.getParentTransaction() != null && + isTxSpendable(output.getParentTransaction()) && + isTxOutputSpendable(output)) { + selected.add(output); + total += output.getValue().value; + } + } + } + // Total may be lower than target here, if the given candidates were insufficient to create to requested + // transaction. + return new CoinSelection(Coin.valueOf(total), selected); + } + + protected abstract boolean isDustAttackUtxo(TransactionOutput output); + + public Coin getChange(Coin target, CoinSelection coinSelection) throws InsufficientMoneyException { + long value = target.value; + long available = coinSelection.valueGathered.value; + long change = available - value; + if (change < 0) + throw new InsufficientMoneyException(Coin.valueOf(change * -1)); + + return Coin.valueOf(change); + } + + // We allow spending from own unconfirmed txs and if permitForeignPendingTx is set as well from foreign + // unconfirmed txs. + protected boolean isTxSpendable(Transaction tx) { + TransactionConfidence confidence = tx.getConfidence(); + TransactionConfidence.ConfidenceType type = confidence.getConfidenceType(); + boolean isConfirmed = type.equals(TransactionConfidence.ConfidenceType.BUILDING); + boolean isPending = type.equals(TransactionConfidence.ConfidenceType.PENDING); + boolean isOwnTx = confidence.getSource().equals(TransactionConfidence.Source.SELF); + return isConfirmed || (isPending && (permitForeignPendingTx || isOwnTx)); + } + + abstract boolean isTxOutputSpendable(TransactionOutput output); + + // TODO Why it uses coin age and not try to minimize number of inputs as the highest priority? + // Asked Oscar and he also don't knows why coin age is used. Should be changed so that min. number of inputs is + // target. + protected void sortOutputs(ArrayList outputs) { + Collections.sort(outputs, (a, b) -> { + int depth1 = a.getParentTransactionDepthInBlocks(); + int depth2 = b.getParentTransactionDepthInBlocks(); + Coin aValue = a.getValue(); + Coin bValue = b.getValue(); + BigInteger aCoinDepth = BigInteger.valueOf(aValue.value).multiply(BigInteger.valueOf(depth1)); + BigInteger bCoinDepth = BigInteger.valueOf(bValue.value).multiply(BigInteger.valueOf(depth2)); + int c1 = bCoinDepth.compareTo(aCoinDepth); + if (c1 != 0) return c1; + // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. + int c2 = bValue.compareTo(aValue); + if (c2 != 0) return c2; + // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. + BigInteger aHash = a.getParentTransactionHash() != null ? + a.getParentTransactionHash().toBigInteger() : BigInteger.ZERO; + BigInteger bHash = b.getParentTransactionHash() != null ? + b.getParentTransactionHash().toBigInteger() : BigInteger.ZERO; + return aHash.compareTo(bHash); + }); + } + +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BisqRiskAnalysis.java b/core/src/main/java/bisq/core/btc/wallet/BisqRiskAnalysis.java new file mode 100644 index 0000000000..8b665c67c9 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BisqRiskAnalysis.java @@ -0,0 +1,298 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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 bisq.core.btc.wallet; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.ECKey.ECDSASignature; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.script.ScriptPattern; +import org.bitcoinj.wallet.RiskAnalysis; +import org.bitcoinj.wallet.Wallet; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkState; + +// Copied from DefaultRiskAnalysis as DefaultRiskAnalysis has mostly private methods and constructor so we cannot +// override it. +// The changes to DefaultRiskAnalysis are: removal of the RBF check and accept as standard an OP_RETURN outputs +// with 0 value. +// For Bisq's use cases RBF is not considered risky. Requiring a confirmation for RBF payments from a user's +// external wallet to Bisq would hurt usability. The trade transaction requires anyway a confirmation and we don't see +// a use case where a Bisq user accepts unconfirmed payment from untrusted peers and would not wait anyway for at least +// one confirmation. + +/** + *

    The default risk analysis. Currently, it only is concerned with whether a tx/dependency is non-final or not, and + * whether a tx/dependency violates the dust rules. Outside of specialised protocols you should not encounter non-final + * transactions.

    + */ +public class BisqRiskAnalysis implements RiskAnalysis { + private static final Logger log = LoggerFactory.getLogger(BisqRiskAnalysis.class); + + /** + * Any standard output smaller than this value (in satoshis) will be considered risky, as it's most likely be + * rejected by the network. This is usually the same as {@link Transaction#MIN_NONDUST_OUTPUT} but can be + * different when the fee is about to change in Bitcoin Core. + */ + public static final Coin MIN_ANALYSIS_NONDUST_OUTPUT = Transaction.MIN_NONDUST_OUTPUT; + + protected final Transaction tx; + protected final List dependencies; + @Nullable + protected final Wallet wallet; + + private Transaction nonStandard; + protected Transaction nonFinal; + protected boolean analyzed; + + private BisqRiskAnalysis(Wallet wallet, Transaction tx, List dependencies) { + this.tx = tx; + this.dependencies = dependencies; + this.wallet = wallet; + } + + @Override + public Result analyze() { + checkState(!analyzed); + analyzed = true; + + Result result = analyzeIsFinal(); + if (result != null && result != Result.OK) + return result; + + return analyzeIsStandard(); + } + + @Nullable + private Result analyzeIsFinal() { + // Transactions we create ourselves are, by definition, not at risk of double spending against us. + if (tx.getConfidence().getSource() == TransactionConfidence.Source.SELF) + return Result.OK; + + // Commented out to accept replace-by-fee txs. + // // We consider transactions that opt into replace-by-fee at risk of double spending. + // if (tx.isOptInFullRBF()) { + // nonFinal = tx; + // return Result.NON_FINAL; + // } + + // Relative time-locked transactions are risky too. We can't check the locks because usually we don't know the + // spent outputs (to know when they were created). + if (tx.hasRelativeLockTime()) { + nonFinal = tx; + return Result.NON_FINAL; + } + + if (wallet == null) + return null; + + final int height = wallet.getLastBlockSeenHeight(); + final long time = wallet.getLastBlockSeenTimeSecs(); + // If the transaction has a lock time specified in blocks, we consider that if the tx would become final in the + // next block it is not risky (as it would confirm normally). + final int adjustedHeight = height + 1; + + if (!tx.isFinal(adjustedHeight, time)) { + nonFinal = tx; + return Result.NON_FINAL; + } + for (Transaction dep : dependencies) { + if (!dep.isFinal(adjustedHeight, time)) { + nonFinal = dep; + return Result.NON_FINAL; + } + } + + return Result.OK; + } + + /** + * The reason a transaction is considered non-standard, returned by + * {@link #isStandard(org.bitcoinj.core.Transaction)}. + */ + public enum RuleViolation { + NONE, + VERSION, + DUST, + SHORTEST_POSSIBLE_PUSHDATA, + NONEMPTY_STACK, // Not yet implemented (for post 0.12) + SIGNATURE_CANONICAL_ENCODING + } + + /** + *

    Checks if a transaction is considered "standard" by Bitcoin Core's IsStandardTx and AreInputsStandard + * functions.

    + * + *

    Note that this method currently only implements a minimum of checks. More to be added later.

    + */ + public static RuleViolation isStandard(Transaction tx) { + // TODO: Finish this function off. + if (tx.getVersion() > 2 || tx.getVersion() < 1) { + log.warn("TX considered non-standard due to unknown version number {}", tx.getVersion()); + return RuleViolation.VERSION; + } + + final List outputs = tx.getOutputs(); + for (int i = 0; i < outputs.size(); i++) { + TransactionOutput output = outputs.get(i); + RuleViolation violation = isOutputStandard(output); + if (violation != RuleViolation.NONE) { + log.warn("TX considered non-standard due to output {} violating rule {}", i, violation); + return violation; + } + } + + final List inputs = tx.getInputs(); + for (int i = 0; i < inputs.size(); i++) { + TransactionInput input = inputs.get(i); + RuleViolation violation = isInputStandard(input); + if (violation != RuleViolation.NONE) { + log.warn("TX considered non-standard due to input {} violating rule {}", i, violation); + return violation; + } + } + + return RuleViolation.NONE; + } + + /** + * Checks the output to see if the script violates a standardness rule. Not complete. + */ + public static RuleViolation isOutputStandard(TransactionOutput output) { + // OP_RETURN has usually output value zero, so we exclude that from the MIN_ANALYSIS_NONDUST_OUTPUT check + if (!ScriptPattern.isOpReturn(output.getScriptPubKey()) + && output.getValue().compareTo(MIN_ANALYSIS_NONDUST_OUTPUT) < 0) + return RuleViolation.DUST; + for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) { + if (chunk.isPushData() && !chunk.isShortestPossiblePushData()) + return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; + } + return RuleViolation.NONE; + } + + /** Checks if the given input passes some of the AreInputsStandard checks. Not complete. */ + public static RuleViolation isInputStandard(TransactionInput input) { + for (ScriptChunk chunk : input.getScriptSig().getChunks()) { + if (chunk.data != null && !chunk.isShortestPossiblePushData()) + return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; + if (chunk.isPushData()) { + ECDSASignature signature; + try { + signature = ECKey.ECDSASignature.decodeFromDER(chunk.data); + } catch (SignatureDecodeException x) { + // Doesn't look like a signature. + signature = null; + } + if (signature != null) { + if (!TransactionSignature.isEncodingCanonical(chunk.data)) + return RuleViolation.SIGNATURE_CANONICAL_ENCODING; + if (!signature.isCanonical()) + return RuleViolation.SIGNATURE_CANONICAL_ENCODING; + } + } + } + return RuleViolation.NONE; + } + + private Result analyzeIsStandard() { + // The IsStandard rules don't apply on testnet, because they're just a safety mechanism and we don't want to + // crush innovation with valueless test coins. + if (wallet != null && !wallet.getNetworkParameters().getId().equals(NetworkParameters.ID_MAINNET)) + return Result.OK; + + RuleViolation ruleViolation = isStandard(tx); + if (ruleViolation != RuleViolation.NONE) { + nonStandard = tx; + return Result.NON_STANDARD; + } + + for (Transaction dep : dependencies) { + ruleViolation = isStandard(dep); + if (ruleViolation != RuleViolation.NONE) { + nonStandard = dep; + return Result.NON_STANDARD; + } + } + + return Result.OK; + } + + /** Returns the transaction that was found to be non-standard, or null. */ + @Nullable + public Transaction getNonStandard() { + return nonStandard; + } + + /** Returns the transaction that was found to be non-final, or null. */ + @Nullable + public Transaction getNonFinal() { + return nonFinal; + } + + @Override + public String toString() { + if (!analyzed) + return "Pending risk analysis for " + tx.getTxId().toString(); + else if (nonFinal != null) + return "Risky due to non-finality of " + nonFinal.getTxId().toString(); + else if (nonStandard != null) + return "Risky due to non-standard tx " + nonStandard.getTxId().toString(); + else + return "Non-risky"; + } + + public static class Analyzer implements RiskAnalysis.Analyzer { + @Override + public BisqRiskAnalysis create(Wallet wallet, Transaction tx, List dependencies) { + return new BisqRiskAnalysis(wallet, tx, dependencies); + } + } + + public static Analyzer FACTORY = new Analyzer(); +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java new file mode 100644 index 0000000000..e6d122a6b8 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +/** + * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. + * We lookup for spendable outputs which matches our address of our address. + */ +@Slf4j +public class BsqCoinSelector extends BisqDefaultCoinSelector { + private final DaoStateService daoStateService; + private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService; + + @Inject + public BsqCoinSelector(DaoStateService daoStateService, UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService) { + // permitForeignPendingTx is not relevant here as we do not support pending foreign utxos anyway. + super(false); + this.daoStateService = daoStateService; + this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService; + } + + @Override + protected boolean isTxOutputSpendable(TransactionOutput output) { + // output.getParentTransaction() cannot be null as it is checked in calling method + Transaction parentTransaction = output.getParentTransaction(); + if (parentTransaction == null) + return false; + + // If it is a normal confirmed BSQ output we use the default lookup at the daoState + if (daoStateService.isTxOutputSpendable(new TxOutputKey(parentTransaction.getTxId().toString(), output.getIndex()))) + return true; + + // It might be that it is an unconfirmed change output which we allow to be used for spending without requiring a confirmation. + // We check if we have the output in the dao state, if so we have a confirmed but unspendable output (e.g. confiscated). + if (daoStateService.getTxOutput(new TxOutputKey(parentTransaction.getTxId().toString(), output.getIndex())).isPresent()) + return false; + + // Only if it's not existing yet in the dao state (unconfirmed) we use our unconfirmedBsqChangeOutputList to + // check if it is an own change output. + return unconfirmedBsqChangeOutputListService.hasTransactionOutput(output); + } + + // For BSQ we do not check for dust attack utxos as they are 5.46 BSQ and a considerable value. + // The default 546 sat dust limit is handled in the BitcoinJ side anyway. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return false; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java new file mode 100644 index 0000000000..4558b0acf7 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java @@ -0,0 +1,60 @@ +package bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.BsqTransferModel; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class BsqTransferService { + + private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + + @Inject + public BsqTransferService(WalletsManager walletsManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService) { + this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + } + + public BsqTransferModel getBsqTransferModel(LegacyAddress address, + Coin receiverAmount, + Coin txFeePerVbyte) + throws TransactionVerificationException, + WalletException, + BsqChangeBelowDustException, + InsufficientMoneyException { + + Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, txFeePerVbyte); + Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); + + return new BsqTransferModel(address, + receiverAmount, + preparedSendTx, + txWithBtcFee, + signedTx); + } + + public void sendFunds(BsqTransferModel bsqTransferModel, TxBroadcaster.Callback callback) { + log.info("Publishing BSQ transfer {}", bsqTransferModel.toShortString()); + walletsManager.publishAndCommitBsqTx(bsqTransferModel.getTxWithBtcFee(), + bsqTransferModel.getTxType(), + callback); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java new file mode 100644 index 0000000000..e8e1afa98c --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -0,0 +1,836 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.InsufficientBsqException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.dao.DaoKillSwitch; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; + +import bisq.common.UserThread; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.ScriptException; +import org.bitcoinj.wallet.CoinSelection; +import org.bitcoinj.wallet.CoinSelector; +import org.bitcoinj.wallet.SendRequest; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.PENDING; + +@Slf4j +public class BsqWalletService extends WalletService implements DaoStateListener { + + public interface WalletTransactionsChangeListener { + + void onWalletTransactionsChange(); + } + + private final DaoKillSwitch daoKillSwitch; + private final BsqCoinSelector bsqCoinSelector; + private final NonBsqCoinSelector nonBsqCoinSelector; + private final DaoStateService daoStateService; + private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService; + private final List walletTransactions = new ArrayList<>(); + private final CopyOnWriteArraySet bsqBalanceListeners = new CopyOnWriteArraySet<>(); + private final List walletTransactionsChangeListeners = new ArrayList<>(); + private boolean updateBsqWalletTransactionsPending; + + // balance of non BSQ satoshis + @Getter + private Coin availableNonBsqBalance = Coin.ZERO; + @Getter + private Coin availableConfirmedBalance = Coin.ZERO; + @Getter + private Coin unverifiedBalance = Coin.ZERO; + @Getter + private Coin unconfirmedChangeBalance = Coin.ZERO; + @Getter + private Coin lockedForVotingBalance = Coin.ZERO; + @Getter + private Coin lockupBondsBalance = Coin.ZERO; + @Getter + private Coin unlockingBondsBalance = Coin.ZERO; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BsqWalletService(WalletsSetup walletsSetup, + BsqCoinSelector bsqCoinSelector, + NonBsqCoinSelector nonBsqCoinSelector, + DaoStateService daoStateService, + UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService, + Preferences preferences, + FeeService feeService, + DaoKillSwitch daoKillSwitch) { + super(walletsSetup, + preferences, + feeService); + + this.bsqCoinSelector = bsqCoinSelector; + this.nonBsqCoinSelector = nonBsqCoinSelector; + this.daoStateService = daoStateService; + this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService; + this.daoKillSwitch = daoKillSwitch; + + nonBsqCoinSelector.setPreferences(preferences); + + walletsSetup.addSetupCompletedHandler(() -> { + wallet = walletsSetup.getBsqWallet(); + if (wallet != null) { + wallet.setCoinSelector(bsqCoinSelector); + addListenersToWallet(); + } + + BlockChain chain = walletsSetup.getChain(); + if (chain != null) { + chain.addNewBestBlockListener(block -> chainHeightProperty.set(block.getHeight())); + chainHeightProperty.set(chain.getBestChainHeight()); + } + }); + + daoStateService.addDaoStateListener(this); + } + + @Override + protected void addListenersToWallet() { + super.addListenersToWallet(); + + wallet.addCoinsReceivedEventListener((wallet, tx, prevBalance, newBalance) -> + updateBsqWalletTransactions() + ); + wallet.addCoinsSentEventListener((wallet, tx, prevBalance, newBalance) -> + updateBsqWalletTransactions() + ); + wallet.addReorganizeEventListener(wallet -> { + log.warn("onReorganize "); + updateBsqWalletTransactions(); + unconfirmedBsqChangeOutputListService.onReorganize(); + }); + wallet.addTransactionConfidenceEventListener((wallet, tx) -> { + // We are only interested in updates from unconfirmed txs and confirmed txs at the + // time when it gets into a block. Otherwise we would get called + // updateBsqWalletTransactions for each tx as the block depth changes for all. + if (tx != null && tx.getConfidence() != null && tx.getConfidence().getDepthInBlocks() <= 1 && + daoStateService.isParseBlockChainComplete()) { + updateBsqWalletTransactions(); + } + unconfirmedBsqChangeOutputListService.onTransactionConfidenceChanged(tx); + }); + wallet.addKeyChainEventListener(keys -> + updateBsqWalletTransactions() + ); + wallet.addScriptsChangeEventListener((wallet, scripts, isAddingScripts) -> + updateBsqWalletTransactions() + ); + wallet.addChangeEventListener(wallet -> + updateBsqWalletTransactions() + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + if (isWalletReady()) { + wallet.getTransactions(false).forEach(unconfirmedBsqChangeOutputListService::onTransactionConfidenceChanged); + updateBsqWalletTransactions(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Overridden Methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + String getWalletAsString(boolean includePrivKeys) { + return wallet.toString(true, includePrivKeys, this.aesKey, true, true, walletsSetup.getChain()) + "\n\n" + + "All pubKeys as hex:\n" + + wallet.printAllPubKeysAsHex(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Balance + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateBsqBalance() { + long ts = System.currentTimeMillis(); + unverifiedBalance = Coin.valueOf( + getTransactions(false).stream() + .filter(tx -> tx.getConfidence().getConfidenceType() == PENDING) + .mapToLong(tx -> { + // Sum up outputs into BSQ wallet and subtract the inputs using lockup or unlocking + // outputs since those inputs will be accounted for in lockupBondsBalance and + // unlockingBondsBalance + long outputs = tx.getOutputs().stream() + .filter(out -> out.isMine(wallet)) + .filter(TransactionOutput::isAvailableForSpending) + .mapToLong(out -> out.getValue().value) + .sum(); + // Account for spending of locked connectedOutputs + long lockedInputs = tx.getInputs().stream() + .filter(in -> { + TransactionOutput connectedOutput = in.getConnectedOutput(); + if (connectedOutput != null) { + Transaction parentTransaction = connectedOutput.getParentTransaction(); + // TODO SQ + if (parentTransaction != null/* && + parentTransaction.getConfidence().getConfidenceType() == BUILDING*/) { + TxOutputKey key = new TxOutputKey(parentTransaction.getTxId().toString(), + connectedOutput.getIndex()); + + return (connectedOutput.isMine(wallet) + && (daoStateService.isLockupOutput(key) + || daoStateService.isUnlockingAndUnspent(key))); + } + } + return false; + }) + .mapToLong(in -> in.getValue() != null ? in.getValue().value : 0) + .sum(); + return outputs - lockedInputs; + }) + .sum() + ); + + Set confirmedTxIdSet = getTransactions(false).stream() + .filter(tx -> tx.getConfidence().getConfidenceType() == BUILDING) + .map(Transaction::getTxId) + .map(Sha256Hash::toString) + .collect(Collectors.toSet()); + + lockedForVotingBalance = Coin.valueOf(daoStateService.getUnspentBlindVoteStakeTxOutputs().stream() + .filter(txOutput -> confirmedTxIdSet.contains(txOutput.getTxId())) + .mapToLong(TxOutput::getValue) + .sum()); + + lockupBondsBalance = Coin.valueOf(daoStateService.getLockupTxOutputs().stream() + .filter(txOutput -> daoStateService.isUnspent(txOutput.getKey())) + .filter(txOutput -> !daoStateService.isConfiscatedLockupTxOutput(txOutput.getTxId())) + .filter(txOutput -> confirmedTxIdSet.contains(txOutput.getTxId())) + .mapToLong(TxOutput::getValue) + .sum()); + + unlockingBondsBalance = Coin.valueOf(daoStateService.getUnspentUnlockingTxOutputsStream() + .filter(txOutput -> confirmedTxIdSet.contains(txOutput.getTxId())) + .filter(txOutput -> !daoStateService.isConfiscatedUnlockTxOutput(txOutput.getTxId())) + .mapToLong(TxOutput::getValue) + .sum()); + + availableConfirmedBalance = bsqCoinSelector.select(NetworkParameters.MAX_MONEY, + wallet.calculateAllSpendCandidates()).valueGathered; + + if (availableConfirmedBalance.isNegative()) + availableConfirmedBalance = Coin.ZERO; + + unconfirmedChangeBalance = unconfirmedBsqChangeOutputListService.getBalance(); + + availableNonBsqBalance = nonBsqCoinSelector.select(NetworkParameters.MAX_MONEY, + wallet.calculateAllSpendCandidates()).valueGathered; + + bsqBalanceListeners.forEach(e -> e.onUpdateBalances(availableConfirmedBalance, availableNonBsqBalance, unverifiedBalance, + unconfirmedChangeBalance, lockedForVotingBalance, lockupBondsBalance, unlockingBondsBalance)); + log.info("updateBsqBalance took {} ms", System.currentTimeMillis() - ts); + } + + public void addBsqBalanceListener(BsqBalanceListener listener) { + bsqBalanceListeners.add(listener); + } + + public void removeBsqBalanceListener(BsqBalanceListener listener) { + bsqBalanceListeners.remove(listener); + } + + public void addWalletTransactionsChangeListener(WalletTransactionsChangeListener listener) { + walletTransactionsChangeListeners.add(listener); + } + + public void removeWalletTransactionsChangeListener(WalletTransactionsChangeListener listener) { + walletTransactionsChangeListeners.remove(listener); + } + + public List getSpendableBsqTransactionOutputs() { + return new ArrayList<>(bsqCoinSelector.select(NetworkParameters.MAX_MONEY, + wallet.calculateAllSpendCandidates()).gathered); + } + + public List getSpendableNonBsqTransactionOutputs() { + return new ArrayList<>(nonBsqCoinSelector.select(NetworkParameters.MAX_MONEY, + wallet.calculateAllSpendCandidates()).gathered); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BSQ TransactionOutputs and Transactions + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getClonedWalletTransactions() { + return new ArrayList<>(walletTransactions); + } + + public Stream getPendingWalletTransactionsStream() { + return walletTransactions.stream() + .filter(transaction -> transaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING); + } + + private void updateBsqWalletTransactions() { + if (daoStateService.isParseBlockChainComplete()) { + // We get called updateBsqWalletTransactions multiple times from onWalletChanged, onTransactionConfidenceChanged + // and from onParseBlockCompleteAfterBatchProcessing. But as updateBsqBalance is an expensive operation we do + // not want to call it in a short interval series so we use a flag and a delay to not call it multiple times + // in a 100 ms period. + if (!updateBsqWalletTransactionsPending) { + updateBsqWalletTransactionsPending = true; + UserThread.runAfter(() -> { + walletTransactions.clear(); + walletTransactions.addAll(getTransactions(false)); + walletTransactionsChangeListeners.forEach(WalletTransactionsChangeListener::onWalletTransactionsChange); + updateBsqBalance(); + updateBsqWalletTransactionsPending = false; + }, 100, TimeUnit.MILLISECONDS); + } + } + } + + private Set getBsqWalletTransactions() { + return getTransactions(false).stream() + .filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING || + daoStateService.containsTx(transaction.getTxId().toString())) + .collect(Collectors.toSet()); + } + + public Set getUnverifiedBsqTransactions() { + Set bsqWalletTransactions = getBsqWalletTransactions(); + Set walletTxs = new HashSet<>(getTransactions(false)); + checkArgument(walletTxs.size() >= bsqWalletTransactions.size(), + "We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs"); + if (walletTxs.size() == bsqWalletTransactions.size()) { + // As expected + return new HashSet<>(); + } else { + Map map = walletTxs.stream() + .collect(Collectors.toMap(t -> t.getTxId().toString(), Function.identity())); + + Set walletTxIds = walletTxs.stream() + .map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet()); + Set bsqTxIds = bsqWalletTransactions.stream() + .map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet()); + + walletTxIds.stream() + .filter(bsqTxIds::contains) + .forEach(map::remove); + return new HashSet<>(map.values()); + } + } + + @Override + public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException { + Coin result = Coin.ZERO; + // We check all our inputs and get the connected outputs. + for (int i = 0; i < transaction.getInputs().size(); i++) { + TransactionInput input = transaction.getInputs().get(i); + // We grab the connected output for that input + TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput != null) { + // We grab the parent tx of the connected output + final Transaction parentTransaction = connectedOutput.getParentTransaction(); + final boolean isConfirmed = parentTransaction != null && + parentTransaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING; + if (connectedOutput.isMineOrWatched(wallet)) { + if (isConfirmed) { + // We lookup if we have a BSQ tx matching the parent tx + // We cannot make that findTx call outside of the loop as the parent tx can change at each iteration + Optional txOptional = daoStateService.getTx(parentTransaction.getTxId().toString()); + if (txOptional.isPresent()) { + TxOutput txOutput = txOptional.get().getTxOutputs().get(connectedOutput.getIndex()); + if (daoStateService.isBsqTxOutputType(txOutput)) { + //TODO check why values are not the same + if (txOutput.getValue() != connectedOutput.getValue().value) + log.warn("getValueSentToMeForTransaction: Value of BSQ output do not match BitcoinJ tx output. " + + "txOutput.getValue()={}, output.getValue().value={}, txId={}", + txOutput.getValue(), connectedOutput.getValue().value, txOptional.get().getId()); + + // If it is a valid BSQ output we add it + result = result.add(Coin.valueOf(txOutput.getValue())); + } + } + } /*else { + // TODO atm we don't display amounts of unconfirmed txs but that might change so we leave that code + // if it will be required + // If the tx is not confirmed yet we add the value and assume it is a valid BSQ output. + result = result.add(connectedOutput.getValue()); + }*/ + } + } + } + return result; + } + + @Override + public Coin getValueSentToMeForTransaction(Transaction transaction) throws ScriptException { + Coin result = Coin.ZERO; + final String txId = transaction.getTxId().toString(); + // We check if we have a matching BSQ tx. We do that call here to avoid repeated calls in the loop. + Optional txOptional = daoStateService.getTx(txId); + // We check all the outputs of our tx + for (int i = 0; i < transaction.getOutputs().size(); i++) { + TransactionOutput output = transaction.getOutputs().get(i); + final boolean isConfirmed = output.getParentTransaction() != null && + output.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING; + if (output.isMineOrWatched(wallet)) { + if (isConfirmed) { + if (txOptional.isPresent()) { + // The index of the BSQ tx outputs are the same like the bitcoinj tx outputs + TxOutput txOutput = txOptional.get().getTxOutputs().get(i); + if (daoStateService.isBsqTxOutputType(txOutput)) { + //TODO check why values are not the same + if (txOutput.getValue() != output.getValue().value) { + log.warn("getValueSentToMeForTransaction: Value of BSQ output do not match BitcoinJ tx output. " + + "txOutput.getValue()={}, output.getValue().value={}, txId={}", + txOutput.getValue(), output.getValue().value, txId); + } + + // If it is a valid BSQ output we add it + result = result.add(Coin.valueOf(txOutput.getValue())); + } + } + } /*else { + // TODO atm we don't display amounts of unconfirmed txs but that might change so we leave that code + // if it will be required + // If the tx is not confirmed yet we add the value and assume it is a valid BSQ output. + result = result.add(output.getValue()); + }*/ + } + } + return result; + } + + public Optional isWalletTransaction(String txId) { + return walletTransactions.stream().filter(e -> e.getTxId().toString().equals(txId)).findAny(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Sign tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction signTx(Transaction tx) throws WalletException, TransactionVerificationException { + for (int i = 0; i < tx.getInputs().size(); i++) { + TransactionInput txIn = tx.getInputs().get(i); + TransactionOutput connectedOutput = txIn.getConnectedOutput(); + if (connectedOutput != null && connectedOutput.isMine(wallet)) { + signTransactionInput(wallet, aesKey, tx, txIn, i); + checkScriptSig(tx, txIn, i); + } + } + + for (TransactionOutput txo : tx.getOutputs()) { + Coin value = txo.getValue(); + // OpReturn outputs have value 0 + if (value.isPositive()) { + checkArgument(Restrictions.isAboveDust(txo.getValue()), + "An output value is below dust limit. Transaction=" + tx); + } + } + + checkWalletConsistency(wallet); + verifyTransaction(tx); + printTx("BSQ wallet: Signed Tx", tx); + return tx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Commit tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void commitTx(Transaction tx, TxType txType) { + wallet.commitTx(tx); + //printTx("BSQ commit Tx", tx); + + unconfirmedBsqChangeOutputListService.onCommitTx(tx, txType, wallet); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send BSQ with BTC fee + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedSendBsqTx(String receiverAddress, Coin receiverAmount) + throws AddressFormatException, InsufficientBsqException, WalletException, + TransactionVerificationException, BsqChangeBelowDustException { + return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false); + } + + public Transaction getPreparedSendBsqTx(String receiverAddress, + Coin receiverAmount, + @Nullable Set utxoCandidates) + throws AddressFormatException, InsufficientBsqException, WalletException, + TransactionVerificationException, BsqChangeBelowDustException { + if (utxoCandidates != null) { + bsqCoinSelector.setUtxoCandidates(utxoCandidates); + } + return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send BTC (non-BSQ) with BTC fee (e.g. the issuance output from a lost comp. request) + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedSendBtcTx(String receiverAddress, Coin receiverAmount) + throws AddressFormatException, InsufficientBsqException, WalletException, + TransactionVerificationException, BsqChangeBelowDustException { + return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true); + } + + public Transaction getPreparedSendBtcTx(String receiverAddress, + Coin receiverAmount, + @Nullable Set utxoCandidates) + throws AddressFormatException, InsufficientBsqException, WalletException, + TransactionVerificationException, BsqChangeBelowDustException { + if (utxoCandidates != null) { + nonBsqCoinSelector.setUtxoCandidates(utxoCandidates); + } + return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true); + } + + private Transaction getPreparedSendTx(String receiverAddress, Coin receiverAmount, CoinSelector coinSelector, + boolean allowSegwitOuput) + throws AddressFormatException, InsufficientBsqException, WalletException, TransactionVerificationException, BsqChangeBelowDustException { + daoKillSwitch.assertDaoIsNotDisabled(); + Transaction tx = new Transaction(params); + checkArgument(Restrictions.isAboveDust(receiverAmount), + "The amount is too low (dust limit)."); + if (allowSegwitOuput) { + tx.addOutput(receiverAmount, Address.fromString(params, receiverAddress)); + } else { + tx.addOutput(receiverAmount, LegacyAddress.fromBase58(params, receiverAddress)); + } + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.fee = Coin.ZERO; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + sendRequest.signInputs = false; + sendRequest.changeAddress = getChangeAddress(); + sendRequest.coinSelector = coinSelector; + try { + wallet.completeTx(sendRequest); + checkWalletConsistency(wallet); + verifyTransaction(tx); + // printTx("prepareSendTx", tx); + + // Tx has as first output BSQ and an optional second BSQ change output. + // At that stage we do not have added the BTC inputs so there is no BTC change output here. + if (tx.getOutputs().size() == 2) { + Coin bsqChangeOutputValue = tx.getOutputs().get(1).getValue(); + if (!Restrictions.isAboveDust(bsqChangeOutputValue)) { + String msg = "BSQ change output is below dust limit. outputValue=" + bsqChangeOutputValue.value / 100 + " BSQ"; + log.warn(msg); + throw new BsqChangeBelowDustException(msg, bsqChangeOutputValue); + } + } + + return tx; + } catch (InsufficientMoneyException e) { + log.error("getPreparedSendTx: tx={}", tx.toString()); + log.error(e.toString()); + throw new InsufficientBsqException(e.missing); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Burn fee txs + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedTradeFeeTx(Coin fee) throws InsufficientBsqException { + daoKillSwitch.assertDaoIsNotDisabled(); + + Transaction tx = new Transaction(params); + addInputsAndChangeOutputForTx(tx, fee, bsqCoinSelector); + return tx; + } + + // We create a tx with Bsq inputs for the fee and optional BSQ change output. + // As the fee amount will be missing in the output those BSQ fees are burned. + public Transaction getPreparedProposalTx(Coin fee) throws InsufficientBsqException { + return getPreparedTxWithMandatoryBsqChangeOutput(fee); + } + + public Transaction getPreparedIssuanceTx(Coin fee) throws InsufficientBsqException { + return getPreparedTxWithMandatoryBsqChangeOutput(fee); + } + + public Transaction getPreparedProofOfBurnTx(Coin fee) throws InsufficientBsqException { + return getPreparedTxWithMandatoryBsqChangeOutput(fee); + } + + public Transaction getPreparedBurnFeeTxForAssetListing(Coin fee) throws InsufficientBsqException { + return getPreparedTxWithMandatoryBsqChangeOutput(fee); + } + + // We need to require one BSQ change output as we could otherwise not be able to distinguish between 2 + // structurally same transactions where only the BSQ fee is different. In case of asset listing fee and proof of + // burn it is a user input, so it is not known to the parser, instead we derive the burned fee from the parser. + + // In case of proposal fee we could derive it from the params. + + // For issuance txs we also require a BSQ change output before the issuance output gets added. There was a + // minor bug with the old version that multiple inputs would have caused an exception in case there was no + // change output (e.g. inputs of 21 and 6 BSQ for BSQ fee of 21 BSQ would have caused that only 1 input was used + // and then caused an error as we enforced a change output. This new version handles such cases correctly. + + // Examples for the structurally indistinguishable transactions: + // Case 1: 10 BSQ fee to burn + // In: 17 BSQ + // Out: BSQ change 7 BSQ -> valid BSQ + // Out: OpReturn + // Miner fee: 1000 sat (10 BSQ burned) + + // Case 2: 17 BSQ fee to burn + // In: 17 BSQ + // Out: burned BSQ change 7 BSQ -> BTC (7 BSQ burned) + // Out: OpReturn + // Miner fee: 1000 sat (10 BSQ burned) + + private Transaction getPreparedTxWithMandatoryBsqChangeOutput(Coin fee) throws InsufficientBsqException { + daoKillSwitch.assertDaoIsNotDisabled(); + + Transaction tx = new Transaction(params); + // We look for inputs covering out BSQ fee we want to pay. + CoinSelection coinSelection = bsqCoinSelector.select(fee, wallet.calculateAllSpendCandidates()); + try { + Coin change = bsqCoinSelector.getChange(fee, coinSelection); + if (change.isZero() || Restrictions.isDust(change)) { + // If change is zero or below dust we increase required input amount to enforce a BSQ change output. + // All outputs after that are considered BTC and therefore would be burned BSQ if BSQ is left from what + // we use for miner fee. + + Coin minDustThreshold = Coin.valueOf(preferences.getIgnoreDustThreshold()); + Coin increasedRequiredInput = fee.add(minDustThreshold); + coinSelection = bsqCoinSelector.select(increasedRequiredInput, wallet.calculateAllSpendCandidates()); + change = bsqCoinSelector.getChange(fee, coinSelection); + + log.warn("We increased required input as change output was zero or dust: New change value={}", change); + String info = "Available BSQ balance=" + coinSelection.valueGathered.value / 100 + " BSQ. " + + "Intended fee to burn=" + fee.value / 100 + " BSQ. " + + "Please increase your balance to at least " + (coinSelection.valueGathered.value + minDustThreshold.value) / 100 + " BSQ."; + checkArgument(coinSelection.valueGathered.compareTo(fee) > 0, + "This transaction require a change output of at least " + minDustThreshold.value / 100 + " BSQ (dust limit). " + + info); + + checkArgument(!Restrictions.isDust(change), + "This transaction would create a dust output of " + change.value / 100 + " BSQ. " + + "It requires a change output of at least " + minDustThreshold.value / 100 + " BSQ (dust limit). " + + info); + } + + coinSelection.gathered.forEach(tx::addInput); + tx.addOutput(change, getChangeAddress()); + + return tx; + + } catch (InsufficientMoneyException e) { + log.error("coinSelection.gathered={}", coinSelection.gathered); + throw new InsufficientBsqException(e.missing); + } + } + + private void addInputsAndChangeOutputForTx(Transaction tx, + Coin fee, + BsqCoinSelector bsqCoinSelector) + throws InsufficientBsqException { + Coin requiredInput; + // If our fee is less then dust limit we increase it so we are sure to not get any dust output. + if (Restrictions.isDust(fee)) { + requiredInput = fee.add(Restrictions.getMinNonDustOutput()); + } else { + requiredInput = fee; + } + + CoinSelection coinSelection = bsqCoinSelector.select(requiredInput, wallet.calculateAllSpendCandidates()); + coinSelection.gathered.forEach(tx::addInput); + try { + Coin change = bsqCoinSelector.getChange(fee, coinSelection); + // Change can be ZERO, then no change output is created so don't rely on a BSQ change output + if (change.isPositive()) { + checkArgument(Restrictions.isAboveDust(change), + "The change output of " + change.value / 100d + " BSQ is below the min. dust value of " + + Restrictions.getMinNonDustOutput().value / 100d + + ". At least " + Restrictions.getMinNonDustOutput().add(fee).value / 100d + + " BSQ is needed for this transaction"); + tx.addOutput(change, getChangeAddress()); + } + } catch (InsufficientMoneyException e) { + log.error(tx.toString()); + throw new InsufficientBsqException(e.missing); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Blind vote tx + /////////////////////////////////////////////////////////////////////////////////////////// + + // We create a tx with Bsq inputs for the fee, one output for the stake and optional one BSQ change output. + // As the fee amount will be missing in the output those BSQ fees are burned. + public Transaction getPreparedBlindVoteTx(Coin fee, Coin stake) throws InsufficientBsqException { + daoKillSwitch.assertDaoIsNotDisabled(); + Transaction tx = new Transaction(params); + tx.addOutput(new TransactionOutput(params, tx, stake, getUnusedAddress())); + addInputsAndChangeOutputForTx(tx, fee.add(stake), bsqCoinSelector); + //printTx("getPreparedBlindVoteTx", tx); + return tx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MyVote reveal tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedVoteRevealTx(TxOutput stakeTxOutput) { + daoKillSwitch.assertDaoIsNotDisabled(); + Transaction tx = new Transaction(params); + final Coin stake = Coin.valueOf(stakeTxOutput.getValue()); + Transaction blindVoteTx = getTransaction(stakeTxOutput.getTxId()); + checkNotNull(blindVoteTx, "blindVoteTx must not be null"); + TransactionOutPoint outPoint = new TransactionOutPoint(params, stakeTxOutput.getIndex(), blindVoteTx); + // Input is not signed yet so we use new byte[]{} + tx.addInput(new TransactionInput(params, tx, new byte[]{}, outPoint, stake)); + tx.addOutput(new TransactionOutput(params, tx, stake, getUnusedAddress())); + // printTx("getPreparedVoteRevealTx", tx); + return tx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lockup bond tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedLockupTx(Coin lockupAmount) throws AddressFormatException, InsufficientBsqException { + daoKillSwitch.assertDaoIsNotDisabled(); + Transaction tx = new Transaction(params); + checkArgument(Restrictions.isAboveDust(lockupAmount), "The amount is too low (dust limit)."); + tx.addOutput(new TransactionOutput(params, tx, lockupAmount, getUnusedAddress())); + addInputsAndChangeOutputForTx(tx, lockupAmount, bsqCoinSelector); + printTx("prepareLockupTx", tx); + return tx; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Unlock bond tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedUnlockTx(TxOutput lockupTxOutput) throws AddressFormatException { + daoKillSwitch.assertDaoIsNotDisabled(); + Transaction tx = new Transaction(params); + // Unlocking means spending the full value of the locked txOutput to another txOutput with the same value + Coin amountToUnlock = Coin.valueOf(lockupTxOutput.getValue()); + checkArgument(Restrictions.isAboveDust(amountToUnlock), "The amount is too low (dust limit)."); + Transaction lockupTx = getTransaction(lockupTxOutput.getTxId()); + checkNotNull(lockupTx, "lockupTx must not be null"); + TransactionOutPoint outPoint = new TransactionOutPoint(params, lockupTxOutput.getIndex(), lockupTx); + // Input is not signed yet so we use new byte[]{} + tx.addInput(new TransactionInput(params, tx, new byte[]{}, outPoint, amountToUnlock)); + tx.addOutput(new TransactionOutput(params, tx, amountToUnlock, getUnusedAddress())); + printTx("prepareUnlockTx", tx); + return tx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Addresses + /////////////////////////////////////////////////////////////////////////////////////////// + + private LegacyAddress getChangeAddress() { + return getUnusedAddress(); + } + + public LegacyAddress getUnusedAddress() { + return (LegacyAddress) wallet.getIssuedReceiveAddresses().stream() + .filter(this::isAddressUnused) + .findAny() + .orElse(wallet.freshReceiveAddress()); + } + + public String getUnusedBsqAddressAsString() { + return "B" + getUnusedAddress().toString(); + } + + // For BSQ we do not check for dust attack utxos as they are 5.46 BSQ and a considerable value. + // The default 546 sat dust limit is handled in the BitcoinJ side anyway. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return false; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java new file mode 100644 index 0000000000..91b0c84c57 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.btc.wallet; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.TransactionOutput; + +import com.google.common.collect.Sets; + +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +/** + * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. + * We lookup for spendable outputs which matches any of our addresses. + */ +@Slf4j +class BtcCoinSelector extends BisqDefaultCoinSelector { + private final Set
    addresses; + private final long ignoreDustThreshold; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + private BtcCoinSelector(Set
    addresses, long ignoreDustThreshold, boolean permitForeignPendingTx) { + super(permitForeignPendingTx); + this.addresses = addresses; + this.ignoreDustThreshold = ignoreDustThreshold; + } + + BtcCoinSelector(Set
    addresses, long ignoreDustThreshold) { + this(addresses, ignoreDustThreshold, true); + } + + BtcCoinSelector(Address address, long ignoreDustThreshold, @SuppressWarnings("SameParameterValue") boolean permitForeignPendingTx) { + this(Sets.newHashSet(address), ignoreDustThreshold, permitForeignPendingTx); + } + + BtcCoinSelector(Address address, long ignoreDustThreshold) { + this(Sets.newHashSet(address), ignoreDustThreshold, true); + } + + @Override + protected boolean isTxOutputSpendable(TransactionOutput output) { + if (WalletService.isOutputScriptConvertibleToAddress(output)) { + Address address = WalletService.getAddressFromOutput(output); + return addresses.contains(address); + } else { + log.warn("transactionOutput.getScriptPubKey() is not P2PKH nor P2SH nor P2WH"); + return false; + } + } + + // We ignore utxos which are considered dust attacks for spying on users' wallets. + // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust + // value of 546 sat. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return output.getValue().value < ignoreDustThreshold; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java new file mode 100644 index 0000000000..76752a87f6 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -0,0 +1,1335 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.AddressEntryException; +import bisq.core.btc.exceptions.InsufficientFundsException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.AddressEntryList; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.SegwitAddress; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptPattern; +import org.bitcoinj.wallet.SendRequest; +import org.bitcoinj.wallet.Wallet; + +import javax.inject.Inject; + +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class BtcWalletService extends WalletService { + private static final Logger log = LoggerFactory.getLogger(BtcWalletService.class); + + private final AddressEntryList addressEntryList; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BtcWalletService(WalletsSetup walletsSetup, + AddressEntryList addressEntryList, + Preferences preferences, + FeeService feeService) { + super(walletsSetup, + preferences, + feeService); + + this.addressEntryList = addressEntryList; + + walletsSetup.addSetupCompletedHandler(() -> { + wallet = walletsSetup.getBtcWallet(); + addListenersToWallet(); + + walletsSetup.getChain().addNewBestBlockListener(block -> chainHeightProperty.set(block.getHeight())); + chainHeightProperty.set(walletsSetup.getChain().getBestChainHeight()); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Overridden Methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + void decryptWallet(@NotNull KeyParameter key) { + super.decryptWallet(key); + + addressEntryList.getAddressEntriesAsListImmutable().forEach(e -> { + DeterministicKey keyPair = e.getKeyPair(); + if (keyPair.isEncrypted()) + e.setDeterministicKey(keyPair.decrypt(key)); + }); + addressEntryList.requestPersistence(); + } + + @Override + void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { + super.encryptWallet(keyCrypterScrypt, key); + addressEntryList.getAddressEntriesAsListImmutable().forEach(e -> { + DeterministicKey keyPair = e.getKeyPair(); + if (keyPair.isEncrypted()) + e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key)); + }); + addressEntryList.requestPersistence(); + } + + @Override + String getWalletAsString(boolean includePrivKeys) { + StringBuilder sb = new StringBuilder(); + getAddressEntryListAsImmutableList().forEach(e -> sb.append(e.toString()).append("\n")); + //boolean reallyIncludePrivKeys = includePrivKeys && !wallet.isEncrypted(); + return "Address entry list:\n" + + sb.toString() + + "\n\n" + + wallet.toString(true, includePrivKeys, this.aesKey, true, true, walletsSetup.getChain()) + "\n\n" + + "All pubKeys as hex:\n" + + wallet.printAllPubKeysAsHex(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public Methods + /////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////// + // Burn BSQ txs (some proposal txs, asset listing fee tx, proof of burn tx) + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction completePreparedBurnBsqTx(Transaction preparedBurnFeeTx, byte[] opReturnData) + throws WalletException, InsufficientMoneyException, TransactionVerificationException { + return completePreparedProposalTx(preparedBurnFeeTx, opReturnData, null, null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Proposal txs + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { + return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); + } + + public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { + return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); + } + + private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opReturnData, + @Nullable Coin issuanceAmount, @Nullable Address issuanceAddress) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { + + // (BsqFee)tx has following structure: + // inputs [1-n] BSQ inputs (fee) + // outputs [0-1] BSQ request fee change output (>= 546 Satoshi) + + // preparedCompensationRequestTx has following structure: + // inputs [1-n] BSQ inputs for request fee + // inputs [1-n] BTC inputs for BSQ issuance and miner fee + // outputs [1] Mandatory BSQ request fee change output (>= 546 Satoshi) + // outputs [1] Potentially BSQ issuance output (>= 546 Satoshi) - in case of a issuance tx, otherwise that output does not exist + // outputs [0-1] BTC change output from issuance and miner fee inputs (>= 546 Satoshi) + // outputs [1] OP_RETURN with opReturnData and amount 0 + // mining fee: BTC mining fee + burned BSQ fee + + Transaction preparedTx = new Transaction(params); + // Copy inputs from BSQ fee tx + feeTx.getInputs().forEach(preparedTx::addInput); + int indexOfBtcFirstInput = feeTx.getInputs().size(); + + // Need to be first because issuance is not guaranteed to be valid and would otherwise burn change output! + // BSQ change outputs from BSQ fee inputs. + feeTx.getOutputs().forEach(preparedTx::addOutput); + + // For generic proposals there is no issuance output, for compensation and reimburse requests there is + if (issuanceAmount != null && issuanceAddress != null) { + // BSQ issuance output + preparedTx.addOutput(issuanceAmount, issuanceAddress); + } + + // safety check counter to avoid endless loops + int counter = 0; + // estimated size of input sig + int sigSizePerInput = 106; + // typical size for a tx with 3 inputs + int txVsizeWithUnsignedInputs = 300; + Coin txFeePerVbyte = feeService.getTxFeePerVbyte(); + + Address changeAddress = getFreshAddressEntry().getAddress(); + checkNotNull(changeAddress, "changeAddress must not be null"); + + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedTx.getInputs(); + List preparedBsqTxOutputs = preparedTx.getOutputs(); + Tuple2 numInputs = getNumInputs(preparedTx); + int numLegacyInputs = numInputs.first; + int numSegwitInputs = numInputs.second; + Transaction resultTx = null; + boolean isFeeOutsideTolerance; + do { + counter++; + if (counter >= 10) { + checkNotNull(resultTx, "resultTx must not be null"); + log.error("Could not calculate the fee. Tx=" + resultTx); + break; + } + + Transaction tx = new Transaction(params); + preparedBsqTxInputs.forEach(tx::addInput); + preparedBsqTxOutputs.forEach(tx::addOutput); + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) + sendRequest.signInputs = false; + + sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); + + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + + sendRequest.coinSelector = coinSelector; + sendRequest.changeAddress = changeAddress; + wallet.completeTx(sendRequest); + + resultTx = sendRequest.tx; + + // add OP_RETURN output + resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); + + numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txVsizeWithUnsignedInputs = resultTx.getVsize(); + long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; + + // calculated fee must be inside of a tolerance range with tx fee + isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; + } + while (isFeeOutsideTolerance); + + // Sign all BTC inputs + signAllBtcInputs(indexOfBtcFirstInput, resultTx); + + checkWalletConsistency(wallet); + verifyTransaction(resultTx); + + // printTx("BTC wallet: Signed tx", resultTx); + return resultTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Blind vote tx + /////////////////////////////////////////////////////////////////////////////////////////// + + // We add BTC inputs to pay miner fees and sign the BTC tx inputs + + // (BsqFee)tx has following structure: + // inputs [1-n] BSQ inputs (fee + stake) + // outputs [1] BSQ stake + // outputs [0-1] BSQ change output (>= 546 Satoshi) + + // preparedVoteTx has following structure: + // inputs [1-n] BSQ inputs (fee + stake) + // inputs [1-n] BTC inputs for miner fee + // outputs [1] BSQ stake + // outputs [0-1] BSQ change output (>= 546 Satoshi) + // outputs [0-1] BTC change output from miner fee inputs (>= 546 Satoshi) + // outputs [1] OP_RETURN with opReturnData and amount 0 + // mining fee: BTC mining fee + burned BSQ fee + public Transaction completePreparedBlindVoteTx(Transaction preparedTx, byte[] opReturnData) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { + // First input index for btc inputs (they get added after bsq inputs) + return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); + } + + private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, + byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException { + // Remember index for first BTC input + int indexOfBtcFirstInput = preparedTx.getInputs().size(); + + Transaction tx = addInputsForMinerFee(preparedTx, opReturnData); + signAllBtcInputs(indexOfBtcFirstInput, tx); + + checkWalletConsistency(wallet); + verifyTransaction(tx); + + // printTx("BTC wallet: Signed tx", tx); + return tx; + } + + private Transaction addInputsForMinerFee(Transaction preparedTx, + byte[] opReturnData) throws InsufficientMoneyException { + // safety check counter to avoid endless loops + int counter = 0; + // estimated size of input sig + int sigSizePerInput = 106; + // typical size for a tx with 3 inputs + int txVsizeWithUnsignedInputs = 300; + Coin txFeePerVbyte = feeService.getTxFeePerVbyte(); + + Address changeAddress = getFreshAddressEntry().getAddress(); + checkNotNull(changeAddress, "changeAddress must not be null"); + + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedTx.getInputs(); + List preparedBsqTxOutputs = preparedTx.getOutputs(); + Tuple2 numInputs = getNumInputs(preparedTx); + int numLegacyInputs = numInputs.first; + int numSegwitInputs = numInputs.second; + Transaction resultTx = null; + boolean isFeeOutsideTolerance; + do { + counter++; + if (counter >= 10) { + checkNotNull(resultTx, "resultTx must not be null"); + log.error("Could not calculate the fee. Tx=" + resultTx); + break; + } + + Transaction tx = new Transaction(params); + preparedBsqTxInputs.forEach(tx::addInput); + preparedBsqTxOutputs.forEach(tx::addOutput); + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) + sendRequest.signInputs = false; + + sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + + sendRequest.coinSelector = coinSelector; + sendRequest.changeAddress = changeAddress; + wallet.completeTx(sendRequest); + + resultTx = sendRequest.tx; + + // add OP_RETURN output + resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); + + numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txVsizeWithUnsignedInputs = resultTx.getVsize(); + final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; + // calculated fee must be inside of a tolerance range with tx fee + isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; + } + while (isFeeOutsideTolerance); + return resultTx; + } + + private void signAllBtcInputs(int indexOfBtcFirstInput, Transaction tx) throws TransactionVerificationException { + for (int i = indexOfBtcFirstInput; i < tx.getInputs().size(); i++) { + TransactionInput input = tx.getInputs().get(i); + checkArgument(input.getConnectedOutput() != null && input.getConnectedOutput().isMine(wallet), + "input.getConnectedOutput() is not in our wallet. That must not happen."); + signTransactionInput(wallet, aesKey, tx, input, i); + checkScriptSig(tx, input, i); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Vote reveal tx + /////////////////////////////////////////////////////////////////////////////////////////// + + // We add BTC fees to the prepared reveal tx + // (BsqFee)tx has following structure: + // inputs [1] BSQ input (stake) + // output [1] BSQ unlocked stake + + // preparedVoteTx has following structure: + // inputs [1] BSQ inputs (stake) + // inputs [1-n] BTC inputs for miner fee + // outputs [1] BSQ unlocked stake + // outputs [0-1] BTC change output from miner fee inputs (>= 546 Satoshi) + // outputs [1] OP_RETURN with opReturnData and amount 0 + // mining fee: BTC mining fee + burned BSQ fee + public Transaction completePreparedVoteRevealTx(Transaction preparedTx, byte[] opReturnData) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { + return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Add fee input to prepared BSQ send tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { + // preparedBsqTx has following structure: + // inputs [1-n] BSQ inputs + // outputs [1] BSQ receiver's output + // outputs [0-1] BSQ change output + + // We add BTC mining fee. Result tx looks like: + // inputs [1-n] BSQ inputs + // inputs [1-n] BTC inputs + // outputs [1] BSQ receiver's output + // outputs [0-1] BSQ change output + // outputs [0-1] BTC change output + // mining fee: BTC mining fee + Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte(); + return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte); + } + + public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, Coin txFeePerVbyte) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { + return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte); + } + + public Transaction completePreparedBsqTx(Transaction preparedBsqTx, + @Nullable byte[] opReturnData) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { + Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte(); + return completePreparedBsqTx(preparedBsqTx, opReturnData, txFeePerVbyte); + } + + public Transaction completePreparedBsqTx(Transaction preparedBsqTx, + @Nullable byte[] opReturnData, + Coin txFeePerVbyte) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { + + // preparedBsqTx has following structure: + // inputs [1-n] BSQ inputs + // outputs [1] BSQ receiver's output + // outputs [0-1] BSQ change output + // mining fee: optional burned BSQ fee (only if opReturnData != null) + + // We add BTC mining fee. Result tx looks like: + // inputs [1-n] BSQ inputs + // inputs [1-n] BTC inputs + // outputs [0-1] BSQ receiver's output + // outputs [0-1] BSQ change output + // outputs [0-1] BTC change output + // outputs [0-1] OP_RETURN with opReturnData (only if opReturnData != null) + // mining fee: BTC mining fee + optional burned BSQ fee (only if opReturnData != null) + + // In case of txs for burned BSQ fees we have no receiver output and it might be that there is no change outputs + // We need to guarantee that min. 1 valid output is added (OP_RETURN does not count). So we use a higher input + // for BTC to force an additional change output. + + // safety check counter to avoid endless loops + int counter = 0; + // estimated size of input sig + int sigSizePerInput = 106; + // typical size for a tx with 2 inputs + int txVsizeWithUnsignedInputs = 203; + // In case there are no change outputs we force a change by adding min dust to the BTC input + Coin forcedChangeValue = Coin.ZERO; + + Address changeAddress = getFreshAddressEntry().getAddress(); + checkNotNull(changeAddress, "changeAddress must not be null"); + + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedBsqTx.getInputs(); + List preparedBsqTxOutputs = preparedBsqTx.getOutputs(); + // We don't know at this point what type the btc input would be (segwit/legacy). + // We use legacy to be on the safe side. + int numLegacyInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input + int numSegwitInputs = 0; + Transaction resultTx = null; + boolean isFeeOutsideTolerance; + boolean opReturnIsOnlyOutput; + do { + counter++; + if (counter >= 10) { + checkNotNull(resultTx, "resultTx must not be null"); + log.error("Could not calculate the fee. Tx=" + resultTx); + break; + } + + Transaction tx = new Transaction(params); + preparedBsqTxInputs.stream().forEach(tx::addInput); + + if (forcedChangeValue.isZero()) { + preparedBsqTxOutputs.stream().forEach(tx::addOutput); + } else { + //TODO test that case + checkArgument(preparedBsqTxOutputs.size() == 0, "preparedBsqTxOutputs.size must be null in that code branch"); + tx.addOutput(forcedChangeValue, changeAddress); + } + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) + sendRequest.signInputs = false; + + sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + + sendRequest.coinSelector = coinSelector; + sendRequest.changeAddress = changeAddress; + wallet.completeTx(sendRequest); + + resultTx = sendRequest.tx; + + // We might have the rare case that both inputs matched the required fees, so both did not require + // a change output. + // In such cases we need to add artificially a change output (OP_RETURN is not allowed as only output) + opReturnIsOnlyOutput = resultTx.getOutputs().size() == 0; + forcedChangeValue = opReturnIsOnlyOutput ? Restrictions.getMinNonDustOutput() : Coin.ZERO; + + // add OP_RETURN output + if (opReturnData != null) + resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); + + Tuple2 numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txVsizeWithUnsignedInputs = resultTx.getVsize(); + final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; + // calculated fee must be inside of a tolerance range with tx fee + isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; + } + while (opReturnIsOnlyOutput || + isFeeOutsideTolerance || + resultTx.getFee().value < txFeePerVbyte.multiply(resultTx.getVsize()).value); + + // Sign all BTC inputs + signAllBtcInputs(preparedBsqTxInputs.size(), resultTx); + + checkWalletConsistency(wallet); + verifyTransaction(resultTx); + + printTx("BTC wallet: Signed tx", resultTx); + return resultTx; + } + + private Tuple2 getNumInputs(Transaction tx) { + int numLegacyInputs = 0; + int numSegwitInputs = 0; + for (TransactionInput input : tx.getInputs()) { + TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) || + ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) { + // If connectedOutput is null, we don't know here the input type. To avoid underpaying fees, + // we treat it as a legacy input which will result in a higher fee estimation. + numLegacyInputs++; + } else if (ScriptPattern.isP2WPKH(connectedOutput.getScriptPubKey())) { + numSegwitInputs++; + } else { + throw new IllegalArgumentException("Inputs should spend a P2PKH, P2PK or P2WPKH ouput"); + } + } + return new Tuple2(numLegacyInputs, numSegwitInputs); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Commit tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void commitTx(Transaction tx) { + wallet.commitTx(tx); + // printTx("BTC commit Tx", tx); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // AddressEntry + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional getAddressEntry(String offerId, + @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream() + .filter(e -> offerId.equals(e.getOfferId())) + .filter(e -> context == e.getContext()) + .findAny(); + } + + public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) { + Optional addressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> offerId.equals(e.getOfferId())) + .filter(e -> context == e.getContext()) + .findAny(); + if (addressEntry.isPresent()) { + return addressEntry.get(); + } else { + // We try to use available and not yet used entries + Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> AddressEntry.Context.AVAILABLE == e.getContext()) + .filter(e -> isAddressUnused(e.getAddress())) + .filter(e -> Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType())) + .findAny(); + if (emptyAvailableAddressEntry.isPresent()) { + return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); + } else { + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH)); + AddressEntry entry = new AddressEntry(key, context, offerId, true); + log.info("getOrCreateAddressEntry: new AddressEntry={}", entry); + addressEntryList.addAddressEntry(entry); + return entry; + } + } + } + + public AddressEntry getArbitratorAddressEntry() { + AddressEntry.Context context = AddressEntry.Context.ARBITRATOR; + Optional addressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> context == e.getContext()) + .findAny(); + return getOrCreateAddressEntry(context, addressEntry, false); + } + + public AddressEntry getFreshAddressEntry() { + return getFreshAddressEntry(true); + } + + public AddressEntry getFreshAddressEntry(boolean segwit) { + AddressEntry.Context context = AddressEntry.Context.AVAILABLE; + Optional addressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> context == e.getContext()) + .filter(e -> isAddressUnused(e.getAddress())) + .filter(e -> { + boolean isSegwitOutputScriptType = Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType()); + // We need to ensure that we take only addressEntries which matches our segWit flag + return isSegwitOutputScriptType == segwit; + }) + .findAny(); + return getOrCreateAddressEntry(context, addressEntry, segwit); + } + + public void recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { + findAddressEntry(address, AddressEntry.Context.AVAILABLE).ifPresent(addressEntry -> + addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId)); + } + + private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, + Optional addressEntry, + boolean segwit) { + if (addressEntry.isPresent()) { + return addressEntry.get(); + } else { + DeterministicKey key; + if (segwit) { + key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH)); + } else { + key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); + } + AddressEntry entry = new AddressEntry(key, context, segwit); + log.info("getOrCreateAddressEntry: add new AddressEntry {}", entry); + addressEntryList.addAddressEntry(entry); + return entry; + } + } + + private Optional findAddressEntry(String address, AddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream() + .filter(e -> address.equals(e.getAddressString())) + .filter(e -> context == e.getContext()) + .findAny(); + } + + public List getAvailableAddressEntries() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> AddressEntry.Context.AVAILABLE == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntriesForOpenOffer() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> AddressEntry.Context.OFFER_FUNDING == addressEntry.getContext() || + AddressEntry.Context.RESERVED_FOR_TRADE == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntriesForTrade() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> AddressEntry.Context.MULTI_SIG == addressEntry.getContext() || + AddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntries(AddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> context == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getFundedAvailableAddressEntries() { + return getAvailableAddressEntries().stream() + .filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive()) + .collect(Collectors.toList()); + } + + public List getAddressEntryListAsImmutableList() { + return addressEntryList.getAddressEntriesAsListImmutable(); + } + + public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) { + if (context == AddressEntry.Context.MULTI_SIG) { + log.error("swapTradeEntryToAvailableEntry called with MULTI_SIG context. " + + "This in not permitted as we must not reuse those address entries and there " + + "are no redeemable funds on that addresses. Only the keys are used for creating " + + "the Multisig address. offerId={}, context={}", offerId, context); + return; + } + + getAddressEntryListAsImmutableList().stream() + .filter(e -> offerId.equals(e.getOfferId())) + .filter(e -> context == e.getContext()) + .forEach(e -> { + log.info("swap addressEntry with address {} and offerId {} from context {} to available", + e.getAddressString(), e.getOfferId(), context); + addressEntryList.swapToAvailable(e); + }); + } + + // When funds from MultiSig address is spent we reset the coinLockedInMultiSig value to 0. + public void resetCoinLockedInMultiSigAddressEntry(String offerId) { + setCoinLockedInMultiSigAddressEntry(offerId, 0); + } + + public void setCoinLockedInMultiSigAddressEntry(String offerId, long value) { + getAddressEntryListAsImmutableList().stream() + .filter(e -> AddressEntry.Context.MULTI_SIG == e.getContext()) + .filter(e -> offerId.equals(e.getOfferId())) + .forEach(addressEntry -> setCoinLockedInMultiSigAddressEntry(addressEntry, value)); + } + + public void setCoinLockedInMultiSigAddressEntry(AddressEntry addressEntry, long value) { + log.info("Set coinLockedInMultiSig for addressEntry {} to value {}", addressEntry, value); + addressEntryList.setCoinLockedInMultiSigAddressEntry(addressEntry, value); + } + + public void resetAddressEntriesForOpenOffer(String offerId) { + log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING); + swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE); + } + + public void resetAddressEntriesForPendingTrade(String offerId) { + // We must not swap MULTI_SIG entries as those addresses are not detected in the isAddressUnused + // check at getOrCreateAddressEntry and could lead to a reuse of those keys and result in the same 2of2 MS + // address if same peers trade again. + + // We swap TRADE_PAYOUT to be sure all is cleaned up. There might be cases where a user cannot send the funds + // to an external wallet directly in the last step of the trade, but the funds are in the Bisq wallet anyway and + // the dealing with the external wallet is pure UI thing. The user can move the funds to the wallet and then + // send out the funds to the external wallet. As this cleanup is a rare situation and most users do not use + // the feature to send out the funds we prefer that strategy (if we keep the address entry it might cause + // complications in some edge cases after a SPV resync). + swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.TRADE_PAYOUT); + } + + public void swapAnyTradeEntryContextToAvailableEntry(String offerId) { + resetAddressEntriesForOpenOffer(offerId); + resetAddressEntriesForPendingTrade(offerId); + } + + public void saveAddressEntryList() { + addressEntryList.requestPersistence(); + } + + public DeterministicKey getMultiSigKeyPair(String tradeId, byte[] pubKey) { + Optional multiSigAddressEntryOptional = getAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG); + DeterministicKey multiSigKeyPair; + if (multiSigAddressEntryOptional.isPresent()) { + AddressEntry multiSigAddressEntry = multiSigAddressEntryOptional.get(); + multiSigKeyPair = multiSigAddressEntry.getKeyPair(); + if (!Arrays.equals(pubKey, multiSigAddressEntry.getPubKey())) { + log.error("Pub Key from AddressEntry does not match key pair from trade data. Trade ID={}\n" + + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); + multiSigKeyPair = findKeyFromPubKey(pubKey); + } + } else { + log.error("multiSigAddressEntry not found for trade ID={}.\n" + + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); + multiSigKeyPair = findKeyFromPubKey(pubKey); + } + + return multiSigKeyPair; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Balance + /////////////////////////////////////////////////////////////////////////////////////////// + + public Coin getSavingWalletBalance() { + return Coin.valueOf(getFundedAvailableAddressEntries().stream() + .mapToLong(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).value) + .sum()); + } + + public Stream getAddressEntriesForAvailableBalanceStream() { + Stream availableAndPayout = Stream.concat(getAddressEntries(AddressEntry.Context.TRADE_PAYOUT) + .stream(), getFundedAvailableAddressEntries().stream()); + Stream available = Stream.concat(availableAndPayout, + getAddressEntries(AddressEntry.Context.ARBITRATOR).stream()); + available = Stream.concat(available, getAddressEntries(AddressEntry.Context.OFFER_FUNDING).stream()); + return available.filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Double spend unconfirmed transaction (unlock in case we got into a tx with a too low mining fee) + /////////////////////////////////////////////////////////////////////////////////////////// + + public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMessageHandler errorMessageHandler) + throws InsufficientFundsException { + AddressEntry addressEntry = getFreshAddressEntry(); + checkNotNull(addressEntry.getAddress(), "addressEntry.getAddress() must not be null"); + Optional transactionOptional = wallet.getTransactions(true).stream() + .filter(t -> t.getTxId().toString().equals(txId)) + .findAny(); + if (transactionOptional.isPresent()) { + Transaction txToDoubleSpend = transactionOptional.get(); + Address toAddress = addressEntry.getAddress(); + final TransactionConfidence.ConfidenceType confidenceType = txToDoubleSpend.getConfidence().getConfidenceType(); + if (confidenceType == TransactionConfidence.ConfidenceType.PENDING) { + log.debug("txToDoubleSpend no. of inputs " + txToDoubleSpend.getInputs().size()); + + Transaction newTransaction = new Transaction(params); + txToDoubleSpend.getInputs().stream().forEach(input -> { + final TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput != null && + connectedOutput.isMine(wallet) && + connectedOutput.getParentTransaction() != null && + connectedOutput.getParentTransaction().getConfidence() != null && + input.getValue() != null) { + //if (connectedOutput.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + newTransaction.addInput(new TransactionInput(params, + newTransaction, + new byte[]{}, + new TransactionOutPoint(params, input.getOutpoint().getIndex(), + new Transaction(params, connectedOutput.getParentTransaction().bitcoinSerialize())), + Coin.valueOf(input.getValue().value))); + /* } else { + log.warn("Confidence of parent tx is not of type BUILDING: ConfidenceType=" + + connectedOutput.getParentTransaction().getConfidence().getConfidenceType()); + }*/ + } + } + ); + + log.info("newTransaction no. of inputs " + newTransaction.getInputs().size()); + log.info("newTransaction vsize in vkB " + newTransaction.getVsize() / 1024); + + if (!newTransaction.getInputs().isEmpty()) { + Coin amount = Coin.valueOf(newTransaction.getInputs().stream() + .mapToLong(input -> input.getValue() != null ? input.getValue().value : 0) + .sum()); + newTransaction.addOutput(amount, toAddress); + + try { + Coin fee; + int counter = 0; + int txVsize = 0; + Transaction tx; + SendRequest sendRequest; + Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); + do { + counter++; + fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); + newTransaction.clearOutputs(); + newTransaction.addOutput(amount.subtract(fee), toAddress); + + sendRequest = SendRequest.forTx(newTransaction); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold()); + sendRequest.changeAddress = toAddress; + wallet.completeTx(sendRequest); + tx = sendRequest.tx; + txVsize = tx.getVsize(); + printTx("FeeEstimationTransaction", tx); + sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString())); + } + while (feeEstimationNotSatisfied(counter, tx)); + + if (counter == 10) + log.error("Could not calculate the fee. Tx=" + tx); + + + Wallet.SendResult sendResult = null; + try { + sendRequest = SendRequest.forTx(newTransaction); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold()); + sendRequest.changeAddress = toAddress; + sendResult = wallet.sendCoins(sendRequest); + } catch (InsufficientMoneyException e) { + // in some cases getFee did not calculate correctly and we still get an InsufficientMoneyException + log.warn("We still have a missing fee " + (e.missing != null ? e.missing.toFriendlyString() : "")); + + amount = amount.subtract(e.missing); + newTransaction.clearOutputs(); + newTransaction.addOutput(amount, toAddress); + + sendRequest = SendRequest.forTx(newTransaction); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.coinSelector = new BtcCoinSelector(toAddress, + preferences.getIgnoreDustThreshold(), false); + sendRequest.changeAddress = toAddress; + + try { + sendResult = wallet.sendCoins(sendRequest); + printTx("FeeEstimationTransaction", newTransaction); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + } catch (InsufficientMoneyException e2) { + errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. " + (e2.missing != null ? e2.missing.toFriendlyString() : "")); + } + } + if (sendResult != null) { + log.info("Broadcasting double spending transaction. " + sendResult.tx); + Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() { + @Override + public void onSuccess(Transaction result) { + log.info("Double spending transaction published. " + result); + resultHandler.run(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + log.error("Broadcasting double spending transaction failed. " + t.getMessage()); + errorMessageHandler.handleErrorMessage(t.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + } catch (InsufficientMoneyException e) { + throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + + "or the resulting output value is below the min. dust value:\n" + + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); + } + } else { + String errorMessage = "We could not find inputs we control in the transaction we want to double spend."; + log.warn(errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + } + } else if (confidenceType == TransactionConfidence.ConfidenceType.BUILDING) { + errorMessageHandler.handleErrorMessage("That transaction is already in the blockchain so we cannot double spend it."); + } else if (confidenceType == TransactionConfidence.ConfidenceType.DEAD) { + errorMessageHandler.handleErrorMessage("One of the inputs of that transaction has been already double spent."); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Withdrawal Fee calculation + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getFeeEstimationTransaction(String fromAddress, + String toAddress, + Coin amount, + AddressEntry.Context context) + throws AddressFormatException, AddressEntryException, InsufficientFundsException { + + Optional addressEntry = findAddressEntry(fromAddress, context); + if (!addressEntry.isPresent()) + throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); + + checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); + + try { + Coin fee; + int counter = 0; + int txVsize = 0; + Transaction tx; + Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); + do { + counter++; + fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); + SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context); + wallet.completeTx(sendRequest); + tx = sendRequest.tx; + txVsize = tx.getVsize(); + printTx("FeeEstimationTransaction", tx); + } + while (feeEstimationNotSatisfied(counter, tx)); + if (counter == 10) + log.error("Could not calculate the fee. Tx=" + tx); + + return tx; + } catch (InsufficientMoneyException e) { + throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + + "or the resulting output value is below the min. dust value:\n" + + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); + } + } + + public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, + Coin amount) + throws AddressFormatException, AddressEntryException, InsufficientFundsException { + Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); + return getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amount, txFeeForWithdrawalPerVbyte); + } + + public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, + Coin amount, + Coin txFeeForWithdrawalPerVbyte) + throws AddressFormatException, AddressEntryException, InsufficientFundsException { + Set addressEntries = fromAddresses.stream() + .map(address -> { + Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); + return addressEntryOptional; + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + if (addressEntries.isEmpty()) + throw new AddressEntryException("No Addresses for withdraw found in our wallet"); + + try { + Coin fee; + int counter = 0; + int txVsize = 0; + Transaction tx; + do { + counter++; + fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); + // We use a dummy address for the output + // We don't know here whether the output is segwit or not but we don't care too much because the size of + // a segwit ouput is just 3 byte smaller than the size of a legacy ouput. + final String dummyReceiver = SegwitAddress.fromKey(params, new ECKey()).toString(); + SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey); + wallet.completeTx(sendRequest); + tx = sendRequest.tx; + txVsize = tx.getVsize(); + printTx("FeeEstimationTransactionForMultipleAddresses", tx); + } + while (feeEstimationNotSatisfied(counter, tx)); + if (counter == 10) + log.error("Could not calculate the fee. Tx=" + tx); + + return tx; + } catch (InsufficientMoneyException e) { + throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + + "or the resulting output value is below the min. dust value:\n" + + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); + } + } + + private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { + return feeEstimationNotSatisfied(counter, tx, getTxFeeForWithdrawalPerVbyte()); + } + + private boolean feeEstimationNotSatisfied(int counter, Transaction tx, Coin txFeeForWithdrawalPerVbyte) { + long targetFee = txFeeForWithdrawalPerVbyte.multiply(tx.getVsize()).value; + return counter < 10 && + (tx.getFee().value < targetFee || + tx.getFee().value - targetFee > 1000); + } + + public int getEstimatedFeeTxVsize(List outputValues, Coin txFee) + throws InsufficientMoneyException, AddressFormatException { + Transaction transaction = new Transaction(params); + // In reality txs have a mix of segwit/legacy ouputs, but we don't care too much because the size of + // a segwit ouput is just 3 byte smaller than the size of a legacy ouput. + Address dummyAddress = SegwitAddress.fromKey(params, new ECKey()); + outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress)); + + SendRequest sendRequest = SendRequest.forTx(transaction); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + sendRequest.fee = txFee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.changeAddress = dummyAddress; + wallet.completeTx(sendRequest); + return transaction.getVsize(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Withdrawal Send + /////////////////////////////////////////////////////////////////////////////////////////// + + public String sendFunds(String fromAddress, + String toAddress, + Coin receiverAmount, + Coin fee, + @Nullable KeyParameter aesKey, + @SuppressWarnings("SameParameterValue") AddressEntry.Context context, + @Nullable String memo, + FutureCallback callback) throws AddressFormatException, + AddressEntryException, InsufficientMoneyException { + SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context); + Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); + Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor()); + if (memo != null) { + sendResult.tx.setMemo(memo); + } + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + + return sendResult.tx.getTxId().toString(); + } + + public Transaction sendFundsForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin receiverAmount, + Coin fee, + @Nullable String changeAddress, + @Nullable KeyParameter aesKey, + @Nullable String memo, + FutureCallback callback) throws AddressFormatException, + AddressEntryException, InsufficientMoneyException { + + SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey); + Wallet.SendResult sendResult = wallet.sendCoins(request); + Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor()); + if (memo != null) { + sendResult.tx.setMemo(memo); + } + printTx("sendFunds", sendResult.tx); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + + return sendResult.tx; + } + + private SendRequest getSendRequest(String fromAddress, + String toAddress, + Coin amount, + Coin fee, + @Nullable KeyParameter aesKey, + AddressEntry.Context context) throws AddressFormatException, + AddressEntryException { + Transaction tx = new Transaction(params); + final Coin receiverAmount = amount.subtract(fee); + Preconditions.checkArgument(Restrictions.isAboveDust(receiverAmount), + "The amount is too low (dust limit)."); + tx.addOutput(receiverAmount, Address.fromString(params, toAddress)); + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + Optional addressEntry = findAddressEntry(fromAddress, context); + if (!addressEntry.isPresent()) + throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); + + checkNotNull(addressEntry.get(), "addressEntry.get() must not be null"); + checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null"); + sendRequest.coinSelector = new BtcCoinSelector(addressEntry.get().getAddress(), preferences.getIgnoreDustThreshold()); + sendRequest.changeAddress = addressEntry.get().getAddress(); + return sendRequest; + } + + private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin amount, + Coin fee, + @Nullable String changeAddress, + @Nullable KeyParameter aesKey) throws + AddressFormatException, AddressEntryException { + Transaction tx = new Transaction(params); + final Coin netValue = amount.subtract(fee); + checkArgument(Restrictions.isAboveDust(netValue), + "The amount is too low (dust limit)."); + + tx.addOutput(netValue, Address.fromString(params, toAddress)); + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + Set addressEntries = fromAddresses.stream() + .map(address -> { + Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); + return addressEntryOptional; + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + if (addressEntries.isEmpty()) + throw new AddressEntryException("No Addresses for withdraw found in our wallet"); + + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries), + preferences.getIgnoreDustThreshold()); + Optional addressEntryOptional = Optional.empty(); + + if (changeAddress != null) + addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE); + + AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry); + checkNotNull(changeAddressAddressEntry, "change address must not be null"); + sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); + return sendRequest; + } + + // We ignore utxos which are considered dust attacks for spying on users' wallets. + // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust + // value of 546 sat. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return output.getValue().value < preferences.getIgnoreDustThreshold(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Refund payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction createRefundPayoutTx(Coin buyerAmount, + Coin sellerAmount, + Coin fee, + String buyerAddressString, + String sellerAddressString) + throws AddressFormatException, InsufficientMoneyException, WalletException, TransactionVerificationException { + Transaction tx = new Transaction(params); + Preconditions.checkArgument(buyerAmount.add(sellerAmount).isPositive(), + "The sellerAmount + buyerAmount must be positive."); + // buyerAmount can be 0 + if (buyerAmount.isPositive()) { + Preconditions.checkArgument(Restrictions.isAboveDust(buyerAmount), + "The buyerAmount is too low (dust limit)."); + + tx.addOutput(buyerAmount, Address.fromString(params, buyerAddressString)); + } + // sellerAmount can be 0 + if (sellerAmount.isPositive()) { + Preconditions.checkArgument(Restrictions.isAboveDust(sellerAmount), + "The sellerAmount is too low (dust limit)."); + + tx.addOutput(sellerAmount, Address.fromString(params, sellerAddressString)); + } + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + sendRequest.changeAddress = getFreshAddressEntry().getAddress(); + + checkNotNull(wallet); + wallet.completeTx(sendRequest); + + Transaction resultTx = sendRequest.tx; + checkWalletConsistency(wallet); + verifyTransaction(resultTx); + + WalletService.printTx("createRefundPayoutTx", resultTx); + + return resultTx; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java new file mode 100644 index 0000000000..8de488ae8e --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.user.Preferences; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionOutput; + +import javax.inject.Inject; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. + * We lookup for spendable outputs which matches our address of our address. + */ +@Slf4j +public class NonBsqCoinSelector extends BisqDefaultCoinSelector { + private DaoStateService daoStateService; + @Setter + private Preferences preferences; + + @Inject + public NonBsqCoinSelector(DaoStateService daoStateService) { + super(false); + this.daoStateService = daoStateService; + } + + @Override + protected boolean isTxOutputSpendable(TransactionOutput output) { + // output.getParentTransaction() cannot be null as it is checked in calling method + Transaction parentTransaction = output.getParentTransaction(); + if (parentTransaction == null) + return false; + + // It is important to not allow pending txs as otherwise unconfirmed BSQ txs would be considered nonBSQ as + // below outputIsNotInBsqState would be true. + if (parentTransaction.getConfidence().getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING) + return false; + + TxOutputKey key = new TxOutputKey(parentTransaction.getTxId().toString(), output.getIndex()); + // It might be that we received BTC in a non-BSQ tx so that will not be stored in out state and not found. + // So we consider any txOutput which is not in the state as BTC output. + return !daoStateService.existsTxOutput(key) || daoStateService.isRejectedIssuanceOutput(key); + } + + // Prevent usage of dust attack utxos + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return output.getValue().value < preferences.getIgnoreDustThreshold(); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/Restrictions.java b/core/src/main/java/bisq/core/btc/wallet/Restrictions.java new file mode 100644 index 0000000000..e7d4f92b6b --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/Restrictions.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +public class Restrictions { + private static Coin MIN_TRADE_AMOUNT; + private static Coin MIN_BUYER_SECURITY_DEPOSIT; + // 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. + private static Coin SELLER_SECURITY_DEPOSIT; + // 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 Coin MIN_REFUND_AT_MEDIATED_DISPUTE; + + public static Coin getMinNonDustOutput() { + if (minNonDustOutput == null) + minNonDustOutput = Config.baseCurrencyNetwork().getParameters().getMinNonDustOutput(); + return minNonDustOutput; + } + + private static Coin minNonDustOutput; + + public static boolean isAboveDust(Coin amount) { + return amount.compareTo(getMinNonDustOutput()) >= 0; + } + + public static boolean isDust(Coin amount) { + return !isAboveDust(amount); + } + + public static Coin getMinTradeAmount() { + if (MIN_TRADE_AMOUNT == null) + MIN_TRADE_AMOUNT = Coin.valueOf(10_000); // 0,7 USD @ 7000 USD/BTC + return MIN_TRADE_AMOUNT; + } + + public static double getDefaultBuyerSecurityDepositAsPercent() { + return 0.15; // 15% of trade amount. + } + + public static double getMinBuyerSecurityDepositAsPercent() { + return 0.15; // 15% of trade amount. + } + + public static double getMaxBuyerSecurityDepositAsPercent() { + return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC + } + + // 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 Coin getMinBuyerSecurityDepositAsCoin() { + if (MIN_BUYER_SECURITY_DEPOSIT == null) + MIN_BUYER_SECURITY_DEPOSIT = Coin.parseCoin("0.001"); // 0.001 BTC is 60 USD @ 60000 USD/BTC + return MIN_BUYER_SECURITY_DEPOSIT; + } + + + public static double getSellerSecurityDepositAsPercent() { + return 0.15; // 15% of trade amount. + } + + public static Coin getMinSellerSecurityDepositAsCoin() { + if (SELLER_SECURITY_DEPOSIT == null) + SELLER_SECURITY_DEPOSIT = Coin.parseCoin("0.001"); // 0.001 BTC is 60 USD @ 60000 USD/BTC + return SELLER_SECURITY_DEPOSIT; + } + + // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT + public static Coin getMinRefundAtMediatedDispute() { + if (MIN_REFUND_AT_MEDIATED_DISPUTE == null) + MIN_REFUND_AT_MEDIATED_DISPUTE = Coin.parseCoin("0.0005"); // 0.0005 BTC is 30 USD @ 60000 USD/BTC + return MIN_REFUND_AT_MEDIATED_DISPUTE; + } + + public static int getLockTime(boolean isAsset) { + // 10 days for altcoins, 20 days for other payment methods + return isAsset ? 144 * 10 : 144 * 20; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java new file mode 100644 index 0000000000..4244e78534 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -0,0 +1,1420 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.SigningException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.InputsAndChangeOutput; +import bisq.core.btc.model.PreparedDepositTxAndMakerInputs; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.setup.WalletConfig; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import bisq.common.config.Config; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.SegwitAddress; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; +import org.bitcoinj.core.Utils; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptPattern; +import org.bitcoinj.wallet.SendRequest; +import org.bitcoinj.wallet.Wallet; + +import javax.inject.Inject; + +import com.google.common.collect.ImmutableList; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class TradeWalletService { + private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class); + private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000); + + private final WalletsSetup walletsSetup; + private final Preferences preferences; + private final NetworkParameters params; + + @Nullable + private Wallet wallet; + @Nullable + private WalletConfig walletConfig; + @Nullable + private KeyParameter aesKey; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) { + this.walletsSetup = walletsSetup; + this.preferences = preferences; + this.params = Config.baseCurrencyNetworkParameters(); + walletsSetup.addSetupCompletedHandler(() -> { + walletConfig = walletsSetup.getWalletConfig(); + wallet = walletsSetup.getBtcWallet(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // AesKey + /////////////////////////////////////////////////////////////////////////////////////////// + + void setAesKey(@Nullable KeyParameter newAesKey) { + this.aesKey = newAesKey; + } + + @Nullable + public KeyParameter getAesKey() { + return aesKey; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trade fee + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create a BTC trading fee transaction for the maker or taker of an offer. The first output of the tx is for the + * fee receiver. The second output is the reserve of the trade. There is an optional third output for change. + * + * @param fundingAddress the provided source of funds in case the savings wallet is not used + * @param reservedForTradeAddress the address of the trade reserve + * @param changeAddress the change address to use in case of overpayment or use of the savings wallet + * @param reservedFundsForOffer the amount to reserve for the trade + * @param useSavingsWallet {@code true} to use the savings wallet, {@code false} to use the funding address + * @param tradingFee the amount of the trading fee + * @param txFee the mining fee for this transaction + * @param feeReceiverAddress the address of the receiver of the trading fee + * @param doBroadcast {@code true} to broadcast the transaction, {@code false} otherwise + * @param callback an optional callback to use when broadcasting the transaction + * @return the optionally broadcast transaction + * @throws InsufficientMoneyException if the request could not be completed due to not enough balance + * @throws AddressFormatException if the fee receiver base58 address doesn't parse or its checksum is invalid + */ + public Transaction createBtcTradingFeeTx(Address fundingAddress, + Address reservedForTradeAddress, + Address changeAddress, + Coin reservedFundsForOffer, + boolean useSavingsWallet, + Coin tradingFee, + Coin txFee, + String feeReceiverAddress, + boolean doBroadcast, + @Nullable TxBroadcaster.Callback callback) throws InsufficientMoneyException, AddressFormatException { + Transaction tradingFeeTx = new Transaction(params); + SendRequest sendRequest = null; + try { + tradingFeeTx.addOutput(tradingFee, Address.fromString(params, feeReceiverAddress)); + // the reserved amount we need for the trade we send to our trade reservedForTradeAddress + tradingFeeTx.addOutput(reservedFundsForOffer, reservedForTradeAddress); + + // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to + // wait for 1 confirmation) + // In case of double spend we will detect later in the trade process and use a ban score to penalize bad behaviour (not impl. yet) + sendRequest = SendRequest.forTx(tradingFeeTx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + if (useSavingsWallet) { + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + } else { + sendRequest.coinSelector = new BtcCoinSelector(fundingAddress, preferences.getIgnoreDustThreshold()); + } + // We use a fixed fee + + sendRequest.fee = txFee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + + // Change is optional in case of overpay or use of funds from savings wallet + sendRequest.changeAddress = changeAddress; + + checkNotNull(wallet, "Wallet must not be null"); + wallet.completeTx(sendRequest); + if (removeDust(tradingFeeTx)) { + wallet.signTransaction(sendRequest); + } + WalletService.printTx("tradingFeeTx", tradingFeeTx); + + if (doBroadcast && callback != null) { + broadcastTx(tradingFeeTx, callback); + } + + return tradingFeeTx; + } catch (Throwable t) { + if (wallet != null && sendRequest != null && sendRequest.coinSelector != null) { + log.error("Balance = {}; CoinSelector = {}", wallet.getBalance(sendRequest.coinSelector), sendRequest.coinSelector); + } + log.error("createBtcTradingFeeTx failed: tradingFeeTx={}, txOutputs={}", tradingFeeTx.toString(), + tradingFeeTx.getOutputs()); + throw t; + } + } + + public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, + Address fundingAddress, + Address reservedForTradeAddress, + Address changeAddress, + Coin reservedFundsForOffer, + boolean useSavingsWallet, + Coin txFee) + throws TransactionVerificationException, WalletException, InsufficientMoneyException, AddressFormatException { + try { + // preparedBsqTx has following structure: + // inputs [1-n] BSQ inputs + // outputs [0-1] BSQ change output + // mining fee: burned BSQ fee + + // We add BTC mining fee. Result tx looks like: + // inputs [1-n] BSQ inputs + // inputs [1-n] BTC inputs + // outputs [0-1] BSQ change output + // outputs [1] BTC reservedForTrade output + // outputs [0-1] BTC change output + // mining fee: BTC mining fee + burned BSQ fee + + // In case all BSQ were burnt as fees we have no receiver output and it might be that there are no change outputs + // We need to guarantee that min. 1 valid output is added (OP_RETURN does not count). So we use a higher input + // for BTC to force an additional change output. + + final int preparedBsqTxInputsSize = preparedBsqTx.getInputs().size(); + final boolean hasBsqOutputs = !preparedBsqTx.getOutputs().isEmpty(); + + // If there are no BSQ change outputs an output larger than the burnt BSQ amount has to be added as the first + // output to make sure the reserved funds are in output 1, deposit tx input creation depends on the reserve + // being output 1. The amount has to be larger than the BSQ input to make sure the inputs get burnt. + // The BTC changeAddress is used, so it might get used for both output 0 and output 2. + if (!hasBsqOutputs) { + var bsqInputValue = preparedBsqTx.getInputs().stream() + .map(TransactionInput::getValue) + .reduce(Coin.valueOf(0), Coin::add); + + preparedBsqTx.addOutput(bsqInputValue.add(Coin.valueOf(1)), changeAddress); + } + // the reserved amount we need for the trade we send to our trade reservedForTradeAddress + preparedBsqTx.addOutput(reservedFundsForOffer, reservedForTradeAddress); + + // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to + // wait for 1 confirmation) + // In case of double spend we will detect later in the trade process and use a ban score to penalize bad behaviour (not impl. yet) + + SendRequest sendRequest = SendRequest.forTx(preparedBsqTx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + if (useSavingsWallet) { + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + } else { + sendRequest.coinSelector = new BtcCoinSelector(fundingAddress, preferences.getIgnoreDustThreshold()); + } + // We use a fixed fee + sendRequest.fee = txFee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + + sendRequest.signInputs = false; + + // Change is optional in case of overpay or use of funds from savings wallet + sendRequest.changeAddress = changeAddress; + + checkNotNull(wallet, "Wallet must not be null"); + wallet.completeTx(sendRequest); + Transaction resultTx = sendRequest.tx; + removeDust(resultTx); + + // Sign all BTC inputs + for (int i = preparedBsqTxInputsSize; i < resultTx.getInputs().size(); i++) { + TransactionInput txIn = resultTx.getInputs().get(i); + checkArgument(txIn.getConnectedOutput() != null && + txIn.getConnectedOutput().isMine(wallet), + "txIn.getConnectedOutput() is not in our wallet. That must not happen."); + WalletService.signTransactionInput(wallet, aesKey, resultTx, txIn, i); + WalletService.checkScriptSig(resultTx, txIn, i); + } + + WalletService.checkWalletConsistency(wallet); + WalletService.verifyTransaction(resultTx); + + WalletService.printTx(Res.getBaseCurrencyCode() + " wallet: Signed tx", resultTx); + return resultTx; + } catch (Throwable t) { + log.error("completeBsqTradingFeeTx: preparedBsqTx={}", preparedBsqTx.toString()); + throw t; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Deposit tx + /////////////////////////////////////////////////////////////////////////////////////////// + + + // We construct the deposit transaction in the way that the buyer is always the first entry (inputs, outputs, MS keys) and then the seller. + // In the creation of the deposit tx the taker/maker roles are the determining roles instead of buyer/seller. + // In the payout tx it is the buyer/seller role. We keep the buyer/seller ordering over all transactions to not get confusion with ordering, + // which is important to follow correctly specially for the order of the MS keys. + + + /** + * The taker creates a dummy transaction to get the input(s) and optional change output for the amount and the + * taker's address for that trade. That will be used to send to the maker for creating the deposit transaction. + * + * @param takeOfferFeeTx the take offer fee tx + * @param inputAmount amount of takers input + * @param txFee mining fee + * @return a data container holding the inputs, the output value and address + * @throws TransactionVerificationException if there was an unexpected problem with the created dummy tx + */ + public InputsAndChangeOutput takerCreatesDepositTxInputs(Transaction takeOfferFeeTx, + Coin inputAmount, + Coin txFee) + throws TransactionVerificationException { + // We add the mining fee 2 times to the deposit tx: + // 1. Will be spent when publishing the deposit tx (paid by buyer) + // 2. Will be added to the MS amount, so when publishing the payout tx the fee is already there and the outputs are not changed by fee reduction + // The fee for the payout will be paid by the seller. + + /* + The tx we create has that structure: + + IN[0] input from taker fee tx > inputAmount (including tx fee) (unsigned) + OUT[0] dummyOutputAmount (inputAmount - tx fee) + + We are only interested in the inputs. + We get the exact input value from the taker fee tx so we don't create a change output. + */ + + // inputAmount includes the tx fee. So we subtract the fee to get the dummyOutputAmount. + Coin dummyOutputAmount = inputAmount.subtract(txFee); + + Transaction dummyTX = new Transaction(params); + // The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything. + // We don't care about fee calculation differences between the real tx and that dummy tx as we use a static tx fee. + TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, SegwitAddress.fromKey(params, new ECKey())); + dummyTX.addOutput(dummyOutput); + + // Find the needed inputs to pay the output, optionally add 1 change output. + // Normally only 1 input and no change output is used, but we support multiple inputs and 1 change output. + // Our spending transaction output is from the create offer fee payment. + + // We created the take offer fee tx in the structure that the second output is for the funds for the deposit tx. + TransactionOutput reservedForTradeOutput = takeOfferFeeTx.getOutputs().get(1); + checkArgument(reservedForTradeOutput.getValue().equals(inputAmount), + "Reserve amount does not equal input amount"); + dummyTX.addInput(reservedForTradeOutput); + + WalletService.verifyTransaction(dummyTX); + + //WalletService.printTx("dummyTX", dummyTX); + + List rawTransactionInputList = dummyTX.getInputs().stream().map(e -> { + checkNotNull(e.getConnectedOutput(), "e.getConnectedOutput() must not be null"); + checkNotNull(e.getConnectedOutput().getParentTransaction(), + "e.getConnectedOutput().getParentTransaction() must not be null"); + checkNotNull(e.getValue(), "e.getValue() must not be null"); + return getRawInputFromTransactionInput(e); + }).collect(Collectors.toList()); + + + // TODO changeOutputValue and changeOutputAddress is not used as taker spends exact amount from fee tx. + // Change is handled already at the fee tx creation so the handling of a change output for the deposit tx + // can be removed here. We still keep it atm as we prefer to not introduce a larger + // refactoring. When new trade protocol gets implemented this can be cleaned. + // The maker though can have a change output if the taker takes less as the max. offer amount! + return new InputsAndChangeOutput(new ArrayList<>(rawTransactionInputList), 0, null); + } + + public PreparedDepositTxAndMakerInputs sellerAsMakerCreatesDepositTx(byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { + return makerCreatesDepositTx(false, + contractHash, + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + } + + public PreparedDepositTxAndMakerInputs buyerAsMakerCreatesAndSignsDepositTx(byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { + return makerCreatesDepositTx(true, + contractHash, + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + } + + /** + * The maker creates the deposit transaction using the takers input(s) and optional output and signs his input(s). + * + * @param makerIsBuyer the flag indicating if we are in the maker as buyer role or the opposite + * @param contractHash the hash of the contract to be added to the OP_RETURN output + * @param makerInputAmount the input amount of the maker + * @param msOutputAmount the output amount to our MS output + * @param takerRawTransactionInputs raw data for the connected outputs for all inputs of the taker (normally 1 input) + * @param takerChangeOutputValue optional taker change output value + * @param takerChangeAddressString optional taker change address + * @param makerAddress the maker's address + * @param makerChangeAddress the maker's change address + * @param buyerPubKey the public key of the buyer + * @param sellerPubKey the public key of the seller + * @return a data container holding the serialized transaction and the maker raw inputs + * @throws SigningException if there was an unexpected problem signing (one of) the input(s) from the maker's wallet + * @throws AddressFormatException if the taker base58 change address doesn't parse or its checksum is invalid + * @throws TransactionVerificationException if there was an unexpected problem with the deposit tx or its signature(s) + * @throws WalletException if the maker's wallet is null or there was an error choosing deposit tx input(s) from it + */ + private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuyer, + byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { + checkArgument(!takerRawTransactionInputs.isEmpty()); + + // First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx. + // Similar to the way we did in the createTakerDepositTxInputs method. + Transaction dummyTx = new Transaction(params); + TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, SegwitAddress.fromKey(params, new ECKey())); + dummyTx.addOutput(dummyOutput); + addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress); + // Normally we have only 1 input but we support multiple inputs if the user has paid in with several transactions. + List makerInputs = dummyTx.getInputs(); + TransactionOutput makerOutput = null; + + // We don't support more than 1 optional change output + checkArgument(dummyTx.getOutputs().size() < 3, "dummyTx.getOutputs().size() >= 3"); + + // Only save change outputs, the dummy output is ignored (that's why we start with index 1) + if (dummyTx.getOutputs().size() > 1) { + makerOutput = dummyTx.getOutput(1); + } + + // Now we construct the real deposit tx + Transaction preparedDepositTx = new Transaction(params); + + ArrayList makerRawTransactionInputs = new ArrayList<>(); + if (makerIsBuyer) { + // Add buyer inputs + for (TransactionInput input : makerInputs) { + preparedDepositTx.addInput(input); + makerRawTransactionInputs.add(getRawInputFromTransactionInput(input)); + } + + // Add seller inputs + // the seller's input is not signed so we attach empty script bytes + for (RawTransactionInput rawTransactionInput : takerRawTransactionInputs) + preparedDepositTx.addInput(getTransactionInput(preparedDepositTx, new byte[]{}, rawTransactionInput)); + } else { + // taker is buyer role + + // Add buyer inputs + // the seller's input is not signed so we attach empty script bytes + for (RawTransactionInput rawTransactionInput : takerRawTransactionInputs) + preparedDepositTx.addInput(getTransactionInput(preparedDepositTx, new byte[]{}, rawTransactionInput)); + + // Add seller inputs + for (TransactionInput input : makerInputs) { + preparedDepositTx.addInput(input); + makerRawTransactionInputs.add(getRawInputFromTransactionInput(input)); + } + } + + + // Add MultiSig output + Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); + + // Tx fee for deposit tx will be paid by buyer. + TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount, + hashedMultiSigOutputScript.getProgram()); + preparedDepositTx.addOutput(hashedMultiSigOutput); + + // We add the hash ot OP_RETURN with a 0 amount output + TransactionOutput contractHashOutput = new TransactionOutput(params, preparedDepositTx, Coin.ZERO, + ScriptBuilder.createOpReturnScript(contractHash).getProgram()); + preparedDepositTx.addOutput(contractHashOutput); + + TransactionOutput takerTransactionOutput = null; + if (takerChangeOutputValue > 0 && takerChangeAddressString != null) { + takerTransactionOutput = new TransactionOutput(params, preparedDepositTx, Coin.valueOf(takerChangeOutputValue), + Address.fromString(params, takerChangeAddressString)); + } + + if (makerIsBuyer) { + // Add optional buyer outputs + if (makerOutput != null) { + preparedDepositTx.addOutput(makerOutput); + } + + // Add optional seller outputs + if (takerTransactionOutput != null) { + preparedDepositTx.addOutput(takerTransactionOutput); + } + } else { + // taker is buyer role + + // Add optional seller outputs + if (takerTransactionOutput != null) { + preparedDepositTx.addOutput(takerTransactionOutput); + } + + // Add optional buyer outputs + if (makerOutput != null) { + preparedDepositTx.addOutput(makerOutput); + } + } + + int start = makerIsBuyer ? 0 : takerRawTransactionInputs.size(); + int end = makerIsBuyer ? makerInputs.size() : preparedDepositTx.getInputs().size(); + for (int i = start; i < end; i++) { + TransactionInput input = preparedDepositTx.getInput(i); + signInput(preparedDepositTx, input, i); + WalletService.checkScriptSig(preparedDepositTx, input, i); + } + + WalletService.printTx("makerCreatesDepositTx", preparedDepositTx); + WalletService.verifyTransaction(preparedDepositTx); + + return new PreparedDepositTxAndMakerInputs(makerRawTransactionInputs, preparedDepositTx.bitcoinSerialize()); + } + + /** + * The taker signs the deposit transaction he received from the maker and publishes it. + * + * @param takerIsSeller the flag indicating if we are in the taker as seller role or the opposite + * @param contractHash the hash of the contract to be added to the OP_RETURN output + * @param makersDepositTxSerialized the prepared deposit transaction signed by the maker + * @param msOutputAmount the MultiSig output amount, as determined by the taker + * @param buyerInputs the connected outputs for all inputs of the buyer + * @param sellerInputs the connected outputs for all inputs of the seller + * @param buyerPubKey the public key of the buyer + * @param sellerPubKey the public key of the seller + * @throws SigningException if (one of) the taker input(s) was of an unrecognized type for signing + * @throws TransactionVerificationException if a non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig + * script, contract hash or output amount doesn't match the taker's, or there was an unexpected problem with the + * final deposit tx or its signatures + * @throws WalletException if the taker's wallet is null or structurally inconsistent + */ + public Transaction takerSignsDepositTx(boolean takerIsSeller, + byte[] contractHash, + byte[] makersDepositTxSerialized, + Coin msOutputAmount, + List buyerInputs, + List sellerInputs, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException { + Transaction makersDepositTx = new Transaction(params, makersDepositTxSerialized); + + checkArgument(!buyerInputs.isEmpty()); + checkArgument(!sellerInputs.isEmpty()); + + // Check if maker's MultiSig script is identical to the taker's + Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); + if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(hashedMultiSigOutputScript)) { + throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match taker's hashedMultiSigOutputScript"); + } + + // Check if maker's MultiSig output value is identical to the taker's + if (!makersDepositTx.getOutput(0).getValue().equals(msOutputAmount)) { + throw new TransactionVerificationException("Maker's MultiSig output amount does not match taker's MultiSig output amount"); + } + + // The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new + // depositTx + Transaction depositTx = new Transaction(params); + + if (takerIsSeller) { + // Add buyer inputs and apply signature + // We grab the signature from the makersDepositTx and apply it to the new tx input + for (int i = 0; i < buyerInputs.size(); i++) { + TransactionInput makersInput = makersDepositTx.getInputs().get(i); + byte[] makersScriptSigProgram = makersInput.getScriptSig().getProgram(); + TransactionInput input = getTransactionInput(depositTx, makersScriptSigProgram, buyerInputs.get(i)); + Script scriptPubKey = checkNotNull(input.getConnectedOutput()).getScriptPubKey(); + if (makersScriptSigProgram.length == 0 && !ScriptPattern.isP2WH(scriptPubKey)) { + throw new TransactionVerificationException("Non-segwit inputs from maker not signed."); + } + if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) { + input.setWitness(makersInput.getWitness()); + } + depositTx.addInput(input); + } + + // Add seller inputs + for (RawTransactionInput rawTransactionInput : sellerInputs) { + depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); + } + } else { + // taker is buyer + // Add buyer inputs and apply signature + for (RawTransactionInput rawTransactionInput : buyerInputs) { + depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); + } + + // Add seller inputs + // We grab the signature from the makersDepositTx and apply it to the new tx input + for (int i = buyerInputs.size(), k = 0; i < makersDepositTx.getInputs().size(); i++, k++) { + TransactionInput transactionInput = makersDepositTx.getInputs().get(i); + // We get the deposit tx unsigned if maker is seller + depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, sellerInputs.get(k))); + } + } + + // Check if OP_RETURN output with contract hash matches the one from the maker + TransactionOutput contractHashOutput = new TransactionOutput(params, makersDepositTx, Coin.ZERO, + ScriptBuilder.createOpReturnScript(contractHash).getProgram()); + log.debug("contractHashOutput {}", contractHashOutput); + TransactionOutput makersContractHashOutput = makersDepositTx.getOutputs().get(1); + log.debug("makersContractHashOutput {}", makersContractHashOutput); + if (!makersContractHashOutput.getScriptPubKey().equals(contractHashOutput.getScriptPubKey())) { + throw new TransactionVerificationException("Maker's transaction output for the contract hash is not matching taker's version."); + } + + // Add all outputs from makersDepositTx to depositTx + makersDepositTx.getOutputs().forEach(depositTx::addOutput); + WalletService.printTx("makersDepositTx", makersDepositTx); + + // Sign inputs + int start = takerIsSeller ? buyerInputs.size() : 0; + int end = takerIsSeller ? depositTx.getInputs().size() : buyerInputs.size(); + for (int i = start; i < end; i++) { + TransactionInput input = depositTx.getInput(i); + signInput(depositTx, input, i); + WalletService.checkScriptSig(depositTx, input, i); + } + + WalletService.printTx("takerSignsDepositTx", depositTx); + + WalletService.verifyTransaction(depositTx); + WalletService.checkWalletConsistency(wallet); + + return depositTx; + } + + + public void sellerAsMakerFinalizesDepositTx(Transaction myDepositTx, + Transaction takersDepositTx, + int numTakersInputs) + throws TransactionVerificationException, AddressFormatException { + + // We add takers signature from his inputs and add it to out tx which was already signed earlier. + for (int i = 0; i < numTakersInputs; i++) { + TransactionInput takersInput = takersDepositTx.getInput(i); + Script takersScriptSig = takersInput.getScriptSig(); + TransactionInput txInput = myDepositTx.getInput(i); + txInput.setScriptSig(takersScriptSig); + TransactionWitness witness = takersInput.getWitness(); + if (!TransactionWitness.EMPTY.equals(witness)) { + txInput.setWitness(witness); + } + } + + WalletService.printTx("sellerAsMakerFinalizesDepositTx", myDepositTx); + WalletService.verifyTransaction(myDepositTx); + } + + + public void sellerAddsBuyerWitnessesToDepositTx(Transaction myDepositTx, + Transaction buyersDepositTxWithWitnesses) { + int numberInputs = myDepositTx.getInputs().size(); + for (int i = 0; i < numberInputs; i++) { + var txInput = myDepositTx.getInput(i); + var witnessFromBuyer = buyersDepositTxWithWitnesses.getInput(i).getWitness(); + + if (TransactionWitness.EMPTY.equals(txInput.getWitness()) && + !TransactionWitness.EMPTY.equals(witnessFromBuyer)) { + txInput.setWitness(witnessFromBuyer); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, + String donationAddressString, + Coin minerFee, + long lockTime) + throws AddressFormatException, TransactionVerificationException { + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + Transaction delayedPayoutTx = new Transaction(params); + delayedPayoutTx.addInput(hashedMultiSigOutput); + applyLockTime(lockTime, delayedPayoutTx); + Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee); + delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString)); + WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + return delayedPayoutTx; + } + + public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx, + Transaction preparedDepositTx, + DeterministicKey myMultiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException { + + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + Sha256Hash sigHash; + Coin delayedPayoutTxInputValue = preparedDepositTx.getOutput(0).getValue(); + sigHash = delayedPayoutTx.hashForWitnessSignature(0, redeemScript, + delayedPayoutTxInputValue, Transaction.SigHash.ALL, false); + checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); + if (myMultiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + + ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + WalletService.printTx("delayedPayoutTx for sig creation", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + return mySignature.encodeToDER(); + } + + public Transaction finalizeUnconnectedDelayedPayoutTx(Transaction delayedPayoutTx, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] buyerSignature, + byte[] sellerSignature, + Coin inputValue) + throws AddressFormatException, TransactionVerificationException, SignatureDecodeException { + + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature); + ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature); + TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false); + TransactionInput input = delayedPayoutTx.getInput(0); + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + + if (checkNotNull(inputValue).isLessThan(delayedPayoutTx.getOutputSum().add(MIN_DELAYED_PAYOUT_TX_FEE))) { + throw new TransactionVerificationException("Delayed payout tx is paying less than the minimum allowed tx fee"); + } + Script scriptPubKey = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); + input.getScriptSig().correctlySpends(delayedPayoutTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS); + return delayedPayoutTx; + } + + public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] buyerSignature, + byte[] sellerSignature) + throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { + + TransactionInput input = delayedPayoutTx.getInput(0); + finalizeUnconnectedDelayedPayoutTx(delayedPayoutTx, buyerPubKey, sellerPubKey, buyerSignature, sellerSignature, input.getValue()); + + WalletService.checkWalletConsistency(wallet); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return delayedPayoutTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Standard payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Seller signs payout transaction, buyer has not signed yet. + * + * @param depositTx deposit transaction + * @param buyerPayoutAmount payout amount for buyer + * @param sellerPayoutAmount payout amount for seller + * @param buyerPayoutAddressString address for buyer + * @param sellerPayoutAddressString address for seller + * @param multiSigKeyPair DeterministicKey for MultiSig from seller + * @param buyerPubKey the public key of the buyer + * @param sellerPubKey the public key of the seller + * @return DER encoded canonical signature + * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid + * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signature + */ + public byte[] buyerSignsPayoutTx(Transaction depositTx, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey multiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException { + Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, + buyerPayoutAddressString, sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + // MS output from prev. tx is index 0 + Sha256Hash sigHash; + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + if (ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey())) { + sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } + checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); + if (multiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + ECKey.ECDSASignature buyerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + WalletService.printTx("prepared payoutTx", preparedPayoutTx); + WalletService.verifyTransaction(preparedPayoutTx); + return buyerSignature.encodeToDER(); + } + + + /** + * Seller creates and signs payout transaction and adds signature of buyer to complete the transaction. + * + * @param depositTx deposit transaction + * @param buyerSignature DER encoded canonical signature of buyer + * @param buyerPayoutAmount payout amount for buyer + * @param sellerPayoutAmount payout amount for seller + * @param buyerPayoutAddressString address for buyer + * @param sellerPayoutAddressString address for seller + * @param multiSigKeyPair seller's key pair for MultiSig + * @param buyerPubKey the public key of the buyer + * @param sellerPubKey the public key of the seller + * @return the payout transaction + * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid + * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signatures + * @throws WalletException if the seller's wallet is null or structurally inconsistent + */ + public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, + byte[] buyerSignature, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey multiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { + Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + // MS output from prev. tx is index 0 + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } + checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); + if (multiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + ECKey.ECDSASignature sellerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), + Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(sellerSignature, Transaction.SigHash.ALL, false); + // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer) + TransactionInput input = payoutTx.getInput(0); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } + WalletService.printTx("payoutTx", payoutTx); + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + WalletService.checkScriptSig(payoutTx, input, 0); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return payoutTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Mediated payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + public byte[] signMediatedPayoutTx(Transaction depositTx, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey myMultiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException { + Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + // MS output from prev. tx is index 0 + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } + checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); + if (myMultiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + WalletService.printTx("prepared mediated payoutTx for sig creation", preparedPayoutTx); + WalletService.verifyTransaction(preparedPayoutTx); + return mySignature.encodeToDER(); + } + + public Transaction finalizeMediatedPayoutTx(Transaction depositTx, + byte[] buyerSignature, + byte[] sellerSignature, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey multiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { + Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + // MS output from prev. tx is index 0 + checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); + TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), + Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature), + Transaction.SigHash.ALL, false); + // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer) + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + TransactionInput input = payoutTx.getInput(0); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } + WalletService.printTx("mediated payoutTx", payoutTx); + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + WalletService.checkScriptSig(payoutTx, input, 0); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return payoutTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Arbitrated payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + // TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well. + // Atm it is still used by ArbitrationManager. + + /** + * A trader who got the signed tx from the arbitrator finalizes the payout tx. + * + * @param depositTxSerialized serialized deposit tx + * @param arbitratorSignature DER encoded canonical signature of arbitrator + * @param buyerPayoutAmount payout amount of the buyer + * @param sellerPayoutAmount payout amount of the seller + * @param buyerAddressString the address of the buyer + * @param sellerAddressString the address of the seller + * @param tradersMultiSigKeyPair the key pair for the MultiSig of the trader who calls that method + * @param buyerPubKey the public key of the buyer + * @param sellerPubKey the public key of the seller + * @param arbitratorPubKey the public key of the arbitrator + * @return the completed payout tx + * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid + * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signature + * @throws WalletException if the trade wallet is null or structurally inconsistent + */ + public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSerialized, + byte[] arbitratorSignature, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerAddressString, + String sellerAddressString, + DeterministicKey tradersMultiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] arbitratorPubKey) + throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { + Transaction depositTx = new Transaction(params, depositTxSerialized); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + Transaction payoutTx = new Transaction(params); + payoutTx.addInput(hashedMultiSigOutput); + if (buyerPayoutAmount.isPositive()) { + payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); + } + if (sellerPayoutAmount.isPositive()) { + payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); + } + + // take care of sorting! + Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Sha256Hash sigHash; + boolean hashedMultiSigOutputIsLegacy = !ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } + checkNotNull(tradersMultiSigKeyPair, "tradersMultiSigKeyPair must not be null"); + if (tradersMultiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + ECKey.ECDSASignature tradersSignature = tradersMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false); + TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature), + Transaction.SigHash.ALL, false); + TransactionInput input = payoutTx.getInput(0); + // Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer) + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript( + ImmutableList.of(arbitratorTxSig, tradersTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, arbitratorTxSig, tradersTxSig); + input.setWitness(witness); + } + WalletService.printTx("disputed payoutTx", payoutTx); + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + WalletService.checkScriptSig(payoutTx, input, 0); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return payoutTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Emergency payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Tuple2 emergencyBuildPayoutTxFrom2of2MultiSig(String depositTxHex, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + Coin txFee, + String buyerAddressString, + String sellerAddressString, + String buyerPubKeyAsHex, + String sellerPubKeyAsHex, + boolean hashedMultiSigOutputIsLegacy) { + byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); + byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); + Transaction payoutTx = new Transaction(params); + Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex); + payoutTx.addInput(new TransactionInput(params, payoutTx, new byte[]{}, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue)); + + if (buyerPayoutAmount.isPositive()) { + payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); + } + if (sellerPayoutAmount.isPositive()) { + payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); + } + + String redeemScriptHex = Utils.HEX.encode(redeemScript.getProgram()); + String unsignedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); + return new Tuple2<>(redeemScriptHex, unsignedTxHex); + } + + public String emergencyGenerateSignature(String rawTxHex, String redeemScriptHex, Coin inputValue, String myPrivKeyAsHex) + throws IllegalArgumentException { + boolean hashedMultiSigOutputIsLegacy = true; + if (rawTxHex.startsWith("010000000001")) + hashedMultiSigOutputIsLegacy = false; + byte[] payload = Utils.HEX.decode(rawTxHex); + Transaction payoutTx = new Transaction(params, payload, null, params.getDefaultSerializer(), payload.length); + Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } + + ECKey myPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(myPrivKeyAsHex)); + checkNotNull(myPrivateKey, "key must not be null"); + ECKey.ECDSASignature myECDSASignature = myPrivateKey.sign(sigHash, aesKey).toCanonicalised(); + TransactionSignature myTxSig = new TransactionSignature(myECDSASignature, Transaction.SigHash.ALL, false); + return Utils.HEX.encode(myTxSig.encodeToBitcoin()); + } + + public Tuple2 emergencyApplySignatureToPayoutTxFrom2of2MultiSig(String unsignedTxHex, + String redeemScriptHex, + String buyerSignatureAsHex, + String sellerSignatureAsHex, + boolean hashedMultiSigOutputIsLegacy) + throws AddressFormatException, SignatureDecodeException { + Transaction payoutTx = new Transaction(params, Utils.HEX.decode(unsignedTxHex)); + TransactionSignature buyerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(buyerSignatureAsHex), true, true); + TransactionSignature sellerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(sellerSignatureAsHex), true, true); + Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); + + TransactionInput input = payoutTx.getInput(0); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } + String txId = payoutTx.getTxId().toString(); + String signedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); + return new Tuple2<>(txId, signedTxHex); + } + + public void emergencyPublishPayoutTxFrom2of2MultiSig(String signedTxHex, TxBroadcaster.Callback callback) + throws AddressFormatException, TransactionVerificationException, WalletException { + Transaction payoutTx = new Transaction(params, Utils.HEX.decode(signedTxHex)); + WalletService.printTx("payoutTx", payoutTx); + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + broadcastTx(payoutTx, callback, 20); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Broadcast tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback) { + checkNotNull(walletConfig); + TxBroadcaster.broadcastTx(wallet, walletConfig.peerGroup(), tx, callback); + } + + public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int timeoutInSec) { + checkNotNull(walletConfig); + TxBroadcaster.broadcastTx(wallet, walletConfig.peerGroup(), tx, callback, timeoutInSec); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Misc + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Returns the local existing wallet transaction with the given ID, or {@code null} if missing. + * + * @param txId the transaction ID of the transaction we want to lookup + */ + public Transaction getWalletTx(Sha256Hash txId) { + checkNotNull(wallet); + return wallet.getTransaction(txId); + } + + public void commitTx(Transaction tx) { + checkNotNull(wallet); + wallet.commitTx(tx); + } + + public Transaction getClonedTransaction(Transaction tx) { + return new Transaction(params, tx.bitcoinSerialize()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + private RawTransactionInput getRawInputFromTransactionInput(@NotNull TransactionInput input) { + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + checkNotNull(input.getConnectedOutput().getParentTransaction(), + "input.getConnectedOutput().getParentTransaction() must not be null"); + checkNotNull(input.getValue(), "input.getValue() must not be null"); + + // bitcoinSerialize(false) is used just in case the serialized tx is parsed by a bisq node still using + // bitcoinj 0.14. This is not supposed to happen ever since Version.TRADE_PROTOCOL_VERSION was set to 3, + // but it costs nothing to be on the safe side. + // The serialized tx is just used to obtain its hash, so the witness data is not relevant. + return new RawTransactionInput(input.getOutpoint().getIndex(), + input.getConnectedOutput().getParentTransaction().bitcoinSerialize(false), + input.getValue().value); + } + + private TransactionInput getTransactionInput(Transaction depositTx, + byte[] scriptProgram, + RawTransactionInput rawTransactionInput) { + return new TransactionInput(params, depositTx, scriptProgram, getConnectedOutPoint(rawTransactionInput), + Coin.valueOf(rawTransactionInput.value)); + } + + private TransactionOutPoint getConnectedOutPoint(RawTransactionInput rawTransactionInput) { + return new TransactionOutPoint(params, rawTransactionInput.index, + new Transaction(params, rawTransactionInput.parentTransaction)); + } + + public boolean isP2WH(RawTransactionInput rawTransactionInput) { + return ScriptPattern.isP2WH( + checkNotNull(getConnectedOutPoint(rawTransactionInput).getConnectedOutput()).getScriptPubKey()); + } + + + // TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well. + // Atm it is still used by traderSignAndFinalizeDisputedPayoutTx which is used by ArbitrationManager. + + // Don't use ScriptBuilder.createRedeemScript and ScriptBuilder.createP2SHOutputScript as they use a sorting + // (Collections.sort(pubKeys, ECKey.PUBKEY_COMPARATOR);) which can lead to a non-matching list of signatures with pubKeys and the executeMultiSig does + // not iterate all possible combinations of sig/pubKeys leading to a verification fault. That nasty bug happens just randomly as the list after sorting + // might differ from the provided one or not. + // Changing the while loop in executeMultiSig to fix that does not help as the reference implementation seems to behave the same (not iterating all + // possibilities) . + // Furthermore the executed list is reversed to the provided. + // Best practice is to provide the list sorted by the least probable successful candidates first (arbitrator is first -> will be last in execution loop, so + // avoiding unneeded expensive ECKey.verify calls) + private Script get2of3MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { + ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); + ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); + ECKey arbitratorKey = ECKey.fromPublicOnly(arbitratorPubKey); + // Take care of sorting! Need to reverse to the order we use normally (buyer, seller, arbitrator) + List keys = ImmutableList.of(arbitratorKey, sellerKey, buyerKey); + return ScriptBuilder.createMultiSigOutputScript(2, keys); + } + + private Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey) { + ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); + ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); + // Take care of sorting! Need to reverse to the order we use normally (buyer, seller) + List keys = ImmutableList.of(sellerKey, buyerKey); + return ScriptBuilder.createMultiSigOutputScript(2, keys); + } + + private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) { + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + if (legacy) { + return ScriptBuilder.createP2SHOutputScript(redeemScript); + } else { + return ScriptBuilder.createP2WSHOutputScript(redeemScript); + } + } + + private Transaction createPayoutTx(Transaction depositTx, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerAddressString, + String sellerAddressString) throws AddressFormatException { + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + Transaction transaction = new Transaction(params); + transaction.addInput(hashedMultiSigOutput); + if (buyerPayoutAmount.isPositive()) { + transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); + } + if (sellerPayoutAmount.isPositive()) { + transaction.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); + } + checkArgument(transaction.getOutputs().size() >= 1, "We need at least one output."); + return transaction; + } + + private void signInput(Transaction transaction, TransactionInput input, int inputIndex) throws SigningException { + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + Script scriptPubKey = input.getConnectedOutput().getScriptPubKey(); + checkNotNull(wallet); + ECKey sigKey = input.getOutpoint().getConnectedKey(wallet); + checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=" + + input.getOutpoint().toString()); + if (sigKey.isEncrypted()) { + checkNotNull(aesKey); + } + + if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { + Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false); + ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey); + TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false); + if (ScriptPattern.isP2PK(scriptPubKey)) { + input.setScriptSig(ScriptBuilder.createInputScript(txSig)); + } else if (ScriptPattern.isP2PKH(scriptPubKey)) { + input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey)); + } + } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { + // scriptCode is expected to have the format of a legacy P2PKH output script + Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey); + Coin value = input.getValue(); + TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, aesKey, scriptCode, value, + Transaction.SigHash.ALL, false); + input.setScriptSig(ScriptBuilder.createEmpty()); + input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey)); + } else { + throw new SigningException("Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey); + } + } + + private void addAvailableInputsAndChangeOutputs(Transaction transaction, + Address address, + Address changeAddress) throws WalletException { + SendRequest sendRequest = null; + try { + // Let the framework do the work to find the right inputs + sendRequest = SendRequest.forTx(transaction); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + // We use a fixed fee + sendRequest.fee = Coin.ZERO; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation) + sendRequest.coinSelector = new BtcCoinSelector(address, preferences.getIgnoreDustThreshold()); + // We use always the same address in a trade for all transactions + sendRequest.changeAddress = changeAddress; + // With the usage of completeTx() we get all the work done with fee calculation, validation and coin selection. + // We don't commit that tx to the wallet as it will be changed later and it's not signed yet. + // So it will not change the wallet balance. + checkNotNull(wallet, "wallet must not be null"); + wallet.completeTx(sendRequest); + } catch (Throwable t) { + if (sendRequest != null && sendRequest.tx != null) { + log.warn("addAvailableInputsAndChangeOutputs: sendRequest.tx={}, sendRequest.tx.getOutputs()={}", + sendRequest.tx, sendRequest.tx.getOutputs()); + } + + throw new WalletException(t); + } + } + + private void applyLockTime(long lockTime, Transaction tx) { + checkArgument(!tx.getInputs().isEmpty(), "The tx must have inputs. tx={}", tx); + tx.getInputs().forEach(input -> input.setSequenceNumber(TransactionInput.NO_SEQUENCE - 1)); + tx.setLockTime(lockTime); + } + + // BISQ issue #4039: prevent dust outputs from being created. + // check all the outputs in a proposed transaction, if any are below the dust threshold + // remove them, noting the details in the log. returns 'true' to indicate if any dust was + // removed. + private boolean removeDust(Transaction transaction) { + List originalTransactionOutputs = transaction.getOutputs(); + List keepTransactionOutputs = new ArrayList<>(); + for (TransactionOutput transactionOutput : originalTransactionOutputs) { + if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) { + log.info("your transaction would have contained a dust output of {}", transactionOutput.toString()); + } else { + keepTransactionOutputs.add(transactionOutput); + } + } + // if dust was detected, keepTransactionOutputs will have fewer elements than originalTransactionOutputs + // set the transaction outputs to what we saved in keepTransactionOutputs, thus discarding dust. + if (keepTransactionOutputs.size() != originalTransactionOutputs.size()) { + log.info("dust output was detected and removed, the new output is as follows:"); + transaction.clearOutputs(); + for (TransactionOutput transactionOutput : keepTransactionOutputs) { + transaction.addOutput(transactionOutput); + log.info("{}", transactionOutput.toString()); + } + return true; // dust was removed + } + return false; // no action necessary + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java new file mode 100644 index 0000000000..3f9d2cfd0e --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java @@ -0,0 +1,152 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.TxBroadcastTimeoutException; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; + +import bisq.common.Timer; +import bisq.common.UserThread; + +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; +import org.bitcoinj.wallet.Wallet; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +@Slf4j +public class TxBroadcaster { + public interface Callback { + void onSuccess(Transaction transaction); + + default void onTimeout(TxBroadcastTimeoutException exception) { + Transaction tx = exception.getLocalTx(); + if (tx != null) { + // We optimistically assume that the tx broadcast succeeds later and call onSuccess on the callback handler. + // This behaviour carries less potential problems than if we would trigger a failure (e.g. which would cause + // a failed create offer attempt or failed take offer attempt). + // We have no guarantee how long it will take to get the information that sufficiently many BTC nodes have + // reported back to BitcoinJ that the tx is in their mempool. + // In normal situations that's very fast but in some cases it can take minutes (mostly related to Tor + // connection issues). So if we just go on in the application logic and treat it as successful and the + // tx will be broadcast successfully later all is fine. + // If it will fail to get broadcast, it will lead to a failure state, the same as if we would trigger a + // failure due the timeout. + // So we can assume that this behaviour will lead to less problems as otherwise. + // Long term we should implement better monitoring for Tor and the provided Bitcoin nodes to find out + // why those delays happen and add some rollback behaviour to the app state in case the tx will never + // get broadcast. + log.warn("TxBroadcaster.onTimeout called: {}", exception.toString()); + onSuccess(tx); + } else { + log.error("TxBroadcaster.onTimeout: Tx is null. exception={} ", exception.toString()); + onFailure(exception); + } + } + + void onFailure(TxBroadcastException exception); + } + + // Currently there is a bug in BitcoinJ causing the timeout at all BSQ transactions. + // It is because BitcoinJ does not handle confidence object correctly in case as tx got altered after the + // Wallet.complete() method is called which is the case for all BSQ txs. We will work on a fix for that but that + // will take more time. In the meantime we reduce the timeout to 5 seconds to avoid that the trade protocol runs + // into a timeout when using BSQ for trade fee. + // For trade fee txs we set only 1 sec timeout for now. + // FIXME + private static final int DEFAULT_BROADCAST_TIMEOUT = 5; + private static final Map broadcastTimerMap = new HashMap<>(); + + public static void broadcastTx(Wallet wallet, PeerGroup peerGroup, Transaction localTx, Callback callback) { + broadcastTx(wallet, peerGroup, localTx, callback, DEFAULT_BROADCAST_TIMEOUT); + } + + public static void broadcastTx(Wallet wallet, PeerGroup peerGroup, Transaction tx, Callback callback, int timeOut) { + Timer timeoutTimer; + final String txId = tx.getTxId().toString(); + log.info("Txid: {} hex: {}", txId, Utils.HEX.encode(tx.bitcoinSerialize())); + if (!broadcastTimerMap.containsKey(txId)) { + timeoutTimer = UserThread.runAfter(() -> { + log.warn("Broadcast of tx {} not completed after {} sec.", txId, timeOut); + stopAndRemoveTimer(txId); + UserThread.execute(() -> callback.onTimeout(new TxBroadcastTimeoutException(tx, timeOut, wallet))); + }, timeOut); + + broadcastTimerMap.put(txId, timeoutTimer); + } else { + // Would be the wrong way how to use the API (calling 2 times a broadcast with same tx). + // An arbitrator reported that got the error after a manual payout, need to investigate why... + stopAndRemoveTimer(txId); + UserThread.execute(() -> callback.onFailure(new TxBroadcastException("We got broadcastTx called with a tx " + + "which has an open timeoutTimer. txId=" + txId, txId))); + } + + // We decided the least risky scenario is to commit the tx to the wallet and broadcast it later. + // If it's a bsq tx WalletManager.publishAndCommitBsqTx() should have committed the tx to both bsq and btc + // wallets so the next line causes no effect. + // If it's a btc tx, the next line adds the tx to the wallet. + wallet.maybeCommitTx(tx); + + Futures.addCallback(peerGroup.broadcastTransaction(tx).future(), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Transaction result) { + // We expect that there is still a timeout in our map, otherwise the timeout got triggered + if (broadcastTimerMap.containsKey(txId)) { + stopAndRemoveTimer(txId); + // At regtest we get called immediately back but we want to make sure that the handler is not called + // before the caller is finished. + UserThread.execute(() -> callback.onSuccess(tx)); + } else { + log.warn("We got an onSuccess callback for a broadcast which already triggered the timeout. txId={}", txId); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + stopAndRemoveTimer(txId); + UserThread.execute(() -> callback.onFailure(new TxBroadcastException("We got an onFailure from " + + "the peerGroup.broadcastTransaction callback.", throwable))); + } + }, MoreExecutors.directExecutor()); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(tx); + } + + private static void stopAndRemoveTimer(String txId) { + Timer timer = broadcastTimerMap.get(txId); + if (timer != null) + timer.stop(); + + broadcastTimerMap.remove(txId); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java new file mode 100644 index 0000000000..0b893e470a --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -0,0 +1,868 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.listeners.AddressConfidenceListener; +import bisq.core.btc.listeners.BalanceListener; +import bisq.core.btc.listeners.TxConfidenceListener; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; + +import bisq.common.config.Config; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.NewBestBlockListener; +import org.bitcoinj.core.listeners.TransactionConfidenceEventListener; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.script.ScriptException; +import org.bitcoinj.script.ScriptPattern; +import org.bitcoinj.signers.TransactionSigner; +import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.DecryptingKeyBag; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.KeyBag; +import org.bitcoinj.wallet.RedeemData; +import org.bitcoinj.wallet.SendRequest; +import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.wallet.listeners.WalletChangeEventListener; +import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; +import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener; +import org.bitcoinj.wallet.listeners.WalletReorganizeEventListener; + +import javax.inject.Inject; + +import com.google.common.collect.ImmutableMultiset; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Multiset; +import com.google.common.collect.SetMultimap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Abstract base class for BTC and BSQ wallet. Provides all non-trade specific functionality. + */ +@Slf4j +public abstract class WalletService { + protected final WalletsSetup walletsSetup; + protected final Preferences preferences; + protected final FeeService feeService; + protected final NetworkParameters params; + private final BisqWalletListener walletEventListener = new BisqWalletListener(); + private final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); + private final WalletChangeEventListener cacheInvalidationListener; + private final AtomicReference> txOutputAddressCache = new AtomicReference<>(); + private final AtomicReference> addressToMatchingTxSetCache = new AtomicReference<>(); + @Getter + protected Wallet wallet; + @Getter + protected KeyParameter aesKey; + @Getter + protected IntegerProperty chainHeightProperty = new SimpleIntegerProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + WalletService(WalletsSetup walletsSetup, + Preferences preferences, + FeeService feeService) { + this.walletsSetup = walletsSetup; + this.preferences = preferences; + this.feeService = feeService; + + params = walletsSetup.getParams(); + + cacheInvalidationListener = wallet -> { + txOutputAddressCache.set(null); + addressToMatchingTxSetCache.set(null); + }; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void addListenersToWallet() { + wallet.addCoinsReceivedEventListener(walletEventListener); + wallet.addCoinsSentEventListener(walletEventListener); + wallet.addReorganizeEventListener(walletEventListener); + wallet.addTransactionConfidenceEventListener(walletEventListener); + wallet.addChangeEventListener(Threading.SAME_THREAD, cacheInvalidationListener); + } + + public void shutDown() { + if (wallet != null) { + wallet.removeCoinsReceivedEventListener(walletEventListener); + wallet.removeCoinsSentEventListener(walletEventListener); + wallet.removeReorganizeEventListener(walletEventListener); + wallet.removeTransactionConfidenceEventListener(walletEventListener); + wallet.removeChangeEventListener(cacheInvalidationListener); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Package scope Methods + /////////////////////////////////////////////////////////////////////////////////////////// + + void decryptWallet(@NotNull KeyParameter key) { + wallet.decrypt(key); + aesKey = null; + } + + void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { + if (this.aesKey != null) { + log.warn("encryptWallet called but we have a aesKey already set. " + + "We decryptWallet with the old key before we apply the new key."); + decryptWallet(this.aesKey); + } + + wallet.encrypt(keyCrypterScrypt, key); + aesKey = key; + } + + void setAesKey(KeyParameter aesKey) { + this.aesKey = aesKey; + } + + abstract String getWalletAsString(boolean includePrivKeys); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addAddressConfidenceListener(AddressConfidenceListener listener) { + addressConfidenceListeners.add(listener); + } + + public void removeAddressConfidenceListener(AddressConfidenceListener listener) { + addressConfidenceListeners.remove(listener); + } + + public void addTxConfidenceListener(TxConfidenceListener listener) { + txConfidenceListeners.add(listener); + } + + public void removeTxConfidenceListener(TxConfidenceListener listener) { + txConfidenceListeners.remove(listener); + } + + public void addBalanceListener(BalanceListener listener) { + balanceListeners.add(listener); + } + + public void removeBalanceListener(BalanceListener listener) { + balanceListeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Checks + /////////////////////////////////////////////////////////////////////////////////////////// + + public static void checkWalletConsistency(Wallet wallet) throws WalletException { + try { + checkNotNull(wallet); + checkState(wallet.isConsistent()); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.getMessage()); + throw new WalletException(t); + } + } + + public static void verifyTransaction(Transaction transaction) throws TransactionVerificationException { + try { + transaction.verify(); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.getMessage()); + throw new TransactionVerificationException(t); + } + } + + public static void checkAllScriptSignaturesForTx(Transaction transaction) throws TransactionVerificationException { + for (int i = 0; i < transaction.getInputs().size(); i++) { + WalletService.checkScriptSig(transaction, transaction.getInputs().get(i), i); + } + } + + public static void checkScriptSig(Transaction transaction, + TransactionInput input, + int inputIndex) throws TransactionVerificationException { + try { + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.getScriptSig().correctlySpends(transaction, inputIndex, input.getWitness(), input.getValue(), input.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.getMessage()); + throw new TransactionVerificationException(t); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Sign tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public static void signTransactionInput(Wallet wallet, + KeyParameter aesKey, + Transaction tx, + TransactionInput txIn, + int index) { + KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(wallet, aesKey); + if (txIn.getConnectedOutput() != null) { + try { + // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when + // we sign missing pieces (to check this would require either assuming any signatures are signing + // standard output types or a way to get processed signatures out of script execution) + txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); + return; + } catch (ScriptException e) { + // Expected. + } + + Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); + RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); + checkNotNull(redeemData, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); + txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)); + + TransactionSigner.ProposedTransaction propTx = new TransactionSigner.ProposedTransaction(tx); + Transaction partialTx = propTx.partialTx; + txIn = partialTx.getInput(index); + if (txIn.getConnectedOutput() != null) { + // If we don't have a sig we don't do the check to avoid error reports of failed sig checks + final List chunks = txIn.getConnectedOutput().getScriptPubKey().getChunks(); + if (!chunks.isEmpty() && chunks.get(0).data != null && chunks.get(0).data.length > 0) { + try { + // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when + // we sign missing pieces (to check this would require either assuming any signatures are signing + // standard output types or a way to get processed signatures out of script execution) + txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); + return; + } catch (ScriptException e) { + // Expected. + } + } + + redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); + scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); + + checkNotNull(redeemData, "redeemData must not be null"); + ECKey pubKey = redeemData.keys.get(0); + if (pubKey instanceof DeterministicKey) + propTx.keyPaths.put(scriptPubKey, (((DeterministicKey) pubKey).getPath())); + + ECKey key; + if ((key = redeemData.getFullKey()) == null) { + log.warn("No local key found for input {}", index); + return; + } + + Script inputScript = txIn.getScriptSig(); + byte[] script = redeemData.redeemScript.getProgram(); + + if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { + try { + TransactionSignature signature = partialTx.calculateSignature(index, key, script, Transaction.SigHash.ALL, false); + inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), 0); + txIn.setScriptSig(inputScript); + } catch (ECKey.KeyIsEncryptedException e1) { + throw e1; + } catch (ECKey.MissingPrivateKeyException e1) { + log.warn("No private key in keypair for input {}", index); + } + } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { + try { + // scriptCode is expected to have the format of a legacy P2PKH output script + Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key); + Coin value = txIn.getValue(); + TransactionSignature txSig = tx.calculateWitnessSignature(index, key, aesKey, scriptCode, value, + Transaction.SigHash.ALL, false); + txIn.setScriptSig(ScriptBuilder.createEmpty()); + txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key)); + } catch (ECKey.KeyIsEncryptedException e1) { + throw e1; + } catch (ECKey.MissingPrivateKeyException e1) { + log.warn("No private key in keypair for input {}", index); + } + } else { + // log.error("Unexpected script type."); + throw new RuntimeException("Unexpected script type."); + } + } else { + log.warn("Missing connected output, assuming input {} is already signed.", index); + } + } else { + log.error("Missing connected output, assuming already signed."); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Broadcast tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback) { + TxBroadcaster.broadcastTx(wallet, walletsSetup.getPeerGroup(), tx, callback); + } + + public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int timeOut) { + TxBroadcaster.broadcastTx(wallet, walletsSetup.getPeerGroup(), tx, callback, timeOut); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TransactionConfidence + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + public TransactionConfidence getConfidenceForAddress(Address address) { + List transactionConfidenceList = new ArrayList<>(); + if (wallet != null) { + Set transactions = getAddressToMatchingTxSetMultiset().get(address); + transactionConfidenceList.addAll(transactions.stream().map(tx -> + getTransactionConfidence(tx, address)).collect(Collectors.toList())); + } + return getMostRecentConfidence(transactionConfidenceList); + } + + private SetMultimap getAddressToMatchingTxSetMultiset() { + return addressToMatchingTxSetCache.updateAndGet(set -> set != null ? set : computeAddressToMatchingTxSetMultimap()); + } + + private SetMultimap computeAddressToMatchingTxSetMultimap() { + return wallet.getTransactions(false).stream() + .collect(ImmutableSetMultimap.flatteningToImmutableSetMultimap( + Function.identity(), + (Function>) ( + t -> getOutputsWithConnectedOutputs(t).stream() + .map(WalletService::getAddressFromOutput) + .filter(Objects::nonNull)))) + .inverse(); + } + + @Nullable + public TransactionConfidence getConfidenceForTxId(String txId) { + if (wallet != null) { + Set transactions = wallet.getTransactions(false); + for (Transaction tx : transactions) { + if (tx.getTxId().toString().equals(txId)) + return tx.getConfidence(); + } + } + return null; + } + + @Nullable + private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) { + List transactionConfidenceList = getOutputsWithConnectedOutputs(tx).stream() + .filter(output -> address != null && address.equals(getAddressFromOutput(output))) + .flatMap(o -> Stream.ofNullable(o.getParentTransaction())) + .map(Transaction::getConfidence) + .collect(Collectors.toList()); + return getMostRecentConfidence(transactionConfidenceList); + } + + + private List getOutputsWithConnectedOutputs(Transaction tx) { + List transactionOutputs = tx.getOutputs(); + List connectedOutputs = new ArrayList<>(); + + // add all connected outputs from any inputs as well + List transactionInputs = tx.getInputs(); + for (TransactionInput transactionInput : transactionInputs) { + TransactionOutput transactionOutput = transactionInput.getConnectedOutput(); + if (transactionOutput != null) { + connectedOutputs.add(transactionOutput); + } + } + + List mergedOutputs = new ArrayList<>(); + mergedOutputs.addAll(transactionOutputs); + mergedOutputs.addAll(connectedOutputs); + return mergedOutputs; + } + + @Nullable + private TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) { + TransactionConfidence transactionConfidence = null; + for (TransactionConfidence confidence : transactionConfidenceList) { + if (confidence != null) { + if (transactionConfidence == null || + confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING) || + (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) && + transactionConfidence.getConfidenceType().equals( + TransactionConfidence.ConfidenceType.BUILDING) && + confidence.getDepthInBlocks() < transactionConfidence.getDepthInBlocks())) { + transactionConfidence = confidence; + } + } + } + return transactionConfidence; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Balance + /////////////////////////////////////////////////////////////////////////////////////////// + + public Coin getAvailableConfirmedBalance() { + return wallet != null ? wallet.getBalance(Wallet.BalanceType.AVAILABLE) : Coin.ZERO; + } + + public Coin getEstimatedBalance() { + return wallet != null ? wallet.getBalance(Wallet.BalanceType.ESTIMATED) : Coin.ZERO; + } + + public Coin getBalanceForAddress(Address address) { + return wallet != null ? getBalance(wallet.calculateAllSpendCandidates(), address) : Coin.ZERO; + } + + protected Coin getBalance(List transactionOutputs, Address address) { + Coin balance = Coin.ZERO; + for (TransactionOutput output : transactionOutputs) { + if (!isDustAttackUtxo(output)) { + if (isOutputScriptConvertibleToAddress(output) && + address != null && + address.equals(getAddressFromOutput(output))) + balance = balance.add(output.getValue()); + } + } + return balance; + } + + protected abstract boolean isDustAttackUtxo(TransactionOutput output); + + public Coin getBalance(TransactionOutput output) { + return getBalanceForAddress(getAddressFromOutput(output)); + } + + public Coin getTxFeeForWithdrawalPerVbyte() { + Coin fee = (preferences.isUseCustomWithdrawalTxFee()) ? + Coin.valueOf(preferences.getWithdrawalTxFeeInVbytes()) : + feeService.getTxFeePerVbyte(); + log.info("tx fee = " + fee.toFriendlyString()); + return fee; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Tx outputs + /////////////////////////////////////////////////////////////////////////////////////////// + + public int getNumTxOutputsForAddress(Address address) { + return getTxOutputAddressMultiset().count(address); + } + + private Multiset
    getTxOutputAddressMultiset() { + return txOutputAddressCache.updateAndGet(set -> set != null ? set : computeTxOutputAddressMultiset()); + } + + private Multiset
    computeTxOutputAddressMultiset() { + return wallet.getTransactions(false).stream() + .flatMap(t -> t.getOutputs().stream()) + .map(WalletService::getAddressFromOutput) + .filter(Objects::nonNull) + .collect(ImmutableMultiset.toImmutableMultiset()); + } + + public boolean isAddressUnused(Address address) { + return getNumTxOutputsForAddress(address) == 0; + } + + // BISQ issue #4039: Prevent dust outputs from being created. + // Check the outputs of a proposed transaction. If any are below the dust threshold, + // add up the dust, log the details, and return the cumulative dust amount. + public Coin getDust(Transaction proposedTransaction) { + Coin dust = Coin.ZERO; + for (TransactionOutput transactionOutput : proposedTransaction.getOutputs()) { + if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) { + dust = dust.add(transactionOutput.getValue()); + log.info("Dust TXO = {}", transactionOutput.toString()); + } + } + return dust; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Empty complete Wallet + /////////////////////////////////////////////////////////////////////////////////////////// + + public void emptyBtcWallet(String toAddress, + KeyParameter aesKey, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) + throws InsufficientMoneyException, AddressFormatException { + SendRequest sendRequest = SendRequest.emptyWallet(Address.fromString(params, toAddress)); + sendRequest.fee = Coin.ZERO; + sendRequest.feePerKb = getTxFeeForWithdrawalPerVbyte().multiply(1000); + sendRequest.aesKey = aesKey; + Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); + printTx("empty btc wallet", sendResult.tx); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + + Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() { + @Override + public void onSuccess(Transaction result) { + log.info("emptyBtcWallet onSuccess Transaction=" + result); + resultHandler.handleResult(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + log.error("emptyBtcWallet onFailure " + t.toString()); + errorMessageHandler.handleErrorMessage(t.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getTxFromSerializedTx(byte[] tx) { + return new Transaction(params, tx); + } + + public NetworkParameters getParams() { + return params; + } + + public int getBestChainHeight() { + final BlockChain chain = walletsSetup.getChain(); + return isWalletReady() && chain != null ? chain.getBestChainHeight() : 0; + } + + public boolean isChainHeightSyncedWithinTolerance() { + return walletsSetup.isChainHeightSyncedWithinTolerance(); + } + + public Transaction getClonedTransaction(Transaction tx) { + return new Transaction(params, tx.bitcoinSerialize()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Wallet delegates to avoid direct access to wallet outside the service class + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addChangeEventListener(WalletChangeEventListener listener) { + wallet.addChangeEventListener(Threading.USER_THREAD, listener); + } + + public void removeChangeEventListener(WalletChangeEventListener listener) { + wallet.removeChangeEventListener(listener); + } + + public void addNewBestBlockListener(NewBestBlockListener listener) { + final BlockChain chain = walletsSetup.getChain(); + if (isWalletReady() && chain != null) + chain.addNewBestBlockListener(listener); + } + + public void removeNewBestBlockListener(NewBestBlockListener listener) { + final BlockChain chain = walletsSetup.getChain(); + if (isWalletReady() && chain != null) + chain.removeNewBestBlockListener(listener); + } + + public boolean isWalletReady() { + return wallet != null; + } + + public DeterministicSeed getKeyChainSeed() { + return wallet.getKeyChainSeed(); + } + + @Nullable + public KeyCrypter getKeyCrypter() { + return wallet.getKeyCrypter(); + } + + public boolean checkAESKey(KeyParameter aesKey) { + return wallet.checkAESKey(aesKey); + } + + @Nullable + public DeterministicKey findKeyFromPubKey(byte[] pubKey) { + return (DeterministicKey) wallet.findKeyFromPubKey(pubKey); + } + + public boolean isEncrypted() { + return wallet.isEncrypted(); + } + + public List getRecentTransactions(int numTransactions, boolean includeDead) { + // Returns a list ordered by tx.getUpdateTime() desc + return wallet.getRecentTransactions(numTransactions, includeDead); + } + + public int getLastBlockSeenHeight() { + return wallet.getLastBlockSeenHeight(); + } + + /** + * Check if there are more than 20 unconfirmed transactions in the chain right now. + * + * @return true when queue is full + */ + public boolean isUnconfirmedTransactionsLimitHit() { + // For published delayed payout transactions we do not receive the tx confidence + // so we cannot check if it is confirmed so we ignore it for that check. The check is any arbitrarily + // using a limit of 20, so we don't need to be exact here. Should just reduce the likelihood of issues with + // the too long chains of unconfirmed transactions. + return getTransactions(false).stream() + .filter(tx -> tx.getLockTime() == 0) + .filter(Transaction::isPending) + .count() > 20; + } + + public Set getTransactions(boolean includeDead) { + return wallet.getTransactions(includeDead); + } + + public Coin getBalance(@SuppressWarnings("SameParameterValue") Wallet.BalanceType balanceType) { + return wallet.getBalance(balanceType); + } + + @Nullable + public Transaction getTransaction(Sha256Hash hash) { + return wallet.getTransaction(hash); + } + + @Nullable + public Transaction getTransaction(String txId) { + if (txId == null) { + return null; + } + return getTransaction(Sha256Hash.wrap(txId)); + } + + + public boolean isTransactionOutputMine(TransactionOutput transactionOutput) { + return transactionOutput.isMine(wallet); + } + + /* public boolean isTxOutputMine(TxOutput txOutput) { + try { + Script script = txOutput.getScript(); + if (script.isSentToRawPubKey()) { + byte[] pubkey = script.getPubKey(); + return wallet.isPubKeyMine(pubkey); + } + if (script.isPayToScriptHash()) { + return wallet.isPayToScriptHashMine(script.getPubKeyHash()); + } else { + byte[] pubkeyHash = script.getPubKeyHash(); + return wallet.isPubKeyHashMine(pubkeyHash); + } + } catch (ScriptException e) { + // Just means we didn't understand the output of this transaction: ignore it. + log.debug("Could not parse tx output script: {}", e.toString()); + return false; + } + }*/ + + public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException { + return transaction.getValueSentFromMe(wallet); + } + + public Coin getValueSentToMeForTransaction(Transaction transaction) throws ScriptException { + return transaction.getValueSentToMe(wallet); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Util + /////////////////////////////////////////////////////////////////////////////////////////// + + public static void printTx(String tracePrefix, Transaction tx) { + log.info("\n" + tracePrefix + ":\n" + tx.toString()); + } + + public static boolean isOutputScriptConvertibleToAddress(TransactionOutput output) { + return ScriptPattern.isP2PKH(output.getScriptPubKey()) || + ScriptPattern.isP2SH(output.getScriptPubKey()) || + ScriptPattern.isP2WH(output.getScriptPubKey()); + } + + @Nullable + public static Address getAddressFromOutput(TransactionOutput output) { + return isOutputScriptConvertibleToAddress(output) ? + output.getScriptPubKey().getToAddress(Config.baseCurrencyNetworkParameters()) : null; + } + + @Nullable + public static String getAddressStringFromOutput(TransactionOutput output) { + return isOutputScriptConvertibleToAddress(output) ? + output.getScriptPubKey().getToAddress(Config.baseCurrencyNetworkParameters()).toString() : null; + } + + + /** + * @param serializedTransaction The serialized transaction to be added to the wallet + * @return The transaction we added to the wallet, which is different as the one we passed as argument! + * @throws VerificationException + */ + public static Transaction maybeAddTxToWallet(byte[] serializedTransaction, + Wallet wallet, + TransactionConfidence.Source source) throws VerificationException { + Transaction tx = new Transaction(wallet.getParams(), serializedTransaction); + Transaction walletTransaction = wallet.getTransaction(tx.getTxId()); + + if (walletTransaction == null) { + // We need to recreate the transaction otherwise we get a null pointer... + tx.getConfidence(Context.get()).setSource(source); + //wallet.maybeCommitTx(tx); + wallet.receivePending(tx, null, true); + return tx; + } else { + return walletTransaction; + } + } + + public static Transaction maybeAddNetworkTxToWallet(byte[] serializedTransaction, + Wallet wallet) throws VerificationException { + return maybeAddTxToWallet(serializedTransaction, wallet, TransactionConfidence.Source.NETWORK); + } + + public static Transaction maybeAddSelfTxToWallet(Transaction transaction, + Wallet wallet) throws VerificationException { + return maybeAddTxToWallet(transaction, wallet, TransactionConfidence.Source.SELF); + } + + public static Transaction maybeAddTxToWallet(Transaction transaction, + Wallet wallet, + TransactionConfidence.Source source) throws VerificationException { + return maybeAddTxToWallet(transaction.bitcoinSerialize(), wallet, source); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // bisqWalletEventListener + /////////////////////////////////////////////////////////////////////////////////////////// + + public class BisqWalletListener implements WalletCoinsReceivedEventListener, WalletCoinsSentEventListener, WalletReorganizeEventListener, TransactionConfidenceEventListener { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { + notifyBalanceListeners(tx); + } + + @Override + public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { + notifyBalanceListeners(tx); + } + + @Override + public void onReorganize(Wallet wallet) { + log.warn("onReorganize "); + } + + @Override + public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) { + for (AddressConfidenceListener addressConfidenceListener : addressConfidenceListeners) { + TransactionConfidence confidence = getTransactionConfidence(tx, addressConfidenceListener.getAddress()); + addressConfidenceListener.onTransactionConfidenceChanged(confidence); + } + txConfidenceListeners.stream() + .filter(txConfidenceListener -> tx != null && + tx.getTxId().toString() != null && + txConfidenceListener != null && + tx.getTxId().toString().equals(txConfidenceListener.getTxID())) + .forEach(txConfidenceListener -> + txConfidenceListener.onTransactionConfidenceChanged(tx.getConfidence())); + } + + void notifyBalanceListeners(Transaction tx) { + for (BalanceListener balanceListener : balanceListeners) { + Coin balance; + if (balanceListener.getAddress() != null) + balance = getBalanceForAddress(balanceListener.getAddress()); + else + balance = getAvailableConfirmedBalance(); + + balanceListener.onBalanceChanged(balance, tx); + } + } + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java new file mode 100644 index 0000000000..2e6e9b6476 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java @@ -0,0 +1,164 @@ +/* + * 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 bisq.core.btc.wallet; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.crypto.ScryptUtil; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.locale.Res; + +import bisq.common.handlers.ExceptionHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.Wallet; + +import com.google.inject.Inject; + +import org.bouncycastle.crypto.params.KeyParameter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +// Convenience class to handle methods applied to several wallets +public class WalletsManager { + private static final Logger log = LoggerFactory.getLogger(WalletsManager.class); + + private final BtcWalletService btcWalletService; + private final TradeWalletService tradeWalletService; + private final BsqWalletService bsqWalletService; + private final WalletsSetup walletsSetup; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public WalletsManager(BtcWalletService btcWalletService, + TradeWalletService tradeWalletService, + BsqWalletService bsqWalletService, + WalletsSetup walletsSetup) { + this.btcWalletService = btcWalletService; + this.tradeWalletService = tradeWalletService; + this.bsqWalletService = bsqWalletService; + this.walletsSetup = walletsSetup; + } + + public void decryptWallets(KeyParameter aesKey) { + btcWalletService.decryptWallet(aesKey); + bsqWalletService.decryptWallet(aesKey); + tradeWalletService.setAesKey(null); + } + + public void encryptWallets(KeyCrypterScrypt keyCrypterScrypt, KeyParameter aesKey) { + try { + btcWalletService.encryptWallet(keyCrypterScrypt, aesKey); + bsqWalletService.encryptWallet(keyCrypterScrypt, aesKey); + + // we save the key for the trade wallet as we don't require passwords here + tradeWalletService.setAesKey(aesKey); + } catch (Throwable t) { + log.error(t.toString()); + throw t; + } + } + + public String getWalletsAsString(boolean includePrivKeys) { + final String baseCurrencyWalletDetails = Res.getBaseCurrencyCode() + " Wallet:\n" + + btcWalletService.getWalletAsString(includePrivKeys); + final String bsqWalletDetails = "\n\nBSQ Wallet:\n" + bsqWalletService.getWalletAsString(includePrivKeys); + return baseCurrencyWalletDetails + bsqWalletDetails; + } + + public void restoreSeedWords(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + walletsSetup.restoreSeedWords(seed, resultHandler, exceptionHandler); + } + + public void backupWallets() { + walletsSetup.backupWallets(); + } + + public void clearBackup() { + walletsSetup.clearBackups(); + } + + public boolean areWalletsEncrypted() { + return areWalletsAvailable() && + btcWalletService.isEncrypted() && bsqWalletService.isEncrypted(); + } + + public boolean areWalletsAvailable() { + return btcWalletService.isWalletReady() && bsqWalletService.isWalletReady(); + } + + public KeyCrypterScrypt getKeyCrypterScrypt() { + if (areWalletsEncrypted() && btcWalletService.getKeyCrypter() != null) + return (KeyCrypterScrypt) btcWalletService.getKeyCrypter(); + else + return ScryptUtil.getKeyCrypterScrypt(); + } + + public boolean checkAESKey(KeyParameter aesKey) { + return btcWalletService.checkAESKey(aesKey); + } + + public long getChainSeedCreationTimeSeconds() { + return btcWalletService.getKeyChainSeed().getCreationTimeSeconds(); + } + + public boolean hasPositiveBalance() { + final Coin bsqWalletServiceBalance = bsqWalletService.getBalance(Wallet.BalanceType.AVAILABLE); + return btcWalletService.getBalance(Wallet.BalanceType.AVAILABLE) + .add(bsqWalletServiceBalance) + .isPositive(); + } + + public void setAesKey(KeyParameter aesKey) { + btcWalletService.setAesKey(aesKey); + bsqWalletService.setAesKey(aesKey); + tradeWalletService.setAesKey(aesKey); + } + + public DeterministicSeed getDecryptedSeed(KeyParameter aesKey, DeterministicSeed keyChainSeed, KeyCrypter keyCrypter) { + if (keyCrypter != null) { + return keyChainSeed.decrypt(keyCrypter, "", aesKey); + } else { + log.warn("keyCrypter is null"); + return null; + } + } + + // A bsq tx has miner fees in btc included. Thus we need to handle it on both wallets. + public void publishAndCommitBsqTx(Transaction tx, TxType txType, TxBroadcaster.Callback callback) { + // We need to create another instance, otherwise the tx would trigger an invalid state exception + // if it gets committed 2 times + // We clone before commit to avoid unwanted side effects + Transaction clonedTx = btcWalletService.getClonedTransaction(tx); + btcWalletService.commitTx(clonedTx); + bsqWalletService.commitTx(tx, txType); + + // We use a short timeout as there are issues with BSQ txs. See comment in TxBroadcaster + bsqWalletService.broadcastTx(tx, callback, 1); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java new file mode 100644 index 0000000000..ab11627364 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -0,0 +1,156 @@ +/* + * 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 bisq.core.btc.wallet.http; + +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpException; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MemPoolSpaceTxBroadcaster { + private static Socks5ProxyProvider socks5ProxyProvider; + private static Preferences preferences; + private static LocalBitcoinNode localBitcoinNode; + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService( + "MemPoolSpaceTxBroadcaster", 3, 5, 10 * 60); + + public static void init(Socks5ProxyProvider socks5ProxyProvider, + Preferences preferences, + LocalBitcoinNode localBitcoinNode) { + MemPoolSpaceTxBroadcaster.socks5ProxyProvider = socks5ProxyProvider; + MemPoolSpaceTxBroadcaster.preferences = preferences; + MemPoolSpaceTxBroadcaster.localBitcoinNode = localBitcoinNode; + } + + public static void broadcastTx(Transaction tx) { + if (!Config.baseCurrencyNetwork().isMainnet()) { + log.info("MemPoolSpaceTxBroadcaster only supports mainnet"); + return; + } + + if (localBitcoinNode.shouldBeUsed()) { + log.info("A localBitcoinNode is detected and used. For privacy reasons we do not use the tx " + + "broadcast to mempool nodes in that case."); + return; + } + + if (socks5ProxyProvider == null) { + log.warn("We got broadcastTx called before init was called."); + return; + } + + String txIdToSend = tx.getTxId().toString(); + String rawTx = Utils.HEX.encode(tx.bitcoinSerialize(true)); + + List txBroadcastServices = new ArrayList<>(preferences.getDefaultTxBroadcastServices()); + // Broadcast to first service + String serviceAddress = broadcastTx(txIdToSend, rawTx, txBroadcastServices); + if (serviceAddress != null) { + // Broadcast to second service + txBroadcastServices.remove(serviceAddress); + broadcastTx(txIdToSend, rawTx, txBroadcastServices); + } + } + + @Nullable + private static String broadcastTx(String txIdToSend, String rawTx, List txBroadcastServices) { + String serviceAddress = getRandomServiceAddress(txBroadcastServices); + if (serviceAddress == null) { + log.warn("We don't have a serviceAddress available. txBroadcastServices={}", txBroadcastServices); + return null; + } + broadcastTx(serviceAddress, txIdToSend, rawTx); + return serviceAddress; + } + + private static void broadcastTx(String serviceAddress, String txIdToSend, String rawTx) { + TxBroadcastHttpClient httpClient = new TxBroadcastHttpClient(socks5ProxyProvider); + httpClient.setBaseUrl(serviceAddress); + httpClient.setIgnoreSocks5Proxy(false); + + log.info("We broadcast rawTx {} to {}", rawTx, serviceAddress); + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName("MemPoolSpaceTxBroadcaster @ " + serviceAddress); + return httpClient.post(rawTx, "User-Agent", "bisq/" + Version.VERSION); + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(String txId) { + if (txId.equals(txIdToSend)) { + log.info("Broadcast of raw tx with txId {} to {} was successful. rawTx={}", + txId, serviceAddress, rawTx); + } else { + log.error("The txId we got returned from the service does not match " + + "out tx of the sending tx. txId={}; txIdToSend={}", + txId, txIdToSend); + } + } + + public void onFailure(@NotNull Throwable throwable) { + Throwable cause = throwable.getCause(); + if (cause instanceof HttpException) { + int responseCode = ((HttpException) cause).getResponseCode(); + String message = cause.getMessage(); + // See all error codes at: https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h + if (responseCode == 400 && message.contains("code\":-27")) { + log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed", + serviceAddress, txIdToSend); + } else { + log.info("Broadcast of raw tx to {} failed for transaction {}. responseCode={}, error={}", + serviceAddress, txIdToSend, responseCode, message); + } + } else { + log.warn("Broadcast of raw tx with txId {} to {} failed. Error={}", + txIdToSend, serviceAddress, throwable.toString()); + } + } + }, MoreExecutors.directExecutor()); + } + + @Nullable + private static String getRandomServiceAddress(List txBroadcastServices) { + List list = checkNotNull(txBroadcastServices); + return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java b/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java new file mode 100644 index 0000000000..58dfad3f8b --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java @@ -0,0 +1,32 @@ +/* + * 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 bisq.core.btc.wallet.http; + +import bisq.core.trade.txproof.AssetTxProofHttpClient; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class TxBroadcastHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { + TxBroadcastHttpClient(Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } +} diff --git a/core/src/main/java/bisq/core/crypto/ScryptUtil.java b/core/src/main/java/bisq/core/crypto/ScryptUtil.java new file mode 100644 index 0000000000..d3d7c2c8ac --- /dev/null +++ b/core/src/main/java/bisq/core/crypto/ScryptUtil.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.core.crypto; + +import bisq.common.UserThread; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.wallet.Protos; + +import org.bouncycastle.crypto.params.KeyParameter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//TODO: Borrowed form BitcoinJ/Lighthouse. Remove Protos dependency, check complete code logic. +public class ScryptUtil { + private static final Logger log = LoggerFactory.getLogger(ScryptUtil.class); + + public interface DeriveKeyResultHandler { + void handleResult(KeyParameter aesKey); + } + + public static KeyCrypterScrypt getKeyCrypterScrypt() { + Protos.ScryptParameters scryptParameters = Protos.ScryptParameters.newBuilder() + .setP(6) + .setR(8) + .setN(32768) + .setSalt(ByteString.copyFrom(KeyCrypterScrypt.randomSalt())) + .build(); + return new KeyCrypterScrypt(scryptParameters); + } + + public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) { + Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> { + try { + log.debug("Doing key derivation"); + long start = System.currentTimeMillis(); + KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); + long duration = System.currentTimeMillis() - start; + log.debug("Key derivation took {} msec", duration); + UserThread.execute(() -> { + try { + resultHandler.handleResult(aesKey); + } catch (Throwable t) { + t.printStackTrace(); + log.error("Executing task failed. " + t.getMessage()); + throw t; + } + }); + } catch (Throwable t) { + t.printStackTrace(); + log.error("Executing task failed. " + t.getMessage()); + throw t; + } + }); + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java b/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java new file mode 100644 index 0000000000..4c8cc0f409 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.dao; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.model.blockchain.Block; + +import javax.inject.Inject; + +public class DaoEventCoordinator implements DaoSetupService, DaoStateListener { + private final DaoStateService daoStateService; + private final DaoStateSnapshotService daoStateSnapshotService; + private final DaoStateMonitoringService daoStateMonitoringService; + + @Inject + public DaoEventCoordinator(DaoStateService daoStateService, + DaoStateSnapshotService daoStateSnapshotService, + DaoStateMonitoringService daoStateMonitoringService) { + this.daoStateService = daoStateService; + this.daoStateSnapshotService = daoStateSnapshotService; + this.daoStateMonitoringService = daoStateMonitoringService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + this.daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + // We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing. + // We need to listen during batch processing as well to write snapshots during that process. + @Override + public void onDaoStateChanged(Block block) { + // We need to execute first the daoStateMonitoringService + daoStateMonitoringService.createHashFromBlock(block); + daoStateSnapshotService.maybeCreateSnapshot(block); + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java new file mode 100644 index 0000000000..701849b48b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -0,0 +1,792 @@ +/* + * 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 bisq.core.dao; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.dao.governance.ballot.BallotListPresentation; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.bond.lockup.LockupReason; +import bisq.core.dao.governance.bond.lockup.LockupTxService; +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; +import bisq.core.dao.governance.bond.reputation.MyBondedReputation; +import bisq.core.dao.governance.bond.reputation.MyBondedReputationRepository; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.governance.bond.unlock.UnlockTxService; +import bisq.core.dao.governance.myvote.MyVote; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.MyProposalListService; +import bisq.core.dao.governance.proposal.ProposalConsensus; +import bisq.core.dao.governance.proposal.ProposalListPresentation; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.governance.proposal.compensation.CompensationConsensus; +import bisq.core.dao.governance.proposal.compensation.CompensationProposalFactory; +import bisq.core.dao.governance.proposal.confiscatebond.ConfiscateBondProposalFactory; +import bisq.core.dao.governance.proposal.generic.GenericProposalFactory; +import bisq.core.dao.governance.proposal.param.ChangeParamProposalFactory; +import bisq.core.dao.governance.proposal.reimbursement.ReimbursementConsensus; +import bisq.core.dao.governance.proposal.reimbursement.ReimbursementProposalFactory; +import bisq.core.dao.governance.proposal.removeAsset.RemoveAssetProposalFactory; +import bisq.core.dao.governance.proposal.role.RoleProposalFactory; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.BaseTxOutput; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BondedRoleType; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.dao.state.model.governance.Vote; +import bisq.core.dao.state.storage.DaoStateStorageService; + +import bisq.asset.Asset; + +import bisq.common.config.Config; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ExceptionHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Provides a facade to interact with the Dao domain. Hides complexity and domain details to clients (e.g. UI or APIs) + * by providing a reduced API and/or aggregating subroutines. + */ +@Slf4j +public class DaoFacade implements DaoSetupService { + private final ProposalListPresentation proposalListPresentation; + private final ProposalService proposalService; + private final BallotListService ballotListService; + private final BallotListPresentation ballotListPresentation; + private final MyProposalListService myProposalListService; + private final DaoStateService daoStateService; + private final PeriodService periodService; + private final MyBlindVoteListService myBlindVoteListService; + private final MyVoteListService myVoteListService; + private final CompensationProposalFactory compensationProposalFactory; + private final ReimbursementProposalFactory reimbursementProposalFactory; + private final ChangeParamProposalFactory changeParamProposalFactory; + private final ConfiscateBondProposalFactory confiscateBondProposalFactory; + private final RoleProposalFactory roleProposalFactory; + private final GenericProposalFactory genericProposalFactory; + private final RemoveAssetProposalFactory removeAssetProposalFactory; + private final BondedRolesRepository bondedRolesRepository; + private final BondedReputationRepository bondedReputationRepository; + private final MyBondedReputationRepository myBondedReputationRepository; + private final LockupTxService lockupTxService; + private final UnlockTxService unlockTxService; + private final DaoStateStorageService daoStateStorageService; + + private final ObjectProperty phaseProperty = new SimpleObjectProperty<>(DaoPhase.Phase.UNDEFINED); + + @Inject + public DaoFacade(MyProposalListService myProposalListService, + ProposalListPresentation proposalListPresentation, + ProposalService proposalService, + BallotListService ballotListService, + BallotListPresentation ballotListPresentation, + DaoStateService daoStateService, + PeriodService periodService, + MyBlindVoteListService myBlindVoteListService, + MyVoteListService myVoteListService, + CompensationProposalFactory compensationProposalFactory, + ReimbursementProposalFactory reimbursementProposalFactory, + ChangeParamProposalFactory changeParamProposalFactory, + ConfiscateBondProposalFactory confiscateBondProposalFactory, + RoleProposalFactory roleProposalFactory, + GenericProposalFactory genericProposalFactory, + RemoveAssetProposalFactory removeAssetProposalFactory, + BondedRolesRepository bondedRolesRepository, + BondedReputationRepository bondedReputationRepository, + MyBondedReputationRepository myBondedReputationRepository, + LockupTxService lockupTxService, + UnlockTxService unlockTxService, + DaoStateStorageService daoStateStorageService) { + this.proposalListPresentation = proposalListPresentation; + this.proposalService = proposalService; + this.ballotListService = ballotListService; + this.ballotListPresentation = ballotListPresentation; + this.myProposalListService = myProposalListService; + this.daoStateService = daoStateService; + this.periodService = periodService; + this.myBlindVoteListService = myBlindVoteListService; + this.myVoteListService = myVoteListService; + this.compensationProposalFactory = compensationProposalFactory; + this.reimbursementProposalFactory = reimbursementProposalFactory; + this.changeParamProposalFactory = changeParamProposalFactory; + this.confiscateBondProposalFactory = confiscateBondProposalFactory; + this.roleProposalFactory = roleProposalFactory; + this.genericProposalFactory = genericProposalFactory; + this.removeAssetProposalFactory = removeAssetProposalFactory; + this.bondedRolesRepository = bondedRolesRepository; + this.bondedReputationRepository = bondedReputationRepository; + this.myBondedReputationRepository = myBondedReputationRepository; + this.lockupTxService = lockupTxService; + this.unlockTxService = unlockTxService; + this.daoStateStorageService = daoStateStorageService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(new DaoStateListener() { + @Override + public void onNewBlockHeight(int blockHeight) { + if (blockHeight > 0 && periodService.getCurrentCycle() != null) + periodService.getCurrentCycle().getPhaseForHeight(blockHeight).ifPresent(phaseProperty::set); + } + }); + } + + @Override + public void start() { + } + + + public void addBsqStateListener(DaoStateListener listener) { + daoStateService.addDaoStateListener(listener); + } + + public void removeBsqStateListener(DaoStateListener listener) { + daoStateService.removeDaoStateListener(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // + // Phase: Proposal + // + /////////////////////////////////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Present lists + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObservableList getActiveOrMyUnconfirmedProposals() { + return proposalListPresentation.getActiveOrMyUnconfirmedProposals(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Create proposal + /////////////////////////////////////////////////////////////////////////////////////////// + + // Creation of Proposal and proposalTransaction + public ProposalWithTransaction getCompensationProposalWithTransaction(String name, + String link, + Coin requestedBsq) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return compensationProposalFactory.createProposalWithTransaction(name, + link, + requestedBsq); + } + + public ProposalWithTransaction getReimbursementProposalWithTransaction(String name, + String link, + Coin requestedBsq) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return reimbursementProposalFactory.createProposalWithTransaction(name, + link, + requestedBsq); + } + + public ProposalWithTransaction getParamProposalWithTransaction(String name, + String link, + Param param, + String paramValue) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return changeParamProposalFactory.createProposalWithTransaction(name, + link, + param, + paramValue); + } + + public ProposalWithTransaction getConfiscateBondProposalWithTransaction(String name, + String link, + String lockupTxId) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return confiscateBondProposalFactory.createProposalWithTransaction(name, + link, + lockupTxId); + } + + public ProposalWithTransaction getBondedRoleProposalWithTransaction(Role role) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return roleProposalFactory.createProposalWithTransaction(role); + } + + public ProposalWithTransaction getGenericProposalWithTransaction(String name, + String link) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return genericProposalFactory.createProposalWithTransaction(name, link); + } + + public ProposalWithTransaction getRemoveAssetProposalWithTransaction(String name, + String link, + Asset asset) + throws ProposalValidationException, InsufficientMoneyException, TxException { + return removeAssetProposalFactory.createProposalWithTransaction(name, link, asset); + } + + public ObservableList getBondedRoles() { + return bondedRolesRepository.getBonds(); + } + + public List getAcceptedBondedRoles() { + return bondedRolesRepository.getAcceptedBonds(); + } + + // Show fee + public Coin getProposalFee(int chainHeight) { + return ProposalConsensus.getFee(daoStateService, chainHeight); + } + + // Publish proposal tx, proposal payload and persist it to myProposalList + public void publishMyProposal(Proposal proposal, Transaction transaction, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + myProposalListService.publishTxAndPayload(proposal, transaction, resultHandler, errorMessageHandler); + } + + // Check if it is my proposal + public boolean isMyProposal(Proposal proposal) { + return myProposalListService.isMine(proposal); + } + + // Remove my proposal + public boolean removeMyProposal(Proposal proposal) { + return myProposalListService.remove(proposal); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // + // Phase: Blind Vote + // + /////////////////////////////////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Present lists + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObservableList getAllBallots() { + return ballotListPresentation.getAllBallots(); + } + + public List getAllValidBallots() { + return ballotListPresentation.getAllValidBallots(); + } + + public FilteredList getBallotsOfCycle() { + return ballotListPresentation.getBallotsOfCycle(); + } + + public Tuple2 getMeritAndStakeForProposal(String proposalTxId) { + return myVoteListService.getMeritAndStakeForProposal(proposalTxId, myBlindVoteListService); + } + + public long getAvailableMerit() { + return myBlindVoteListService.getCurrentlyAvailableMerit(); + } + + public List getMyVoteListForCycle() { + return myVoteListService.getMyVoteListForCycle(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Vote + /////////////////////////////////////////////////////////////////////////////////////////// + + // Vote on ballot + public void setVote(Ballot ballot, @Nullable Vote vote) { + ballotListService.setVote(ballot, vote); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Create blindVote + /////////////////////////////////////////////////////////////////////////////////////////// + + // When creating blind vote we present fee + public Coin getBlindVoteFeeForCycle() { + return BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight()); + } + + public Tuple2 getBlindVoteMiningFeeAndTxVsize(Coin stake) + throws WalletException, InsufficientMoneyException, TransactionVerificationException { + return myBlindVoteListService.getMiningFeeAndTxVsize(stake); + } + + // Publish blindVote tx and broadcast blindVote to p2p network and store to blindVoteList. + public void publishBlindVote(Coin stake, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + myBlindVoteListService.publishBlindVote(stake, resultHandler, exceptionHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // + // Generic + // + /////////////////////////////////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Presentation of phases + /////////////////////////////////////////////////////////////////////////////////////////// + + // Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the + // next block which would be already the next phase we hide that last block to the user and add it to the break. + public int getFirstBlockOfPhaseForDisplay(int height, DaoPhase.Phase phase) { + int firstBlock = periodService.getFirstBlockOfPhase(height, phase); + switch (phase) { + case UNDEFINED: + break; + case PROPOSAL: + break; + case BREAK1: + firstBlock--; + break; + case BLIND_VOTE: + break; + case BREAK2: + firstBlock--; + break; + case VOTE_REVEAL: + break; + case BREAK3: + firstBlock--; + break; + case RESULT: + break; + } + return firstBlock; + } + + public Map getBlockStartDateByCycleIndex() { + AtomicInteger index = new AtomicInteger(); + Map map = new HashMap<>(); + periodService.getCycles() + .forEach(cycle -> daoStateService.getBlockAtHeight(cycle.getHeightOfFirstBlock()) + .ifPresent(block -> map.put(index.getAndIncrement(), new Date(block.getTime())))); + return map; + } + + // Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the + // next block which would be already the next phase we hide that last block to the user and add it to the break. + public int getLastBlockOfPhaseForDisplay(int height, DaoPhase.Phase phase) { + int lastBlock = periodService.getLastBlockOfPhase(height, phase); + switch (phase) { + case UNDEFINED: + break; + case PROPOSAL: + lastBlock--; + break; + case BREAK1: + break; + case BLIND_VOTE: + lastBlock--; + break; + case BREAK2: + break; + case VOTE_REVEAL: + lastBlock--; + break; + case BREAK3: + break; + case RESULT: + break; + } + return lastBlock; + } + + // Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the + // next block which would be already the next phase we hide that last block to the user and add it to the break. + public int getDurationForPhaseForDisplay(DaoPhase.Phase phase) { + int duration = periodService.getDurationForPhase(phase, daoStateService.getChainHeight()); + switch (phase) { + case UNDEFINED: + break; + case PROPOSAL: + duration--; + break; + case BREAK1: + duration++; + break; + case BLIND_VOTE: + duration--; + break; + case BREAK2: + duration++; + break; + case VOTE_REVEAL: + duration--; + break; + case BREAK3: + duration++; + break; + case RESULT: + break; + } + return duration; + } + + public int getCurrentCycleDuration() { + Cycle currentCycle = periodService.getCurrentCycle(); + return currentCycle != null ? currentCycle.getDuration() : 0; + } + + // listeners for phase change + public ReadOnlyObjectProperty phaseProperty() { + return phaseProperty; + } + + public int getChainHeight() { + return daoStateService.getChainHeight(); + } + + public Optional getBlockAtChainHeight() { + return getBlockAtHeight(getChainHeight()); + } + + public Optional getBlockAtHeight(int chainHeight) { + return daoStateService.getBlockAtHeight(chainHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Bonding + /////////////////////////////////////////////////////////////////////////////////////////// + + public void publishLockupTx(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash, + Consumer resultHandler, ExceptionHandler exceptionHandler) { + lockupTxService.publishLockupTx(lockupAmount, lockTime, lockupReason, hash, resultHandler, exceptionHandler); + } + + public Tuple2 getLockupTxMiningFeeAndTxVsize(Coin lockupAmount, + int lockTime, + LockupReason lockupReason, + byte[] hash) + throws InsufficientMoneyException, IOException, TransactionVerificationException, WalletException { + return lockupTxService.getMiningFeeAndTxVsize(lockupAmount, lockTime, lockupReason, hash); + } + + public void publishUnlockTx(String lockupTxId, Consumer resultHandler, + ExceptionHandler exceptionHandler) { + unlockTxService.publishUnlockTx(lockupTxId, resultHandler, exceptionHandler); + } + + public Tuple2 getUnlockTxMiningFeeAndTxVsize(String lockupTxId) + throws InsufficientMoneyException, TransactionVerificationException, WalletException { + return unlockTxService.getMiningFeeAndTxVsize(lockupTxId); + } + + public long getTotalLockupAmount() { + return daoStateService.getTotalLockupAmount(); + } + + public long getTotalAmountOfUnLockingTxOutputs() { + return daoStateService.getTotalAmountOfUnLockingTxOutputs(); + } + + public long getTotalAmountOfUnLockedTxOutputs() { + return daoStateService.getTotalAmountOfUnLockedTxOutputs(); + } + + public long getTotalAmountOfConfiscatedTxOutputs() { + return daoStateService.getTotalAmountOfConfiscatedTxOutputs(); + } + + public long getTotalAmountOfInvalidatedBsq() { + return daoStateService.getTotalAmountOfInvalidatedBsq(); + } + + // Contains burned fee and invalidated bsq due invalid txs + public long getTotalAmountOfBurntBsq() { + return daoStateService.getTotalAmountOfBurntBsq(); + } + + public List getInvalidTxs() { + return daoStateService.getInvalidTxs(); + } + + public List getIrregularTxs() { + return daoStateService.getIrregularTxs(); + } + + public long getTotalAmountOfUnspentTxOutputs() { + // Does not consider confiscated outputs (they stay as utxo) + return daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum(); + } + + public Optional getLockTime(String txId) { + return daoStateService.getLockTime(txId); + } + + + public List getAllBonds() { + List bonds = new ArrayList<>(bondedReputationRepository.getBonds()); + bonds.addAll(bondedRolesRepository.getBonds()); + return bonds; + } + + public List getAllActiveBonds() { + List bonds = new ArrayList<>(bondedReputationRepository.getActiveBonds()); + bonds.addAll(bondedRolesRepository.getActiveBonds()); + return bonds; + } + + public ObservableList getMyBondedReputations() { + return myBondedReputationRepository.getMyBondedReputations(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Use case: Present transaction related state + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional getTx(String txId) { + return daoStateService.getTx(txId); + } + + public int getGenesisBlockHeight() { + return daoStateService.getGenesisBlockHeight(); + } + + public String getGenesisTxId() { + return daoStateService.getGenesisTxId(); + } + + public Coin getGenesisTotalSupply() { + return daoStateService.getGenesisTotalSupply(); + } + + public int getNumIssuanceTransactions(IssuanceType issuanceType) { + return daoStateService.getIssuanceSetForType(issuanceType).size(); + } + + public Set getBurntFeeTxs() { + return daoStateService.getBurntFeeTxs(); + } + + public Set getUnspentTxOutputs() { + return daoStateService.getUnspentTxOutputs(); + } + + public int getNumTxs() { + return daoStateService.getNumTxs(); + } + + public Optional getLockupTxOutput(String txId) { + return daoStateService.getLockupTxOutput(txId); + } + + public long getTotalIssuedAmount(IssuanceType issuanceType) { + return daoStateService.getTotalIssuedAmount(issuanceType); + } + + public long getBlockTime(int issuanceBlockHeight) { + return daoStateService.getBlockTime(issuanceBlockHeight); + } + + public int getIssuanceBlockHeight(String txId) { + return daoStateService.getIssuanceBlockHeight(txId); + } + + public boolean isIssuanceTx(String txId, IssuanceType issuanceType) { + return daoStateService.isIssuanceTx(txId, issuanceType); + } + + public boolean hasTxBurntFee(String hashAsString) { + return daoStateService.hasTxBurntFee(hashAsString); + } + + public Optional getOptionalTxType(String txId) { + return daoStateService.getOptionalTxType(txId); + } + + public TxType getTxType(String txId) { + return daoStateService.getTx(txId).map(Tx::getTxType).orElse(TxType.UNDEFINED_TX_TYPE); + } + + public boolean isInPhaseButNotLastBlock(DaoPhase.Phase phase) { + return periodService.isInPhaseButNotLastBlock(phase); + } + + public boolean isTxInCorrectCycle(int txHeight, int chainHeight) { + return periodService.isTxInCorrectCycle(txHeight, chainHeight); + } + + public boolean isTxInCorrectCycle(String txId, int chainHeight) { + return periodService.isTxInCorrectCycle(txId, chainHeight); + } + + public Coin getMinCompensationRequestAmount() { + return CompensationConsensus.getMinCompensationRequestAmount(daoStateService, periodService.getChainHeight()); + } + + public Coin getMaxCompensationRequestAmount() { + return CompensationConsensus.getMaxCompensationRequestAmount(daoStateService, periodService.getChainHeight()); + } + + public Coin getMinReimbursementRequestAmount() { + return ReimbursementConsensus.getMinReimbursementRequestAmount(daoStateService, periodService.getChainHeight()); + } + + public Coin getMaxReimbursementRequestAmount() { + return ReimbursementConsensus.getMaxReimbursementRequestAmount(daoStateService, periodService.getChainHeight()); + } + + public String getParamValue(Param param) { + return getParamValue(param, periodService.getChainHeight()); + } + + public String getParamValue(Param param, int blockHeight) { + return daoStateService.getParamValue(param, blockHeight); + } + + public void resyncDaoStateFromGenesis(Runnable resultHandler) { + daoStateStorageService.resyncDaoStateFromGenesis(resultHandler); + } + + public void resyncDaoStateFromResources(File storageDir) throws IOException { + daoStateStorageService.resyncDaoStateFromResources(storageDir); + } + + public boolean isMyRole(Role role) { + return bondedRolesRepository.isMyRole(role); + } + + public Optional getBondByLockupTxId(String lockupTxId) { + return getAllBonds().stream().filter(e -> lockupTxId.equals(e.getLockupTxId())).findAny(); + } + + public double getRequiredThreshold(Proposal proposal) { + return proposalService.getRequiredThreshold(proposal); + } + + public Coin getRequiredQuorum(Proposal proposal) { + return proposalService.getRequiredQuorum(proposal); + } + + public long getRequiredBond(Optional roleProposal) { + Optional bondedRoleType = roleProposal.map(e -> e.getRole().getBondedRoleType()); + checkArgument(bondedRoleType.isPresent(), "bondedRoleType must be present"); + int height = roleProposal.flatMap(p -> daoStateService.getTx(p.getTxId())) + .map(BaseTx::getBlockHeight) + .orElse(daoStateService.getChainHeight()); + long requiredBondUnit = roleProposal.map(RoleProposal::getRequiredBondUnit) + .orElse(bondedRoleType.get().getRequiredBondUnit()); + long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value; + return requiredBondUnit * baseFactor; + } + + public long getRequiredBond(RoleProposal roleProposal) { + return getRequiredBond(Optional.of(roleProposal)); + } + + public long getRequiredBond(BondedRoleType bondedRoleType) { + int height = daoStateService.getChainHeight(); + long requiredBondUnit = bondedRoleType.getRequiredBondUnit(); + long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value; + return requiredBondUnit * baseFactor; + } + + public Set getAllPastParamValues(Param param) { + Set set = new HashSet<>(); + periodService.getCycles().forEach(cycle -> { + set.add(getParamValue(param, cycle.getHeightOfFirstBlock())); + }); + return set; + } + + public Set getAllDonationAddresses() { + // We support any of the past addresses as well as in case the peer has not enabled the DAO or is out of sync we + // do not want to break validation. + Set allPastParamValues = getAllPastParamValues(Param.RECIPIENT_BTC_ADDRESS); + + // If Dao is deactivated we need to add the default address as getAllPastParamValues will not return us any. + if (allPastParamValues.isEmpty()) { + allPastParamValues.add(Param.RECIPIENT_BTC_ADDRESS.getDefaultValue()); + } + + if (Config.baseCurrencyNetwork().isMainnet()) { + // If Dao is deactivated we need to add the past addresses used as well. + // This list need to be updated once a new address gets defined. + allPastParamValues.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); // burning man 2019 + allPastParamValues.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); // burningman2 + allPastParamValues.add("34VLFgtFKAtwTdZ5rengTT2g2zC99sWQLC"); // burningman3 (https://github.com/bisq-network/roles/issues/80#issuecomment-723577776) + } + + return allPastParamValues; + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoKillSwitch.java b/core/src/main/java/bisq/core/dao/DaoKillSwitch.java new file mode 100644 index 0000000000..2dbf54c53c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoKillSwitch.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.dao; + +import bisq.core.dao.exceptions.DaoDisabledException; +import bisq.core.filter.Filter; +import bisq.core.filter.FilterManager; + +import bisq.common.app.Version; + +import javax.inject.Inject; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class DaoKillSwitch implements DaoSetupService { + private final FilterManager filterManager; + + @Getter + private boolean daoDisabled; + + @Inject + public DaoKillSwitch(FilterManager filterManager) { + this.filterManager = filterManager; + } + + @Override + public void addListeners() { + filterManager.filterProperty().addListener((observable, oldValue, newValue) -> applyFilter(newValue)); + } + + @Override + public void start() { + applyFilter(filterManager.getFilter()); + } + + private void applyFilter(@Nullable Filter filter) { + if (filter == null) { + daoDisabled = false; + return; + } + + boolean requireUpdateToNewVersion = false; + String disableDaoBelowVersion = filter.getDisableDaoBelowVersion(); + if (disableDaoBelowVersion != null && !disableDaoBelowVersion.isEmpty()) { + requireUpdateToNewVersion = Version.isNewVersion(disableDaoBelowVersion); + } + + daoDisabled = requireUpdateToNewVersion || filter.isDisableDao(); + } + + public void assertDaoIsNotDisabled() { + if (isDaoDisabled()) { + throw new DaoDisabledException("The DAO features have been disabled by the Bisq developers. " + + "Please check out the Bisq Forum for further information."); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoModule.java b/core/src/main/java/bisq/core/dao/DaoModule.java new file mode 100644 index 0000000000..87d5f9e7a0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoModule.java @@ -0,0 +1,229 @@ +/* + * 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 bisq.core.dao; + +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.dao.governance.ballot.BallotListPresentation; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.BlindVoteValidator; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.blindvote.network.RepublishGovernanceDataHandler; +import bisq.core.dao.governance.blindvote.storage.BlindVoteStorageService; +import bisq.core.dao.governance.blindvote.storage.BlindVoteStore; +import bisq.core.dao.governance.bond.lockup.LockupTxService; +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; +import bisq.core.dao.governance.bond.reputation.MyBondedReputationRepository; +import bisq.core.dao.governance.bond.reputation.MyReputationListService; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.governance.bond.unlock.UnlockTxService; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.governance.proposal.MyProposalListService; +import bisq.core.dao.governance.proposal.ProposalListPresentation; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.ProposalValidatorProvider; +import bisq.core.dao.governance.proposal.compensation.CompensationProposalFactory; +import bisq.core.dao.governance.proposal.compensation.CompensationValidator; +import bisq.core.dao.governance.proposal.confiscatebond.ConfiscateBondProposalFactory; +import bisq.core.dao.governance.proposal.confiscatebond.ConfiscateBondValidator; +import bisq.core.dao.governance.proposal.generic.GenericProposalFactory; +import bisq.core.dao.governance.proposal.generic.GenericProposalValidator; +import bisq.core.dao.governance.proposal.param.ChangeParamProposalFactory; +import bisq.core.dao.governance.proposal.param.ChangeParamValidator; +import bisq.core.dao.governance.proposal.reimbursement.ReimbursementProposalFactory; +import bisq.core.dao.governance.proposal.reimbursement.ReimbursementValidator; +import bisq.core.dao.governance.proposal.removeAsset.RemoveAssetProposalFactory; +import bisq.core.dao.governance.proposal.removeAsset.RemoveAssetValidator; +import bisq.core.dao.governance.proposal.role.RoleProposalFactory; +import bisq.core.dao.governance.proposal.role.RoleValidator; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStorageService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStore; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalStorageService; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalStore; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; +import bisq.core.dao.governance.voteresult.VoteResultService; +import bisq.core.dao.governance.voteresult.issuance.IssuanceService; +import bisq.core.dao.governance.votereveal.VoteRevealService; +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService; +import bisq.core.dao.monitoring.network.DaoStateNetworkService; +import bisq.core.dao.monitoring.network.ProposalStateNetworkService; +import bisq.core.dao.node.BsqNodeProvider; +import bisq.core.dao.node.explorer.ExportJsonFilesService; +import bisq.core.dao.node.full.FullNode; +import bisq.core.dao.node.full.RpcService; +import bisq.core.dao.node.full.network.FullNodeNetworkService; +import bisq.core.dao.node.lite.LiteNode; +import bisq.core.dao.node.lite.network.LiteNodeNetworkService; +import bisq.core.dao.node.parser.BlockParser; +import bisq.core.dao.node.parser.TxParser; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.GenesisTxInfo; +import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.storage.DaoStateStorageService; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +import static com.google.inject.name.Names.named; + +public class DaoModule extends AppModule { + + public DaoModule(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(DaoSetup.class).in(Singleton.class); + bind(DaoFacade.class).in(Singleton.class); + bind(DaoEventCoordinator.class).in(Singleton.class); + bind(DaoKillSwitch.class).in(Singleton.class); + + // Node, parser + bind(BsqNodeProvider.class).in(Singleton.class); + bind(FullNode.class).in(Singleton.class); + bind(LiteNode.class).in(Singleton.class); + bind(RpcService.class).in(Singleton.class); + bind(BlockParser.class).in(Singleton.class); + bind(FullNodeNetworkService.class).in(Singleton.class); + bind(LiteNodeNetworkService.class).in(Singleton.class); + + // DaoState + bind(GenesisTxInfo.class).in(Singleton.class); + bind(DaoState.class).in(Singleton.class); + bind(DaoStateService.class).in(Singleton.class); + bind(DaoStateSnapshotService.class).in(Singleton.class); + bind(DaoStateStorageService.class).in(Singleton.class); + bind(DaoStateMonitoringService.class).in(Singleton.class); + bind(DaoStateNetworkService.class).in(Singleton.class); + bind(ProposalStateMonitoringService.class).in(Singleton.class); + bind(ProposalStateNetworkService.class).in(Singleton.class); + bind(BlindVoteStateMonitoringService.class).in(Singleton.class); + bind(BlindVoteStateNetworkService.class).in(Singleton.class); + bind(UnconfirmedBsqChangeOutputListService.class).in(Singleton.class); + + bind(ExportJsonFilesService.class).in(Singleton.class); + + // Period + bind(CycleService.class).in(Singleton.class); + bind(PeriodService.class).in(Singleton.class); + + // blockchain parser + bind(TxParser.class).in(Singleton.class); + + // Proposal + bind(ProposalService.class).in(Singleton.class); + + bind(MyProposalListService.class).in(Singleton.class); + bind(ProposalListPresentation.class).in(Singleton.class); + + bind(ProposalStore.class).in(Singleton.class); + bind(ProposalStorageService.class).in(Singleton.class); + bind(TempProposalStore.class).in(Singleton.class); + bind(TempProposalStorageService.class).in(Singleton.class); + + bind(ProposalValidatorProvider.class).in(Singleton.class); + + bind(CompensationValidator.class).in(Singleton.class); + bind(CompensationProposalFactory.class).in(Singleton.class); + + bind(ReimbursementValidator.class).in(Singleton.class); + bind(ReimbursementProposalFactory.class).in(Singleton.class); + + bind(ChangeParamValidator.class).in(Singleton.class); + bind(ChangeParamProposalFactory.class).in(Singleton.class); + + bind(RoleValidator.class).in(Singleton.class); + bind(RoleProposalFactory.class).in(Singleton.class); + + bind(ConfiscateBondValidator.class).in(Singleton.class); + bind(ConfiscateBondProposalFactory.class).in(Singleton.class); + + bind(GenericProposalValidator.class).in(Singleton.class); + bind(GenericProposalFactory.class).in(Singleton.class); + + bind(RemoveAssetValidator.class).in(Singleton.class); + bind(RemoveAssetProposalFactory.class).in(Singleton.class); + + // Ballot + bind(BallotListService.class).in(Singleton.class); + bind(BallotListPresentation.class).in(Singleton.class); + + // MyVote + bind(MyVoteListService.class).in(Singleton.class); + + // BlindVote + bind(BlindVoteListService.class).in(Singleton.class); + bind(BlindVoteStore.class).in(Singleton.class); + bind(BlindVoteStorageService.class).in(Singleton.class); + bind(BlindVoteValidator.class).in(Singleton.class); + bind(MyBlindVoteListService.class).in(Singleton.class); + + // VoteReveal + bind(VoteRevealService.class).in(Singleton.class); + + // VoteResult + bind(VoteResultService.class).in(Singleton.class); + bind(MissingDataRequestService.class).in(Singleton.class); + bind(IssuanceService.class).in(Singleton.class); + bind(RepublishGovernanceDataHandler.class).in(Singleton.class); + + // Genesis + bindConstant().annotatedWith(named(Config.GENESIS_TX_ID)).to(config.genesisTxId); + bindConstant().annotatedWith(named(Config.GENESIS_BLOCK_HEIGHT)).to(config.genesisBlockHeight); + bindConstant().annotatedWith(named(Config.GENESIS_TOTAL_SUPPLY)).to(config.genesisTotalSupply); + + // Bonds + bind(LockupTxService.class).in(Singleton.class); + bind(UnlockTxService.class).in(Singleton.class); + bind(BondedRolesRepository.class).in(Singleton.class); + bind(BondedReputationRepository.class).in(Singleton.class); + bind(MyReputationListService.class).in(Singleton.class); + bind(MyBondedReputationRepository.class).in(Singleton.class); + + // Asset + bind(AssetService.class).in(Singleton.class); + + // Proof of burn + bind(ProofOfBurnService.class).in(Singleton.class); + bind(MyProofOfBurnListService.class).in(Singleton.class); + + // Options + bindConstant().annotatedWith(named(Config.RPC_USER)).to(config.rpcUser); + bindConstant().annotatedWith(named(Config.RPC_PASSWORD)).to(config.rpcPassword); + bindConstant().annotatedWith(named(Config.RPC_HOST)).to(config.rpcHost); + bindConstant().annotatedWith(named(Config.RPC_PORT)).to(config.rpcPort); + bindConstant().annotatedWith(named(Config.RPC_BLOCK_NOTIFICATION_PORT)).to(config.rpcBlockNotificationPort); + bindConstant().annotatedWith(named(Config.RPC_BLOCK_NOTIFICATION_HOST)).to(config.rpcBlockNotificationHost); + bindConstant().annotatedWith(named(Config.DUMP_BLOCKCHAIN_DATA)).to(config.dumpBlockchainData); + bindConstant().annotatedWith(named(Config.FULL_DAO_NODE)).to(config.fullDaoNode); + bindConstant().annotatedWith(named(Config.DAO_ACTIVATED)).to(config.daoActivated); + } +} + diff --git a/core/src/main/java/bisq/core/dao/DaoSetup.java b/core/src/main/java/bisq/core/dao/DaoSetup.java new file mode 100644 index 0000000000..b8c13582f7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoSetup.java @@ -0,0 +1,133 @@ +/* + * 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 bisq.core.dao; + +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; +import bisq.core.dao.governance.bond.reputation.MyBondedReputationRepository; +import bisq.core.dao.governance.bond.reputation.MyReputationListService; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.governance.proposal.ProposalListPresentation; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; +import bisq.core.dao.governance.voteresult.VoteResultService; +import bisq.core.dao.governance.votereveal.VoteRevealService; +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.node.BsqNode; +import bisq.core.dao.node.BsqNodeProvider; +import bisq.core.dao.node.explorer.ExportJsonFilesService; +import bisq.core.dao.state.DaoStateService; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * High level entry point for Dao domain. + * We initialize all main service classes here to be sure they are started. + */ +public class DaoSetup { + private final BsqNode bsqNode; + private final List daoSetupServices = new ArrayList<>(); + + @Inject + public DaoSetup(BsqNodeProvider bsqNodeProvider, + DaoStateService daoStateService, + CycleService cycleService, + BallotListService ballotListService, + ProposalService proposalService, + ProposalListPresentation proposalListPresentation, + BlindVoteListService blindVoteListService, + MyBlindVoteListService myBlindVoteListService, + VoteRevealService voteRevealService, + VoteResultService voteResultService, + MissingDataRequestService missingDataRequestService, + BondedReputationRepository bondedReputationRepository, + BondedRolesRepository bondedRolesRepository, + MyReputationListService myReputationListService, + MyBondedReputationRepository myBondedReputationRepository, + AssetService assetService, + ProofOfBurnService proofOfBurnService, + DaoFacade daoFacade, + ExportJsonFilesService exportJsonFilesService, + DaoKillSwitch daoKillSwitch, + DaoStateMonitoringService daoStateMonitoringService, + ProposalStateMonitoringService proposalStateMonitoringService, + BlindVoteStateMonitoringService blindVoteStateMonitoringService, + DaoEventCoordinator daoEventCoordinator) { + + bsqNode = bsqNodeProvider.getBsqNode(); + + // We need to take care of order of execution. + + // For order critical event flow we use the daoEventCoordinator to delegate the calls from anonymous listeners + // to concrete clients. + daoSetupServices.add(daoEventCoordinator); + + daoSetupServices.add(daoStateService); + daoSetupServices.add(cycleService); + daoSetupServices.add(ballotListService); + daoSetupServices.add(proposalService); + daoSetupServices.add(proposalListPresentation); + daoSetupServices.add(blindVoteListService); + daoSetupServices.add(myBlindVoteListService); + daoSetupServices.add(voteRevealService); + daoSetupServices.add(voteResultService); + daoSetupServices.add(missingDataRequestService); + daoSetupServices.add(bondedReputationRepository); + daoSetupServices.add(bondedRolesRepository); + daoSetupServices.add(myReputationListService); + daoSetupServices.add(myBondedReputationRepository); + daoSetupServices.add(assetService); + daoSetupServices.add(proofOfBurnService); + daoSetupServices.add(daoFacade); + daoSetupServices.add(exportJsonFilesService); + daoSetupServices.add(daoKillSwitch); + daoSetupServices.add(daoStateMonitoringService); + daoSetupServices.add(proposalStateMonitoringService); + daoSetupServices.add(blindVoteStateMonitoringService); + + daoSetupServices.add(bsqNodeProvider.getBsqNode()); + } + + public void onAllServicesInitialized(Consumer errorMessageHandler, + Consumer warnMessageHandler) { + bsqNode.setErrorMessageHandler(errorMessageHandler); + bsqNode.setWarnMessageHandler(warnMessageHandler); + + // We add first all listeners at all services and then call the start methods. + // Some services are listening on others so we need to make sure that the + // listeners are set before we call start as that might trigger state change + // which triggers listeners. + daoSetupServices.forEach(DaoSetupService::addListeners); + daoSetupServices.forEach(DaoSetupService::start); + } + + public void shutDown() { + bsqNode.shutDown(); + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoSetupService.java b/core/src/main/java/bisq/core/dao/DaoSetupService.java new file mode 100644 index 0000000000..86974dd386 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoSetupService.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.dao; + +/** + * All main service classes implements that interface to guarantee a controlled + * startup sequence. + */ +public interface DaoSetupService { + void addListeners(); + + void start(); +} diff --git a/core/src/main/java/bisq/core/dao/exceptions/DaoDisabledException.java b/core/src/main/java/bisq/core/dao/exceptions/DaoDisabledException.java new file mode 100644 index 0000000000..7845ebc98d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/exceptions/DaoDisabledException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.exceptions; + +public class DaoDisabledException extends RuntimeException { + public DaoDisabledException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/dao/exceptions/PublishToP2PNetworkException.java b/core/src/main/java/bisq/core/dao/exceptions/PublishToP2PNetworkException.java new file mode 100644 index 0000000000..e3be1947c4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/exceptions/PublishToP2PNetworkException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.exceptions; + +public class PublishToP2PNetworkException extends RuntimeException { + public PublishToP2PNetworkException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/ConsensusCritical.java b/core/src/main/java/bisq/core/dao/governance/ConsensusCritical.java new file mode 100644 index 0000000000..1162a5e5b1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/ConsensusCritical.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.core.dao.governance; + +/** + * Marker interface for classes which are critical in the vote consensus process. Any changes in that class might cause + * consensus failures with older versions. + */ +public interface ConsensusCritical { +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java new file mode 100644 index 0000000000..f144f00eb3 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.dao.governance.asset; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.crypto.Hash; + +import org.bitcoinj.core.Coin; + +import com.google.common.base.Charsets; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AssetConsensus { + public static Coin getFeePerDay(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.ASSET_LISTING_FEE_PER_DAY, chainHeight); + } + + public static byte[] getHash(StatefulAsset statefulAsset) { + String stringInput = "AssetListingFee-" + statefulAsset.getTickerSymbol(); + final byte[] bytes = stringInput.getBytes(Charsets.UTF_8); + return Hash.getSha256Ripemd160hash(bytes); + } + + public static byte[] getOpReturnData(byte[] hash) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.ASSET_LISTING_FEE.getType()); + outputStream.write(Version.ASSET_LISTING_FEE); + outputStream.write(hash); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + return new byte[0]; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java new file mode 100644 index 0000000000..0a6843a644 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -0,0 +1,335 @@ +/* + * 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 bisq.core.dao.governance.asset; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; +import bisq.core.locale.CurrencyUtil; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.app.DevEnv; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class AssetService implements DaoSetupService, DaoStateListener { + private static final long DEFAULT_LOOK_BACK_PERIOD = 120; // 120 days + + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + private final TradeStatisticsManager tradeStatisticsManager; + private final DaoStateService daoStateService; + private final BsqFormatter bsqFormatter; + + // Only accessed via getter which fills the list on demand + private final List lazyLoadedStatefulAssets = new ArrayList<>(); + private long bsqFeePerDay; + private long minVolumeInBtc; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AssetService(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + TradeStatisticsManager tradeStatisticsManager, + DaoStateService daoStateService, + BsqFormatter bsqFormatter) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.tradeStatisticsManager = tradeStatisticsManager; + this.daoStateService = daoStateService; + this.bsqFormatter = bsqFormatter; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + } + + @Override + @SuppressWarnings({"EmptyMethod"}) + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + int chainHeight = daoStateService.getChainHeight(); + bsqFeePerDay = daoStateService.getParamValueAsCoin(Param.ASSET_LISTING_FEE_PER_DAY, chainHeight).value; + minVolumeInBtc = daoStateService.getParamValueAsCoin(Param.ASSET_MIN_VOLUME, chainHeight).value; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getStatefulAssets() { + if (lazyLoadedStatefulAssets.isEmpty()) { + lazyLoadedStatefulAssets.addAll(CurrencyUtil.getSortedAssetStream() + .filter(asset -> !asset.getTickerSymbol().equals("BSQ")) + .map(StatefulAsset::new) + .collect(Collectors.toList())); + } + return lazyLoadedStatefulAssets; + } + + // Call takes bout 22 ms. Should be only called on demand (e.g. view is showing the data) + public void updateAssetStates() { + // For performance optimisation we map the trade stats to a temporary lookup map and convert it to a custom + // TradeAmountDateTuple object holding only the data we need. + Map> lookupMap = new HashMap<>(); + tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> CurrencyUtil.isCryptoCurrency(e.getCurrency())) + .forEach(e -> { + lookupMap.putIfAbsent(e.getCurrency(), new ArrayList<>()); + lookupMap.get(e.getCurrency()).add(new TradeAmountDateTuple(e.getAmount(), e.getDateAsLong())); + }); + + getStatefulAssets().stream() + .filter(e -> AssetState.REMOVED_BY_VOTING != e.getAssetState()) // if once set to REMOVED_BY_VOTING we ignore it for further processing + .forEach(statefulAsset -> { + AssetState assetState; + String tickerSymbol = statefulAsset.getTickerSymbol(); + if (wasAssetRemovedByVoting(tickerSymbol)) { + assetState = AssetState.REMOVED_BY_VOTING; + } else { + statefulAsset.setFeePayments(getFeePayments(statefulAsset)); + long lookBackPeriodInDays = getLookBackPeriodInDays(statefulAsset); + statefulAsset.setLookBackPeriodInDays(lookBackPeriodInDays); + long lookupDate = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(lookBackPeriodInDays); + long tradeVolume = getTradeVolume(lookupDate, lookupMap.get(tickerSymbol)); + statefulAsset.setTradeVolume(tradeVolume); + if (isInTrialPeriod(statefulAsset)) { + assetState = AssetState.IN_TRIAL_PERIOD; + } else if (tradeVolume >= minVolumeInBtc) { + assetState = AssetState.ACTIVELY_TRADED; + } else { + assetState = AssetState.DE_LISTED; + } + } + statefulAsset.setAssetState(assetState); + }); + + lookupMap.clear(); + } + + public boolean isActive(String tickerSymbol) { + return DevEnv.isDaoActivated() ? findAsset(tickerSymbol).map(StatefulAsset::isActive).orElse(false) : true; + } + + public Transaction payFee(StatefulAsset statefulAsset, + long listingFee) throws InsufficientMoneyException, TxException { + checkArgument(!statefulAsset.wasRemovedByVoting(), "Asset must not have been removed"); + checkArgument(listingFee >= getFeePerDay().value, "Fee must not be less then listing fee for 1 day."); + checkArgument(listingFee % 100 == 0, "Fee must be a multiple of 1 BSQ (100 satoshi)."); + try { + // We create a prepared Bsq Tx for the listing fee. + Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTxForAssetListing(Coin.valueOf(listingFee)); + byte[] hash = AssetConsensus.getHash(statefulAsset); + byte[] opReturnData = AssetConsensus.getOpReturnData(hash); + // We add the BTC inputs for the miner fee. + Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); + // We sign the BSQ inputs of the final tx. + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Asset listing fee tx: " + transaction); + return transaction; + } catch (WalletException | TransactionVerificationException e) { + throw new TxException(e); + } + } + + public Coin getFeePerDay() { + return AssetConsensus.getFeePerDay(daoStateService, daoStateService.getChainHeight()); + } + + public void publishTransaction(Transaction transaction, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + walletsManager.publishAndCommitBsqTx(transaction, TxType.ASSET_LISTING_FEE, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("Asset listing fee tx has been published. TxId={}", transaction.getTxId().toString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // Get the trade volume from lookupDate until current date + private long getTradeVolume(long lookupDate, @Nullable List tradeAmountDateTupleList) { + if (tradeAmountDateTupleList == null) { + // Was never traded + return 0; + } + + return tradeAmountDateTupleList.stream() + .filter(e -> e.getTradeDate() > lookupDate) + .mapToLong(TradeAmountDateTuple::getTradeAmount) + .sum(); + } + + private boolean isInTrialPeriod(StatefulAsset statefulAsset) { + for (FeePayment feePayment : statefulAsset.getFeePayments()) { + Optional passedDays = feePayment.getPassedDays(daoStateService); + if (passedDays.isPresent()) { + long daysCoveredByFee = feePayment.daysCoveredByFee(bsqFeePerDay); + if (daysCoveredByFee >= passedDays.get()) { + return true; + } + } + } + return false; + } + + + @NotNull + private Long getLookBackPeriodInDays(StatefulAsset statefulAsset) { + // We need to use the block height of the fee payment tx not the current one as feePerDay might have been + // changed in the meantime. + long bsqFeePerDay = statefulAsset.getLastFeePayment() + .flatMap(feePayment -> daoStateService.getTx(feePayment.getTxId())) + .map(tx -> daoStateService.getParamValueAsCoin(Param.ASSET_LISTING_FEE_PER_DAY, tx.getBlockHeight()).value) + .orElse(bsqFormatter.parseParamValueToCoin(Param.ASSET_LISTING_FEE_PER_DAY, Param.ASSET_LISTING_FEE_PER_DAY.getDefaultValue()).value); + + return statefulAsset.getLastFeePayment() + .map(feePayment -> feePayment.daysCoveredByFee(bsqFeePerDay)) + .orElse(DEFAULT_LOOK_BACK_PERIOD); + } + + private List getFeePayments(StatefulAsset statefulAsset) { + return getFeeTxs(statefulAsset).stream() + .map(tx -> { + String txId = tx.getId(); + long burntFee = tx.getBurntFee(); + return new FeePayment(txId, burntFee); + }) + .collect(Collectors.toList()); + } + + private List getFeeTxs(StatefulAsset statefulAsset) { + return daoStateService.getAssetListingFeeOpReturnTxOutputs().stream() + .filter(txOutput -> { + byte[] hash = AssetConsensus.getHash(statefulAsset); + byte[] opReturnData = AssetConsensus.getOpReturnData(hash); + return Arrays.equals(opReturnData, txOutput.getOpReturnData()); + }) + .map(txOutput -> daoStateService.getTx(txOutput.getTxId()).orElse(null)) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(BaseTx::getTime)) + .collect(Collectors.toList()); + } + + private Optional findAsset(String tickerSymbol) { + return getStatefulAssets().stream().filter(e -> e.getTickerSymbol().equals(tickerSymbol)).findAny(); + } + + private boolean wasAssetRemovedByVoting(String tickerSymbol) { + boolean isRemoved = getAcceptedRemoveAssetProposalStream() + .anyMatch(proposal -> proposal.getTickerSymbol().equals(tickerSymbol)); + if (isRemoved) + log.info("Asset '{}' was removed", CurrencyUtil.getNameAndCode(tickerSymbol)); + + return isRemoved; + } + + private Stream getAcceptedRemoveAssetProposalStream() { + return daoStateService.getEvaluatedProposalList().stream() + .filter(evaluatedProposal -> evaluatedProposal.getProposal() instanceof RemoveAssetProposal) + .filter(EvaluatedProposal::isAccepted) + .map(e -> ((RemoveAssetProposal) e.getProposal())); + } + + @Value + private static final class TradeAmountDateTuple { + private final long tradeAmount; + private final long tradeDate; + + TradeAmountDateTuple(long tradeAmount, long tradeDate) { + this.tradeAmount = tradeAmount; + this.tradeDate = tradeDate; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java new file mode 100644 index 0000000000..24fed67670 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.core.dao.governance.asset; + +/** + * Maintain translation stings ("dao.assetState.*") + */ +public enum AssetState { + UNDEFINED, + IN_TRIAL_PERIOD, + ACTIVELY_TRADED, + DE_LISTED, + REMOVED_BY_VOTING // Was removed by voting +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java b/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java new file mode 100644 index 0000000000..55c26c7ebd --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.dao.governance.asset; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; + +import java.util.Optional; + +import lombok.Value; + +@Value +public class FeePayment { + private final String txId; + private final long fee; + + FeePayment(String txId, long fee) { + this.txId = txId; + this.fee = fee; + } + + public long daysCoveredByFee(long bsqFeePerDay) { + return bsqFeePerDay > 0 ? fee / bsqFeePerDay : 0; + } + + public Optional getPassedDays(DaoStateService daoStateService) { + Optional optionalTx = daoStateService.getTx(txId); + if (optionalTx.isPresent()) { + int passedBlocks = daoStateService.getChainHeight() - optionalTx.get().getBlockHeight(); + return Optional.of(passedBlocks / 144); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return "FeePayment{" + + "\n txId='" + txId + '\'' + + ",\n fee=" + fee + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java new file mode 100644 index 0000000000..4846e3906c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.dao.governance.asset; + +import bisq.core.locale.CurrencyUtil; + +import bisq.asset.Asset; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +public class StatefulAsset { + private final Asset asset; + @Setter + private AssetState assetState = AssetState.UNDEFINED; + private List feePayments = new ArrayList<>(); + @Setter + private long tradeVolume; + @Setter + private long lookBackPeriodInDays; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public StatefulAsset(Asset asset) { + this.asset = asset; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getNameAndCode() { + return CurrencyUtil.getNameAndCode(getTickerSymbol()); + } + + public String getTickerSymbol() { + return asset.getTickerSymbol(); + } + + public void setFeePayments(List feePayments) { + this.feePayments = feePayments; + } + + public Optional getLastFeePayment() { + return feePayments.isEmpty() ? Optional.empty() : Optional.of(feePayments.get(feePayments.size() - 1)); + } + + public long getTotalFeesPaid() { + return feePayments.stream().mapToLong(FeePayment::getFee).sum(); + } + + public long getFeeOfTrialPeriod() { + return getLastFeePayment() + .map(FeePayment::getFee) + .filter(e -> assetState == AssetState.IN_TRIAL_PERIOD) + .orElse(0L); + } + + public boolean isActive() { + return !wasRemovedByVoting() && !isDeListed(); + } + + public boolean wasRemovedByVoting() { + return assetState == AssetState.REMOVED_BY_VOTING; + } + + private boolean isDeListed() { + return assetState == AssetState.DE_LISTED; + } + + + @Override + public String toString() { + return "StatefulAsset{" + + "\n asset=" + asset + + ",\n assetState=" + assetState + + ",\n feePayments=" + feePayments + + ",\n tradeVolume=" + tradeVolume + + ",\n lookBackPeriodInDays=" + lookBackPeriodInDays + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java new file mode 100644 index 0000000000..e7e148061a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.dao.governance.ballot; + +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.governance.proposal.ProposalValidatorProvider; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.Proposal; + +import com.google.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Provides the ballots as observableList for presentation classes. + */ +@Slf4j +public class BallotListPresentation implements BallotListService.BallotListChangeListener, DaoStateListener { + private final BallotListService ballotListService; + private final PeriodService periodService; + private final ProposalValidatorProvider proposalValidatorProvider; + + @Getter + private final ObservableList allBallots = FXCollections.observableArrayList(); + @Getter + private final FilteredList ballotsOfCycle = new FilteredList<>(allBallots); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BallotListPresentation(BallotListService ballotListService, + PeriodService periodService, + DaoStateService daoStateService, + ProposalValidatorProvider proposalValidatorProvider) { + this.ballotListService = ballotListService; + this.periodService = periodService; + this.proposalValidatorProvider = proposalValidatorProvider; + + daoStateService.addDaoStateListener(this); + ballotListService.addListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + ballotsOfCycle.setPredicate(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), block.getHeight())); + onListChanged(ballotListService.getValidatedBallotList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BallotListService.BallotListChangeListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onListChanged(List list) { + allBallots.clear(); + allBallots.addAll(list); + } + + // We cannot do a phase and cycle check as we are interested in historical ballots as well + public List getAllValidBallots() { + return allBallots.stream() + .filter(ballot -> { + Proposal proposal = ballot.getProposal(); + ProposalValidator validator = proposalValidatorProvider.getValidator(proposal); + return validator.areDataFieldsValid(proposal) && validator.isTxTypeValid(proposal); + }) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java new file mode 100644 index 0000000000..9662bdd9b1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java @@ -0,0 +1,180 @@ +/* + * 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 bisq.core.dao.governance.ballot; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.ProposalValidatorProvider; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Vote; + +import bisq.common.app.DevEnv; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import javax.inject.Inject; + +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Takes the proposals from the append only store and makes Ballots out of it (vote is null). + * Applies voting on individual ballots and persist the list. + * The BallotList contains all ballots of all cycles. + */ +@Slf4j +public class BallotListService implements PersistedDataHost, DaoSetupService { + public interface BallotListChangeListener { + void onListChanged(List list); + } + + private final ProposalService proposalService; + private final PeriodService periodService; + private final ProposalValidatorProvider validatorProvider; + private final PersistenceManager persistenceManager; + + private final BallotList ballotList = new BallotList(); + private final List listeners = new CopyOnWriteArrayList<>(); + + @Inject + public BallotListService(ProposalService proposalService, + PeriodService periodService, + ProposalValidatorProvider validatorProvider, + PersistenceManager persistenceManager) { + this.proposalService = proposalService; + this.periodService = periodService; + this.validatorProvider = validatorProvider; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(ballotList, PersistenceManager.Source.NETWORK); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public final void addListeners() { + ObservableList payloads = proposalService.getProposalPayloads(); + payloads.addListener(this::onChanged); + } + + private void onChanged(Change change) { + change.next(); + if (change.wasAdded()) { + List addedPayloads = change.getAddedSubList(); + addedPayloads.stream() + .map(ProposalPayload::getProposal) + .filter(this::isNewProposal) + .forEach(this::registerProposalAsBallot); + requestPersistence(); + } + } + + private boolean isNewProposal(Proposal proposal) { + return ballotList.stream() + .map(Ballot::getProposal) + .noneMatch(proposal::equals); + } + + private void registerProposalAsBallot(Proposal proposal) { + Ballot ballot = new Ballot(proposal); // vote is null + if (log.isInfoEnabled()) { + log.debug("We create a new ballot with a proposal and add it to our list. " + + "Vote is null at that moment. proposalTxId={}", proposal.getTxId()); + } + if (ballotList.contains(ballot)) { + log.warn("Ballot {} already exists on our ballotList", ballot); + } else { + ballotList.add(ballot); + listeners.forEach(listener -> listener.onListChanged(ballotList.getList())); + } + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + ballotList.setAll(persisted.getList()); + listeners.forEach(l -> l.onListChanged(ballotList.getList())); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setVote(Ballot ballot, @Nullable Vote vote) { + ballot.setVote(vote); + requestPersistence(); + } + + public void addListener(BallotListChangeListener listener) { + listeners.add(listener); + } + + public List getValidatedBallotList() { + return ballotList.stream() + .filter(ballot -> validatorProvider.getValidator(ballot.getProposal()).isTxTypeValid(ballot.getProposal())) + .collect(Collectors.toList()); + } + + public List getValidBallotsOfCycle() { + return ballotList.stream() + .filter(ballot -> validatorProvider.getValidator(ballot.getProposal()).isTxTypeValid(ballot.getProposal())) + .filter(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), periodService.getChainHeight())) + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java new file mode 100644 index 0000000000..5449139010 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java @@ -0,0 +1,128 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.governance.ConsensusCritical; + +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Map; +import java.util.Optional; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.concurrent.Immutable; + +/** + * Holds encryptedVotes, encryptedMeritList, txId of blindVote tx and stake. + * A encryptedVotes for 1 proposal is 304 bytes + */ +@Immutable +@Slf4j +@Value +public final class BlindVote implements PersistablePayload, NetworkPayload, ConsensusCritical { + private final byte[] encryptedVotes; // created from voteWithProposalTxIdList + private final String txId; + // Stake is revealed in the BSQ tx anyway as output value so no reason to encrypt it here. + private final long stake; + private byte[] encryptedMeritList; + // Publish date of the proposal. + // We do not use the date at the moment but we prefer to keep it here as it might be + // used as a relevant protection tool for late publishing attacks. + // We don't have a clear concept now how to do it but as it will be part of the opReturn data it will impossible + // to game the publish date. Together with the block time we can use that for some checks. But as said no clear + // concept yet... + // As adding that field later would break consensus we prefer to add it now. In the worst case it will stay + // an unused field. + private final long date; + + // This hash map allows addition of data in future versions without breaking consensus + private final Map extraDataMap; + + public BlindVote(byte[] encryptedVotes, + String txId, + long stake, + byte[] encryptedMeritList, + long date, + Map extraDataMap) { + this.encryptedVotes = encryptedVotes; + this.txId = txId; + this.stake = stake; + this.encryptedMeritList = encryptedMeritList; + this.date = date; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + // Used for sending over the network + @Override + public protobuf.BlindVote toProtoMessage() { + return getBuilder().build(); + } + + @NotNull + public protobuf.BlindVote.Builder getBuilder() { + protobuf.BlindVote.Builder builder = protobuf.BlindVote.newBuilder(); + builder.setEncryptedVotes(ByteString.copyFrom(encryptedVotes)) + .setTxId(txId) + .setStake(stake) + .setEncryptedMeritList(ByteString.copyFrom(encryptedMeritList)) + .setDate(date); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return builder; + } + + public static BlindVote fromProto(protobuf.BlindVote proto) { + return new BlindVote(proto.getEncryptedVotes().toByteArray(), + proto.getTxId(), + proto.getStake(), + proto.getEncryptedMeritList().toByteArray(), + proto.getDate(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String toString() { + return "BlindVotePayload{" + + "\n encryptedVotes=" + Utilities.bytesAsHexString(encryptedVotes) + + ",\n txId='" + txId + '\'' + + ",\n stake=" + stake + + ",\n encryptedMeritList=" + Utilities.bytesAsHexString(encryptedMeritList) + + ",\n date=" + date + + ",\n extraDataMap=" + extraDataMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteConsensus.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteConsensus.java new file mode 100644 index 0000000000..1841ff2cb5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteConsensus.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.model.governance.MeritList; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Encryption; +import bisq.common.crypto.Hash; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import javax.crypto.SecretKey; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * All consensus critical aspects are handled here. + */ +@Slf4j +public class BlindVoteConsensus { + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } + + public static BallotList getSortedBallotList(BallotListService ballotListService) { + List ballotList = ballotListService.getValidBallotsOfCycle().stream() + .sorted(Comparator.comparing(Ballot::getTxId)) + .collect(Collectors.toList()); + log.info("Sorted ballotList: " + ballotList); + return new BallotList(ballotList); + } + + public static List getSortedBlindVoteListOfCycle(BlindVoteListService blindVoteListService) { + return getSortedBlindVoteListOfCycle(blindVoteListService.getBlindVotesInPhaseAndCycle()); + } + + @VisibleForTesting + static List getSortedBlindVoteListOfCycle(List blindVoteList) { + List list = blindVoteList.stream() + .sorted(Comparator.comparing(BlindVote::getTxId)) + .collect(Collectors.toList()); + log.debug("Sorted blindVote txId list: " + list.stream() + .map(BlindVote::getTxId) + .collect(Collectors.toList())); + return list; + } + + // 128 bit AES key is good enough for our use case + public static SecretKey createSecretKey() { + return Encryption.generateSecretKey(128); + } + + public static byte[] getEncryptedVotes(VoteWithProposalTxIdList voteWithProposalTxIdList, SecretKey secretKey) throws CryptoException { + byte[] bytes = voteWithProposalTxIdList.toProtoMessage().toByteArray(); + byte[] encrypted = Encryption.encrypt(bytes, secretKey); + log.info("EncryptedVotes: " + Utilities.bytesAsHexString(encrypted)); + return encrypted; + } + + public static byte[] getEncryptedMeritList(MeritList meritList, SecretKey secretKey) throws CryptoException { + byte[] bytes = meritList.toProtoMessage().toByteArray(); + return Encryption.encrypt(bytes, secretKey); + } + + public static byte[] getHashOfEncryptedVotes(byte[] encryptedVotes) { + return Hash.getSha256Ripemd160hash(encryptedVotes); + } + + public static byte[] getOpReturnData(byte[] hash) throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.BLIND_VOTE.getType()); + outputStream.write(Version.BLIND_VOTE); + outputStream.write(hash); + final byte[] bytes = outputStream.toByteArray(); + log.info("OpReturnData: " + Utilities.bytesAsHexString(bytes)); + return bytes; + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + throw e; + } + } + + public static Coin getFee(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.BLIND_VOTE_FEE, chainHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java new file mode 100644 index 0000000000..4ed289d881 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java @@ -0,0 +1,186 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; +import bisq.core.dao.governance.blindvote.storage.BlindVoteStorageService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.config.Config; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Listens for new BlindVotePayload and adds it to appendOnlyStoreList. + */ +@Slf4j +public class BlindVoteListService implements AppendOnlyDataStoreListener, DaoStateListener, DaoSetupService { + private final DaoStateService daoStateService; + private final P2PService p2PService; + private final PeriodService periodService; + private final BlindVoteStorageService blindVoteStorageService; + private final BlindVoteValidator blindVoteValidator; + @Getter + private final ObservableList blindVotePayloads = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BlindVoteListService(DaoStateService daoStateService, + P2PService p2PService, + PeriodService periodService, + BlindVoteStorageService blindVoteStorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + BlindVoteValidator blindVoteValidator, + Config config) { + this.daoStateService = daoStateService; + this.p2PService = p2PService; + this.periodService = periodService; + this.blindVoteStorageService = blindVoteStorageService; + this.blindVoteValidator = blindVoteValidator; + + if (config.daoActivated) + appendOnlyDataStoreService.addService(blindVoteStorageService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + fillListFromAppendOnlyDataStore(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + // We only add blindVotes to blindVoteStorageService if we are not in the vote reveal phase. + blindVoteStorageService.setNotInVoteRevealPhase(notInVoteRevealPhase(blockHeight)); + } + + @Override + public void onParseBlockChainComplete() { + fillListFromAppendOnlyDataStore(); + + // We set the listener after parsing is complete to be sure we have a consistent state for the phase check. + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // AppendOnlyDataStoreListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAdded(PersistableNetworkPayload payload) { + onAppendOnlyDataAdded(payload, true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getBlindVotesInPhaseAndCycle() { + return blindVotePayloads.stream() + .filter(blindVotePayload -> blindVoteValidator.isTxInPhaseAndCycle(blindVotePayload.getBlindVote())) + .map(BlindVotePayload::getBlindVote) + .collect(Collectors.toList()); + } + + public List getConfirmedBlindVotes() { + return blindVotePayloads.stream() + .filter(blindVotePayload -> blindVoteValidator.areDataFieldsValidAndTxConfirmed(blindVotePayload.getBlindVote())) + .map(BlindVotePayload::getBlindVote) + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void fillListFromAppendOnlyDataStore() { + blindVoteStorageService.getMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); + } + + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean fromBroadcastMessage) { + if (persistableNetworkPayload instanceof BlindVotePayload) { + BlindVotePayload blindVotePayload = (BlindVotePayload) persistableNetworkPayload; + if (!blindVotePayloads.contains(blindVotePayload)) { + BlindVote blindVote = blindVotePayload.getBlindVote(); + String txId = blindVote.getTxId(); + + if (blindVoteValidator.areDataFieldsValid(blindVote)) { + if (fromBroadcastMessage) { + if (notInVoteRevealPhase(daoStateService.getChainHeight())) { + // We received the payload outside the vote reveal phase and add the payload. + // If we would accept it during the vote reveal phase we would be vulnerable to a late + // publishing attack where the attacker tries to pollute the data view of the voters and + // render the whole voting cycle invalid if the majority hash is not at least 80% of the + // vote stake. + blindVotePayloads.add(blindVotePayload); + } + } else { + // In case we received the data from the seed node at startup we cannot apply the phase check as + // even in the vote reveal phase we want to receive missed blind votes. + blindVotePayloads.add(blindVotePayload); + } + } else { + log.warn("We received an invalid blindVotePayload. blindVoteTxId={}", txId); + } + } + } + } + + private boolean notInVoteRevealPhase(int blockHeight) { + return !periodService.isInPhase(blockHeight, DaoPhase.Phase.VOTE_REVEAL); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java new file mode 100644 index 0000000000..4b1035a25e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java @@ -0,0 +1,117 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.btc.wallet.Restrictions; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.common.util.ExtraDataMapValidator; + +import javax.inject.Inject; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BlindVoteValidator { + + private final DaoStateService daoStateService; + private final PeriodService periodService; + + @Inject + public BlindVoteValidator(DaoStateService daoStateService, PeriodService periodService) { + this.daoStateService = daoStateService; + this.periodService = periodService; + } + + public boolean areDataFieldsValid(BlindVote blindVote) { + try { + validateDataFields(blindVote); + return true; + } catch (Throwable e) { + return false; + } + } + + private void validateDataFields(BlindVote blindVote) throws ProposalValidationException { + try { + checkNotNull(blindVote.getEncryptedVotes(), "encryptedProposalList must not be null"); + checkArgument(blindVote.getEncryptedVotes().length > 0, + "encryptedProposalList must not be empty"); + checkArgument(blindVote.getEncryptedVotes().length <= 100000, + "encryptedProposalList must not exceed 100kb"); + + checkNotNull(blindVote.getTxId(), "Tx ID must not be null"); + checkArgument(blindVote.getTxId().length() == 64, "Tx ID must be 64 chars"); + checkArgument(blindVote.getStake() >= Restrictions.getMinNonDustOutput().value, "Stake must be at least MinNonDustOutput"); + + checkNotNull(blindVote.getEncryptedMeritList(), "getEncryptedMeritList must not be null"); + checkArgument(blindVote.getEncryptedMeritList().length > 0, + "getEncryptedMeritList must not be empty"); + checkArgument(blindVote.getEncryptedMeritList().length <= 100000, + "getEncryptedMeritList must not exceed 100kb"); + + ExtraDataMapValidator.validate(blindVote.getExtraDataMap()); + } catch (Throwable e) { + log.warn(e.toString()); + throw new ProposalValidationException(e); + } + } + + public boolean areDataFieldsValidAndTxConfirmed(BlindVote blindVote) { + if (!areDataFieldsValid(blindVote)) { + log.warn("blindVote is invalid. blindVote={}", blindVote); + return false; + } + + // Check if tx is already confirmed and in DaoState + boolean isConfirmed = daoStateService.getTx(blindVote.getTxId()).isPresent(); + if (daoStateService.isParseBlockChainComplete() && !isConfirmed) + log.warn("blindVoteTx is not confirmed. blindVoteTxId={}", blindVote.getTxId()); + + return isConfirmed; + } + + public boolean isTxInPhaseAndCycle(BlindVote blindVote) { + String txId = blindVote.getTxId(); + Optional optionalTx = daoStateService.getTx(txId); + if (!optionalTx.isPresent()) { + log.debug("Tx is not in daoStateService. blindVoteTxId={}", txId); + return false; + } + + int txHeight = optionalTx.get().getBlockHeight(); + if (!periodService.isTxInCorrectCycle(txHeight, daoStateService.getChainHeight())) { + log.debug("Tx is not in current cycle. blindVote={}", blindVote); + return false; + } + if (!periodService.isTxInPhase(txId, DaoPhase.Phase.BLIND_VOTE)) { + log.debug("Tx is not in BLIND_VOTE phase. blindVote={}", blindVote); + return false; + } + return true; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java new file mode 100644 index 0000000000..be0890b653 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.governance.ConsensusCritical; + +import bisq.common.proto.persistable.PersistableList; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * List of my own blind votes. Blind votes received from other voters are stored in the BlindVoteStore. + */ +@EqualsAndHashCode(callSuper = true) +public class MyBlindVoteList extends PersistableList implements ConsensusCritical { + + MyBlindVoteList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public MyBlindVoteList(List list) { + super(list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setMyBlindVoteList(protobuf.MyBlindVoteList.newBuilder() + .addAllBlindVote(getList().stream() + .map(BlindVote::toProtoMessage) + .collect(Collectors.toList()))) + .build(); + } + + public static MyBlindVoteList fromProto(protobuf.MyBlindVoteList proto) { + return new MyBlindVoteList(new ArrayList<>(proto.getBlindVoteList().stream() + .map(BlindVote::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "MyBlindVoteList: " + getList().stream() + .map(BlindVote::getTxId) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java new file mode 100644 index 0000000000..d0edacc099 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -0,0 +1,417 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.exceptions.PublishToP2PNetworkException; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; +import bisq.core.dao.governance.merit.MeritConsensus; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.MyProposalListService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.Merit; +import bisq.core.dao.state.model.governance.MeritList; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.P2PService; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.crypto.CryptoException; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ExceptionHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import javax.inject.Inject; + +import javafx.beans.value.ChangeListener; + +import javax.crypto.SecretKey; + +import java.io.IOException; + +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Publishes blind vote tx and blind vote payload to p2p network. + * Maintains myBlindVoteList for own blind votes. Triggers republishing of my blind votes at startup during blind + * vote phase of current cycle. + * Publishes a BlindVote and the blind vote transaction. + */ +@Slf4j +public class MyBlindVoteListService implements PersistedDataHost, DaoStateListener, DaoSetupService { + private final P2PService p2PService; + private final DaoStateService daoStateService; + private final PeriodService periodService; + private final WalletsManager walletsManager; + private final PersistenceManager persistenceManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final BallotListService ballotListService; + private final MyVoteListService myVoteListService; + private final MyProposalListService myProposalListService; + private final ChangeListener numConnectedPeersListener; + @Getter + private final MyBlindVoteList myBlindVoteList = new MyBlindVoteList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyBlindVoteListService(P2PService p2PService, + DaoStateService daoStateService, + PeriodService periodService, + WalletsManager walletsManager, + PersistenceManager persistenceManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + BallotListService ballotListService, + MyVoteListService myVoteListService, + MyProposalListService myProposalListService) { + this.p2PService = p2PService; + this.daoStateService = daoStateService; + this.periodService = periodService; + this.walletsManager = walletsManager; + this.persistenceManager = persistenceManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.ballotListService = ballotListService; + this.myVoteListService = myVoteListService; + this.myProposalListService = myProposalListService; + + this.persistenceManager.initialize(myBlindVoteList, PersistenceManager.Source.PRIVATE); + + numConnectedPeersListener = (observable, oldValue, newValue) -> maybeRePublishMyBlindVote(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + myBlindVoteList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockChainComplete() { + maybeRePublishMyBlindVote(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Tuple2 getMiningFeeAndTxVsize(Coin stake) + throws InsufficientMoneyException, WalletException, TransactionVerificationException { + // We set dummy opReturn data + Coin blindVoteFee = BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight()); + Transaction dummyTx = getBlindVoteTx(stake, blindVoteFee, new byte[22]); + Coin miningFee = dummyTx.getFee(); + int txVsize = dummyTx.getVsize(); + return new Tuple2<>(miningFee, txVsize); + } + + public void publishBlindVote(Coin stake, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + try { + SecretKey secretKey = BlindVoteConsensus.createSecretKey(); + BallotList sortedBallotList = BlindVoteConsensus.getSortedBallotList(ballotListService); + byte[] encryptedVotes = getEncryptedVotes(sortedBallotList, secretKey); + byte[] opReturnData = getOpReturnData(encryptedVotes); + Coin blindVoteFee = BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight()); + Transaction blindVoteTx = getBlindVoteTx(stake, blindVoteFee, opReturnData); + String blindVoteTxId = blindVoteTx.getTxId().toString(); + + byte[] encryptedMeritList = getEncryptedMeritList(blindVoteTxId, secretKey); + + // We prefer to not wait for the tx broadcast as if the tx broadcast would fail we still prefer to have our + // blind vote stored and broadcasted to the p2p network. The tx might get re-broadcasted at a restart and + // in worst case if it does not succeed the blind vote will be ignored anyway. + // Inconsistently propagated blind votes in the p2p network could have potentially worse effects. + BlindVote blindVote = new BlindVote(encryptedVotes, + blindVoteTxId, + stake.value, + encryptedMeritList, + new Date().getTime(), + new HashMap<>()); + addBlindVoteToList(blindVote); + + addToP2PNetwork(blindVote, errorMessage -> { + log.error(errorMessage); + exceptionHandler.handleException(new PublishToP2PNetworkException(errorMessage)); + }); + + // We store our source data for the blind vote in myVoteList + myVoteListService.createAndAddMyVote(sortedBallotList, secretKey, blindVote); + + publishTx(resultHandler, exceptionHandler, blindVoteTx); + } catch (CryptoException | TransactionVerificationException | InsufficientMoneyException | + WalletException | IOException exception) { + log.error(exception.toString()); + exception.printStackTrace(); + exceptionHandler.handleException(exception); + } + } + + public long getCurrentlyAvailableMerit() { + MeritList meritList = getMerits(null); + return MeritConsensus.getCurrentlyAvailableMerit(meritList, daoStateService.getChainHeight()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + + private byte[] getEncryptedVotes(BallotList sortedBallotList, SecretKey secretKey) throws CryptoException { + // We don't want to store the proposal but only use the proposalTxId as reference in our encrypted list. + // So we convert it to the VoteWithProposalTxIdList. + // The VoteWithProposalTxIdList is used for serialisation with protobuffer, it is not actually persisted but we + // use the PersistableList base class for convenience. + final List list = sortedBallotList.stream() + .map(ballot -> new VoteWithProposalTxId(ballot.getTxId(), ballot.getVote())) + .collect(Collectors.toList()); + final VoteWithProposalTxIdList voteWithProposalTxIdList = new VoteWithProposalTxIdList(list); + log.info("voteWithProposalTxIdList used in blind vote. voteWithProposalTxIdList={}", voteWithProposalTxIdList); + return BlindVoteConsensus.getEncryptedVotes(voteWithProposalTxIdList, secretKey); + } + + private byte[] getOpReturnData(byte[] encryptedVotes) throws IOException { + // We cannot use hash of whole blindVote data because we create the merit signature with the blindVoteTxId + // So we use the encryptedVotes for the hash only. + final byte[] hash = BlindVoteConsensus.getHashOfEncryptedVotes(encryptedVotes); + log.info("Sha256Ripemd160 hash of encryptedVotes: " + Utilities.bytesAsHexString(hash)); + return BlindVoteConsensus.getOpReturnData(hash); + } + + private byte[] getEncryptedMeritList(String blindVoteTxId, SecretKey secretKey) throws CryptoException { + MeritList meritList = getMerits(blindVoteTxId); + return BlindVoteConsensus.getEncryptedMeritList(meritList, secretKey); + } + + // blindVoteTxId is null if we use the method from the getCurrentlyAvailableMerit call. + public MeritList getMerits(@Nullable String blindVoteTxId) { + // Create a lookup set for txIds of own comp. requests from past cycles (we ignore request form that cycle) + Set myCompensationProposalTxIs = myProposalListService.getList().stream() + .filter(proposal -> proposal instanceof CompensationProposal) + .map(Proposal::getTxId) + .filter(txId -> periodService.isTxInPastCycle(txId, periodService.getChainHeight())) + .collect(Collectors.toSet()); + + return new MeritList(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream() + .map(issuance -> { + checkArgument(issuance.getIssuanceType() == IssuanceType.COMPENSATION, + "IssuanceType must be COMPENSATION for MeritList"); + // We check if it is our proposal + if (!myCompensationProposalTxIs.contains(issuance.getTxId())) + return null; + + byte[] signatureAsBytes; + if (blindVoteTxId != null) { + String pubKey = issuance.getPubKey(); + if (pubKey == null) { + // Maybe add exception + log.error("We did not have a pubKey in our issuance object. " + + "txId={}, issuance={}", issuance.getTxId(), issuance); + return null; + } + + DeterministicKey key = bsqWalletService.findKeyFromPubKey(Utilities.decodeFromHex(pubKey)); + if (key == null) { + // Maybe add exception + log.error("We did not find the key for our compensation request. txId={}", + issuance.getTxId()); + return null; + } + + // We sign the txId so we be sure that the signature could not be used by anyone else + // In the verification the txId will be checked as well. + + // As we use BitcoinJ EC keys we extend our consensus dependency to BitcoinJ. + // Alternative would be to use our own Sig Key but then we need to share the key separately. + // The EC key is in the blockchain already. We prefer here to stick with EC key. If any change + // in BitcoinJ would break our consensus we would need to fall back to the old BitcoinJ EC + // implementation. + ECKey.ECDSASignature signature = bsqWalletService.isEncrypted() ? + key.sign(Sha256Hash.wrap(blindVoteTxId), bsqWalletService.getAesKey()) : + key.sign(Sha256Hash.wrap(blindVoteTxId)); + signatureAsBytes = signature.toCanonicalised().encodeToDER(); + } else { + // In case we use it for requesting the currently available merit we don't apply a signature + signatureAsBytes = new byte[0]; + } + return new Merit(issuance, signatureAsBytes); + + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Merit::getIssuanceTxId)) + .collect(Collectors.toList())); + } + + private void publishTx(ResultHandler resultHandler, ExceptionHandler exceptionHandler, Transaction blindVoteTx) { + log.info("blindVoteTx={}", blindVoteTx.toString()); + walletsManager.publishAndCommitBsqTx(blindVoteTx, TxType.BLIND_VOTE, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("BlindVote tx published. txId={}", transaction.getTxId().toString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + exceptionHandler.handleException(exception); + } + }); + } + + private Transaction getBlindVoteTx(Coin stake, Coin fee, byte[] opReturnData) + throws InsufficientMoneyException, WalletException, TransactionVerificationException { + Transaction preparedTx = bsqWalletService.getPreparedBlindVoteTx(fee, stake); + Transaction txWithBtcFee = btcWalletService.completePreparedBlindVoteTx(preparedTx, opReturnData); + return bsqWalletService.signTx(txWithBtcFee); + } + + private void maybeRePublishMyBlindVote() { + // We do not republish during vote reveal phase as peer would reject blindVote data to protect against + // late publishing attacks. + // This attack is only relevant during the vote reveal phase as there it could cause damage by disturbing the + // data view of the blind votes of the voter for creating the majority hash. + // To republish after the vote reveal phase still makes sense to reduce risk that some nodes have not received + // it and would need to request the data then in the vote result phase. + if (!periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.VOTE_REVEAL)) { + // We republish at each startup at any block during the cycle. We filter anyway for valid blind votes + // of that cycle so it is 1 blind vote getting rebroadcast at each startup to my neighbors. + // Republishing only will have effect if the payload creation date is < 5 hours as other nodes would not + // accept payloads which are too old or are in future. + // Only payloads received from seed nodes would ignore that date check. + int minPeers = Config.baseCurrencyNetwork().isMainnet() ? 4 : 1; + if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || + Config.baseCurrencyNetwork().isRegtest()) { + myBlindVoteList.stream() + .filter(blindVote -> periodService.isTxInPhaseAndCycle(blindVote.getTxId(), + DaoPhase.Phase.BLIND_VOTE, + periodService.getChainHeight())) + .forEach(blindVote -> addToP2PNetwork(blindVote, null)); + + // We delay removal of listener as we call that inside listener itself. + UserThread.execute(() -> p2PService.getNumConnectedPeers().removeListener(numConnectedPeersListener)); + } + } + } + + private void addToP2PNetwork(BlindVote blindVote, @Nullable ErrorMessageHandler errorMessageHandler) { + BlindVotePayload blindVotePayload = new BlindVotePayload(blindVote); + // We use reBroadcast flag here as we only broadcast our own blindVote and want to be sure it gets distributed + // well. + boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); + + if (success) { + log.info("We added a blindVotePayload to the P2P network as append only data. blindVoteTxId={}", + blindVote.getTxId()); + } else { + String msg = "Adding of blindVotePayload to P2P network failed. blindVoteTxId=" + blindVote.getTxId(); + log.error(msg); + if (errorMessageHandler != null) + errorMessageHandler.handleErrorMessage(msg); + } + } + + private void addBlindVoteToList(BlindVote blindVote) { + if (!myBlindVoteList.getList().contains(blindVote)) { + myBlindVoteList.add(blindVote); + requestPersistence(); + } + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxId.java b/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxId.java new file mode 100644 index 0000000000..1c60733fcf --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxId.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.state.model.governance.Vote; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Optional; + +import lombok.Value; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + + +@Value +public class VoteWithProposalTxId implements PersistablePayload { + private final String proposalTxId; + @Nullable + private final Vote vote; + + VoteWithProposalTxId(String proposalTxId, @Nullable Vote vote) { + this.proposalTxId = proposalTxId; + this.vote = vote; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + // Used for sending over the network + @Override + public protobuf.VoteWithProposalTxId toProtoMessage() { + return getBuilder().build(); + } + + @NotNull + private protobuf.VoteWithProposalTxId.Builder getBuilder() { + final protobuf.VoteWithProposalTxId.Builder builder = protobuf.VoteWithProposalTxId.newBuilder() + .setProposalTxId(proposalTxId); + Optional.ofNullable(vote).ifPresent(e -> builder.setVote((protobuf.Vote) e.toProtoMessage())); + return builder; + } + + public static VoteWithProposalTxId fromProto(protobuf.VoteWithProposalTxId proto) { + return new VoteWithProposalTxId(proto.getProposalTxId(), + proto.hasVote() ? Vote.fromProto(proto.getVote()) : null); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxIdList.java b/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxIdList.java new file mode 100644 index 0000000000..4556e88221 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/VoteWithProposalTxIdList.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.core.dao.governance.blindvote; + +import bisq.core.dao.governance.ConsensusCritical; + +import bisq.common.Proto; + +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +/** + * We encode the VoteWithProposalTxId list to PB bytes in the blindVote. The bytes get encrypted and later decrypted. + * To use a ByteOutputStream and add all list elements would work for encryption but for decrypting we don't know the + * length of a list entry and it would make the process complicated (e.g. require a custom serialisation format). + */ +@Slf4j +@Value +public class VoteWithProposalTxIdList implements Proto, ConsensusCritical { + private final List list; + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static VoteWithProposalTxIdList getVoteWithProposalTxIdListFromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return VoteWithProposalTxIdList.fromProto(protobuf.VoteWithProposalTxIdList.parseFrom(bytes)); + } + + @Override + public protobuf.VoteWithProposalTxIdList toProtoMessage() { + return getBuilder().build(); + } + + private protobuf.VoteWithProposalTxIdList.Builder getBuilder() { + return protobuf.VoteWithProposalTxIdList.newBuilder() + .addAllItem(getList().stream() + .map(VoteWithProposalTxId::toProtoMessage) + .collect(Collectors.toList())); + } + + private static VoteWithProposalTxIdList fromProto(protobuf.VoteWithProposalTxIdList proto) { + final ArrayList list = proto.getItemList().stream() + .map(VoteWithProposalTxId::fromProto).collect(Collectors.toCollection(ArrayList::new)); + return new VoteWithProposalTxIdList(list); + } + + @Override + public String toString() { + return "VoteWithProposalTxIdList: " + getList().stream() + .map(VoteWithProposalTxId::getProposalTxId) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java b/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java new file mode 100644 index 0000000000..9701159f72 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java @@ -0,0 +1,216 @@ +/* + * 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 bisq.core.dao.governance.blindvote.network; + +import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.peers.peerexchange.Peer; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; + +import javax.inject.Inject; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +/** + * Responsible for sending a RepublishGovernanceDataRequest to full nodes. + * Processing of RepublishBlindVotesRequests at full nodes is done in the FullNodeNetworkService. + */ +@Slf4j +public final class RepublishGovernanceDataHandler { + private static final long TIMEOUT = 120; + + private final Collection seedNodeAddresses; + private final NetworkNode networkNode; + private final PeerManager peerManager; + + private boolean stopped; + private Timer timeoutTimer; + + @Inject + public RepublishGovernanceDataHandler(NetworkNode networkNode, + PeerManager peerManager, + SeedNodeRepository seedNodesRepository) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.seedNodeAddresses = new HashSet<>(seedNodesRepository.getSeedNodeAddresses()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void sendRepublishRequest() { + // First try if we have a seed node in our connections. All seed nodes are full nodes. + if (!stopped) + connectToNextNode(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void sendRepublishRequest(NodeAddress nodeAddress) { + RepublishGovernanceDataRequest republishGovernanceDataRequest = new RepublishGovernanceDataRequest(); + if (timeoutTimer == null) { + timeoutTimer = UserThread.runAfter(() -> { + // setup before sending to avoid race conditions + if (!stopped) { + String errorMessage = "A timeout occurred at sending republishGovernanceDataRequest:" + + " to nodeAddress:" + nodeAddress; + log.warn(errorMessage); + connectToNextNode(); + } else { + log.warn("We have stopped already. We ignore that timeoutTimer.run call. " + + "Might be caused by a previous networkNode.sendMessage.onFailure."); + } + }, + TIMEOUT); + } + + log.info("We send to peer {} a republishGovernanceDataRequest.", nodeAddress); + SettableFuture future = networkNode.sendMessage(nodeAddress, republishGovernanceDataRequest); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.info("Sending of RepublishGovernanceDataRequest message to peer {} succeeded.", nodeAddress.getFullAddress()); + stop(); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + + "Might be caused by a previous timeout."); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending republishGovernanceDataRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "\n\tException=" + throwable.getMessage(); + log.info(errorMessage); + handleFault(nodeAddress); + connectToNextNode(); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + + "Might be caused by a previous timeout."); + } + } + }, MoreExecutors.directExecutor()); + } + + private void connectToNextNode() { + // First we try our connected seed nodes + Optional connectionToSeedNodeOptional = networkNode.getConfirmedConnections().stream() + .filter(peerManager::isSeedNode) + .findAny(); + if (connectionToSeedNodeOptional.isPresent() && + connectionToSeedNodeOptional.get().getPeersNodeAddressOptional().isPresent()) { + NodeAddress nodeAddress = connectionToSeedNodeOptional.get().getPeersNodeAddressOptional().get(); + sendRepublishRequest(nodeAddress); + } else { + // If connected seed nodes did not confirm receipt of message we try next seed node from seedNodeAddresses + List list = seedNodeAddresses.stream() + .filter(e -> peerManager.isSeedNode(e) && !peerManager.isSelf(e)) + .collect(Collectors.toList()); + Collections.shuffle(list); + + if (!list.isEmpty()) { + NodeAddress nodeAddress = list.get(0); + seedNodeAddresses.remove(nodeAddress); + sendRepublishRequest(nodeAddress); + } else { + log.warn("No more seed nodes available. We try any of our other peers."); + connectToAnyFullNode(); + } + } + } + + // TODO support also lite nodes + private void connectToAnyFullNode() { + Capabilities required = new Capabilities(Capability.DAO_FULL_NODE); + + List list = peerManager.getLivePeers().stream() + .filter(peer -> peer.getCapabilities().containsAll(required)) + .collect(Collectors.toList()); + + if (list.isEmpty()) + list = peerManager.getReportedPeers().stream() + .filter(peer -> peer.getCapabilities().containsAll(required)) + .collect(Collectors.toList()); + + if (list.isEmpty()) + list = peerManager.getPersistedPeers().stream() + .filter(peer -> peer.getCapabilities().containsAll(required)) + .collect(Collectors.toList()); + + if (!list.isEmpty()) { + // We avoid the complexity to maintain the results of all our peers and to iterate all until we find a good peer, + // but we prefer simplicity with the risk that we don't get the data so we request from max 4 peers in parallel + // assuming that at least one will republish and therefore we should receive all data. + list = new ArrayList<>(list.subList(0, Math.min(list.size(), 4))); + list.stream() + .map(Peer::getNodeAddress) + .forEach(this::sendRepublishRequest); + } else { + log.warn("No other nodes found. We try again in 60 seconds."); + UserThread.runAfter(this::connectToNextNode, 60); + } + } + + private void handleFault(NodeAddress nodeAddress) { + peerManager.handleConnectionFault(nodeAddress); + } + + private void stop() { + stopped = true; + stopTimeoutTimer(); + } + + private void stopTimeoutTimer() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/network/messages/RepublishGovernanceDataRequest.java b/core/src/main/java/bisq/core/dao/governance/blindvote/network/messages/RepublishGovernanceDataRequest.java new file mode 100644 index 0000000000..258149a5fc --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/network/messages/RepublishGovernanceDataRequest.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.dao.governance.blindvote.network.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + + +// This message is sent only to full DAO nodes +@EqualsAndHashCode(callSuper = true) +@Getter +public final class RepublishGovernanceDataRequest extends NetworkEnvelope implements DirectMessage, CapabilityRequiringPayload { + + public RepublishGovernanceDataRequest() { + this(Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RepublishGovernanceDataRequest(int messageVersion) { + super(messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setRepublishGovernanceDataRequest(protobuf.RepublishGovernanceDataRequest.newBuilder()) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.RepublishGovernanceDataRequest proto, int messageVersion) { + return new RepublishGovernanceDataRequest(messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_FULL_NODE); + } + + @Override + public String toString() { + return "RepublishGovernanceDataRequest{" + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVotePayload.java b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVotePayload.java new file mode 100644 index 0000000000..1a07756221 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVotePayload.java @@ -0,0 +1,106 @@ +/* + * 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 bisq.core.dao.governance.blindvote.storage; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.blindvote.BlindVote; + +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.crypto.Hash; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Wrapper for proposal to be stored in the append-only BlindVoteStore storage. + * + * Data size: 185 bytes + */ +@Immutable +@Slf4j +@Getter +@EqualsAndHashCode +public final class BlindVotePayload implements PersistableNetworkPayload, ConsensusCritical { + + private final BlindVote blindVote; + protected final byte[] hash; // 20 byte + + public BlindVotePayload(BlindVote blindVote) { + this(blindVote, Hash.getRipemd160hash(blindVote.toProtoMessage().toByteArray())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private BlindVotePayload(BlindVote blindVote, byte[] hash) { + this.blindVote = blindVote; + this.hash = hash; + } + + private protobuf.BlindVotePayload.Builder getBlindVoteBuilder() { + return protobuf.BlindVotePayload.newBuilder() + .setBlindVote(blindVote.toProtoMessage()) + .setHash(ByteString.copyFrom(hash)); + } + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + return protobuf.PersistableNetworkPayload.newBuilder().setBlindVotePayload(getBlindVoteBuilder()).build(); + } + + public protobuf.BlindVotePayload toProtoBlindVotePayload() { + return getBlindVoteBuilder().build(); + } + + + public static BlindVotePayload fromProto(protobuf.BlindVotePayload proto) { + return new BlindVotePayload(BlindVote.fromProto(proto.getBlindVote()), + proto.getHash().toByteArray()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistableNetworkPayload + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean verifyHashSize() { + return hash.length == 20; + } + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public String toString() { + return "BlindVotePayload{" + + "\n blindVote=" + blindVote + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStorageService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStorageService.java new file mode 100644 index 0000000000..0271fdce7f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStorageService.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.dao.governance.blindvote.storage; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.MapStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.Map; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BlindVoteStorageService extends MapStoreService { + private static final String FILE_NAME = "BlindVoteStore"; + + // At startup it is true, so the data we receive from the seed node are not checked against the phase as we have + // not started up the DAO domain at that moment. + @Setter + private boolean notInVoteRevealPhase = true; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BlindVoteStorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public Map getMap() { + return store.getMap(); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof BlindVotePayload && notInVoteRevealPhase; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected BlindVoteStore createStore() { + return new BlindVoteStore(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStore.java b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStore.java new file mode 100644 index 0000000000..41ed5b0368 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/storage/BlindVoteStore.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.core.dao.governance.blindvote.storage; + +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class BlindVoteStore extends PersistableNetworkPayloadStore { + BlindVoteStore() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private BlindVoteStore(List list) { + super(list); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setBlindVoteStore(getBuilder()) + .build(); + } + + private protobuf.BlindVoteStore.Builder getBuilder() { + final List protoList = map.values().stream() + .map(payload -> (BlindVotePayload) payload) + .map(BlindVotePayload::toProtoBlindVotePayload) + .collect(Collectors.toList()); + return protobuf.BlindVoteStore.newBuilder().addAllItems(protoList); + } + + public static BlindVoteStore fromProto(protobuf.BlindVoteStore proto) { + List list = proto.getItemsList().stream() + .map(BlindVotePayload::fromProto).collect(Collectors.toList()); + return new BlindVoteStore(list); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/Bond.java b/core/src/main/java/bisq/core/dao/governance/bond/Bond.java new file mode 100644 index 0000000000..d2ab83a8db --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/Bond.java @@ -0,0 +1,94 @@ +/* + * 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 bisq.core.dao.governance.bond; + +import java.util.Objects; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +/** + * Base class for BondedRole and BondedReputation. Holds the state of the bonded asset. + */ +@Getter +public abstract class Bond { + @Getter + protected final T bondedAsset; + @Setter + @Nullable + protected String lockupTxId; + @Setter + @Nullable + protected String unlockTxId; + @Setter + protected BondState bondState = BondState.READY_FOR_LOCKUP; + @Setter + private long amount; + @Setter + private long lockupDate; + @Setter + private long unlockDate; + @Setter + private int lockTime; + + protected Bond(T bondedAsset) { + this.bondedAsset = bondedAsset; + } + + public boolean isActive() { + return bondState.isActive(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Bond)) return false; + Bond bond = (Bond) o; + return amount == bond.amount && + lockupDate == bond.lockupDate && + unlockDate == bond.unlockDate && + lockTime == bond.lockTime && + Objects.equals(bondedAsset, bond.bondedAsset) && + Objects.equals(lockupTxId, bond.lockupTxId) && + Objects.equals(unlockTxId, bond.unlockTxId) && + bondState.name().equals(bond.bondState.name()); + } + + @Override + public int hashCode() { + return Objects.hash(bondedAsset, lockupTxId, unlockTxId, bondState.name(), amount, lockupDate, unlockDate, lockTime); + } + + @Override + public String toString() { + return "Bond{" + + "\n bondedAsset=" + bondedAsset + + ",\n lockupTxId='" + lockupTxId + '\'' + + ",\n unlockTxId='" + unlockTxId + '\'' + + ",\n bondState=" + bondState + + ",\n amount=" + amount + + ",\n lockupDate=" + lockupDate + + ",\n unlockDate=" + unlockDate + + ",\n lockTime=" + lockTime + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondConsensus.java b/core/src/main/java/bisq/core/dao/governance/bond/BondConsensus.java new file mode 100644 index 0000000000..25fd40deec --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondConsensus.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.dao.governance.bond; + +import bisq.core.dao.governance.bond.lockup.LockupReason; +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Arrays; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BondConsensus { + // In the UI we don't allow 0 as that would mean that the tx gets spent + // in the same block as the unspent tx and we don't support unconfirmed txs in the DAO. Technically though 0 + // works as well. + @Getter + private static int minLockTime = 1; + + // Max value is max of a short int as we use only 2 bytes in the opReturn for the lockTime + @Getter + private static int maxLockTime = 65535; + + public static byte[] getLockupOpReturnData(int lockTime, LockupReason type, byte[] hash) throws IOException { + // PushData of <= 4 bytes is converted to int when returned from bitcoind and not handled the way we + // require by btcd-cli4j, avoid opReturns with 4 bytes or less + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.LOCKUP.getType()); + outputStream.write(Version.LOCKUP); + outputStream.write(type.getId()); + byte[] bytes = Utilities.integerToByteArray(lockTime, 2); + outputStream.write(bytes[0]); + outputStream.write(bytes[1]); + outputStream.write(hash); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + throw e; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 25; + } + + public static int getLockTime(byte[] opReturnData) { + return Utilities.byteArrayToInteger(Arrays.copyOfRange(opReturnData, 3, 5)); + } + + public static byte[] getHashFromOpReturnData(byte[] opReturnData) { + return Arrays.copyOfRange(opReturnData, 5, 25); + } + + public static boolean isLockTimeInValidRange(int lockTime) { + return lockTime >= BondConsensus.getMinLockTime() && lockTime <= BondConsensus.getMaxLockTime(); + } + + public static Optional getLockupReason(byte[] opReturnData) { + return LockupReason.getLockupReason(opReturnData[2]); + } + + public static boolean isLockTimeOver(long unlockBlockHeight, long currentBlockHeight) { + return currentBlockHeight >= unlockBlockHeight; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java new file mode 100644 index 0000000000..ade5ecd783 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java @@ -0,0 +1,246 @@ +/* + * 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 bisq.core.dao.governance.bond; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTxOutput; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.SpentInfo; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; + +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.ScriptPattern; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Collect bonds and bond asset data from other sources and provides access to the collection. + * Gets updated after a new block is parsed or at bsqWallet transaction change to detect also state changes by + * unconfirmed txs. + */ +@Slf4j +public abstract class BondRepository implements DaoSetupService, + BsqWalletService.WalletTransactionsChangeListener { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + public static void applyBondState(DaoStateService daoStateService, Bond bond, Tx lockupTx, TxOutput lockupTxOutput) { + if (bond.getBondState() != BondState.LOCKUP_TX_PENDING || bond.getBondState() != BondState.UNLOCK_TX_PENDING) + bond.setBondState(BondState.LOCKUP_TX_CONFIRMED); + + bond.setLockupTxId(lockupTx.getId()); + // We use the tx time as we want to have a unique time for all users + bond.setLockupDate(lockupTx.getTime()); + bond.setAmount(lockupTx.getLockedAmount()); + bond.setLockTime(lockupTx.getLockTime()); + + if (!daoStateService.isUnspent(lockupTxOutput.getKey())) { + // Lockup is already spent (in unlock tx) + daoStateService.getSpentInfo(lockupTxOutput) + .map(SpentInfo::getTxId) + .flatMap(daoStateService::getTx) + .filter(unlockTx -> unlockTx.getTxType() == TxType.UNLOCK) + .ifPresent(unlockTx -> { + // cross check if it is in daoStateService.getUnlockTxOutputs() ? + String unlockTxId = unlockTx.getId(); + bond.setUnlockTxId(unlockTxId); + bond.setBondState(BondState.UNLOCK_TX_CONFIRMED); + bond.setUnlockDate(unlockTx.getTime()); + boolean unlocking = daoStateService.isUnlockingAndUnspent(unlockTxId); + if (unlocking) { + bond.setBondState(BondState.UNLOCKING); + } else { + bond.setBondState(BondState.UNLOCKED); + } + }); + } + + if ((bond.getLockupTxId() != null && daoStateService.isConfiscatedLockupTxOutput(bond.getLockupTxId())) || + (bond.getUnlockTxId() != null && daoStateService.isConfiscatedUnlockTxOutput(bond.getUnlockTxId()))) { + bond.setBondState(BondState.CONFISCATED); + } + } + + public static boolean isLockupTxUnconfirmed(BsqWalletService bsqWalletService, BondedAsset bondedAsset) { + return bsqWalletService.getPendingWalletTransactionsStream() + .map(transaction -> transaction.getOutputs().get(transaction.getOutputs().size() - 1)) + .filter(lastOutput -> ScriptPattern.isOpReturn(lastOutput.getScriptPubKey())) + .map(lastOutput -> lastOutput.getScriptPubKey().getChunks()) + .filter(chunks -> chunks.size() > 1) + .map(chunks -> chunks.get(1).data) + .anyMatch(data -> Arrays.equals(BondConsensus.getHashFromOpReturnData(data), bondedAsset.getHash())); + } + + public static boolean isUnlockTxUnconfirmed(BsqWalletService bsqWalletService, DaoStateService daoStateService, BondedAsset bondedAsset) { + return bsqWalletService.getPendingWalletTransactionsStream() + .filter(transaction -> transaction.getInputs().size() > 1) + .flatMap(transaction -> transaction.getInputs().stream()) // We need to iterate all inputs + .map(TransactionInput::getConnectedOutput) + .filter(Objects::nonNull) + .filter(transactionOutput -> transactionOutput.getIndex() == 0) // The output at the lockupTx must be index 0 + .map(TransactionOutput::getParentTransaction) + .filter(Objects::nonNull) + .map(Transaction::getTxId) + .map(Sha256Hash::toString) + .map(lockupTxId -> daoStateService.getLockupOpReturnTxOutput(lockupTxId).orElse(null)) + .filter(Objects::nonNull) + .map(BaseTxOutput::getOpReturnData) + .anyMatch(data -> Arrays.equals(BondConsensus.getHashFromOpReturnData(data), bondedAsset.getHash())); + } + + public static boolean isConfiscated(Bond bond, DaoStateService daoStateService) { + return (bond.getLockupTxId() != null && daoStateService.isConfiscatedLockupTxOutput(bond.getLockupTxId())) || + (bond.getUnlockTxId() != null && daoStateService.isConfiscatedUnlockTxOutput(bond.getUnlockTxId())); + } + + + protected final DaoStateService daoStateService; + protected final BsqWalletService bsqWalletService; + + // This map is just for convenience. The data which are used to fill the map are stored in the DaoState (role, txs). + protected final Map bondByUidMap = new HashMap<>(); + @Getter + protected final ObservableList bonds = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BondRepository(DaoStateService daoStateService, BsqWalletService bsqWalletService) { + this.daoStateService = daoStateService; + this.bsqWalletService = bsqWalletService; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(new DaoStateListener() { + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + update(); + } + }); + bsqWalletService.addWalletTransactionsChangeListener(this); + } + + @Override + public void start() { + update(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqWalletService.WalletTransactionsChangeListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onWalletTransactionsChange() { + update(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isBondedAssetAlreadyInBond(R bondedAsset) { + boolean contains = bondByUidMap.containsKey(bondedAsset.getUid()); + return contains && bondByUidMap.get(bondedAsset.getUid()).getLockupTxId() != null; + } + + public List getActiveBonds() { + return bonds.stream().filter(Bond::isActive).collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract T createBond(R bondedAsset); + + protected abstract void updateBond(T bond, R bondedAsset, TxOutput lockupTxOutput); + + protected abstract Stream getBondedAssetStream(); + + protected void update() { + log.debug("update"); + getBondedAssetStream().forEach(bondedAsset -> { + String uid = bondedAsset.getUid(); + bondByUidMap.putIfAbsent(uid, createBond(bondedAsset)); + T bond = bondByUidMap.get(uid); + + daoStateService.getLockupTxOutputs().forEach(lockupTxOutput -> { + updateBond(bond, bondedAsset, lockupTxOutput); + }); + }); + + updateBondStateFromUnconfirmedLockupTxs(); + updateBondStateFromUnconfirmedUnlockTxs(); + + bonds.setAll(bondByUidMap.values()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateBondStateFromUnconfirmedLockupTxs() { + getBondedAssetStream().filter(bondedAsset -> isLockupTxUnconfirmed(bsqWalletService, bondedAsset)) + .map(bondedAsset -> bondByUidMap.get(bondedAsset.getUid())) + .filter(bond -> bond.getBondState() == BondState.READY_FOR_LOCKUP) + .forEach(bond -> bond.setBondState(isConfiscated(bond, daoStateService) ? BondState.CONFISCATED : BondState.LOCKUP_TX_PENDING)); + } + + private void updateBondStateFromUnconfirmedUnlockTxs() { + getBondedAssetStream().filter(bondedAsset -> isUnlockTxUnconfirmed(bsqWalletService, daoStateService, bondedAsset)) + .map(bondedAsset -> bondByUidMap.get(bondedAsset.getUid())) + .filter(bond -> bond.getBondState() == BondState.LOCKUP_TX_CONFIRMED) + .forEach(bond -> bond.setBondState(isConfiscated(bond, daoStateService) ? BondState.CONFISCATED : BondState.UNLOCK_TX_PENDING)); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondState.java b/core/src/main/java/bisq/core/dao/governance/bond/BondState.java new file mode 100644 index 0000000000..eb7dfe11f0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondState.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.dao.governance.bond; + +/** + * Holds the different states of a bond. + * Used also in string properties ("dao.bond.bondState.*") + */ +public enum BondState { + UNDEFINED, + READY_FOR_LOCKUP, // Accepted by voting (if role) but no lockup tx made yet. + LOCKUP_TX_PENDING, // Tx broadcasted but not confirmed. Used only by tx publisher. + LOCKUP_TX_CONFIRMED, + UNLOCK_TX_PENDING, // Tx broadcasted but not confirmed. Used only by tx publisher. + UNLOCK_TX_CONFIRMED, + UNLOCKING, // Lock time still not expired + UNLOCKED, // Fully unlocked + CONFISCATED; // Bond got confiscated by DAO voting + + public boolean isActive() { + return this == BondState.LOCKUP_TX_CONFIRMED || + this == BondState.UNLOCK_TX_PENDING || + this == BondState.UNLOCK_TX_CONFIRMED || + this == BondState.UNLOCKING; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondedAsset.java b/core/src/main/java/bisq/core/dao/governance/bond/BondedAsset.java new file mode 100644 index 0000000000..46f5a7c573 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondedAsset.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.core.dao.governance.bond; + +/** + * Represents the bonded asset (e.g. Role or Reputation). + */ +public interface BondedAsset { + byte[] getHash(); + + String getUid(); + + String getDisplayString(); +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupReason.java b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupReason.java new file mode 100644 index 0000000000..75da221ce9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupReason.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.dao.governance.bond.lockup; + +import java.util.Arrays; +import java.util.Optional; + +import lombok.Getter; + +/** + * Reason for locking up a bond. + */ +public enum LockupReason { + UNDEFINED((byte) 0x00), + BONDED_ROLE((byte) 0x01), + REPUTATION((byte) 0x02); + + @Getter + private byte id; + + LockupReason(byte id) { + this.id = id; + } + + public static Optional getLockupReason(byte id) { + return Arrays.stream(LockupReason.values()) + .filter(lockupType -> lockupType.id == id) + .findAny(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java new file mode 100644 index 0000000000..901793c74d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.dao.governance.bond.lockup; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.handlers.ExceptionHandler; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.io.IOException; + +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Service for publishing the lockup transaction. + */ +@Slf4j +public class LockupTxService { + private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public LockupTxService(WalletsManager walletsManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService) { + this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + } + + public void publishLockupTx(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash, + Consumer resultHandler, ExceptionHandler exceptionHandler) { + checkArgument(lockTime <= BondConsensus.getMaxLockTime() && + lockTime >= BondConsensus.getMinLockTime(), "lockTime not in range"); + try { + Transaction lockupTx = getLockupTx(lockupAmount, lockTime, lockupReason, hash); + walletsManager.publishAndCommitBsqTx(lockupTx, TxType.LOCKUP, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + resultHandler.accept(transaction.getTxId().toString()); + } + + @Override + public void onFailure(TxBroadcastException exception) { + exceptionHandler.handleException(exception); + } + }); + + } catch (TransactionVerificationException | InsufficientMoneyException | WalletException | + IOException exception) { + exceptionHandler.handleException(exception); + } + } + + public Tuple2 getMiningFeeAndTxVsize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash) + throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException { + Transaction tx = getLockupTx(lockupAmount, lockTime, lockupReason, hash); + Coin miningFee = tx.getFee(); + int txVsize = tx.getVsize(); + return new Tuple2<>(miningFee, txVsize); + } + + private Transaction getLockupTx(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash) + throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException { + byte[] opReturnData = BondConsensus.getLockupOpReturnData(lockTime, lockupReason, hash); + Transaction preparedTx = bsqWalletService.getPreparedLockupTx(lockupAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, opReturnData); + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Lockup tx: " + transaction); + return transaction; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputation.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputation.java new file mode 100644 index 0000000000..a630672771 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputation.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.dao.governance.bond.Bond; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Wrapper for reputation which contains the mutable state of a bonded reputation. Only kept in memory. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public final class BondedReputation extends Bond { + + BondedReputation(Reputation reputation) { + super(reputation); + } + + @Override + public String toString() { + return "BondedReputation{" + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputationRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputationRepository.java new file mode 100644 index 0000000000..8ec84f5677 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/BondedReputationRepository.java @@ -0,0 +1,137 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.bond.BondRepository; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxOutput; + +import javax.inject.Inject; + +import javafx.collections.ListChangeListener; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +/** + * Collect bonded reputations from the daoState blockchain data excluding bonded roles + * and provides access to the collection. + * Gets updated after a new block is parsed or at bsqWallet transaction change to detect also state changes by + * unconfirmed txs. + */ +@Slf4j +public class BondedReputationRepository extends BondRepository { + private final BondedRolesRepository bondedRolesRepository; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BondedReputationRepository(DaoStateService daoStateService, BsqWalletService bsqWalletService, + BondedRolesRepository bondedRolesRepository) { + super(daoStateService, bsqWalletService); + + this.bondedRolesRepository = bondedRolesRepository; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + super.addListeners(); + + // As event listeners do not have a deterministic ordering of callback we need to ensure + // that we get updated our data after the bondedRolesRepository was updated. + // The update gets triggered by daoState or wallet changes. It could be that we get triggered first the + // listeners and update our data with stale data from bondedRolesRepository. After that the bondedRolesRepository + // gets triggered the listeners and we would miss the current state if we would not listen here as well on the + // bond list. + bondedRolesRepository.getBonds().addListener((ListChangeListener) c -> update()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected BondedReputation createBond(Reputation reputation) { + return new BondedReputation(reputation); + } + + @Override + protected Stream getBondedAssetStream() { + return getBondedReputationStream().map(Bond::getBondedAsset); + } + + @Override + protected void update() { + bondByUidMap.clear(); + getBondedReputationStream().forEach(bondedReputation -> bondByUidMap.put(bondedReputation.getBondedAsset().getUid(), bondedReputation)); + bonds.setAll(bondByUidMap.values()); + } + + private Stream getBondedReputationStream() { + return getLockupTxOutputsForBondedReputation() + .map(lockupTxOutput -> { + String lockupTxId = lockupTxOutput.getTxId(); + Optional optionalOpReturnTxOutput = daoStateService.getLockupOpReturnTxOutput(lockupTxId); + if (optionalOpReturnTxOutput.isPresent()) { + TxOutput opReturnTxOutput = optionalOpReturnTxOutput.get(); + byte[] hash = BondConsensus.getHashFromOpReturnData(opReturnTxOutput.getOpReturnData()); + Reputation reputation = new Reputation(hash); + BondedReputation bondedReputation = new BondedReputation(reputation); + updateBond(bondedReputation, reputation, lockupTxOutput); + return bondedReputation; + } else { + return null; + } + + }) + .filter(Objects::nonNull); + } + + private Stream getLockupTxOutputsForBondedReputation() { + // We exclude bonded roles, so we store those in a lookup set. + Set bondedRolesLockupTxIdSet = bondedRolesRepository.getBonds().stream().map(Bond::getLockupTxId).collect(Collectors.toSet()); + return daoStateService.getLockupTxOutputs().stream() + .filter(e -> !bondedRolesLockupTxIdSet.contains(e.getTxId())); + } + + @Override + protected void updateBond(BondedReputation bond, Reputation bondedAsset, TxOutput lockupTxOutput) { + // Lets see if we have a lock up tx. + String lockupTxId = lockupTxOutput.getTxId(); + daoStateService.getTx(lockupTxId).ifPresent(lockupTx -> { + BondRepository.applyBondState(daoStateService, bond, lockupTx, lockupTxOutput); + }); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputation.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputation.java new file mode 100644 index 0000000000..f9af093c4a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputation.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.dao.governance.bond.Bond; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Wrapper for reputation which contains the mutable state of my bonded reputation. Only kept in memory. + * As it carries MyReputation it has access to the private salt data. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public final class MyBondedReputation extends Bond { + + public MyBondedReputation(MyReputation myReputation) { + super(myReputation); + } + + @Override + public String toString() { + return "MyBondedReputation{" + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java new file mode 100644 index 0000000000..b3935408cb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java @@ -0,0 +1,155 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.bond.BondRepository; +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Collect MyBondedReputations from the myReputationListService and provides access to the collection. + * Gets updated after a new block is parsed or at bsqWallet transaction change to detect also state changes by + * unconfirmed txs. + */ +@Slf4j +public class MyBondedReputationRepository implements DaoSetupService, BsqWalletService.WalletTransactionsChangeListener { + private final DaoStateService daoStateService; + private final BsqWalletService bsqWalletService; + private final MyReputationListService myReputationListService; + @Getter + private final ObservableList myBondedReputations = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyBondedReputationRepository(DaoStateService daoStateService, + BsqWalletService bsqWalletService, + MyReputationListService myReputationListService) { + this.daoStateService = daoStateService; + this.bsqWalletService = bsqWalletService; + this.myReputationListService = myReputationListService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(new DaoStateListener() { + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + update(); + } + }); + bsqWalletService.addWalletTransactionsChangeListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqWalletService.WalletTransactionsChangeListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onWalletTransactionsChange() { + update(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void update() { + log.debug("update"); + // It can be that the same salt/hash is in several lockupTxs, so we use the bondByLockupTxIdMap to eliminate + // duplicates by the collection algorithm. + Map bondByLockupTxIdMap = new HashMap<>(); + myReputationListService.getMyReputationList().stream() + .flatMap(this::getMyBondedReputation) + .forEach(e -> bondByLockupTxIdMap.putIfAbsent(e.getLockupTxId(), e)); + + myBondedReputations.setAll(bondByLockupTxIdMap.values().stream() + .peek(myBondedReputation -> { + if (BondRepository.isConfiscated(myBondedReputation, daoStateService)) { + myBondedReputation.setBondState(BondState.CONFISCATED); + } else { + // We don't have a UI use case for showing LOCKUP_TX_PENDING yet, but let's keep the code so if needed + // it's there. + if (BondRepository.isLockupTxUnconfirmed(bsqWalletService, myBondedReputation.getBondedAsset()) && + myBondedReputation.getBondState() == BondState.READY_FOR_LOCKUP) { + myBondedReputation.setBondState(BondState.LOCKUP_TX_PENDING); + } else if (BondRepository.isUnlockTxUnconfirmed(bsqWalletService, daoStateService, myBondedReputation.getBondedAsset()) && + myBondedReputation.getBondState() == BondState.LOCKUP_TX_CONFIRMED) { + myBondedReputation.setBondState(BondState.UNLOCK_TX_PENDING); + } + } + }) + .collect(Collectors.toList())); + } + + private Stream getMyBondedReputation(MyReputation myReputation) { + return daoStateService.getLockupTxOutputs().stream() + .flatMap(lockupTxOutput -> { + String lockupTxId = lockupTxOutput.getTxId(); + return daoStateService.getTx(lockupTxId) + .map(lockupTx -> { + byte[] opReturnData = lockupTx.getLastTxOutput().getOpReturnData(); + byte[] hash = BondConsensus.getHashFromOpReturnData(opReturnData); + // There could be multiple txs with the same hash, so we collect a stream and not use an optional. + if (Arrays.equals(hash, myReputation.getHash())) { + MyBondedReputation myBondedReputation = new MyBondedReputation(myReputation); + BondRepository.applyBondState(daoStateService, myBondedReputation, lockupTx, lockupTxOutput); + return myBondedReputation; + } else { + return null; + } + }) + .stream(); + }) + .filter(Objects::nonNull); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputation.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputation.java new file mode 100644 index 0000000000..f4bef01ac4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputation.java @@ -0,0 +1,130 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.dao.governance.bond.BondedAsset; + +import bisq.common.crypto.Hash; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * MyReputation is persisted locally and carries the private salt data. In contrast to Reputation which is the public + * data everyone can derive from the blockchain data (hash in opReturn). + */ +@Immutable +@Value +@Slf4j +public final class MyReputation implements PersistablePayload, NetworkPayload, BondedAsset { + // Uid is needed to be sure that 2 objects with the same salt are kept separate. + private final String uid; + private final byte[] salt; + private final transient byte[] hash; // Not persisted as it is derived from salt. Stored for caching purpose only. + + public MyReputation(byte[] salt) { + this(UUID.randomUUID().toString(), salt); + checkArgument(salt.length <= 20, "salt must not be longer then 20 bytes"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MyReputation(String uid, byte[] salt) { + this.uid = uid; + this.salt = salt; + this.hash = Hash.getSha256Ripemd160hash(salt); + } + + @Override + public protobuf.MyReputation toProtoMessage() { + return protobuf.MyReputation.newBuilder() + .setUid(uid) + .setSalt(ByteString.copyFrom(salt)) + .build(); + } + + public static MyReputation fromProto(protobuf.MyReputation proto) { + return new MyReputation(proto.getUid(), proto.getSalt().toByteArray()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BondedAsset implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public String getDisplayString() { + return Utilities.bytesAsHexString(hash); + } + + @Override + public String getUid() { + return Utilities.bytesAsHexString(hash); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MyReputation)) return false; + if (!super.equals(o)) return false; + MyReputation that = (MyReputation) o; + return Objects.equals(uid, that.uid) && + Arrays.equals(salt, that.salt); + } + + @Override + public int hashCode() { + + int result = Objects.hash(super.hashCode(), uid); + result = 31 * result + Arrays.hashCode(salt); + return result; + } +/////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String toString() { + return "MyReputation{" + + "\n uid=" + uid + + "\n salt=" + Utilities.bytesAsHexString(salt) + + "\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationList.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationList.java new file mode 100644 index 0000000000..2a39f78317 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationList.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.common.proto.persistable.PersistableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * PersistableEnvelope wrapper for list of MyReputations. + */ +@EqualsAndHashCode(callSuper = true) +public class MyReputationList extends PersistableList { + + private MyReputationList(List list) { + super(list); + } + + MyReputationList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableEnvelope toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setMyReputationList(getBuilder()).build(); + } + + private protobuf.MyReputationList.Builder getBuilder() { + return protobuf.MyReputationList.newBuilder() + .addAllMyReputation(getList().stream() + .map(MyReputation::toProtoMessage) + .collect(Collectors.toList())); + } + + public static MyReputationList fromProto(protobuf.MyReputationList proto) { + return new MyReputationList(new ArrayList<>(proto.getMyReputationList().stream() + .map(MyReputation::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of salts in MyReputationList: " + getList().stream() + .map(MyReputation::getSalt) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java new file mode 100644 index 0000000000..5995a3be35 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java @@ -0,0 +1,106 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.dao.DaoSetupService; + +import bisq.common.app.DevEnv; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * Manages the persistence of myReputation objects. + */ +@Slf4j +public class MyReputationListService implements PersistedDataHost, DaoSetupService { + + private final PersistenceManager persistenceManager; + private final MyReputationList myReputationList = new MyReputationList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyReputationListService(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + persistenceManager.initialize(myReputationList, PersistenceManager.Source.PRIVATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + myReputationList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addReputation(MyReputation reputation) { + if (!myReputationList.contains(reputation)) { + myReputationList.add(reputation); + requestPersistence(); + } + } + + public List getMyReputationList() { + return myReputationList.getList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/Reputation.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/Reputation.java new file mode 100644 index 0000000000..e7394604be --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/Reputation.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.core.dao.governance.bond.reputation; + +import bisq.core.dao.governance.bond.BondedAsset; + +import bisq.common.util.Utilities; + +import java.util.Arrays; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Reputation objects we found on the blockchain. We only know the hash of it. + * In contrast to MyReputation which represents the object we created and contains the + * private salt data. + */ +@Immutable +@Value +@Slf4j +public final class Reputation implements BondedAsset { + private final byte[] hash; + + public Reputation(byte[] hash) { + this.hash = hash; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BondedAsset implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public String getDisplayString() { + return Utilities.bytesAsHexString(hash); + } + + @Override + public String getUid() { + return Utilities.bytesAsHexString(hash); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Reputation)) return false; + if (!super.equals(o)) return false; + Reputation that = (Reputation) o; + return Arrays.equals(hash, that.hash); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(hash); + return result; + } + + @Override + public String toString() { + return "Reputation{" + + "\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRole.java b/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRole.java new file mode 100644 index 0000000000..f5a7026241 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRole.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.core.dao.governance.bond.role; + +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.state.model.governance.Role; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Wrapper for role which contains the mutable state of a bonded role. Only kept in memory. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class BondedRole extends Bond { + + BondedRole(Role role) { + super(role); + } + + @Override + public String toString() { + return "BondedRole{" + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRolesRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRolesRepository.java new file mode 100644 index 0000000000..d7a10ae1e0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/role/BondedRolesRepository.java @@ -0,0 +1,134 @@ +/* + * 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 bisq.core.dao.governance.bond.role; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.bond.BondRepository; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; + +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +/** + * Collect bonded roles from the evaluatedProposals from the daoState and provides access to the collection. + */ +@Slf4j +public class BondedRolesRepository extends BondRepository { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BondedRolesRepository(DaoStateService daoStateService, BsqWalletService bsqWalletService) { + super(daoStateService, bsqWalletService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isMyRole(Role role) { + Set myWalletTransactionIds = bsqWalletService.getClonedWalletTransactions().stream() + .map(Transaction::getTxId) + .map(Sha256Hash::toString) + .collect(Collectors.toSet()); + return getAcceptedBondedRoleProposalStream() + .filter(roleProposal -> roleProposal.getRole().equals(role)) + .map(Proposal::getTxId) + .anyMatch(myWalletTransactionIds::contains); + } + + public Optional getAcceptedBondedRoleProposal(Role role) { + return getAcceptedBondedRoleProposalStream().filter(e -> e.getRole().getUid().equals(role.getUid())).findAny(); + } + + + public List getAcceptedBonds() { + return bonds.stream() + .filter(bondedRole -> getAcceptedBondedRoleProposal(bondedRole.getBondedAsset()).isPresent()) + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected BondedRole createBond(Role role) { + return new BondedRole(role); + } + + @Override + protected Stream getBondedAssetStream() { + return getBondedRoleProposalStream().map(RoleProposal::getRole); + } + + @Override + protected void updateBond(BondedRole bond, Role bondedAsset, TxOutput lockupTxOutput) { + // Lets see if we have a lock up tx. + String lockupTxId = lockupTxOutput.getTxId(); + daoStateService.getTx(lockupTxId).ifPresent(lockupTx -> { + byte[] opReturnData = lockupTx.getLastTxOutput().getOpReturnData(); + // We used the hash of the bonded bondedAsset object as our hash in OpReturn of the lock up tx to have a + // unique binding of the tx to the data object. + byte[] hash = BondConsensus.getHashFromOpReturnData(opReturnData); + Optional candidate = findBondedAssetByHash(hash); + if (candidate.isPresent() && bondedAsset.equals(candidate.get())) + applyBondState(daoStateService, bond, lockupTx, lockupTxOutput); + }); + } + + private Optional findBondedAssetByHash(byte[] hash) { + return getBondedAssetStream() + .filter(bondedAsset -> Arrays.equals(bondedAsset.getHash(), hash)) + .findAny(); + } + + private Stream getBondedRoleProposalStream() { + return daoStateService.getEvaluatedProposalList().stream() + .filter(evaluatedProposal -> evaluatedProposal.getProposal() instanceof RoleProposal) + .map(e -> ((RoleProposal) e.getProposal())); + } + + private Stream getAcceptedBondedRoleProposalStream() { + return daoStateService.getEvaluatedProposalList().stream() + .filter(evaluatedProposal -> evaluatedProposal.getProposal() instanceof RoleProposal) + .filter(EvaluatedProposal::isAccepted) + .map(e -> ((RoleProposal) e.getProposal())); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java new file mode 100644 index 0000000000..a4c6691cf6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.dao.governance.bond.unlock; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.handlers.ExceptionHandler; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Service for publishing the unlock transaction. + */ +@Slf4j +public class UnlockTxService { + private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final DaoStateService daoStateService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public UnlockTxService(WalletsManager walletsManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService) { + this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.daoStateService = daoStateService; + } + + public void publishUnlockTx(String lockupTxId, Consumer resultHandler, ExceptionHandler exceptionHandler) { + try { + Transaction unlockTx = getUnlockTx(lockupTxId); + walletsManager.publishAndCommitBsqTx(unlockTx, TxType.UNLOCK, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + resultHandler.accept(transaction.getTxId().toString()); + } + + @Override + public void onFailure(TxBroadcastException exception) { + exceptionHandler.handleException(exception); + } + }); + } catch (TransactionVerificationException | InsufficientMoneyException | WalletException exception) { + exceptionHandler.handleException(exception); + } + } + + public Tuple2 getMiningFeeAndTxVsize(String lockupTxId) + throws InsufficientMoneyException, WalletException, TransactionVerificationException { + Transaction tx = getUnlockTx(lockupTxId); + Coin miningFee = tx.getFee(); + int txVsize = tx.getVsize(); + return new Tuple2<>(miningFee, txVsize); + } + + private Transaction getUnlockTx(String lockupTxId) + throws InsufficientMoneyException, WalletException, TransactionVerificationException { + Optional optionalLockupTxOutput = daoStateService.getLockupTxOutput(lockupTxId); + checkArgument(optionalLockupTxOutput.isPresent(), "lockupTxOutput must be present"); + TxOutput lockupTxOutput = optionalLockupTxOutput.get(); + Transaction preparedTx = bsqWalletService.getPreparedUnlockTx(lockupTxOutput); + Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, null); + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Unlock tx: " + transaction); + return transaction; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java b/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java new file mode 100644 index 0000000000..6ea2c7e075 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java @@ -0,0 +1,170 @@ +/* + * 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 bisq.core.dao.governance.merit; + +import bisq.core.dao.governance.voteresult.VoteResultException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.MeritList; + +import bisq.common.crypto.Encryption; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Sha256Hash; + +import com.google.common.annotations.VisibleForTesting; + +import javax.crypto.SecretKey; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class MeritConsensus { + // Value with 144 blocks a day and 365 days would be 52560. We take a close round number instead. + private static final int BLOCKS_PER_YEAR = 50_000; + + public static MeritList decryptMeritList(byte[] encryptedMeritList, SecretKey secretKey) + throws VoteResultException.DecryptionException { + try { + byte[] decrypted = Encryption.decrypt(encryptedMeritList, secretKey); + return MeritList.getMeritListFromBytes(decrypted); + } catch (Throwable t) { + throw new VoteResultException.DecryptionException(t); + } + } + + public static long getMeritStake(String blindVoteTxId, MeritList meritList, DaoStateService daoStateService) { + // We need to take the chain height when the blindVoteTx got published so we get the same merit for the vote even at + // later blocks (merit decreases with each block). + int blindVoteTxHeight = daoStateService.getTx(blindVoteTxId).map(Tx::getBlockHeight).orElse(0); + if (blindVoteTxHeight == 0) { + log.error("Error at getMeritStake: blindVoteTx not found in daoStateService. blindVoteTxId=" + blindVoteTxId); + return 0; + } + + // We only use past issuance. In case we would calculate the merit after the vote result phase we have the + // issuance from the same cycle but we must not add that to the merit. + return meritList.getList().stream() + .filter(merit -> isSignatureValid(merit.getSignature(), merit.getIssuance().getPubKey(), blindVoteTxId)) + .filter(merit -> merit.getIssuance().getChainHeight() <= blindVoteTxHeight) + .mapToLong(merit -> { + try { + Issuance issuance = merit.getIssuance(); + checkArgument(issuance.getIssuanceType() == IssuanceType.COMPENSATION, + "issuance must be of type COMPENSATION"); + return getWeightedMeritAmount(issuance.getAmount(), + issuance.getChainHeight(), + blindVoteTxHeight, + BLOCKS_PER_YEAR); + } catch (Throwable t) { + log.error("Error at getMeritStake: error={}, merit={}", t.toString(), merit); + return 0; + } + }) + .sum(); + } + + @VisibleForTesting + private static boolean isSignatureValid(byte[] signatureFromMerit, String pubKeyAsHex, String blindVoteTxId) { + // We verify if signature of hash of blindVoteTxId is correct. EC key from first input for blind vote tx is + // used for signature. + if (pubKeyAsHex == null) { + log.error("Error at isSignatureValid: pubKeyAsHex is null"); + return false; + } + + boolean result = false; + try { + ECKey pubKey = ECKey.fromPublicOnly(Utilities.decodeFromHex(pubKeyAsHex)); + ECKey.ECDSASignature signature = ECKey.ECDSASignature.decodeFromDER(signatureFromMerit).toCanonicalised(); + Sha256Hash msg = Sha256Hash.wrap(blindVoteTxId); + result = pubKey.verify(msg, signature); + } catch (Throwable t) { + log.error("Signature verification of issuance failed: " + t.toString()); + } + if (!result) { + log.error("Signature verification of issuance failed: blindVoteTxId={}, pubKeyAsHex={}", + blindVoteTxId, pubKeyAsHex); + } + return result; + } + + public static long getWeightedMeritAmount(long amount, int issuanceHeight, int blockHeight, int blocksPerYear) { + if (issuanceHeight > blockHeight) + throw new IllegalArgumentException("issuanceHeight must not be larger than blockHeight. issuanceHeight=" + issuanceHeight + "; blockHeight=" + blockHeight); + if (blockHeight < 0) + throw new IllegalArgumentException("blockHeight must not be negative. blockHeight=" + blockHeight); + if (amount < 0) + throw new IllegalArgumentException("amount must not be negative. amount" + amount); + if (blocksPerYear < 0) + throw new IllegalArgumentException("blocksPerYear must not be negative. blocksPerYear=" + blocksPerYear); + if (issuanceHeight < 0) + throw new IllegalArgumentException("issuanceHeight must not be negative. issuanceHeight=" + issuanceHeight); + + // We use a linear function to apply a factor for the issuance amount of 1 if the issuance was recent and 0 + // if the issuance was 2 years old or older. + // To avoid rounding issues with double values we multiply initially with a large number and divide at the end + // by that number again. As we multiply the amount in satoshis we get a reasonable good precision even the long + // division is not using rounding. Sticking with long values makes that operation safer against consensus + // failures causes by rounding differences with double. + + long maxAge = 2 * blocksPerYear; // maxAge=100 000 (MeritConsensus.BLOCKS_PER_YEAR is 50_000) + long age = Math.min(maxAge, blockHeight - issuanceHeight); + long inverseAge = maxAge - age; + + // We want a resolution of 1 block so we use the inverseAge and divide by maxAge afterwards to get the + // weighted amount + // We need to multiply first before we divide! + long weightedAmount = (amount * inverseAge) / maxAge; + + log.debug("getWeightedMeritAmount: age={}, inverseAge={}, weightedAmount={}, amount={}", age, inverseAge, weightedAmount, amount); + return weightedAmount; + } + + public static long getCurrentlyAvailableMerit(MeritList meritList, int currentChainHeight) { + // We need to take the chain height when the blindVoteTx got published so we get the same merit for the vote even at + // later blocks (merit decreases with each block). + // We add 1 block to currentChainHeight so that the displayed merit would match the merit in case we get the + // blind vote tx into the next block. + int height = currentChainHeight + 1; + return meritList.getList().stream() + .mapToLong(merit -> { + try { + Issuance issuance = merit.getIssuance(); + checkArgument(issuance.getIssuanceType() == IssuanceType.COMPENSATION, "issuance must be of type COMPENSATION"); + int issuanceHeight = issuance.getChainHeight(); + checkArgument(issuanceHeight <= height, + "issuanceHeight must not be larger as currentChainHeight"); + return getWeightedMeritAmount(issuance.getAmount(), + issuanceHeight, + height, + BLOCKS_PER_YEAR); + } catch (Throwable t) { + log.error("Error at getCurrentlyAvailableMerit: " + t.toString()); + return 0; + } + }) + .sum(); + } + +} diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java new file mode 100644 index 0000000000..6036bc6260 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java @@ -0,0 +1,149 @@ +/* + * 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 bisq.core.dao.governance.myvote; + +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.merit.MeritConsensus; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.model.governance.MeritList; + +import bisq.common.crypto.Encryption; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import javax.crypto.SecretKey; + +import java.util.Date; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Holds all my vote related data. Is not immutable as revealTxId is set later. Only used for local persistence and not + * in consensus critical operations. + */ +@EqualsAndHashCode +@Slf4j +@Getter +public class MyVote implements PersistablePayload { + private int height; + private final BallotList ballotList; + private final byte[] secretKeyEncoded; + private final BlindVote blindVote; + private final long date; + @Setter + @Nullable + private String revealTxId; + + // Used just for caching + @JsonExclude + private final transient SecretKey secretKey; + + MyVote(int height, + BallotList ballotList, + byte[] secretKeyEncoded, + BlindVote blindVote) { + this(height, + ballotList, + secretKeyEncoded, + blindVote, + new Date().getTime(), + null); + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MyVote(int height, + BallotList ballotList, + byte[] secretKeyEncoded, + BlindVote blindVote, + long date, + @Nullable String revealTxId) { + this.height = height; + this.ballotList = ballotList; + this.secretKeyEncoded = secretKeyEncoded; + this.blindVote = blindVote; + this.date = date; + this.revealTxId = revealTxId; + + secretKey = Encryption.getSecretKeyFromBytes(secretKeyEncoded); + } + + @Override + public protobuf.MyVote toProtoMessage() { + final protobuf.MyVote.Builder builder = protobuf.MyVote.newBuilder() + .setHeight(height) + .setBlindVote(blindVote.getBuilder()) + .setBallotList(ballotList.getBuilder()) + .setSecretKeyEncoded(ByteString.copyFrom(secretKeyEncoded)) + .setDate(date); + Optional.ofNullable(revealTxId).ifPresent(builder::setRevealTxId); + return builder.build(); + } + + public static MyVote fromProto(protobuf.MyVote proto) { + return new MyVote(proto.getHeight(), + BallotList.fromProto(proto.getBallotList()), + proto.getSecretKeyEncoded().toByteArray(), + BlindVote.fromProto(proto.getBlindVote()), + proto.getDate(), + proto.getRevealTxId().isEmpty() ? null : proto.getRevealTxId()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getBlindVoteTxId() { + return blindVote.getTxId(); + } + + public long getMerit(MyBlindVoteListService myBlindVoteListService, DaoStateService daoStateService) { + MeritList meritList = myBlindVoteListService.getMerits(blindVote.getTxId()); + if (daoStateService.getTx(blindVote.getTxId()).isPresent()) + return MeritConsensus.getMeritStake(blindVote.getTxId(), meritList, daoStateService); + else + return MeritConsensus.getCurrentlyAvailableMerit(meritList, daoStateService.getChainHeight()); + } + + @Override + public String toString() { + return "MyVote{" + + "\n ballotList=" + ballotList + + ",\n secretKeyEncoded=" + Utilities.bytesAsHexString(secretKeyEncoded) + + ",\n blindVotePayload=" + blindVote + + ",\n date=" + new Date(date) + + ",\n revealTxId='" + revealTxId + '\'' + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java new file mode 100644 index 0000000000..05610f8dcb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.core.dao.governance.myvote; + +import bisq.common.proto.persistable.PersistableList; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public class MyVoteList extends PersistableList { + + MyVoteList() { + super(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MyVoteList(List list) { + super(list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setMyVoteList(protobuf.MyVoteList.newBuilder() + .addAllMyVote(getList().stream() + .map(MyVote::toProtoMessage) + .collect(Collectors.toList()))) + .build(); + } + + public static MyVoteList fromProto(protobuf.MyVoteList proto) { + return new MyVoteList(new ArrayList<>(proto.getMyVoteList().stream() + .map(MyVote::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of TxId's in MyVoteList: " + getList().stream() + .map(MyVote::getBlindVoteTxId) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java new file mode 100644 index 0000000000..dacf524344 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.dao.governance.myvote; + +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BallotList; + +import bisq.common.app.DevEnv; +import bisq.common.crypto.Encryption; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.util.Tuple2; + +import javax.inject.Inject; + +import javax.crypto.SecretKey; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates and stores myVote items. Persist in MyVoteList. + */ +@Slf4j +public class MyVoteListService implements PersistedDataHost { + private final DaoStateService daoStateService; + private final PersistenceManager persistenceManager; + private final MyVoteList myVoteList = new MyVoteList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyVoteListService(DaoStateService daoStateService, + PersistenceManager persistenceManager) { + this.daoStateService = daoStateService; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(myVoteList, PersistenceManager.Source.PRIVATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + myVoteList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void createAndAddMyVote(BallotList sortedBallotListForCycle, SecretKey secretKey, BlindVote blindVote) { + final byte[] secretKeyBytes = Encryption.getSecretKeyBytes(secretKey); + MyVote myVote = new MyVote(daoStateService.getChainHeight(), sortedBallotListForCycle, secretKeyBytes, blindVote); + log.info("Add new MyVote to myVotesList list.\nMyVote=" + myVote); + myVoteList.add(myVote); + requestPersistence(); + } + + public void applyRevealTxId(MyVote myVote, String voteRevealTxId) { + myVote.setRevealTxId(voteRevealTxId); + log.debug("Applied revealTxId to myVote.\nmyVote={}\nvoteRevealTxId={}", myVote, voteRevealTxId); + requestPersistence(); + } + + public Tuple2 getMeritAndStakeForProposal(String proposalTxId, + MyBlindVoteListService myBlindVoteListService) { + long merit = 0; + long stake = 0; + List list = new ArrayList<>(myVoteList.getList()); + list.sort(Comparator.comparing(MyVote::getDate)); + for (MyVote myVote : list) { + for (Ballot ballot : myVote.getBallotList().getList()) { + if (ballot.getTxId().equals(proposalTxId)) { + merit = myVote.getMerit(myBlindVoteListService, daoStateService); + stake = myVote.getBlindVote().getStake(); + break; + } + } + } + return new Tuple2<>(merit, stake); + } + + public MyVoteList getMyVoteList() { + return myVoteList; + } + + public List getMyVoteListForCycle() { + return myVoteList.getList().stream() + .filter(e -> daoStateService.getCurrentCycle() != null) + .filter(e -> daoStateService.getCurrentCycle().isInCycle(e.getHeight())) + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java new file mode 100644 index 0000000000..7202c414aa --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/param/Param.java @@ -0,0 +1,263 @@ +/* + * 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 bisq.core.dao.governance.param; + +import bisq.core.locale.Res; + +import bisq.common.config.Config; +import bisq.common.proto.ProtoUtil; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * All parameters in the Bisq DAO which can be changed by voting. + * We will add more on demand. + * We need to support updates with new types in future. We use in the persisted data only the enum name, thus the names + * must not change once the dao has started. Default values must not be changed as well. + * For parameters which are used by Bisq users (trade fee,...) we have more strict requirements for backward compatibility. + * Parameters which are only used in proposals and voting are less strict limited as we can require that those users are + * using the latest software version. + * The UNDEFINED entry is used as fallback for error cases and will get ignored. + * + * Name of the params must not change as that is used for serialisation in Protobuffer. The data fields are not part of + * the PB serialisation so changes for those would not change the hash for the dao state hash chain. + * Though changing the values might break consensus as the validations might return a different result (e.g. a param + * change proposal was accepted with older min/max values but then after a change it is not valid anymore and + * might break the consequences of that change. So in fact we MUST not change anything here, only way is to add new + * entries and don't use the deprecated enum in future releases anymore. + */ +@Slf4j +public enum Param { + UNDEFINED("N/A", ParamType.UNDEFINED), + + // Fee in BTC for a 1 BTC trade. 0.001 is 0.1%. @5000 USD/BTC price 0.1% fee is 5 USD. + DEFAULT_MAKER_FEE_BTC("0.001", ParamType.BTC, 5, 5), + DEFAULT_TAKER_FEE_BTC("0.003", ParamType.BTC, 5, 5), // 0.2% of trade amount + MIN_MAKER_FEE_BTC("0.00005", ParamType.BTC, 5, 5), // 0.005% of trade amount + MIN_TAKER_FEE_BTC("0.00005", ParamType.BTC, 5, 5), + + // Fee in BSQ satoshis for a 1 BTC trade. 100 satoshis = 1 BSQ + // If 1 BTS is 1 USD the fee @5000 USD/BTC is 0.5 USD which is 10% of the BTC fee of 5 USD. + // Might need adjustment if BSQ/BTC rate changes. + DEFAULT_MAKER_FEE_BSQ("0.50", ParamType.BSQ, 5, 5), // ~ 0.01% of trade amount + DEFAULT_TAKER_FEE_BSQ("1.5", ParamType.BSQ, 5, 5), + // Min fee is the smallest fee allowed for a trade. If the default fee would be less than min fee the + // min fee is used instead. + // 0.03 BSQ (3 satoshis) for a 1 BTC trade. 0.05 USD if 1 BSQ = 1 USD, 10 % of the BTC fee + MIN_MAKER_FEE_BSQ("0.03", ParamType.BSQ, 5, 5), // 0.0003%. + MIN_TAKER_FEE_BSQ("0.03", ParamType.BSQ, 5, 5), + + // Fees proposal/voting. Atm we don't use diff. fees for diff. proposal types + // See: https://github.com/bisq-network/proposals/issues/46 + PROPOSAL_FEE("2", ParamType.BSQ, 5, 5), // 2 BSQ + BLIND_VOTE_FEE("2", ParamType.BSQ, 5, 5), // 2 BSQ + + // As BSQ based validation values can change over time if BSQ value rise we need to support that in the Params as well + COMPENSATION_REQUEST_MIN_AMOUNT("10", ParamType.BSQ, 4, 2), + COMPENSATION_REQUEST_MAX_AMOUNT("100000", ParamType.BSQ, 4, 2), + REIMBURSEMENT_MIN_AMOUNT("10", ParamType.BSQ, 4, 2), + REIMBURSEMENT_MAX_AMOUNT("10000", ParamType.BSQ, 4, 2), + + // Quorum required for voting result to be valid. + // Quorum is the min. amount of total BSQ (earned+stake) which was used for voting on a request. + // E.g. If only 2000 BSQ was used on a vote but 10 000 is required the result is invalid even if the voters voted + // 100% for acceptance. This should prevent that changes can be done with low stakeholder participation. + QUORUM_COMP_REQUEST("20000", ParamType.BSQ, 2, 2), + QUORUM_REIMBURSEMENT("20000", ParamType.BSQ, 2, 2), + QUORUM_CHANGE_PARAM("100000", ParamType.BSQ, 2, 2), + QUORUM_ROLE("50000", ParamType.BSQ, 2, 2), + QUORUM_CONFISCATION("200000", ParamType.BSQ, 2, 2), + QUORUM_GENERIC("5000", ParamType.BSQ, 2, 2), + QUORUM_REMOVE_ASSET("10000", ParamType.BSQ, 2, 2), + + // Threshold for voting in % with precision of 2 (e.g. 5000 -> 50.00%) + // This is the required amount of weighted vote result needed for acceptance of the result. + // E.g. If the result ends up in 65% weighted vote for acceptance and threshold was 50% it is accepted. + // The result must be larger than the threshold. A 50% vote result for a threshold with 50% is not sufficient, + // it requires min. 50.01%. + // The maxDecrease value is only relevant if the decreased value will not result in a value below 50.01%. + THRESHOLD_COMP_REQUEST("50.01", ParamType.PERCENT, 1.2, 1.2), + THRESHOLD_REIMBURSEMENT("50.01", ParamType.PERCENT, 1.2, 1.2), + THRESHOLD_CHANGE_PARAM("75.01", ParamType.PERCENT, 1.2, 1.2), // That might change the THRESHOLD_CHANGE_PARAM and QUORUM_CHANGE_PARAM as well. So we have to be careful here! + THRESHOLD_ROLE("50.01", ParamType.PERCENT, 1.2, 1.2), + THRESHOLD_CONFISCATION("85.01", ParamType.PERCENT, 1.2, 1.2), // Confiscation is considered an exceptional case and need very high consensus among the stakeholders. + THRESHOLD_GENERIC("50.01", ParamType.PERCENT, 1.2, 1.2), + THRESHOLD_REMOVE_ASSET("50.01", ParamType.PERCENT, 1.2, 1.2), + + // BTC address as recipient for BTC trade fee once the arbitration system is replaced as well as destination for + // the time locked payout tx in case the traders do not cooperate. Will be likely a donation address (Bisq, Tor,...) + // but can be also a burner address if we prefer to burn the BTC + @SuppressWarnings("SpellCheckingInspection") + RECIPIENT_BTC_ADDRESS(Config.baseCurrencyNetwork().isMainnet() ? "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7" : // mainnet + Config.baseCurrencyNetwork().isDaoBetaNet() ? "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7" : // daoBetaNet + Config.baseCurrencyNetwork().isTestnet() ? "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV" : // testnet + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w", // regtest or DAO testnet (regtest) + ParamType.ADDRESS), + + // Fee for activating an asset or re-listing after deactivation due lack of trade activity. Fee per day of trial period without activity checks. + ASSET_LISTING_FEE_PER_DAY("1", ParamType.BSQ, 10, 10), + // Min required trade volume to not get de-listed. Check starts after trial period and use trial period afterwards to look back for trade activity. + ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10), + + LOCK_TIME_TRADE_PAYOUT("4320", ParamType.BLOCK), // 30 days, can be disabled by setting to 0 + ARBITRATOR_FEE("0", ParamType.PERCENT), // % of trade. For new trade protocol. Arbitration will become optional and we can apply a fee to it. Initially we start with no fee. + MAX_TRADE_LIMIT("2", ParamType.BTC, 2, 2), // max trade limit for lowest risk payment method. Others will get derived from that. + + // The base factor to multiply the bonded role amount. E.g. If Twitter admin has 20 as amount and BONDED_ROLE_FACTOR is 1000 we get 20000 BSQ as required bond. + // Using BSQ as type is not really the best option but we don't want to introduce a new ParamType just for that one Param. + // As the end rules is in fact BSQ it is not completely incorrect as well. + BONDED_ROLE_FACTOR("1000", ParamType.BSQ, 2, 2), + ISSUANCE_LIMIT("200000", ParamType.BSQ, 2, 2), // Max. issuance+reimbursement per cycle. + + // The last block in the proposal and vote phases are not shown to the user as he cannot make a tx there as it would be + // confirmed in the next block which would be the following break phase. To hide that complexity we show only the + // blocks where the user can be active. To have still round numbers for the durations we add 1 block to those + // phases and subtract 1 block from the following breaks. + // So in the UI the user will see 3600 blocks and the last + // block of the technical 3601 blocks is displayed as part of the break1 phase. + // For testnet we want to have a short cycle of about a week (1012 blocks) + // For regtest we use very short periods + PHASE_UNDEFINED("0", ParamType.BLOCK), + PHASE_PROPOSAL(Config.baseCurrencyNetwork().isMainnet() ? + "3601" : // mainnet; 24 days + Config.baseCurrencyNetwork().isRegtest() ? + "4" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "144" : // daoBetaNet; 1 day + Config.baseCurrencyNetwork().isDaoRegTest() ? + "134" : // dao regtest; 0.93 days + "380", // testnet or dao testnet (server side regtest); 2.6 days + ParamType.BLOCK, 2, 2), + PHASE_BREAK1(Config.baseCurrencyNetwork().isMainnet() ? + "149" : // mainnet; 1 day + Config.baseCurrencyNetwork().isRegtest() ? + "1" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "10" : // daoBetaNet + Config.baseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) + ParamType.BLOCK, 2, 2), + PHASE_BLIND_VOTE(Config.baseCurrencyNetwork().isMainnet() ? + "451" : // mainnet; 3 days + Config.baseCurrencyNetwork().isRegtest() ? + "2" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "144" : // daoBetaNet; 1 day + Config.baseCurrencyNetwork().isDaoRegTest() ? + "134" : // dao regtest; 0.93 days + "300", // testnet or dao testnet (server side regtest); 2 days + ParamType.BLOCK, 2, 2), + PHASE_BREAK2(Config.baseCurrencyNetwork().isMainnet() ? + "9" : // mainnet + Config.baseCurrencyNetwork().isRegtest() ? + "1" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "10" : // daoBetaNet + Config.baseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) + ParamType.BLOCK, 2, 2), + PHASE_VOTE_REVEAL(Config.baseCurrencyNetwork().isMainnet() ? + "451" : // mainnet; 3 days + Config.baseCurrencyNetwork().isRegtest() ? + "2" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "144" : // daoBetaNet; 1 day + Config.baseCurrencyNetwork().isDaoRegTest() ? + "132" : // dao regtest; 0.93 days + "300", // testnet or dao testnet (server side regtest); 2 days + ParamType.BLOCK, 2, 2), + PHASE_BREAK3(Config.baseCurrencyNetwork().isMainnet() ? + "9" : // mainnet + Config.baseCurrencyNetwork().isRegtest() ? + "1" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "10" : // daoBetaNet + Config.baseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) + ParamType.BLOCK, 2, 2), + PHASE_RESULT(Config.baseCurrencyNetwork().isMainnet() ? + "10" : // mainnet + Config.baseCurrencyNetwork().isRegtest() ? + "2" : // regtest + Config.baseCurrencyNetwork().isDaoBetaNet() ? + "10" : // daoBetaNet + Config.baseCurrencyNetwork().isDaoRegTest() ? + "2" : // dao regtest + "2", // testnet or dao testnet (server side regtest) + ParamType.BLOCK, 2, 2); + + @Getter + private final String defaultValue; + @Getter + private final ParamType paramType; + // If 0 we ignore check for max decrease + @Getter + private final double maxDecrease; + // If 0 we ignore check for max increase + @Getter + private final double maxIncrease; + + Param(String defaultValue, ParamType paramType) { + this(defaultValue, paramType, 0, 0); + } + + /** + * @param defaultValue Value at the start of the DAO + * @param paramType Type of parameter + * @param maxDecrease Decrease of param value limited to current value / maxDecrease. If 0 we don't apply the check and any change is possible + * @param maxIncrease Increase of param value limited to current value * maxIncrease. If 0 we don't apply the check and any change is possible + */ + Param(String defaultValue, ParamType paramType, double maxDecrease, double maxIncrease) { + this.defaultValue = defaultValue; + this.paramType = paramType; + this.maxDecrease = maxDecrease; + this.maxIncrease = maxIncrease; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Param fromProto(protobuf.ChangeParamProposal proposalProto) { + Param param; + try { + param = ProtoUtil.enumFromProto(Param.class, proposalProto.getParam()); + checkNotNull(param, "param must not be null"); + } catch (Throwable t) { + log.error("fromProto: " + t.toString()); + param = Param.UNDEFINED; + } + return param; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getDisplayString() { + return name().startsWith("PHASE_") ? + Res.get("dao.phase." + name()) : + Res.get("dao.param." + name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/param/ParamType.java b/core/src/main/java/bisq/core/dao/governance/param/ParamType.java new file mode 100644 index 0000000000..5e89b7a427 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/param/ParamType.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.core.dao.governance.param; + +public enum ParamType { + UNDEFINED, + BSQ, + BTC, + PERCENT, + BLOCK, + ADDRESS +} diff --git a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java new file mode 100644 index 0000000000..acc4cfa031 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java @@ -0,0 +1,186 @@ +/* + * 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 bisq.core.dao.governance.period; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.GenesisTxInfo; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; + +import com.google.inject.Inject; + +import com.google.common.collect.ImmutableList; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CycleService implements DaoStateListener, DaoSetupService { + private final DaoStateService daoStateService; + private final int genesisBlockHeight; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public CycleService(DaoStateService daoStateService, + GenesisTxInfo genesisTxInfo) { + this.daoStateService = daoStateService; + this.genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + addFirstCycle(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + maybeCreateNewCycle(blockHeight, daoStateService.getCycles()).ifPresent(daoStateService::addCycle); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addFirstCycle() { + daoStateService.addCycle(getFirstCycle()); + } + + public int getCycleIndex(Cycle cycle) { + Optional previousCycle = getCycle(cycle.getHeightOfFirstBlock() - 1, daoStateService.getCycles()); + return previousCycle.map(cycle1 -> getCycleIndex(cycle1) + 1).orElse(0); + } + + public boolean isTxInCycle(Cycle cycle, String txId) { + return daoStateService.getTx(txId).filter(tx -> isBlockHeightInCycle(tx.getBlockHeight(), cycle)).isPresent(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isBlockHeightInCycle(int blockHeight, Cycle cycle) { + return blockHeight >= cycle.getHeightOfFirstBlock() && + blockHeight <= cycle.getHeightOfLastBlock(); + } + + private Optional maybeCreateNewCycle(int blockHeight, LinkedList cycles) { + // We want to set the correct phase and cycle before we start parsing a new block. + // For Genesis block we did it already in the start method. + // We copy over the phases from the current block as we get the phase only set in + // applyParamToPhasesInCycle if there was a changeEvent. + // The isFirstBlockInCycle methods returns from the previous cycle the first block as we have not + // applied the new cycle yet. But the first block of the old cycle will always be the same as the + // first block of the new cycle. + Cycle cycle = null; + if (blockHeight > genesisBlockHeight && !cycles.isEmpty() && isFirstBlockAfterPreviousCycle(blockHeight, cycles)) { + // We have the not update daoStateService.getCurrentCycle() so we grab here the previousCycle + Cycle previousCycle = cycles.getLast(); + // We create the new cycle as clone of the previous cycle and only if there have been change events we use + // the new values from the change event. + cycle = createNewCycle(blockHeight, previousCycle); + } + return Optional.ofNullable(cycle); + } + + + private Cycle getFirstCycle() { + // We want to have the initial data set up before the genesis tx gets parsed so we do it here in the constructor + // as onAllServicesInitialized might get called after the parser has started. + // We add the default values from the Param enum to our StateChangeEvent list. + List daoPhasesWithDefaultDuration = Arrays.stream(DaoPhase.Phase.values()) + .map(this::getPhaseWithDefaultDuration) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + return new Cycle(genesisBlockHeight, ImmutableList.copyOf(daoPhasesWithDefaultDuration)); + } + + private Cycle createNewCycle(int blockHeight, Cycle previousCycle) { + List daoPhaseList = previousCycle.getDaoPhaseList().stream() + .map(daoPhase -> { + DaoPhase.Phase phase = daoPhase.getPhase(); + try { + Param param = Param.valueOf("PHASE_" + phase.name()); + int value = daoStateService.getParamValueAsBlock(param, blockHeight); + return new DaoPhase(phase, value); + } catch (Throwable ignore) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return new Cycle(blockHeight, ImmutableList.copyOf(daoPhaseList)); + } + + private boolean isFirstBlockAfterPreviousCycle(int height, LinkedList cycles) { + int previousBlockHeight = height - 1; + Optional previousCycle = getCycle(previousBlockHeight, cycles); + return previousCycle + .filter(cycle -> cycle.getHeightOfLastBlock() + 1 == height) + .isPresent(); + } + + private Optional getPhaseWithDefaultDuration(DaoPhase.Phase phase) { + return Arrays.stream(Param.values()) + .filter(param -> isParamMatchingPhase(param, phase)) + .map(param -> new DaoPhase(phase, Integer.parseInt(param.getDefaultValue()))) + .findAny(); // We will always have a default value defined + } + + private boolean isParamMatchingPhase(Param param, DaoPhase.Phase phase) { + return param.name().contains("PHASE_") && param.name().replace("PHASE_", "").equals(phase.name()); + } + + private Optional getCycle(int height, LinkedList cycles) { + return cycles.stream() + .filter(cycle -> cycle.getHeightOfFirstBlock() <= height) + .filter(cycle -> cycle.getHeightOfLastBlock() >= height) + .findAny(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java b/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java new file mode 100644 index 0000000000..dfe18cb07a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java @@ -0,0 +1,167 @@ +/* + * 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 bisq.core.dao.governance.period; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; + +import com.google.inject.Inject; + +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class PeriodService { + private final DaoStateService daoStateService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PeriodService(DaoStateService daoStateService) { + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getCycles() { + return daoStateService.getCycles(); + } + + @Nullable + public Cycle getCurrentCycle() { + return daoStateService.getCurrentCycle(); + } + + public int getChainHeight() { + return daoStateService.getChainHeight(); + } + + private Optional getOptionalTx(String txId) { + return daoStateService.getTx(txId); + } + + public DaoPhase.Phase getCurrentPhase() { + return getCurrentCycle() != null ? + getCurrentCycle().getPhaseForHeight(this.getChainHeight()).orElse(DaoPhase.Phase.UNDEFINED) : + DaoPhase.Phase.UNDEFINED; + } + + public boolean isFirstBlockInCycle(int height) { + return getCycle(height) + .filter(cycle -> cycle.getHeightOfFirstBlock() == height) + .isPresent(); + } + + public boolean isLastBlockInCycle(int height) { + return getCycle(height) + .filter(cycle -> cycle.getHeightOfLastBlock() == height) + .isPresent(); + } + + public Optional getCycle(int height) { + return daoStateService.getCycle(height); + } + + public boolean isInPhase(int height, DaoPhase.Phase phase) { + return getCycle(height) + .filter(cycle -> cycle.isInPhase(height, phase)) + .isPresent(); + } + + public boolean isTxInPhase(String txId, DaoPhase.Phase phase) { + return getOptionalTx(txId) + .filter(tx -> isInPhase(tx.getBlockHeight(), phase)) + .isPresent(); + } + + public boolean isTxInPhaseAndCycle(String txId, DaoPhase.Phase phase, int currentChainHeadHeight) { + return isTxInPhase(txId, phase) && isTxInCorrectCycle(txId, currentChainHeadHeight); + } + + public DaoPhase.Phase getPhaseForHeight(int height) { + return getCycle(height) + .flatMap(cycle -> cycle.getPhaseForHeight(height)) + .orElse(DaoPhase.Phase.UNDEFINED); + } + + public boolean isTxInCorrectCycle(int txHeight, int currentChainHeadHeight) { + return getCycle(txHeight) + .filter(cycle -> currentChainHeadHeight >= cycle.getHeightOfFirstBlock()) + .filter(cycle -> currentChainHeadHeight <= cycle.getHeightOfLastBlock()) + .isPresent(); + } + + public boolean isTxInCorrectCycle(String txId, int currentChainHeadHeight) { + return getOptionalTx(txId) + .filter(tx -> isTxInCorrectCycle(tx.getBlockHeight(), currentChainHeadHeight)) + .isPresent(); + } + + private boolean isTxInPastCycle(int txHeight, int currentChainHeadHeight) { + return getCycle(txHeight) + .filter(cycle -> currentChainHeadHeight > cycle.getHeightOfLastBlock()) + .isPresent(); + } + + public int getDurationForPhase(DaoPhase.Phase phase, int height) { + return getCycle(height) + .map(cycle -> cycle.getDurationOfPhase(phase)) + .orElse(0); + } + + public boolean isTxInPastCycle(String txId, int chainHeight) { + return getOptionalTx(txId) + .filter(tx -> isTxInPastCycle(tx.getBlockHeight(), chainHeight)) + .isPresent(); + } + + public int getFirstBlockOfPhase(int height, DaoPhase.Phase phase) { + return getCycle(height) + .map(cycle -> cycle.getFirstBlockOfPhase(phase)) + .orElse(0); + } + + public boolean isFirstBlockInCycle() { + final int chainHeight = getChainHeight(); + return getFirstBlockOfPhase(chainHeight, DaoPhase.Phase.PROPOSAL) == chainHeight; + } + + public int getLastBlockOfPhase(int height, DaoPhase.Phase phase) { + return getCycle(height) + .map(cycle -> cycle.getLastBlockOfPhase(phase)) + .orElse(0); + } + + public boolean isInPhaseButNotLastBlock(DaoPhase.Phase phase) { + final int chainHeight = getChainHeight(); + return isInPhase(chainHeight, phase) && + chainHeight != getLastBlockOfPhase(chainHeight, phase); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java new file mode 100644 index 0000000000..7902741a9c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java @@ -0,0 +1,96 @@ +/* + * 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 bisq.core.dao.governance.proofofburn; + +import bisq.common.crypto.Hash; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.common.base.Charsets; + +import java.util.Objects; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * MyProofOfBurn is persisted locally and holds the preImage and txId. + */ +@Immutable +@Value +@Slf4j +public final class MyProofOfBurn implements PersistablePayload, NetworkPayload { + private final String txId; + private final String preImage; + private final transient byte[] hash; // Not persisted as it is derived from preImage. Stored for caching purpose only. + + public MyProofOfBurn(String txId, String preImage) { + this.txId = txId; + this.preImage = preImage; + this.hash = Hash.getSha256Ripemd160hash(preImage.getBytes(Charsets.UTF_8)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.MyProofOfBurn toProtoMessage() { + return protobuf.MyProofOfBurn.newBuilder() + .setTxId(txId) + .setPreImage(preImage) + .build(); + } + + public static MyProofOfBurn fromProto(protobuf.MyProofOfBurn proto) { + return new MyProofOfBurn(proto.getTxId(), proto.getPreImage()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MyProofOfBurn)) return false; + if (!super.equals(o)) return false; + MyProofOfBurn that = (MyProofOfBurn) o; + return Objects.equals(txId, that.txId) && + Objects.equals(preImage, that.preImage); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txId, preImage); + } + + @Override + public String toString() { + return "MyProofOfBurn{" + + "\n txId='" + txId + '\'' + + ",\n preImage=" + preImage + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java new file mode 100644 index 0000000000..a1f55c2499 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.dao.governance.proofofburn; + +import bisq.common.proto.persistable.PersistableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * PersistableEnvelope wrapper for list of MyProofOfBurn objects. + */ +@EqualsAndHashCode(callSuper = true) +public class MyProofOfBurnList extends PersistableList { + + private MyProofOfBurnList(List list) { + super(list); + } + + MyProofOfBurnList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableEnvelope toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setMyProofOfBurnList(getBuilder()).build(); + } + + private protobuf.MyProofOfBurnList.Builder getBuilder() { + return protobuf.MyProofOfBurnList.newBuilder() + .addAllMyProofOfBurn(getList().stream() + .map(MyProofOfBurn::toProtoMessage) + .collect(Collectors.toList())); + } + + public static MyProofOfBurnList fromProto(protobuf.MyProofOfBurnList proto) { + return new MyProofOfBurnList(new ArrayList<>(proto.getMyProofOfBurnList().stream() + .map(MyProofOfBurn::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of txIds in MyProofOfBurnList: " + getList().stream() + .map(MyProofOfBurn::getTxId) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java new file mode 100644 index 0000000000..d61c6a93be --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.dao.governance.proofofburn; + +import bisq.core.dao.DaoSetupService; + +import bisq.common.app.DevEnv; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * Manages the persistence of MyProofOfBurn objects. + */ +@Slf4j +public class MyProofOfBurnListService implements PersistedDataHost, DaoSetupService { + + private final PersistenceManager persistenceManager; + private final MyProofOfBurnList myProofOfBurnList = new MyProofOfBurnList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyProofOfBurnListService(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + persistenceManager.initialize(myProofOfBurnList, PersistenceManager.Source.PRIVATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + myProofOfBurnList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addMyProofOfBurn(MyProofOfBurn myProofOfBurn) { + if (!myProofOfBurnList.contains(myProofOfBurn)) { + myProofOfBurnList.add(myProofOfBurn); + requestPersistence(); + } + } + + public List getMyProofOfBurnList() { + return myProofOfBurnList.getList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java new file mode 100644 index 0000000000..490a2cefc8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.core.dao.governance.proofofburn; + +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.crypto.Hash; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProofOfBurnConsensus { + public static byte[] getHash(byte[] bytes) { + return Hash.getSha256Ripemd160hash(bytes); + } + + public static byte[] getOpReturnData(byte[] hash) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.PROOF_OF_BURN.getType()); + outputStream.write(Version.PROOF_OF_BURN); + outputStream.write(hash); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + return new byte[0]; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } + + public static byte[] getHashFromOpReturnData(byte[] opReturnData) { + return Arrays.copyOfRange(opReturnData, 2, 22); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java new file mode 100644 index 0000000000..47125ad155 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java @@ -0,0 +1,240 @@ +/* + * 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 bisq.core.dao.governance.proofofburn; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import com.google.common.base.Charsets; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import java.security.SignatureException; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.Utils.HEX; + +@Slf4j +public class ProofOfBurnService implements DaoSetupService, DaoStateListener { + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + private final MyProofOfBurnListService myProofOfBurnListService; + private final DaoStateService daoStateService; + + @Getter + private IntegerProperty updateFlag = new SimpleIntegerProperty(0); + @Getter + private final List proofOfBurnTxList = new ArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProofOfBurnService(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + MyProofOfBurnListService myProofOfBurnListService, + DaoStateService daoStateService) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.myProofOfBurnListService = myProofOfBurnListService; + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + } + + private void updateList() { + proofOfBurnTxList.clear(); + proofOfBurnTxList.addAll(getAllProofOfBurnTxs()); + + updateFlag.set(updateFlag.get() + 1); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + updateList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction burn(String preImageAsString, long amount) throws InsufficientMoneyException, TxException { + try { + // We create a prepared Bsq Tx for the burn amount + Transaction preparedBurnFeeTx = bsqWalletService.getPreparedProofOfBurnTx(Coin.valueOf(amount)); + byte[] hash = getHashFromPreImage(preImageAsString); + byte[] opReturnData = ProofOfBurnConsensus.getOpReturnData(hash); + // We add the BTC inputs for the miner fee. + Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); + // We sign the BSQ inputs of the final tx. + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Proof of burn tx: " + transaction); + return transaction; + } catch (WalletException | TransactionVerificationException e) { + throw new TxException(e); + } + } + + public void publishTransaction(Transaction transaction, String preImageAsString, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + walletsManager.publishAndCommitBsqTx(transaction, TxType.PROOF_OF_BURN, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("Proof of burn tx has been published. TxId={}", transaction.getTxId().toString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } + }); + + MyProofOfBurn myProofOfBurn = new MyProofOfBurn(transaction.getTxId().toString(), preImageAsString); + myProofOfBurnListService.addMyProofOfBurn(myProofOfBurn); + } + + public byte[] getHashFromOpReturnData(Tx tx) { + return ProofOfBurnConsensus.getHashFromOpReturnData(tx.getLastTxOutput().getOpReturnData()); + } + + public String getHashAsString(String preImageAsString) { + return Utilities.bytesAsHexString(getHashFromPreImage(preImageAsString)); + } + + public Optional getTx(String txId) { + return daoStateService.getTx(txId); + } + + // Of connected output of first input. Used for signing and verification. + // Proofs ownership of the proof of burn tx. + public byte[] getPubKey(String txId) { + return daoStateService.getTx(txId) + .map(tx -> tx.getTxInputs().get(0)) + .map(e -> Utilities.decodeFromHex(e.getPubKey())) + .orElse(new byte[0]); + } + + public String getPubKeyAsHex(String proofOfBurnTxId) { + return Utilities.bytesAsHexString(getPubKey(proofOfBurnTxId)); + } + + public Optional sign(String proofOfBurnTxId, String message) { + byte[] pubKey = getPubKey(proofOfBurnTxId); + ECKey key = bsqWalletService.findKeyFromPubKey(pubKey); + if (key == null) + return Optional.empty(); + + try { + String signatureBase64 = bsqWalletService.isEncrypted() + ? key.signMessage(message, bsqWalletService.getAesKey()) + : key.signMessage(message); + return Optional.of(signatureBase64); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + return Optional.empty(); + } + } + + public void verify(String message, String pubKey, String signatureBase64) throws SignatureException { + ECKey key = ECKey.fromPublicOnly(HEX.decode(pubKey)); + checkNotNull(key, "ECKey must not be null"); + key.verifyMessage(message, signatureBase64); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private List getAllProofOfBurnTxs() { + return daoStateService.getProofOfBurnOpReturnTxOutputs().stream() + .map(txOutput -> daoStateService.getTx(txOutput.getTxId()).orElse(null)) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(BaseTx::getTime).reversed()) + .collect(Collectors.toList()); + } + + private byte[] getHashFromPreImage(String preImageAsString) { + byte[] preImage = preImageAsString.getBytes(Charsets.UTF_8); + return ProofOfBurnConsensus.getHash(preImage); + } + + public long getAmount(Tx tx) { + return tx.getBurntFee(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java new file mode 100644 index 0000000000..53c2967feb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Base class for proposalFactory classes. Provides creation of a transaction. Proposal creation is delegated to + * concrete classes. + */ +@Slf4j +public abstract class BaseProposalFactory { + protected final BsqWalletService bsqWalletService; + protected final BtcWalletService btcWalletService; + private final DaoStateService daoStateService; + private final ProposalValidator proposalValidator; + @Nullable + protected String name; + @Nullable + protected String link; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + protected BaseProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + ProposalValidator proposalValidator) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.daoStateService = daoStateService; + this.proposalValidator = proposalValidator; + } + + protected ProposalWithTransaction createProposalWithTransaction(String name, + String link) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.name = name; + this.link = link; + // As we don't know the txId yet we create a temp proposal with txId set to an empty string. + R proposal = createProposalWithoutTxId(); + proposalValidator.validateDataFields(proposal); + Transaction transaction = createTransaction(proposal); + Proposal proposalWithTxId = proposal.cloneProposalAndAddTxId(transaction.getTxId().toString()); + return new ProposalWithTransaction(proposalWithTxId, transaction); + } + + protected abstract R createProposalWithoutTxId(); + + // We have txId set to null in proposal as we cannot know it before the tx is created. + // Once the tx is known we will create a new object including the txId. + // The hashOfPayload used in the opReturnData is created with the txId set to null. + private Transaction createTransaction(R proposal) throws InsufficientMoneyException, TxException { + try { + Coin fee = ProposalConsensus.getFee(daoStateService, daoStateService.getChainHeight()); + // We create a prepared Bsq Tx for the proposal fee. + Transaction preparedBurnFeeTx = proposal instanceof IssuanceProposal ? + bsqWalletService.getPreparedIssuanceTx(fee) : + bsqWalletService.getPreparedProposalTx(fee); + + // payload does not have txId at that moment + byte[] hashOfPayload = ProposalConsensus.getHashOfPayload(proposal); + byte[] opReturnData = getOpReturnData(hashOfPayload); + + // We add the BTC inputs for the miner fee. + Transaction txWithBtcFee = completeTx(preparedBurnFeeTx, opReturnData, proposal); + + // We sign the BSQ inputs of the final tx. + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Proposal tx: " + transaction); + return transaction; + } catch (WalletException | TransactionVerificationException e) { + throw new TxException(e); + } + } + + protected byte[] getOpReturnData(byte[] hashOfPayload) { + return ProposalConsensus.getOpReturnData(hashOfPayload, OpReturnType.PROPOSAL.getType(), Version.PROPOSAL); + } + + protected Transaction completeTx(Transaction preparedBurnFeeTx, byte[] opReturnData, Proposal proposal) + throws WalletException, InsufficientMoneyException, TransactionVerificationException { + return btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/IssuanceProposal.java b/core/src/main/java/bisq/core/dao/governance/proposal/IssuanceProposal.java new file mode 100644 index 0000000000..a466b99965 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/IssuanceProposal.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import org.bitcoinj.core.Coin; + +/** + * Marker interface for proposals which can lead to new BSQ issuance + */ +public interface IssuanceProposal { + Coin getRequestedBsq(); + + String getBsqAddress(); + + String getTxId(); +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java new file mode 100644 index 0000000000..7ec120d719 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.common.proto.persistable.PersistableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * PersistableEnvelope wrapper for list of proposals. Used in vote consensus, so changes can break consensus! + */ +@EqualsAndHashCode(callSuper = true) +public class MyProposalList extends PersistableList implements ConsensusCritical { + + public MyProposalList(List list) { + super(list); + } + + MyProposalList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableEnvelope toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setMyProposalList(getBuilder()).build(); + } + + private protobuf.MyProposalList.Builder getBuilder() { + return protobuf.MyProposalList.newBuilder() + .addAllProposal(getList().stream() + .map(Proposal::toProtoMessage) + .collect(Collectors.toList())); + } + + public static MyProposalList fromProto(protobuf.MyProposalList proto) { + return new MyProposalList(new ArrayList<>(proto.getProposalList().stream() + .map(Proposal::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of TxId's in MyProposalList: " + getList().stream() + .map(Proposal::getTxId) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java new file mode 100644 index 0000000000..96ab94ed66 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java @@ -0,0 +1,250 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.P2PService; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import org.bitcoinj.core.Transaction; + +import com.google.inject.Inject; + +import javafx.beans.value.ChangeListener; + +import java.security.PublicKey; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import lombok.extern.slf4j.Slf4j; + +/** + * Publishes proposal tx and proposalPayload to p2p network. + * Allow removal of proposal if in proposal phase. + * Maintains myProposalList for own proposals. + * Triggers republishing of my proposals at startup. + */ +@Slf4j +public class MyProposalListService implements PersistedDataHost, DaoStateListener { + public interface Listener { + void onListChanged(List list); + } + + private final P2PService p2PService; + private final DaoStateService daoStateService; + private final PeriodService periodService; + private final WalletsManager walletsManager; + private final PersistenceManager persistenceManager; + private final PublicKey signaturePubKey; + + private final MyProposalList myProposalList = new MyProposalList(); + private final ChangeListener numConnectedPeersListener; + private final List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyProposalListService(P2PService p2PService, + DaoStateService daoStateService, + PeriodService periodService, + WalletsManager walletsManager, + PersistenceManager persistenceManager, + PubKeyRing pubKeyRing) { + this.p2PService = p2PService; + this.daoStateService = daoStateService; + this.periodService = periodService; + this.walletsManager = walletsManager; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(myProposalList, PersistenceManager.Source.PRIVATE); + + signaturePubKey = pubKeyRing.getSignaturePubKey(); + + numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishMyProposalsOnceWellConnected(); + daoStateService.addDaoStateListener(this); + p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + myProposalList.setAll(persisted.getList()); + listeners.forEach(l -> l.onListChanged(getList())); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockChainComplete() { + rePublishMyProposalsOnceWellConnected(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // Broadcast tx and publish proposal to P2P network + public void publishTxAndPayload(Proposal proposal, Transaction transaction, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + walletsManager.publishAndCommitBsqTx(transaction, proposal.getTxType(), new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("Proposal tx has been published. TxId={}", transaction.getTxId().toString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } + }); + + // We prefer to not wait for the tx broadcast as if the tx broadcast would fail we still prefer to have our + // proposal stored and broadcasted to the p2p network. The tx might get re-broadcasted at a restart and + // in worst case if it does not succeed the proposal will be ignored anyway. + // Inconsistently propagated payloads in the p2p network could have potentially worse effects. + addToP2PNetworkAsProtectedData(proposal, errorMessageHandler); + + // Add to list + if (!getList().contains(proposal)) { + myProposalList.add(proposal); + listeners.forEach(l -> l.onListChanged(getList())); + requestPersistence(); + } + } + + public boolean remove(Proposal proposal) { + if (canRemoveProposal(proposal, daoStateService, periodService)) { + boolean success = p2PService.removeData(new TempProposalPayload(proposal, signaturePubKey)); + if (!success) + log.warn("Removal of proposal from p2p network failed. proposal={}", proposal); + + if (myProposalList.remove(proposal)) { + listeners.forEach(l -> l.onListChanged(getList())); + requestPersistence(); + } else { + log.warn("We called remove at a proposal which was not in our list"); + } + return success; + } else { + final String msg = "remove called with a proposal which is outside of the proposal phase."; + DevEnv.logErrorAndThrowIfDevMode(msg); + return false; + } + } + + public boolean isMine(Proposal proposal) { + return getList().contains(proposal); + } + + public List getList() { + return myProposalList.getList(); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addToP2PNetworkAsProtectedData(Proposal proposal, ErrorMessageHandler errorMessageHandler) { + final boolean success = addToP2PNetworkAsProtectedData(proposal); + if (success) { + log.info("TempProposalPayload has been added to P2P network. ProposalTxId={}", proposal.getTxId()); + } else { + final String msg = "Adding of proposal to P2P network failed. proposal=" + proposal; + log.error(msg); + errorMessageHandler.handleErrorMessage(msg); + } + } + + private boolean addToP2PNetworkAsProtectedData(Proposal proposal) { + return p2PService.addProtectedStorageEntry(new TempProposalPayload(proposal, signaturePubKey)); + } + + private void rePublishMyProposalsOnceWellConnected() { + // We republish at each startup at any block during the cycle. We filter anyway for valid blind votes + // of that cycle so it is 1 blind vote getting rebroadcast at each startup to my neighbors. + int minPeers = Config.baseCurrencyNetwork().isMainnet() ? 4 : 1; + if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || + Config.baseCurrencyNetwork().isRegtest()) { + myProposalList.stream() + .filter(proposal -> periodService.isTxInPhaseAndCycle(proposal.getTxId(), + DaoPhase.Phase.PROPOSAL, + periodService.getChainHeight())) + .forEach(this::addToP2PNetworkAsProtectedData); + + // We delay removal of listener as we call that inside listener itself. + UserThread.execute(() -> p2PService.getNumConnectedPeers().removeListener(numConnectedPeersListener)); + } + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } + + private boolean canRemoveProposal(Proposal proposal, DaoStateService daoStateService, PeriodService periodService) { + boolean inProposalPhase = periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.PROPOSAL); + return isMine(proposal) && inProposalPhase; + + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalConsensus.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalConsensus.java new file mode 100644 index 0000000000..067086faf8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalConsensus.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.common.crypto.Hash; + +import org.bitcoinj.core.Coin; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +/** + * Encapsulates consensus critical aspects. + */ +@Slf4j +public class ProposalConsensus { + public static Coin getFee(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.PROPOSAL_FEE, chainHeight); + } + + public static byte[] getHashOfPayload(Proposal payload) { + final byte[] bytes = payload.toProtoMessage().toByteArray(); + return Hash.getSha256Ripemd160hash(bytes); + } + + public static byte[] getOpReturnData(byte[] hashOfPayload, byte opReturnType, byte version) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(opReturnType); + outputStream.write(version); + outputStream.write(hashOfPayload); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + return new byte[0]; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java new file mode 100644 index 0000000000..6e8209c7c2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java @@ -0,0 +1,164 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.common.UserThread; + +import org.bitcoinj.core.TransactionConfidence; + +import com.google.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Provides filtered observableLists of the Proposals from proposalService and myProposalListService. + * We want to show the own proposals in unconfirmed state (validation of phase and cycle cannot be done but as it is + * our own proposal that is not critical). Foreign proposals are only shown if confirmed and fully validated. + */ +@Slf4j +public class ProposalListPresentation implements DaoStateListener, MyProposalListService.Listener, DaoSetupService { + private final ProposalService proposalService; + private final DaoStateService daoStateService; + private final MyProposalListService myProposalListService; + private final BsqWalletService bsqWalletService; + private final ProposalValidatorProvider validatorProvider; + private final ObservableList allProposals = FXCollections.observableArrayList(); + @Getter + private final FilteredList activeOrMyUnconfirmedProposals = new FilteredList<>(allProposals); + private final ListChangeListener proposalListChangeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalListPresentation(ProposalService proposalService, + DaoStateService daoStateService, + MyProposalListService myProposalListService, + BsqWalletService bsqWalletService, + ProposalValidatorProvider validatorProvider) { + this.proposalService = proposalService; + this.daoStateService = daoStateService; + this.myProposalListService = myProposalListService; + this.bsqWalletService = bsqWalletService; + this.validatorProvider = validatorProvider; + + daoStateService.addDaoStateListener(this); + myProposalListService.addListener(this); + + proposalListChangeListener = c -> updateLists(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + // We must set the listeners initially and not on onParseBlockChainComplete as activeOrMyUnconfirmedProposals + // is used in voteResults which can be called earlier during sync. + // To avoid unneeded upDateLists calls we delay one render frame so that once the proposalService is complete we + // register out listeners. + UserThread.execute(() -> { + proposalService.getTempProposals().addListener(proposalListChangeListener); + proposalService.getProposalPayloads().addListener((ListChangeListener) c -> updateLists()); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + updateLists(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // MyProposalListService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onListChanged(List list) { + updateLists(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateLists() { + List tempProposals = proposalService.getTempProposals(); + Set verifiedProposals = proposalService.getProposalPayloads().stream() + .map(ProposalPayload::getProposal) + .filter(proposal -> !daoStateService.isParseBlockChainComplete() || + validatorProvider.getValidator(proposal).isValidAndConfirmed(proposal)) + .collect(Collectors.toSet()); + Set set = new HashSet<>(tempProposals); + set.addAll(verifiedProposals); + + // We want to show our own unconfirmed proposals. Unconfirmed proposals from other users are not included + // in the list. + // If a tx is not found in the daoStateService it can be that it is either unconfirmed or invalid. + // To avoid inclusion of invalid txs we add a check for the confidence type PENDING from the bsqWalletService. + // So we only add proposals if they are unconfirmed and therefore not yet parsed. Once confirmed they have to be + // found in the daoStateService. + List myUnconfirmedProposals = myProposalListService.getList().stream() + .filter(p -> !daoStateService.getTx(p.getTxId()).isPresent()) // Tx is still not in our bsq blocks + .filter(p -> { + TransactionConfidence confidenceForTxId = bsqWalletService.getConfidenceForTxId(p.getTxId()); + return confidenceForTxId != null && + confidenceForTxId.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING; + }) + .collect(Collectors.toList()); + set.addAll(myUnconfirmedProposals); + + allProposals.clear(); + allProposals.addAll(set); + + activeOrMyUnconfirmedProposals.setPredicate(proposal -> validatorProvider.getValidator(proposal).isValidAndConfirmed(proposal) || + myUnconfirmedProposals.contains(proposal)); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java new file mode 100644 index 0000000000..a50b7e2f35 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java @@ -0,0 +1,338 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStorageService; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalStorageService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; +import bisq.network.p2p.storage.persistence.ProtectedDataStoreService; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Maintains protectedStoreList and appendOnlyStoreList for received proposals. + * Republishes protectedStoreList to append-only data store when entering the break before the blind vote phase. + */ +@Slf4j +public class ProposalService implements HashMapChangedListener, AppendOnlyDataStoreListener, + DaoStateListener, DaoSetupService { + private final P2PService p2PService; + private final PeriodService periodService; + private final ProposalStorageService proposalStorageService; + private final DaoStateService daoStateService; + private final ProposalValidatorProvider validatorProvider; + + // Proposals we receive in the proposal phase. They can be removed in that phase. That list must not be used for + // consensus critical code. + @Getter + private final ObservableList tempProposals = FXCollections.observableArrayList(); + + // Proposals which got added to the append-only data store in the break before the blind vote phase. + // They cannot be removed anymore. This list is used for consensus critical code. Different nodes might have + // different data collections due the eventually consistency of the P2P network. + @Getter + private final ObservableList proposalPayloads = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalService(P2PService p2PService, + PeriodService periodService, + ProposalStorageService proposalStorageService, + TempProposalStorageService tempProposalStorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + ProtectedDataStoreService protectedDataStoreService, + DaoStateService daoStateService, + ProposalValidatorProvider validatorProvider, + @Named(Config.DAO_ACTIVATED) boolean daoActivated) { + this.p2PService = p2PService; + this.periodService = periodService; + this.proposalStorageService = proposalStorageService; + this.daoStateService = daoStateService; + this.validatorProvider = validatorProvider; + + if (daoActivated) { + // We add our stores to the global stores + appendOnlyDataStoreService.addService(proposalStorageService); + protectedDataStoreService.addService(tempProposalStorageService); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + // Listen for tempProposals + p2PService.addHashSetChangedListener(this); + // Listen for proposalPayloads + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(this); + } + + @Override + public void start() { + fillListFromProtectedStore(); + fillListFromAppendOnlyDataStore(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // HashMapChangedListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> { + onProtectedDataAdded(protectedStorageEntry, true); + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + onProtectedDataRemoved(protectedStorageEntries); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // AppendOnlyDataStoreListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAdded(PersistableNetworkPayload payload) { + onAppendOnlyDataAdded(payload, true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + // We try to broadcast at any block in the break1 phase. If we have received the data already we do not + // broadcast so we do not flood the network. + if (periodService.isInPhase(block.getHeight(), DaoPhase.Phase.BREAK1)) { + // We only republish if we are completed with parsing old blocks, otherwise we would republish old + // proposals all the time + maybePublishToAppendOnlyDataStore(); + fillListFromAppendOnlyDataStore(); + } + } + + @Override + public void onParseBlockChainComplete() { + // Fill the lists with the data we have collected in our stores. + fillListFromProtectedStore(); + fillListFromAppendOnlyDataStore(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getValidatedProposals() { + return proposalPayloads.stream() + .map(ProposalPayload::getProposal) + .filter(proposal -> validatorProvider.getValidator(proposal).isTxTypeValid(proposal)) + .collect(Collectors.toList()); + } + + public Coin getRequiredQuorum(Proposal proposal) { + int chainHeight = daoStateService.getTx(proposal.getTxId()) + .map(BaseTx::getBlockHeight). + orElse(daoStateService.getChainHeight()); + return daoStateService.getParamValueAsCoin(proposal.getQuorumParam(), chainHeight); + } + + public double getRequiredThreshold(Proposal proposal) { + int chainHeight = daoStateService.getTx(proposal.getTxId()) + .map(BaseTx::getBlockHeight). + orElse(daoStateService.getChainHeight()); + return daoStateService.getParamValueAsPercentDouble(proposal.getThresholdParam(), chainHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void fillListFromProtectedStore() { + p2PService.getDataMap().values().forEach(e -> onProtectedDataAdded(e, false)); + } + + private void fillListFromAppendOnlyDataStore() { + proposalStorageService.getMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); + } + + private void maybePublishToAppendOnlyDataStore() { + // We set reBroadcast to false to avoid to flood the network. + // If we have 20 proposals and 200 nodes with 10 neighbor peers we would send 40 000 messages if we would set + // reBroadcast to ! + tempProposals.stream() + .filter(proposal -> validatorProvider.getValidator(proposal).isValidAndConfirmed(proposal)) + .map(ProposalPayload::new) + .forEach(proposalPayload -> { + boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, false); + if (success) { + log.info("We published a ProposalPayload to the P2P network as append-only data. proposalTxId={}", + proposalPayload.getProposal().getTxId()); + } + // If we had data already we did not broadcast and success is false + }); + } + + private void onProtectedDataAdded(ProtectedStorageEntry entry, boolean fromBroadcastMessage) { + ProtectedStoragePayload protectedStoragePayload = entry.getProtectedStoragePayload(); + if (protectedStoragePayload instanceof TempProposalPayload) { + Proposal proposal = ((TempProposalPayload) protectedStoragePayload).getProposal(); + // We do not validate if we are in current cycle and if tx is confirmed yet as the tx might be not + // available/confirmed. + // We check if we are in the proposal or break1 phase. We are tolerant to accept tempProposals in the break1 + // phase to avoid risks that a proposal published very closely to the end of the proposal phase will not be + // sufficiently broadcast. + // When we receive tempProposals from the seed node at startup we only keep those which are in the current + // proposal/break1 phase if we are in that phase. We ignore tempProposals in case we are not in the + // proposal/break1 phase as they are not used anyway but the proposalPayloads will be relevant once we + // left the proposal/break1 phase. + if (periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.PROPOSAL) || + periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.BREAK1)) { + if (!tempProposals.contains(proposal)) { + // We only validate in case the blocks are parsed as otherwise some validators like param validator + // might fail as Dao state is not complete. + if (!daoStateService.isParseBlockChainComplete() || + validatorProvider.getValidator(proposal).areDataFieldsValid(proposal)) { + if (fromBroadcastMessage) { + log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}", + proposal.getTxId()); + } + tempProposals.add(proposal); + } else { + log.debug("We received an invalid proposal from the P2P network. Proposal={}, blockHeight={}", + proposal, daoStateService.getChainHeight()); + } + } + } + } + } + + private void onProtectedDataRemoved(Collection protectedStorageEntries) { + + // The listeners of tmpProposals can do large amounts of work that cause performance issues. Apply all of the + // updates at once using retainAll which will cause all listeners to be updated only once. + ArrayList tempProposalsWithUpdates = new ArrayList<>(tempProposals); + + protectedStorageEntries.forEach(protectedStorageEntry -> { + ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); + if (protectedStoragePayload instanceof TempProposalPayload) { + Proposal proposal = ((TempProposalPayload) protectedStoragePayload).getProposal(); + // We allow removal only if we are in the proposal phase. + boolean inPhase = periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.PROPOSAL); + boolean txInPastCycle = periodService.isTxInPastCycle(proposal.getTxId(), daoStateService.getChainHeight()); + Optional tx = daoStateService.getTx(proposal.getTxId()); + boolean unconfirmedOrNonBsqTx = !tx.isPresent(); + // if the tx is unconfirmed we need to be in the PROPOSAL phase, otherwise the tx must be confirmed. + if (inPhase || txInPastCycle || unconfirmedOrNonBsqTx) { + if (tempProposalsWithUpdates.contains(proposal)) { + tempProposalsWithUpdates.remove(proposal); + log.debug("We received a remove request for a TempProposalPayload and have removed the proposal " + + "from our list. proposal creation date={}, proposalTxId={}, inPhase={}, " + + "txInPastCycle={}, unconfirmedOrNonBsqTx={}", + proposal.getCreationDateAsDate(), proposal.getTxId(), inPhase, txInPastCycle, unconfirmedOrNonBsqTx); + } + } else { + log.warn("We received a remove request outside the PROPOSAL phase. " + + "Proposal creation date={}, proposal.txId={}, current blockHeight={}", + proposal.getCreationDateAsDate(), proposal.getTxId(), daoStateService.getChainHeight()); + } + } + }); + + tempProposals.retainAll(tempProposalsWithUpdates); + } + + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean fromBroadcastMessage) { + if (persistableNetworkPayload instanceof ProposalPayload) { + ProposalPayload proposalPayload = (ProposalPayload) persistableNetworkPayload; + if (!proposalPayloads.contains(proposalPayload)) { + Proposal proposal = proposalPayload.getProposal(); + + // We don't validate phase and cycle as we might receive proposals from other cycles or phases at startup. + // Beside that we might receive payloads we requested at the vote result phase in case we missed some + // payloads. We prefer here resilience over protection against late publishing attacks. + + // We only validate in case the blocks are parsed as otherwise some validators like param validator + // might fail as Dao state is not complete. + if (!daoStateService.isParseBlockChainComplete() || + validatorProvider.getValidator(proposal).areDataFieldsValid(proposal)) { + if (fromBroadcastMessage) { + log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}", + proposal.getTxId()); + } + proposalPayloads.add(proposalPayload); + } else { + log.warn("We received a invalid append-only proposal from the P2P network. " + + "Proposal={}, blockHeight={}", + proposal, daoStateService.getChainHeight()); + } + } + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalType.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalType.java new file mode 100644 index 0000000000..079f37b722 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalType.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.locale.Res; + +public enum ProposalType { + UNDEFINED, + COMPENSATION_REQUEST, + REIMBURSEMENT_REQUEST, + CHANGE_PARAM, + BONDED_ROLE, + CONFISCATE_BOND, + GENERIC, + REMOVE_ASSET; + + public String getDisplayName() { + return Res.get("dao.proposal.type." + name()); + } + + public String getShortDisplayName() { + return Res.get("dao.proposal.type.short." + name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidationException.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidationException.java new file mode 100644 index 0000000000..4e0e2065f5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidationException.java @@ -0,0 +1,57 @@ +package bisq.core.dao.governance.proposal; + +import bisq.core.dao.state.model.blockchain.Tx; + +import org.bitcoinj.core.Coin; + +import lombok.Getter; + +import javax.annotation.Nullable; + +@Getter +public class ProposalValidationException extends Exception { + @Nullable + private Coin requestedBsq; + @Nullable + private Coin minRequestAmount; + @Nullable + private Tx tx; + + public ProposalValidationException(String message, Coin requestedBsq, Coin minRequestAmount) { + super(message); + this.requestedBsq = requestedBsq; + this.minRequestAmount = minRequestAmount; + } + + public ProposalValidationException(Throwable cause) { + super(cause); + } + + public ProposalValidationException(String message) { + super(message); + } + + public ProposalValidationException(String message, Throwable throwable) { + super(message, throwable); + + } + + public ProposalValidationException(String message, Tx tx) { + super(message); + this.tx = tx; + } + + public ProposalValidationException(Throwable cause, Tx tx) { + super(cause); + this.tx = tx; + } + + @Override + public String toString() { + return "ProposalValidationException{" + + "\n requestedBsq=" + requestedBsq + + ",\n minRequestAmount=" + minRequestAmount + + ",\n tx=" + tx + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java new file mode 100644 index 0000000000..2f7d97a7f5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java @@ -0,0 +1,160 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.ReimbursementProposal; + +import bisq.common.util.ExtraDataMapValidator; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public abstract class ProposalValidator implements ConsensusCritical { + protected final DaoStateService daoStateService; + protected final PeriodService periodService; + + protected ProposalValidator(DaoStateService daoStateService, PeriodService periodService) { + this.daoStateService = daoStateService; + this.periodService = periodService; + } + + public boolean areDataFieldsValid(Proposal proposal) { + try { + validateDataFields(proposal); + return true; + } catch (ProposalValidationException e) { + log.warn("proposal data fields are invalid. proposal={}, error={}", proposal, e.toString()); + return false; + } + } + + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + notEmpty(proposal.getName(), "name must not be empty"); + notEmpty(proposal.getLink(), "link must not be empty"); + checkArgument(proposal.getName().length() <= 200, "Name must not exceed 200 chars"); + checkArgument(proposal.getLink().length() <= 200, "Link must not exceed 200 chars"); + if (proposal.getTxId() != null) + checkArgument(proposal.getTxId().length() == 64, "Tx ID must be 64 chars"); + + ExtraDataMapValidator.validate(proposal.getExtraDataMap()); + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } + + public boolean isValidOrUnconfirmed(Proposal proposal) { + return isValid(proposal, true); + } + + public boolean isValidAndConfirmed(Proposal proposal) { + return isValid(proposal, false); + } + + public boolean isTxTypeValid(Proposal proposal) { + String txId = proposal.getTxId(); + if (txId == null || txId.equals("")) { + log.warn("txId must be set. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + Optional optionalTxType = daoStateService.getOptionalTxType(txId); + boolean present = optionalTxType.filter(txType -> txType == proposal.getTxType()).isPresent(); + if (!present) + log.debug("optionalTxType not present for proposal {}" + proposal); + return present; + } + + private boolean isValid(Proposal proposal, boolean allowUnconfirmed) { + if (!areDataFieldsValid(proposal)) { + return false; + } + + String txId = proposal.getTxId(); + if (txId == null || txId.equals("")) { + log.warn("txId must be set. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + + Optional optionalTx = daoStateService.getTx(txId); + boolean isTxConfirmed = optionalTx.isPresent(); + int chainHeight = daoStateService.getChainHeight(); + + if (isTxConfirmed) { + int txHeight = optionalTx.get().getBlockHeight(); + if (!periodService.isTxInCorrectCycle(txHeight, chainHeight)) { + log.trace("Tx is not in current cycle. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + if (!periodService.isInPhase(txHeight, DaoPhase.Phase.PROPOSAL)) { + log.debug("Tx is not in PROPOSAL phase. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + if (proposal instanceof CompensationProposal) { + if (optionalTx.get().getTxType() != TxType.COMPENSATION_REQUEST) { + log.error("TxType is not a COMPENSATION_REQUEST. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + } else if (proposal instanceof ReimbursementProposal) { + if (optionalTx.get().getTxType() != TxType.REIMBURSEMENT_REQUEST) { + log.error("TxType is not a REIMBURSEMENT_REQUEST. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + } else { + if (optionalTx.get().getTxType() != TxType.PROPOSAL) { + log.error("TxType is not PROPOSAL. proposal.getTxId()={}", proposal.getTxId()); + return false; + } + } + + return true; + } else if (allowUnconfirmed) { + // We want to show own unconfirmed proposals in the active proposals list. + boolean inPhase = periodService.isInPhase(chainHeight, DaoPhase.Phase.PROPOSAL); + if (inPhase) + log.debug("proposal is unconfirmed and we are in proposal phase: txId={}", txId); + return inPhase; + } else { + return false; + } + } + + protected int getBlockHeight(Proposal proposal) { + // When we receive a temp proposal the tx is usually not confirmed so we cannot lookup the block height of + // the tx. We take the current block height in that case as it would be in the same cycle anyway. + return daoStateService.getTx(proposal.getTxId()) + .map(BaseTx::getBlockHeight) + .orElseGet(daoStateService::getChainHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidatorProvider.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidatorProvider.java new file mode 100644 index 0000000000..cd2ca7b3e4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidatorProvider.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.governance.proposal.compensation.CompensationValidator; +import bisq.core.dao.governance.proposal.confiscatebond.ConfiscateBondValidator; +import bisq.core.dao.governance.proposal.generic.GenericProposalValidator; +import bisq.core.dao.governance.proposal.param.ChangeParamValidator; +import bisq.core.dao.governance.proposal.reimbursement.ReimbursementValidator; +import bisq.core.dao.governance.proposal.removeAsset.RemoveAssetValidator; +import bisq.core.dao.governance.proposal.role.RoleValidator; +import bisq.core.dao.state.model.governance.Proposal; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProposalValidatorProvider { + private final CompensationValidator compensationValidator; + private final ConfiscateBondValidator confiscateBondValidator; + private final GenericProposalValidator genericProposalValidator; + private final ChangeParamValidator changeParamValidator; + private final ReimbursementValidator reimbursementValidator; + private final RemoveAssetValidator removeAssetValidator; + private final RoleValidator roleValidator; + + @Inject + public ProposalValidatorProvider(CompensationValidator compensationValidator, + ConfiscateBondValidator confiscateBondValidator, + GenericProposalValidator genericProposalValidator, + ChangeParamValidator changeParamValidator, + ReimbursementValidator reimbursementValidator, + RemoveAssetValidator removeAssetValidator, + RoleValidator roleValidator) { + this.compensationValidator = compensationValidator; + this.confiscateBondValidator = confiscateBondValidator; + this.genericProposalValidator = genericProposalValidator; + this.changeParamValidator = changeParamValidator; + this.reimbursementValidator = reimbursementValidator; + this.removeAssetValidator = removeAssetValidator; + this.roleValidator = roleValidator; + } + + public ProposalValidator getValidator(Proposal proposal) { + return getValidator(proposal.getType()); + } + + private ProposalValidator getValidator(ProposalType proposalType) { + switch (proposalType) { + case COMPENSATION_REQUEST: + return compensationValidator; + case REIMBURSEMENT_REQUEST: + return reimbursementValidator; + case CHANGE_PARAM: + return changeParamValidator; + case BONDED_ROLE: + return roleValidator; + case CONFISCATE_BOND: + return confiscateBondValidator; + case GENERIC: + return genericProposalValidator; + case REMOVE_ASSET: + return removeAssetValidator; + } + throw new RuntimeException("Proposal type " + proposalType.name() + " was not covered by switch case."); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalWithTransaction.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalWithTransaction.java new file mode 100644 index 0000000000..99d53336ad --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalWithTransaction.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.state.model.governance.Proposal; + +import org.bitcoinj.core.Transaction; + +import lombok.Value; + +@Value +public class ProposalWithTransaction { + private final Proposal proposal; + private final Transaction transaction; + + ProposalWithTransaction(Proposal proposal, Transaction transaction) { + this.proposal = proposal; + this.transaction = transaction; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/TxException.java b/core/src/main/java/bisq/core/dao/governance/proposal/TxException.java new file mode 100644 index 0000000000..3d1b2d3253 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/TxException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +public class TxException extends Exception { + public TxException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationConsensus.java b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationConsensus.java new file mode 100644 index 0000000000..ced36309e2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationConsensus.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.dao.governance.proposal.compensation; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CompensationConsensus { + public static Coin getMinCompensationRequestAmount(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.COMPENSATION_REQUEST_MIN_AMOUNT, chainHeight); + } + + public static Coin getMaxCompensationRequestAmount(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.COMPENSATION_REQUEST_MAX_AMOUNT, chainHeight); + } + +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationProposalFactory.java new file mode 100644 index 0000000000..bd307f55a0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationProposalFactory.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.dao.governance.proposal.compensation; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalConsensus; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates the CompensationProposal and the transaction. + */ +@Slf4j +public class CompensationProposalFactory extends BaseProposalFactory { + + private Coin requestedBsq; + private String bsqAddress; + + @Inject + public CompensationProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + CompensationValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, + String link, + Coin requestedBsq) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.requestedBsq = requestedBsq; + this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString(); + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected CompensationProposal createProposalWithoutTxId() { + return new CompensationProposal( + name, + link, + requestedBsq, + bsqAddress, + new HashMap<>()); + } + + @Override + protected byte[] getOpReturnData(byte[] hashOfPayload) { + return ProposalConsensus.getOpReturnData(hashOfPayload, + OpReturnType.COMPENSATION_REQUEST.getType(), + Version.COMPENSATION_REQUEST); + } + + @Override + protected Transaction completeTx(Transaction preparedBurnFeeTx, byte[] opReturnData, Proposal proposal) + throws WalletException, InsufficientMoneyException, TransactionVerificationException { + CompensationProposal compensationProposal = (CompensationProposal) proposal; + return btcWalletService.completePreparedCompensationRequestTx( + compensationProposal.getRequestedBsq(), + compensationProposal.getAddress(), + preparedBurnFeeTx, + opReturnData); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationValidator.java new file mode 100644 index 0000000000..e76f77fa02 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/compensation/CompensationValidator.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.governance.proposal.compensation; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.Proposal; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class CompensationValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public CompensationValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + + CompensationProposal compensationProposal = (CompensationProposal) proposal; + String bsqAddress = compensationProposal.getBsqAddress(); + notEmpty(bsqAddress, "bsqAddress must not be empty"); + checkArgument(bsqAddress.substring(0, 1).equals("B"), "bsqAddress must start with B"); + compensationProposal.getAddress(); // throws AddressFormatException if wrong address + + Coin requestedBsq = compensationProposal.getRequestedBsq(); + int chainHeight = getBlockHeight(proposal); + Coin maxCompensationRequestAmount = CompensationConsensus.getMaxCompensationRequestAmount(daoStateService, chainHeight); + checkArgument(requestedBsq.compareTo(maxCompensationRequestAmount) <= 0, + "Requested BSQ must not exceed " + (maxCompensationRequestAmount.value / 100L) + " BSQ"); + Coin minCompensationRequestAmount = CompensationConsensus.getMinCompensationRequestAmount(daoStateService, chainHeight); + checkArgument(requestedBsq.compareTo(minCompensationRequestAmount) >= 0, + "Requested BSQ must not be less than " + (minCompensationRequestAmount.value / 100L) + " BSQ"); + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondProposalFactory.java new file mode 100644 index 0000000000..8b4c2a2710 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondProposalFactory.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.governance.proposal.confiscatebond; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.ConfiscateBondProposal; + +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates ConfiscateBondProposal and transaction. + */ +@Slf4j +public class ConfiscateBondProposalFactory extends BaseProposalFactory { + private String lockupTxId; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ConfiscateBondProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + ConfiscateBondValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, + String link, + String lockupTxId) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.lockupTxId = lockupTxId; + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected ConfiscateBondProposal createProposalWithoutTxId() { + return new ConfiscateBondProposal( + name, + link, + lockupTxId, + new HashMap<>()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java new file mode 100644 index 0000000000..02124b7949 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.core.dao.governance.proposal.confiscatebond; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.ConfiscateBondProposal; +import bisq.core.dao.state.model.governance.Proposal; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class ConfiscateBondValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public ConfiscateBondValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + ConfiscateBondProposal confiscateBondProposal = (ConfiscateBondProposal) proposal; + notEmpty(confiscateBondProposal.getLockupTxId(), "LockupTxId must not be empty"); + checkArgument(confiscateBondProposal.getLockupTxId().length() == 64, "LockupTxId must be 64 chars"); + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalFactory.java new file mode 100644 index 0000000000..f824bb323c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalFactory.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.dao.governance.proposal.generic; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.GenericProposal; + +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates GenericProposal and transaction. + */ +@Slf4j +public class GenericProposalFactory extends BaseProposalFactory { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public GenericProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + GenericProposalValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, String link) + throws ProposalValidationException, InsufficientMoneyException, TxException { + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected GenericProposal createProposalWithoutTxId() { + return new GenericProposal(name, link, new HashMap<>()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalValidator.java new file mode 100644 index 0000000000..f9d38a6d63 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/generic/GenericProposalValidator.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.dao.governance.proposal.generic; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Proposal; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class GenericProposalValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public GenericProposalValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamInputValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamInputValidator.java new file mode 100644 index 0000000000..72ad23671a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamInputValidator.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.dao.governance.proposal.param; + +import bisq.core.dao.governance.param.Param; +import bisq.core.util.validation.InputValidator; + +public class ChangeParamInputValidator extends InputValidator { + private final Param param; + private final ChangeParamValidator changeParamValidator; + + public ChangeParamInputValidator(Param param, ChangeParamValidator changeParamValidator) { + this.changeParamValidator = changeParamValidator; + this.param = param; + } + + @Override + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + return validateParam(input); + } + + private ValidationResult validateParam(String input) { + try { + changeParamValidator.validateParamValue(param, input); + return new ValidationResult(true); + } catch (Throwable e) { + return new ValidationResult(false, e.getMessage()); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamProposalFactory.java new file mode 100644 index 0000000000..1935a53b66 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamProposalFactory.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.dao.governance.proposal.param; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.ChangeParamProposal; + +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates ChangeParamProposal and transaction. + */ +@Slf4j +public class ChangeParamProposalFactory extends BaseProposalFactory { + private Param param; + private String paramValue; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ChangeParamProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + ChangeParamValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, + String link, + Param param, + String paramValue) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.param = param; + this.paramValue = paramValue; + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected ChangeParamProposal createProposalWithoutTxId() { + return new ChangeParamProposal( + name, + link, + param, + paramValue, + new HashMap<>()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java new file mode 100644 index 0000000000..6aa2187eed --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java @@ -0,0 +1,322 @@ +/* + * 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 bisq.core.dao.governance.proposal.param; + +import bisq.core.btc.wallet.Restrictions; +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.ChangeParamProposal; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.validation.BtcAddressValidator; +import bisq.core.util.validation.InputValidator; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Changes here can potentially break consensus! + * + * We do not store the values as the actual data type (Coin, int, String) but as Strings. So we need to convert it to the + * expected data type even if we get the data not from user input. + */ +@Slf4j +public class ChangeParamValidator extends ProposalValidator implements ConsensusCritical { + + private final BsqFormatter bsqFormatter; + + @Inject + public ChangeParamValidator(DaoStateService daoStateService, PeriodService periodService, BsqFormatter bsqFormatter) { + super(daoStateService, periodService); + this.bsqFormatter = bsqFormatter; + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + + // Only once parsing is complete we can check for param changes + if (daoStateService.isParseBlockChainComplete()) { + ChangeParamProposal changeParamProposal = (ChangeParamProposal) proposal; + validateParamValue(changeParamProposal.getParam(), changeParamProposal.getParamValue(), getBlockHeight(proposal)); + checkArgument(changeParamProposal.getParamValue().length() <= 200, "ParamValue must not exceed 200 chars"); + } + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } + + public void validateParamValue(Param param, String inputValue) throws ParamValidationException { + int blockHeight = periodService.getChainHeight(); + validateParamValue(param, inputValue, blockHeight); + } + + private void validateParamValue(Param param, String inputValue, int blockHeight) throws ParamValidationException { + String currentParamValue = daoStateService.getParamValue(param, blockHeight); + validateParamValue(param, currentParamValue, inputValue); + } + + private void validateParamValue(Param param, String currentParamValue, String inputValue) throws ParamValidationException { + try { + Coin currentParamValueAsCoin, inputValueAsCoin; + switch (param.getParamType()) { + case UNDEFINED: + break; + case BSQ: + currentParamValueAsCoin = daoStateService.getParamValueAsCoin(param, currentParamValue); + inputValueAsCoin = daoStateService.getParamValueAsCoin(param, inputValue); + validateBsqValue(currentParamValueAsCoin, inputValueAsCoin, param); + break; + case BTC: + currentParamValueAsCoin = daoStateService.getParamValueAsCoin(param, currentParamValue); + inputValueAsCoin = daoStateService.getParamValueAsCoin(param, inputValue); + validateBtcValue(currentParamValueAsCoin, inputValueAsCoin, param); + break; + case PERCENT: + double currentParamValueAsPercentDouble = daoStateService.getParamValueAsPercentDouble(currentParamValue); + double inputValueAsPercentDouble = daoStateService.getParamValueAsPercentDouble(inputValue); + validatePercentValue(currentParamValueAsPercentDouble, inputValueAsPercentDouble, param); + break; + case BLOCK: + int currentParamValueAsBlock = daoStateService.getParamValueAsBlock(currentParamValue); + int inputValueAsBlock = daoStateService.getParamValueAsBlock(inputValue); + validateBlockValue(currentParamValueAsBlock, inputValueAsBlock, param); + break; + case ADDRESS: + validateAddressValue(currentParamValue, inputValue); + break; + default: + log.warn("Param type {} not handled in switch case at validateParamValue", param.getParamType()); + } + } catch (ParamValidationException e) { + throw e; + } catch (NumberFormatException t) { + throw new ParamValidationException(Res.get("validation.numberFormatException", t.getMessage().toLowerCase())); + } catch (Throwable t) { + throw new ParamValidationException(t); + } + } + + private void validateBsqValue(Coin currentParamValueAsCoin, Coin inputValueAsCoin, Param param) throws ParamValidationException { + switch (param) { + case DEFAULT_MAKER_FEE_BSQ: + case DEFAULT_TAKER_FEE_BSQ: + case MIN_MAKER_FEE_BSQ: + case MIN_TAKER_FEE_BSQ: + break; + case PROPOSAL_FEE: + case BLIND_VOTE_FEE: + break; + case COMPENSATION_REQUEST_MIN_AMOUNT: + case REIMBURSEMENT_MIN_AMOUNT: + case COMPENSATION_REQUEST_MAX_AMOUNT: + case REIMBURSEMENT_MAX_AMOUNT: + checkArgument(inputValueAsCoin.value >= Restrictions.getMinNonDustOutput().value, + Res.get("validation.amountBelowDust", Restrictions.getMinNonDustOutput().value)); + checkArgument(inputValueAsCoin.value <= 200000000, + Res.get("validation.inputTooLarge", "200 000 BSQ")); + break; + case QUORUM_COMP_REQUEST: + case QUORUM_REIMBURSEMENT: + case QUORUM_CHANGE_PARAM: + case QUORUM_ROLE: + case QUORUM_CONFISCATION: + case QUORUM_GENERIC: + case QUORUM_REMOVE_ASSET: + checkArgument(inputValueAsCoin.value > 100000, + Res.get("validation.inputTooSmall", "1000 BSQ")); + break; + case ASSET_LISTING_FEE_PER_DAY: + break; + case BONDED_ROLE_FACTOR: + checkArgument(inputValueAsCoin.value > 100, + Res.get("validation.inputTooSmall", "1 BSQ")); + break; + } + checkArgument(inputValueAsCoin.isPositive(), Res.get("validation.inputTooSmall", "0 BSQ")); + validationChange((double) currentParamValueAsCoin.value, (double) inputValueAsCoin.value, param); + } + + private void validateBtcValue(Coin currentParamValueAsCoin, Coin inputValueAsCoin, Param param) throws ParamValidationException { + switch (param) { + case DEFAULT_MAKER_FEE_BTC: + case DEFAULT_TAKER_FEE_BTC: + case MIN_MAKER_FEE_BTC: + case MIN_TAKER_FEE_BTC: + checkArgument(inputValueAsCoin.value >= Restrictions.getMinNonDustOutput().value, + Res.get("validation.amountBelowDust", Restrictions.getMinNonDustOutput().value)); + break; + case ASSET_MIN_VOLUME: + case MAX_TRADE_LIMIT: + checkArgument(inputValueAsCoin.isPositive(), Res.get("validation.inputTooSmall", "0")); + break; + } + validationChange((double) currentParamValueAsCoin.value, (double) inputValueAsCoin.value, param); + } + + private void validatePercentValue(double currentParamValueAsPercentDouble, double inputValueAsPercentDouble, Param param) throws ParamValidationException { + switch (param) { + case THRESHOLD_COMP_REQUEST: + case THRESHOLD_REIMBURSEMENT: + case THRESHOLD_CHANGE_PARAM: + case THRESHOLD_ROLE: + case THRESHOLD_CONFISCATION: + case THRESHOLD_GENERIC: + case THRESHOLD_REMOVE_ASSET: + // We show only 2 decimals in the UI for % value + checkArgument(inputValueAsPercentDouble >= 0.5001, + Res.get("validation.inputTooSmall", "50%")); + checkArgument(inputValueAsPercentDouble <= 1, + Res.get("validation.inputTooLarge", "100%")); + break; + case ARBITRATOR_FEE: + checkArgument(inputValueAsPercentDouble >= 0, Res.get("validation.mustNotBeNegative")); + break; + } + validationChange(currentParamValueAsPercentDouble, inputValueAsPercentDouble, param); + } + + private void validateBlockValue(int currentParamValueAsBlock, int inputValueAsBlock, Param param) throws ParamValidationException { + boolean isMainnet = Config.baseCurrencyNetwork().isMainnet(); + switch (param) { + case LOCK_TIME_TRADE_PAYOUT: + break; + case PHASE_UNDEFINED: + break; + case PHASE_PROPOSAL: + case PHASE_BREAK1: + case PHASE_BLIND_VOTE: + case PHASE_BREAK2: + case PHASE_VOTE_REVEAL: + case PHASE_BREAK3: + if (isMainnet) + checkArgument(inputValueAsBlock >= 6, Res.get("validation.inputToBeAtLeast", "6 blocks")); + break; + case PHASE_RESULT: + if (isMainnet) + checkArgument(inputValueAsBlock >= 1, Res.get("validation.inputToBeAtLeast", "1 block")); + break; + } + + validationChange((double) currentParamValueAsBlock, (double) inputValueAsBlock, param); + // We allow 0 values (e.g. time lock for trade) + checkArgument(inputValueAsBlock >= 0, Res.get("validation.mustNotBeNegative")); + + } + + private void validateAddressValue(String currentParamValue, String inputValue) throws ParamValidationException { + checkArgument(!inputValue.equals(currentParamValue), Res.get("validation.mustBeDifferent")); + InputValidator.ValidationResult validationResult = new BtcAddressValidator().validate(inputValue); + if (!validationResult.isValid) + throw new ParamValidationException(validationResult.errorMessage); + } + + private void validationChange(double currentParamValue, double inputValue, Param param) throws ParamValidationException { + validationChange(currentParamValue, + inputValue, + param.getMaxDecrease(), + param.getMaxIncrease(), + param); + } + + /** + * @param currentValue Current value + * @param newValue New value + * @param min Decrease of param value limited to current value / maxDecrease. If 0 we don't apply the check and any change is possible + * @param max Increase of param value limited to current value * maxIncrease. If 0 we don't apply the check and any change is possible + * @param param + */ + @VisibleForTesting + void validationChange(double currentValue, double newValue, double min, double max, Param param) throws ParamValidationException { + // No need for translation as it would be a developer error to use such min/max values + checkArgument(min >= 0, "Min must be >= 0"); + checkArgument(max >= 0, "Max must be >= 0"); + if (currentValue == newValue) { + throw new ParamValidationException(ParamValidationException.ERROR.SAME, Res.get("validation.mustBeDifferent")); + } + + if (max == 0) + return; + + //TODO some cases with min = 0 and max not 0 or the other way round are not correctly implemented yet. + // Not intended to be used that way anyway but should be fixed... + double change = currentValue != 0 ? newValue / currentValue : 0; + if (change > max) { + double val = currentValue * max; + String value = getFormattedValue(param, val); + throw new ParamValidationException(ParamValidationException.ERROR.TOO_HIGH, Res.get("validation.inputTooLarge", value)); + } + + if (min == 0) + return; + + // If min/max are > 0 and currentValue is 0 it cannot be changed. min/max must be 0 in such cases. + if (currentValue == 0) { + throw new ParamValidationException(ParamValidationException.ERROR.NO_CHANGE_POSSIBLE, Res.get("validation.cannotBeChanged")); + } + + if (change < (1 / min)) { + double val = currentValue / min; + String value = getFormattedValue(param, val); + throw new ParamValidationException(ParamValidationException.ERROR.TOO_LOW, Res.get("validation.inputToBeAtLeast", value)); + } + } + + private String getFormattedValue(Param param, double val) { + String value = String.valueOf(val); + switch (param.getParamType()) { + case UNDEFINED: + // Not used + break; + case BSQ: + value = bsqFormatter.formatBSQSatoshis((long) val); + break; + case BTC: + value = bsqFormatter.formatBTCSatoshis((long) val); + break; + case PERCENT: + value = String.valueOf(val * 100); + break; + case BLOCK: + value = String.valueOf(Math.round(val)); + break; + case ADDRESS: + // Not used here + break; + } + return bsqFormatter.formatParamValue(param, value); + + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/param/ParamValidationException.java b/core/src/main/java/bisq/core/dao/governance/proposal/param/ParamValidationException.java new file mode 100644 index 0000000000..31cb3ac079 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/param/ParamValidationException.java @@ -0,0 +1,41 @@ +package bisq.core.dao.governance.proposal.param; + +import lombok.Getter; + +import javax.annotation.Nullable; + +@Getter +public class +ParamValidationException extends Exception { + enum ERROR { + SAME, + NO_CHANGE_POSSIBLE, + TOO_LOW, + TOO_HIGH + } + + @Nullable + private ParamValidationException.ERROR error; + + + ParamValidationException(ParamValidationException.ERROR error, String errorMessage) { + super(errorMessage); + this.error = error; + } + + ParamValidationException(Throwable throwable) { + super(throwable.getMessage()); + initCause(throwable); + } + + ParamValidationException(String errorMessage) { + super(errorMessage); + } + + @Override + public String toString() { + return "ParamValidationException{" + + "\n error=" + error + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementConsensus.java b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementConsensus.java new file mode 100644 index 0000000000..2c7bce08e0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementConsensus.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.dao.governance.proposal.reimbursement; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ReimbursementConsensus { + public static Coin getMinReimbursementRequestAmount(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.REIMBURSEMENT_MIN_AMOUNT, chainHeight); + } + + public static Coin getMaxReimbursementRequestAmount(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.REIMBURSEMENT_MAX_AMOUNT, chainHeight); + } + +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementProposalFactory.java new file mode 100644 index 0000000000..994b9ef8c5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementProposalFactory.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.dao.governance.proposal.reimbursement; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalConsensus; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.ReimbursementProposal; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates the ReimbursementProposal and the transaction. + */ +@Slf4j +public class ReimbursementProposalFactory extends BaseProposalFactory { + + private Coin requestedBsq; + private String bsqAddress; + + @Inject + public ReimbursementProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + ReimbursementValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, + String link, + Coin requestedBsq) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.requestedBsq = requestedBsq; + this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString(); + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected ReimbursementProposal createProposalWithoutTxId() { + return new ReimbursementProposal( + name, + link, + requestedBsq, + bsqAddress, + new HashMap<>()); + } + + @Override + protected byte[] getOpReturnData(byte[] hashOfPayload) { + return ProposalConsensus.getOpReturnData(hashOfPayload, + OpReturnType.REIMBURSEMENT_REQUEST.getType(), + Version.REIMBURSEMENT_REQUEST); + } + + @Override + protected Transaction completeTx(Transaction preparedBurnFeeTx, byte[] opReturnData, Proposal proposal) + throws WalletException, InsufficientMoneyException, TransactionVerificationException { + ReimbursementProposal reimbursementProposal = (ReimbursementProposal) proposal; + return btcWalletService.completePreparedReimbursementRequestTx( + reimbursementProposal.getRequestedBsq(), + reimbursementProposal.getAddress(), + preparedBurnFeeTx, + opReturnData); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementValidator.java new file mode 100644 index 0000000000..4a80c99450 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/reimbursement/ReimbursementValidator.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.governance.proposal.reimbursement; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.ReimbursementProposal; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class ReimbursementValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public ReimbursementValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + + ReimbursementProposal reimbursementProposal = (ReimbursementProposal) proposal; + String bsqAddress = reimbursementProposal.getBsqAddress(); + notEmpty(bsqAddress, "bsqAddress must not be empty"); + checkArgument(bsqAddress.substring(0, 1).equals("B"), "bsqAddress must start with B"); + reimbursementProposal.getAddress(); // throws AddressFormatException if wrong address + + Coin requestedBsq = reimbursementProposal.getRequestedBsq(); + int chainHeight = getBlockHeight(proposal); + Coin maxReimbursementRequestAmount = ReimbursementConsensus.getMaxReimbursementRequestAmount(daoStateService, chainHeight); + checkArgument(requestedBsq.compareTo(maxReimbursementRequestAmount) <= 0, + "Requested BSQ must not exceed " + (maxReimbursementRequestAmount.value / 100L) + " BSQ"); + Coin minReimbursementRequestAmount = ReimbursementConsensus.getMinReimbursementRequestAmount(daoStateService, chainHeight); + checkArgument(requestedBsq.compareTo(minReimbursementRequestAmount) >= 0, + "Requested BSQ must not be less than " + (minReimbursementRequestAmount.value / 100L) + " BSQ"); + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetProposalFactory.java new file mode 100644 index 0000000000..865700a9e7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetProposalFactory.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.dao.governance.proposal.removeAsset; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; + +import bisq.asset.Asset; + +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates RemoveAssetProposal and transaction. + */ +@Slf4j +public class RemoveAssetProposalFactory extends BaseProposalFactory { + private Asset asset; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RemoveAssetProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + RemoveAssetValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(String name, + String link, + Asset asset) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.asset = asset; + + return super.createProposalWithTransaction(name, link); + } + + @Override + protected RemoveAssetProposal createProposalWithoutTxId() { + return new RemoveAssetProposal( + name, + link, + asset.getTickerSymbol(), + new HashMap<>()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java new file mode 100644 index 0000000000..156e1753c8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.dao.governance.proposal.removeAsset; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class RemoveAssetValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public RemoveAssetValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + + RemoveAssetProposal removeAssetProposal = (RemoveAssetProposal) proposal; + notEmpty(removeAssetProposal.getTickerSymbol(), "TickerSymbol must not be empty"); + + // We want to avoid that someone causes damage by inserting a super long string. Real ticker symbols + // are usually very short but we don't want to add additional restrictions here. + checkArgument(removeAssetProposal.getTickerSymbol().length() <= 100, "TickerSymbol must not exceed 100 chars"); + } catch (ProposalValidationException e) { + throw e; + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleProposalFactory.java new file mode 100644 index 0000000000..4211b3d661 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleProposalFactory.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.dao.governance.proposal.role; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.proposal.BaseProposalFactory; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; + +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * Creates RoleProposal and transaction. + */ +@Slf4j +public class RoleProposalFactory extends BaseProposalFactory { + private Role role; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RoleProposalFactory(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoStateService daoStateService, + RoleValidator proposalValidator) { + super(bsqWalletService, + btcWalletService, + daoStateService, + proposalValidator); + } + + public ProposalWithTransaction createProposalWithTransaction(Role role) + throws ProposalValidationException, InsufficientMoneyException, TxException { + this.role = role; + + return super.createProposalWithTransaction(role.getName(), role.getLink()); + } + + @Override + protected RoleProposal createProposalWithoutTxId() { + return new RoleProposal(role, new HashMap<>()); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleValidator.java new file mode 100644 index 0000000000..9f2ff334eb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/role/RoleValidator.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.core.dao.governance.proposal.role; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalValidator; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.RoleProposal; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static org.apache.commons.lang3.Validate.notNull; + +/** + * Changes here can potentially break consensus! + */ +@Slf4j +public class RoleValidator extends ProposalValidator implements ConsensusCritical { + + @Inject + public RoleValidator(DaoStateService daoStateService, PeriodService periodService) { + super(daoStateService, periodService); + } + + @Override + public void validateDataFields(Proposal proposal) throws ProposalValidationException { + try { + super.validateDataFields(proposal); + + RoleProposal roleProposal = (RoleProposal) proposal; + notNull(roleProposal.getRole(), "Bonded role must not be null"); + } catch (Throwable throwable) { + throw new ProposalValidationException(throwable); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalPayload.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalPayload.java new file mode 100644 index 0000000000..33386f82a9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalPayload.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.appendonly; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.crypto.Hash; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Wrapper for proposal to be stored in the append-only ProposalStore storage. + * Data size: with typical proposal about 272 bytes + */ +@Immutable +@Slf4j +@Value +public class ProposalPayload implements PersistableNetworkPayload, ConsensusCritical { + private final Proposal proposal; + protected final byte[] hash; // 20 byte + + public ProposalPayload(Proposal proposal) { + this(proposal, Hash.getRipemd160hash(proposal.toProtoMessage().toByteArray())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ProposalPayload(Proposal proposal, byte[] hash) { + this.proposal = proposal; + this.hash = hash; + } + + private protobuf.ProposalPayload.Builder getProposalBuilder() { + return protobuf.ProposalPayload.newBuilder() + .setProposal(proposal.toProtoMessage()) + .setHash(ByteString.copyFrom(hash)); + } + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + return protobuf.PersistableNetworkPayload.newBuilder() + .setProposalPayload(getProposalBuilder()) + .build(); + } + + public protobuf.ProposalPayload toProtoProposalPayload() { + return getProposalBuilder().build(); + } + + + public static ProposalPayload fromProto(protobuf.ProposalPayload proto) { + return new ProposalPayload(Proposal.fromProto(proto.getProposal()), + proto.getHash().toByteArray()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistableNetworkPayload + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean verifyHashSize() { + return hash.length == 20; + } + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public String toString() { + return "ProposalPayload{" + + "\n proposal=" + proposal + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStorageService.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStorageService.java new file mode 100644 index 0000000000..b6f2022201 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStorageService.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.appendonly; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.MapStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProposalStorageService extends MapStoreService { + private static final String FILE_NAME = "ProposalStore"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalStorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public Map getMap() { + return store.getMap(); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof ProposalPayload; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected ProposalStore createStore() { + return new ProposalStore(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStore.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStore.java new file mode 100644 index 0000000000..23c9b3732e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/appendonly/ProposalStore.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.appendonly; + +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class ProposalStore extends PersistableNetworkPayloadStore { + + ProposalStore() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ProposalStore(List list) { + super(list); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setProposalStore(getBuilder()) + .build(); + } + + private protobuf.ProposalStore.Builder getBuilder() { + final List protoList = map.values().stream() + .map(payload -> (ProposalPayload) payload) + .map(ProposalPayload::toProtoProposalPayload) + .collect(Collectors.toList()); + return protobuf.ProposalStore.newBuilder().addAllItems(protoList); + } + + public static ProposalStore fromProto(protobuf.ProposalStore proto) { + List list = proto.getItemsList().stream() + .map(ProposalPayload::fromProto).collect(Collectors.toList()); + return new ProposalStore(list); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java new file mode 100644 index 0000000000..71b2c3d798 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java @@ -0,0 +1,132 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.temp; + +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.crypto.Sig; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; + +import com.google.protobuf.ByteString; + +import java.security.PublicKey; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * TempProposalPayload is wrapper for proposal sent over wire as well as it gets persisted. + * Data size: about 1.245 bytes (pubKey makes it big) + */ +@Immutable +@Slf4j +@Getter +@EqualsAndHashCode +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class TempProposalPayload implements ProcessOncePersistableNetworkPayload, ProtectedStoragePayload, + ExpirablePayload, PersistablePayload { + // We keep data 2 months to be safe if we increase durations of cycle. Also give a bit more resilience in case + // of any issues with the append-only data store + public static final long TTL = TimeUnit.DAYS.toMillis(60); + + protected final Proposal proposal; + protected final byte[] ownerPubKeyEncoded; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + protected final Map extraDataMap; + + // Used just for caching. Don't persist. + private final transient PublicKey ownerPubKey; + + public TempProposalPayload(Proposal proposal, + PublicKey ownerPublicKey) { + this(proposal, Sig.getPublicKeyBytes(ownerPublicKey), null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TempProposalPayload(Proposal proposal, + byte[] ownerPubPubKeyEncoded, + @Nullable Map extraDataMap) { + this.proposal = proposal; + this.ownerPubKeyEncoded = ownerPubPubKeyEncoded; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + + ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyEncoded); + } + + private protobuf.TempProposalPayload.Builder getTempProposalPayloadBuilder() { + final protobuf.TempProposalPayload.Builder builder = protobuf.TempProposalPayload.newBuilder() + .setProposal(proposal.getProposalBuilder()) + .setOwnerPubKeyEncoded(ByteString.copyFrom(ownerPubKeyEncoded)); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return builder; + } + + @Override + public protobuf.StoragePayload toProtoMessage() { + return protobuf.StoragePayload.newBuilder().setTempProposalPayload(getTempProposalPayloadBuilder()).build(); + } + + public static TempProposalPayload fromProto(protobuf.TempProposalPayload proto) { + return new TempProposalPayload(Proposal.fromProto(proto.getProposal()), + proto.getOwnerPubKeyEncoded().toByteArray(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TempStoragePayload + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PublicKey getOwnerPubKey() { + return ownerPubKey; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ExpirablePayload + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java new file mode 100644 index 0000000000..b8af27c672 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.temp; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.persistence.MapStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TempProposalStorageService extends MapStoreService { + private static final String FILE_NAME = "TempProposalStore"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TempProposalStorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public Map getMap() { + return store.getMap(); + } + + @Override + public boolean canHandle(ProtectedStorageEntry entry) { + return entry.getProtectedStoragePayload() instanceof TempProposalPayload; + } + + @Override + protected void readFromResources(String postFix, Runnable completeHandler) { + // We do not have a resource file for that store, so we just call the readStore method instead. + readStore(persisted -> completeHandler.run()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected TempProposalStore createStore() { + return new TempProposalStore(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStore.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStore.java new file mode 100644 index 0000000000..6ff483eae2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStore.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.core.dao.governance.proposal.storage.temp; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.proto.network.NetworkProtoResolver; +import bisq.common.proto.persistable.PersistableEnvelope; + +import com.google.protobuf.Message; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class TempProposalStore implements PersistableEnvelope { + @Getter + private final Map map = new ConcurrentHashMap<>(); + + @Inject + TempProposalStore() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TempProposalStore(List list) { + list.forEach(entry -> map.put(P2PDataStorage.get32ByteHashAsByteArray(entry.getProtectedStoragePayload()), entry)); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setTempProposalStore(getBuilder()) + .build(); + } + + private protobuf.TempProposalStore.Builder getBuilder() { + final List protoList = map.values().stream() + .map(ProtectedStorageEntry::toProtectedStorageEntry) + .collect(Collectors.toList()); + return protobuf.TempProposalStore.newBuilder().addAllItems(protoList); + } + + public static TempProposalStore fromProto(protobuf.TempProposalStore proto, NetworkProtoResolver networkProtoResolver) { + List list = proto.getItemsList().stream() + .map(entry -> ProtectedStorageEntry.fromProto(entry, networkProtoResolver)) + .collect(Collectors.toList()); + return new TempProposalStore(list); + } + + public boolean containsKey(P2PDataStorage.ByteArray hash) { + return map.containsKey(hash); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java new file mode 100644 index 0000000000..6d5a5da517 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.core.dao.governance.voteresult; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.network.RepublishGovernanceDataHandler; +import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; + +import bisq.network.p2p.P2PService; + +import bisq.common.UserThread; + +import javax.inject.Inject; + +import javafx.collections.ObservableList; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MissingDataRequestService implements DaoSetupService { + private final RepublishGovernanceDataHandler republishGovernanceDataHandler; + private final BlindVoteListService blindVoteListService; + private final ProposalService proposalService; + private final P2PService p2PService; + private boolean reRepublishAllGovernanceDataDone; + + @Inject + public MissingDataRequestService(RepublishGovernanceDataHandler republishGovernanceDataHandler, + BlindVoteListService blindVoteListService, + ProposalService proposalService, + P2PService p2PService) { + this.republishGovernanceDataHandler = republishGovernanceDataHandler; + this.blindVoteListService = blindVoteListService; + this.proposalService = proposalService; + this.p2PService = p2PService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void sendRepublishRequest() { + republishGovernanceDataHandler.sendRepublishRequest(); + } + + // Can be triggered with shortcut ctrl+h, cmd+h or alt+h + public void reRepublishAllGovernanceData() { + // We only want to do it once in case we would get flooded with requests. + if (!reRepublishAllGovernanceDataDone) { + reRepublishAllGovernanceDataDone = true; + ObservableList proposalPayloads = proposalService.getProposalPayloads(); + proposalPayloads.forEach(proposalPayload -> { + // We want a random delay between 0.1 and 300 sec. depending on the number of items. + // We send all proposals including those from old cycles. + int delay = Math.max(100, Math.min(300_000, new Random().nextInt(proposalPayloads.size() * 1000))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); + String txId = proposalPayload.getProposal().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a proposalPayload to " + + "the P2P network as append only data. proposalTxId={}", txId); + } else { + log.error("Adding of proposalPayload to P2P network failed. proposalTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); + + ObservableList blindVotePayloads = blindVoteListService.getBlindVotePayloads(); + blindVotePayloads.forEach(blindVotePayload -> { + // We want a random delay between 0.1 and 300 sec. depending on the number of items. + // We send all blindVotes including those from old cycles. + int delay = Math.max(100, Math.min(300_000, new Random().nextInt(blindVotePayloads.size() * 1000))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); + String txId = blindVotePayload.getBlindVote().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a blindVotePayload to " + + "the P2P network as append only data. blindVoteTxId={}", txId); + } else { + log.error("Adding of blindVotePayload to P2P network failed. blindVoteTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java new file mode 100644 index 0000000000..672e4bf75f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java @@ -0,0 +1,157 @@ +/* + * 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 bisq.core.dao.governance.voteresult; + +import bisq.core.dao.governance.blindvote.VoteWithProposalTxIdList; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputType; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.common.crypto.Encryption; +import bisq.common.util.Utilities; + +import javax.crypto.SecretKey; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class VoteResultConsensus { + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 38; + } + + // Hash of the list of Blind votes is 20 bytes after version and type bytes + public static byte[] getHashOfBlindVoteList(byte[] opReturnData) { + return Arrays.copyOfRange(opReturnData, 2, 22); + } + + public static VoteWithProposalTxIdList decryptVotes(byte[] encryptedVotes, SecretKey secretKey) + throws VoteResultException.DecryptionException { + try { + byte[] decrypted = Encryption.decrypt(encryptedVotes, secretKey); + return VoteWithProposalTxIdList.getVoteWithProposalTxIdListFromBytes(decrypted); + } catch (Throwable t) { + throw new VoteResultException.DecryptionException(t); + } + } + + // We compare first by stake and in case we have multiple entries with same stake we use the + // hex encoded hashOfProposalList for comparison + @Nullable + public static byte[] getMajorityHash(List hashWithStakeList) + throws VoteResultException.ValidationException, VoteResultException.ConsensusException { + try { + checkArgument(!hashWithStakeList.isEmpty(), "hashWithStakeList must not be empty"); + } catch (Throwable t) { + throw new VoteResultException.ValidationException(t); + } + + hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed() + .thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash()))); + + // If there are conflicting data views (multiple hashes) we only consider the voting round as valid if + // the majority is a super majority with > 80%. + if (hashWithStakeList.size() > 1) { + long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum(); + long stakeOfFirst = hashWithStakeList.get(0).getStake(); + if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) { + log.warn("The winning data view has less then 80% of the " + + "total stake of all data views. We consider the voting cycle as invalid if the " + + "winning data view does not reach a super majority. hashWithStakeList={}", hashWithStakeList); + throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " + + "total stake of all data views. We consider the voting cycle as invalid if the " + + "winning data view does not reach a super majority."); + } + } + return hashWithStakeList.get(0).getHash(); + } + + // Key is stored after version and type bytes and list of Blind votes. It has 16 bytes + public static SecretKey getSecretKey(byte[] opReturnData) { + byte[] secretKeyAsBytes = Arrays.copyOfRange(opReturnData, 22, 38); + return Encryption.getSecretKeyFromBytes(secretKeyAsBytes); + } + + public static TxOutput getConnectedBlindVoteStakeOutput(Tx voteRevealTx, DaoStateService daoStateService) + throws VoteResultException.ValidationException { + try { + // We use the stake output of the blind vote tx as first input + TxInput stakeTxInput = voteRevealTx.getTxInputs().get(0); + Optional optionalBlindVoteStakeOutput = daoStateService.getConnectedTxOutput(stakeTxInput); + checkArgument(optionalBlindVoteStakeOutput.isPresent(), "blindVoteStakeOutput must be present"); + TxOutput blindVoteStakeOutput = optionalBlindVoteStakeOutput.get(); + if (blindVoteStakeOutput.getTxOutputType() != TxOutputType.BLIND_VOTE_LOCK_STAKE_OUTPUT) { + String message = "blindVoteStakeOutput must be of type BLIND_VOTE_LOCK_STAKE_OUTPUT but is " + + blindVoteStakeOutput.getTxOutputType(); + log.warn(message + ". VoteRevealTx=" + voteRevealTx); + throw new VoteResultException.ValidationException(message + ". VoteRevealTxId=" + voteRevealTx.getId()); + } + return blindVoteStakeOutput; + } catch (VoteResultException.ValidationException t) { + throw t; + } catch (Throwable t) { + throw new VoteResultException.ValidationException(t); + } + } + + public static void validateBlindVoteTx(String blindVoteTxId, DaoStateService daoStateService, + PeriodService periodService, int chainHeight) + throws VoteResultException.ValidationException { + try { + Optional optionalBlindVoteTx = daoStateService.getTx(blindVoteTxId); + + checkArgument(optionalBlindVoteTx.isPresent(), "blindVoteTx with txId " + + blindVoteTxId + " not found."); + + Tx blindVoteTx = optionalBlindVoteTx.get(); + Optional optionalTxType = daoStateService.getOptionalTxType(blindVoteTx.getId()); + + checkArgument(optionalTxType.isPresent(), "optionalTxType must be present" + + ". blindVoteTxId=" + blindVoteTx.getId()); + + checkArgument(optionalTxType.get() == TxType.BLIND_VOTE, + "blindVoteTx must have type BLIND_VOTE but is " + optionalTxType.get() + + ". blindVoteTxId=" + blindVoteTx.getId()); + + checkArgument(periodService.isTxInCorrectCycle(blindVoteTx.getBlockHeight(), chainHeight), + "blindVoteTx is not in correct cycle. blindVoteTx.getBlockHeight()=" + + blindVoteTx.getBlockHeight() + ". chainHeight=" + chainHeight + + ". blindVoteTxId=" + blindVoteTx.getId()); + + checkArgument(periodService.isInPhase(blindVoteTx.getBlockHeight(), DaoPhase.Phase.BLIND_VOTE), + "blindVoteTx is not in BLIND_VOTE phase. blindVoteTx.getBlockHeight()=" + + blindVoteTx.getBlockHeight() + ". blindVoteTxId=" + blindVoteTx.getId()); + } catch (Throwable t) { + throw new VoteResultException.ValidationException(t); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java new file mode 100644 index 0000000000..505c4e7655 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.core.dao.governance.voteresult; + +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.Cycle; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +public class VoteResultException extends Exception { + @Getter + private final int heightOfFirstBlockInCycle; + + VoteResultException(Cycle cycle, Throwable cause) { + super(cause); + this.heightOfFirstBlockInCycle = cycle.getHeightOfFirstBlock(); + } + + @Override + public String toString() { + return "VoteResultException{" + + "\n heightOfFirstBlockInCycle=" + heightOfFirstBlockInCycle + + "\n} " + super.toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static sub classes + /////////////////////////////////////////////////////////////////////////////////////////// + + @EqualsAndHashCode(callSuper = true) + public static class ConsensusException extends Exception { + + ConsensusException(String message) { + super(message); + } + + @Override + public String toString() { + return "ConsensusException{" + + "\n} " + super.toString(); + } + } + + @EqualsAndHashCode(callSuper = true) + public static class ValidationException extends Exception { + + ValidationException(Throwable cause) { + super("Validation of vote result failed.", cause); + } + + public ValidationException(String message) { + super(message); + } + + @Override + public String toString() { + return "VoteResultException{" + + "\n cause=" + getCause() + + "\n} " + super.toString(); + } + } + + @EqualsAndHashCode(callSuper = true) + static abstract class MissingDataException extends Exception { + private MissingDataException(String message) { + super(message); + } + } + + @EqualsAndHashCode(callSuper = true) + @Value + public static class MissingBallotException extends MissingDataException { + private List existingBallots; + private List proposalTxIdsOfMissingBallots; + + MissingBallotException(List existingBallots, List proposalTxIdsOfMissingBallots) { + super("Missing ballots. proposalTxIdsOfMissingBallots=" + proposalTxIdsOfMissingBallots); + this.existingBallots = existingBallots; + this.proposalTxIdsOfMissingBallots = proposalTxIdsOfMissingBallots; + } + } + + + @EqualsAndHashCode(callSuper = true) + @Value + public static class DecryptionException extends Exception { + public DecryptionException(Throwable cause) { + super(cause); + } + + @Override + public String toString() { + return "DecryptionException{" + + "\n} " + super.toString(); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java new file mode 100644 index 0000000000..ddf402d323 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -0,0 +1,821 @@ +/* + * 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 bisq.core.dao.governance.voteresult; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.VoteWithProposalTxId; +import bisq.core.dao.governance.blindvote.VoteWithProposalTxIdList; +import bisq.core.dao.governance.merit.MeritConsensus; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.IssuanceProposal; +import bisq.core.dao.governance.proposal.ProposalListPresentation; +import bisq.core.dao.governance.voteresult.issuance.IssuanceService; +import bisq.core.dao.governance.votereveal.VoteRevealConsensus; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.model.governance.ChangeParamProposal; +import bisq.core.dao.state.model.governance.ConfiscateBondProposal; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.MeritList; +import bisq.core.dao.state.model.governance.ParamChange; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.ProposalVoteResult; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.dao.state.model.governance.Vote; +import bisq.core.locale.CurrencyUtil; + +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.util.MathUtils; +import bisq.common.util.PermutationUtil; +import bisq.common.util.Utilities; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import javax.crypto.SecretKey; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Calculates the result of the voting at the VoteResult period. + * We take all data from the bitcoin domain and additionally the blindVote list which we received from the p2p network. + * Due to eventual consistency we use the hash of the data view of the voters (majority by merit+stake). If our local + * blindVote list contains the blindVotes used by the voters we can calculate the result, otherwise we need to request + * the missing blindVotes from the network. + */ +@Slf4j +public class VoteResultService implements DaoStateListener, DaoSetupService { + private final ProposalListPresentation proposalListPresentation; + private final DaoStateService daoStateService; + private final PeriodService periodService; + private final BallotListService ballotListService; + private final BlindVoteListService blindVoteListService; + private final IssuanceService issuanceService; + private final MissingDataRequestService missingDataRequestService; + @Getter + private final ObservableList voteResultExceptions = FXCollections.observableArrayList(); + @Getter + private Set invalidDecryptedBallotsWithMeritItems = new HashSet<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public VoteResultService(ProposalListPresentation proposalListPresentation, + DaoStateService daoStateService, + PeriodService periodService, + BallotListService ballotListService, + BlindVoteListService blindVoteListService, + IssuanceService issuanceService, + MissingDataRequestService missingDataRequestService) { + this.proposalListPresentation = proposalListPresentation; + this.daoStateService = daoStateService; + this.periodService = periodService; + this.ballotListService = ballotListService; + this.blindVoteListService = blindVoteListService; + this.issuanceService = issuanceService; + this.missingDataRequestService = missingDataRequestService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockComplete(Block block) { + maybeCalculateVoteResult(block.getHeight()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeCalculateVoteResult(int chainHeight) { + if (isInVoteResultPhase(chainHeight)) { + log.info("CalculateVoteResult at chainHeight={}", chainHeight); + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + long startTs = System.currentTimeMillis(); + + Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); + if (!decryptedBallotsWithMeritsSet.isEmpty()) { + // From the decryptedBallotsWithMeritsSet we create a map with the hash of the blind vote list as key and the + // aggregated stake as value (no merit as that is part of the P2P network data and might lead to inconsistency). + // That map is used for calculating the majority of the blind vote lists. + // There might be conflicting versions due the eventually consistency of the P2P network (if some blind + // vote payloads do not arrive at all voters) which would lead to consensus failure in the result calculation. + // To solve that problem we will only consider the blind votes valid which are matching the majority hash. + // If multiple data views would have the same stake we sort additionally by the hex value of the + // blind vote hash and use the first one in the sorted list as winner. + // A node which has a local blindVote list which does not match the winner data view will try + // permutations of his local list and if that does not succeed he need to recover it's + // local blindVote list by requesting the correct list from other peers. + Map stakeByHashOfBlindVoteListMap = getStakeByHashOfBlindVoteListMap(decryptedBallotsWithMeritsSet); + + try { + // Get majority hash + byte[] majorityBlindVoteListHash = calculateMajorityBlindVoteListHash(stakeByHashOfBlindVoteListMap); + + // Is our local list matching the majority data view? + Optional> optionalBlindVoteListMatchingMajorityHash = findBlindVoteListMatchingMajorityHash(majorityBlindVoteListHash); + if (optionalBlindVoteListMatchingMajorityHash.isPresent()) { + List blindVoteList = optionalBlindVoteListMatchingMajorityHash.get(); + log.debug("blindVoteListMatchingMajorityHash: {}", + blindVoteList.stream() + .map(e -> "blindVoteTxId=" + e.getTxId() + ", Stake=" + e.getStake()) + .collect(Collectors.toList())); + + Set blindVoteTxIdSet = blindVoteList.stream().map(BlindVote::getTxId).collect(Collectors.toSet()); + // We need to filter out result list according to the majority hash list + Set filteredDecryptedBallotsWithMeritsSet = decryptedBallotsWithMeritsSet.stream() + .filter(decryptedBallotsWithMerits -> { + boolean contains = blindVoteTxIdSet.contains(decryptedBallotsWithMerits.getBlindVoteTxId()); + if (!contains) { + invalidDecryptedBallotsWithMeritItems.add(decryptedBallotsWithMerits); + } + return contains; + }) + .collect(Collectors.toSet()); + + // Only if we have all blind vote payloads and know the right list matching the majority we add + // it to our state. Otherwise we are not in consensus with the network. + daoStateService.addDecryptedBallotsWithMeritsSet(filteredDecryptedBallotsWithMeritsSet); + + Set evaluatedProposals = getEvaluatedProposals(filteredDecryptedBallotsWithMeritsSet, chainHeight); + daoStateService.addEvaluatedProposalSet(evaluatedProposals); + Set acceptedEvaluatedProposals = getAcceptedEvaluatedProposals(evaluatedProposals); + applyAcceptedProposals(acceptedEvaluatedProposals, chainHeight); + log.info("processAllVoteResults completed"); + } else { + String msg = "We could not find a list which matches the majority so we cannot calculate the vote result. Please restart and resync the DAO state."; + log.warn(msg); + voteResultExceptions.add(new VoteResultException(currentCycle, new Exception(msg))); + } + } catch (Throwable e) { + log.warn(e.toString()); + log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); + e.printStackTrace(); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); + } + } else { + log.info("There have not been any votes in that cycle. chainHeight={}", chainHeight); + } + log.info("Evaluating vote result took {} ms", System.currentTimeMillis() - startTs); + } + } + + private Set getDecryptedBallotsWithMeritsSet(int chainHeight) { + // We want all voteRevealTxOutputs which are in current cycle we are processing. + return daoStateService.getVoteRevealOpReturnTxOutputs().stream() + .filter(txOutput -> periodService.isTxInCorrectCycle(txOutput.getTxId(), chainHeight)) + .filter(this::isInVoteRevealPhase) + .map(txOutputToDecryptedBallotsWithMerits(chainHeight)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private boolean isInVoteRevealPhase(TxOutput txOutput) { + String voteRevealTx = txOutput.getTxId(); + boolean txInPhase = periodService.isTxInPhase(voteRevealTx, DaoPhase.Phase.VOTE_REVEAL); + if (!txInPhase) + log.warn("We got a vote reveal tx with was not in the correct phase of that cycle. voteRevealTxId={}", voteRevealTx); + + return txInPhase; + } + + @NotNull + private Function txOutputToDecryptedBallotsWithMerits(int chainHeight) { + return voteRevealTxOutput -> { + String voteRevealTxId = voteRevealTxOutput.getTxId(); + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + try { + byte[] voteRevealOpReturnData = voteRevealTxOutput.getOpReturnData(); + Optional optionalVoteRevealTx = daoStateService.getTx(voteRevealTxId); + checkArgument(optionalVoteRevealTx.isPresent(), "optionalVoteRevealTx must be present. voteRevealTxId=" + voteRevealTxId); + Tx voteRevealTx = optionalVoteRevealTx.get(); + + // Here we use only blockchain tx data so far so we don't have risks with missing P2P network data. + // We work back from the voteRealTx to the blindVoteTx to calculate the majority hash. From that we + // will derive the blind vote list we will use for result calculation and as it was based on + // blockchain data it will be consistent for all peers independent on their P2P network data state. + TxOutput blindVoteStakeOutput = VoteResultConsensus.getConnectedBlindVoteStakeOutput(voteRevealTx, daoStateService); + String blindVoteTxId = blindVoteStakeOutput.getTxId(); + + // If we get a blind vote tx which was published too late we ignore it. + if (!periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight)) { + log.warn("We got a blind vote tx with was not in the correct phase and/or cycle. " + + "We ignore that vote reveal and blind vote tx. voteRevealTx={}, blindVoteTxId={}", + voteRevealTx, blindVoteTxId); + return null; + } + + VoteResultConsensus.validateBlindVoteTx(blindVoteTxId, daoStateService, periodService, chainHeight); + + byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(voteRevealOpReturnData); + long blindVoteStake = blindVoteStakeOutput.getValue(); + + List blindVoteList = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + Optional optionalBlindVote = blindVoteList.stream() + .filter(blindVote -> blindVote.getTxId().equals(blindVoteTxId)) + .findAny(); + if (optionalBlindVote.isPresent()) { + return getDecryptedBallotsWithMerits(voteRevealTxId, currentCycle, voteRevealOpReturnData, + blindVoteTxId, hashOfBlindVoteList, blindVoteStake, optionalBlindVote.get()); + } + + // We are missing P2P network data + return getEmptyDecryptedBallotsWithMerits(voteRevealTxId, blindVoteTxId, hashOfBlindVoteList, + blindVoteStake); + } catch (Throwable e) { + log.error("Could not create DecryptedBallotsWithMerits from voteRevealTxId {} because of " + + "exception: {}", voteRevealTxId, e.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); + return null; + } + }; + } + + @NotNull + private DecryptedBallotsWithMerits getEmptyDecryptedBallotsWithMerits( + String voteRevealTxId, String blindVoteTxId, byte[] hashOfBlindVoteList, long blindVoteStake) { + log.warn("We have a blindVoteTx but we do not have the corresponding blindVote payload.\n" + + "That can happen if the blindVote item was not properly broadcast. " + + "We still add it to our result collection because it might be relevant for the majority " + + "hash by stake calculation. blindVoteTxId={}", blindVoteTxId); + + missingDataRequestService.sendRepublishRequest(); + + // We prefer to use an empty list here instead a null or optional value to avoid that + // client code need to handle nullable or optional values. + BallotList emptyBallotList = new BallotList(new ArrayList<>()); + MeritList emptyMeritList = new MeritList(new ArrayList<>()); + log.debug("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, " + + "blindVoteStake={}, ballotList={}", + blindVoteTxId, voteRevealTxId, blindVoteStake, emptyBallotList); + return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, + blindVoteStake, emptyBallotList, emptyMeritList); + } + + @Nullable + private DecryptedBallotsWithMerits getDecryptedBallotsWithMerits( + String voteRevealTxId, Cycle currentCycle, byte[] voteRevealOpReturnData, String blindVoteTxId, + byte[] hashOfBlindVoteList, long blindVoteStake, BlindVote blindVote) + throws VoteResultException.MissingBallotException { + SecretKey secretKey = VoteResultConsensus.getSecretKey(voteRevealOpReturnData); + try { + VoteWithProposalTxIdList voteWithProposalTxIdList = VoteResultConsensus.decryptVotes(blindVote.getEncryptedVotes(), secretKey); + MeritList meritList = MeritConsensus.decryptMeritList(blindVote.getEncryptedMeritList(), secretKey); + // We lookup for the proposals we have in our local list which match the txId from the + // voteWithProposalTxIdList and create a ballot list with the proposal and the vote from + // the voteWithProposalTxIdList + BallotList ballotList = createBallotList(voteWithProposalTxIdList); + log.debug("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, blindVoteStake={}, ballotList={}", + blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList); + return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList, meritList); + } catch (VoteResultException.DecryptionException decryptionException) { + // We don't consider such vote reveal txs valid for the majority hash + // calculation and don't add it to our result collection + log.error("Could not decrypt blind vote. This vote reveal and blind vote will be ignored. " + + "VoteRevealTxId={}. DecryptionException={}", voteRevealTxId, decryptionException.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, decryptionException)); + return null; + } + } + + private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxIdList) + throws VoteResultException.MissingBallotException { + // voteWithProposalTxIdList is the list of ProposalTxId + vote from the blind vote (decrypted vote data) + + // We convert the list to a map with proposalTxId as key and the vote as value. As the vote can be null we + // wrap it into an optional. + Map> voteByTxIdMap = voteWithProposalTxIdList.getList().stream() + .collect(Collectors.toMap(VoteWithProposalTxId::getProposalTxId, e -> Optional.ofNullable(e.getVote()))); + + // We make a map with proposalTxId as key and the ballot as value out of our stored ballot list. + // This can contain ballots which have been added later and have a null value for the vote. + Map ballotByTxIdMap = ballotListService.getValidBallotsOfCycle().stream() + .collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot)); + + // It could be that we missed some proposalPayloads. + // If we have votes with proposals which are not found in our ballots we add it to missingBallots. + List missingBallots = new ArrayList<>(); + List ballots = voteByTxIdMap.entrySet().stream() + .map(entry -> { + String txId = entry.getKey(); + if (ballotByTxIdMap.containsKey(txId)) { + Ballot ballot = ballotByTxIdMap.get(txId); + // We create a new Ballot with the proposal from the ballot list and the vote from our decrypted votes + // We clone the ballot instead applying the vote to the existing ballot from ballotListService + // The items from ballotListService.getBallotList() contains my votes. + + if (ballot.getVote() != null) { + // If we had set a vote it was an own active vote + if (!entry.getValue().isPresent()) { + log.warn("We found a local vote but don't have that vote in the data from the " + + "blind vote. ballot={}", ballot); + } else if (!ballot.getVote().equals(entry.getValue().get())) { + log.warn("We found a local vote but the vote from the " + + "blind vote does not match. ballot={}, vote from blindVote data={}", + ballot, entry.getValue().get()); + } + } + + // We only return accepted or rejected votes + return entry.getValue().map(vote -> new Ballot(ballot.getProposal(), vote)).orElse(null); + } else { + // We got a vote but we don't have the ballot (which includes the proposal) + // We add it to the missing list to handle it as exception later. We want all missing data so we + // do not throw here. + log.warn("missingBallot for proposal with txId={}. Optional tx={}", txId, daoStateService.getTx(txId)); + missingBallots.add(txId); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!missingBallots.isEmpty()) + throw new VoteResultException.MissingBallotException(ballots, missingBallots); + + // If we received a proposal after we had already voted we consider it as a proposal withhold attack and + // treat the proposal as it was voted with a rejected vote. + ballotByTxIdMap.entrySet().stream() + .filter(e -> !voteByTxIdMap.containsKey(e.getKey())) + .map(Map.Entry::getValue) + .forEach(ballot -> { + log.warn("We have a proposal which was not part of our blind vote and reject it. " + + "Proposal={}", ballot.getProposal()); + ballots.add(new Ballot(ballot.getProposal(), new Vote(false))); + }); + + // Let's keep the data more deterministic by sorting it by txId. Though we are not using the sorting. + ballots.sort(Comparator.comparing(Ballot::getTxId)); + return new BallotList(ballots); + } + + private Map getStakeByHashOfBlindVoteListMap(Set decryptedBallotsWithMeritsSet) { + // Don't use byte[] as key as byte[] uses object identity for equals and hashCode + Map map = new HashMap<>(); + decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> { + P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(decryptedBallotsWithMerits.getHashOfBlindVoteList()); + map.putIfAbsent(hash, 0L); + // We must not use the merit(stake) as that is from the P2P network data and it is not guaranteed that we + // have received it. We must rely only on blockchain data. The stake is from the vote reveal tx input. + long aggregatedStake = map.get(hash); + long stake = decryptedBallotsWithMerits.getStake(); + aggregatedStake += stake; + map.put(hash, aggregatedStake); + + log.debug("blindVoteTxId={}, stake={}", + decryptedBallotsWithMerits.getBlindVoteTxId(), stake); + }); + return map; + } + + private byte[] calculateMajorityBlindVoteListHash(Map stakes) + throws VoteResultException.ValidationException, VoteResultException.ConsensusException { + List stakeList = stakes.entrySet().stream() + .map(entry -> new HashWithStake(entry.getKey().bytes, entry.getValue())) + .collect(Collectors.toList()); + return VoteResultConsensus.getMajorityHash(stakeList); + } + + // Deal with eventually consistency of P2P network + private Optional> findBlindVoteListMatchingMajorityHash(byte[] majorityVoteListHash) { + // We reuse the method at voteReveal domain used when creating the hash + List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + if (isListMatchingMajority(majorityVoteListHash, blindVotes, true)) { + // Out local list is matching the majority hash + return Optional.of(blindVotes); + } else { + log.warn("Our local list of blind vote payloads does not match the majorityVoteListHash. " + + "We try permuting our list to find a matching variant"); + // Each voter has re-published his blind vote list when broadcasting the reveal tx so there should have a very + // high chance that we have received all blind votes which have been used by the majority of the + // voters (majority by stake). + // It still could be that we have additional blind votes so our hash does not match. We can try to permute + // our list with excluding items to see if we get a matching list. If not last resort is to request the + // missing items from the network. + Optional> permutatedList = findPermutatedListMatchingMajority(majorityVoteListHash); + if (permutatedList.isPresent()) { + return permutatedList; + } else { + log.warn("We did not find a permutation of our blindVote list which matches the majority view. " + + "We will request the blindVote data from the peers."); + // This is async operation. We will restart the whole verification process once we received the data. + missingDataRequestService.sendRepublishRequest(); + return Optional.empty(); + } + } + } + + private Optional> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { + List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + long ts = System.currentTimeMillis(); + + BiFunction, Boolean> predicate = (hash, variation) -> + isListMatchingMajority(hash, variation, false); + + List result = PermutationUtil.findMatchingPermutation(majorityVoteListHash, list, predicate, 1000000); + log.info("findPermutatedListMatchingMajority for {} items took {} ms.", + list.size(), (System.currentTimeMillis() - ts)); + if (result.isEmpty()) { + log.info("We did not find a variation of the blind vote list which matches the majority hash."); + return Optional.empty(); + } else { + log.info("We found a variation of the blind vote list which matches the majority hash. variation={}", result); + return Optional.of(result); + } + } + + private boolean isListMatchingMajority(byte[] majorityVoteListHash, List list, boolean doLog) { + byte[] hashOfBlindVoteList = VoteRevealConsensus.getHashOfBlindVoteList(list); + if (doLog) { + log.debug("majorityVoteListHash {}", Utilities.bytesAsHexString(majorityVoteListHash)); + log.debug("hashOfBlindVoteList {}", Utilities.bytesAsHexString(hashOfBlindVoteList)); + log.debug("List of blindVoteTxIds {}", list.stream().map(BlindVote::getTxId) + .collect(Collectors.joining(", "))); + } + return Arrays.equals(majorityVoteListHash, hashOfBlindVoteList); + } + + private Set getEvaluatedProposals(Set decryptedBallotsWithMeritsSet, + int chainHeight) { + // We reorganize the data structure to have a map of proposals with a list of VoteWithStake objects + Map> resultListByProposalMap = getVoteWithStakeListByProposalMap(decryptedBallotsWithMeritsSet); + + Set evaluatedProposals = new HashSet<>(); + resultListByProposalMap.forEach((proposal, voteWithStakeList) -> { + long requiredQuorum = daoStateService.getParamValueAsCoin(proposal.getQuorumParam(), chainHeight).value; + long requiredVoteThreshold = getRequiredVoteThreshold(chainHeight, proposal); + checkArgument(requiredVoteThreshold >= 5000, + "requiredVoteThreshold must be not be less then 50% otherwise we could have conflicting results."); + + // move to consensus class + ProposalVoteResult proposalVoteResult = getResultPerProposal(voteWithStakeList, proposal); + // Quorum is min. required BSQ stake to be considered valid + long reachedQuorum = proposalVoteResult.getQuorum(); + log.debug("proposalTxId: {}, required requiredQuorum: {}, requiredVoteThreshold: {}", + proposal.getTxId(), requiredVoteThreshold / 100D, requiredQuorum); + if (reachedQuorum >= requiredQuorum) { + // We multiply by 10000 as we use a long for reachedThreshold and we want precision of 2 with + // a % value. E.g. 50% is 5000. + // Threshold is percentage of accepted to total stake + long reachedThreshold = proposalVoteResult.getThreshold(); + + log.debug("reached threshold: {} %, required threshold: {} %", + reachedThreshold / 100D, + requiredVoteThreshold / 100D); + // We need to exceed requiredVoteThreshold e.g. 50% is not enough but 50.01%. + // Otherwise we could have 50% vs 50% + if (reachedThreshold > requiredVoteThreshold) { + evaluatedProposals.add(new EvaluatedProposal(true, proposalVoteResult)); + } else { + evaluatedProposals.add(new EvaluatedProposal(false, proposalVoteResult)); + log.debug("Proposal did not reach the requiredVoteThreshold. reachedThreshold={} %, " + + "requiredVoteThreshold={} %", reachedThreshold / 100D, requiredVoteThreshold / 100D); + } + } else { + evaluatedProposals.add(new EvaluatedProposal(false, proposalVoteResult)); + log.debug("Proposal did not reach the requiredQuorum. reachedQuorum={}, requiredQuorum={}", + reachedQuorum, requiredQuorum); + } + }); + + Map evaluatedProposalsByTxIdMap = new HashMap<>(); + evaluatedProposals.forEach(evaluatedProposal -> evaluatedProposalsByTxIdMap.put(evaluatedProposal.getProposalTxId(), evaluatedProposal)); + + // Proposals which did not get any vote need to be set as failed. + // TODO We should not use proposalListPresentation here + proposalListPresentation.getActiveOrMyUnconfirmedProposals().stream() + .filter(proposal -> periodService.isTxInCorrectCycle(proposal.getTxId(), chainHeight)) + .filter(proposal -> !evaluatedProposalsByTxIdMap.containsKey(proposal.getTxId())) + .forEach(proposal -> { + ProposalVoteResult proposalVoteResult = new ProposalVoteResult(proposal, 0, + 0, 0, 0, decryptedBallotsWithMeritsSet.size()); + EvaluatedProposal evaluatedProposal = new EvaluatedProposal(false, proposalVoteResult); + evaluatedProposals.add(evaluatedProposal); + log.info("Proposal ignored by all voters: {}", evaluatedProposal); + }); + + // Check if our issuance sum is not exceeding the limit + long sumIssuance = evaluatedProposals.stream() + .filter(EvaluatedProposal::isAccepted) + .map(EvaluatedProposal::getProposal) + .filter(proposal -> proposal instanceof IssuanceProposal) + .map(proposal -> (IssuanceProposal) proposal) + .mapToLong(proposal -> proposal.getRequestedBsq().value) + .sum(); + long limit = daoStateService.getParamValueAsCoin(Param.ISSUANCE_LIMIT, chainHeight).value; + if (sumIssuance > limit) { + Set evaluatedProposals2 = new HashSet<>(); + evaluatedProposals.stream().filter(EvaluatedProposal::isAccepted) + .forEach(e -> evaluatedProposals2.add(new EvaluatedProposal(false, e.getProposalVoteResult()))); + String msg = "We have a total issuance amount of " + sumIssuance / 100 + " BSQ but our limit for a cycle is " + limit / 100 + " BSQ. " + + "We consider that cycle as invalid and have set all proposals as rejected."; + log.warn(msg); + + checkNotNull(daoStateService.getCurrentCycle(), "daoStateService.getCurrentCycle() must not be null"); + voteResultExceptions.add(new VoteResultException(daoStateService.getCurrentCycle(), new VoteResultException.ConsensusException(msg))); + return evaluatedProposals2; + } + + return evaluatedProposals; + } + + // We use long for calculation to avoid issues with rounding. So we multiply the % value as double (e.g. 0.5 = 50%) + // by 100 to get the percentage value and again by 100 to get 2 decimal -> 5000 = 50.00% + private long getRequiredVoteThreshold(int chainHeight, Proposal proposal) { + double paramValueAsPercentDouble = daoStateService.getParamValueAsPercentDouble(proposal.getThresholdParam(), chainHeight); + return MathUtils.roundDoubleToLong(paramValueAsPercentDouble * 10000); + } + + private Map> getVoteWithStakeListByProposalMap(Set decryptedBallotsWithMeritsSet) { + Map> voteWithStakeByProposalMap = new HashMap<>(); + decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> decryptedBallotsWithMerits.getBallotList() + .forEach(ballot -> { + Proposal proposal = ballot.getProposal(); + voteWithStakeByProposalMap.putIfAbsent(proposal, new ArrayList<>()); + List voteWithStakeList = voteWithStakeByProposalMap.get(proposal); + long sumOfAllMerits = MeritConsensus.getMeritStake(decryptedBallotsWithMerits.getBlindVoteTxId(), + decryptedBallotsWithMerits.getMeritList(), daoStateService); + VoteWithStake voteWithStake = new VoteWithStake(ballot.getVote(), decryptedBallotsWithMerits.getStake(), sumOfAllMerits); + voteWithStakeList.add(voteWithStake); + log.debug("Add entry to voteWithStakeListByProposalMap: proposalTxId={}, voteWithStake={} ", proposal.getTxId(), voteWithStake); + })); + return voteWithStakeByProposalMap; + } + + + private ProposalVoteResult getResultPerProposal(List voteWithStakeList, Proposal proposal) { + int numAcceptedVotes = 0; + int numRejectedVotes = 0; + int numIgnoredVotes = 0; + long stakeOfAcceptedVotes = 0; + long stakeOfRejectedVotes = 0; + + for (VoteWithStake voteWithStake : voteWithStakeList) { + long sumOfAllMerits = voteWithStake.getSumOfAllMerits(); + long stake = voteWithStake.getStake(); + long combinedStake = stake + sumOfAllMerits; + log.debug("proposalTxId={}, stake={}, sumOfAllMerits={}, combinedStake={}", + proposal.getTxId(), stake, sumOfAllMerits, combinedStake); + Vote vote = voteWithStake.getVote(); + if (vote != null) { + if (vote.isAccepted()) { + stakeOfAcceptedVotes += combinedStake; + numAcceptedVotes++; + } else { + stakeOfRejectedVotes += combinedStake; + numRejectedVotes++; + } + } else { + numIgnoredVotes++; + log.debug("Voter ignored proposal"); + } + } + return new ProposalVoteResult(proposal, stakeOfAcceptedVotes, stakeOfRejectedVotes, numAcceptedVotes, numRejectedVotes, numIgnoredVotes); + } + + private void applyAcceptedProposals(Set acceptedEvaluatedProposals, int chainHeight) { + applyIssuance(acceptedEvaluatedProposals, chainHeight); + applyParamChange(acceptedEvaluatedProposals, chainHeight); + applyBondedRole(acceptedEvaluatedProposals); + applyConfiscateBond(acceptedEvaluatedProposals); + applyRemoveAsset(acceptedEvaluatedProposals); + } + + private void applyIssuance(Set acceptedEvaluatedProposals, int chainHeight) { + acceptedEvaluatedProposals.stream() + .map(EvaluatedProposal::getProposal) + .filter(proposal -> proposal instanceof IssuanceProposal) + .forEach(proposal -> issuanceService.issueBsq((IssuanceProposal) proposal, chainHeight)); + } + + private void applyParamChange(Set acceptedEvaluatedProposals, int chainHeight) { + Map> evaluatedProposalsByParam = new HashMap<>(); + acceptedEvaluatedProposals.forEach(evaluatedProposal -> { + if (evaluatedProposal.getProposal() instanceof ChangeParamProposal) { + ChangeParamProposal changeParamProposal = (ChangeParamProposal) evaluatedProposal.getProposal(); + ParamChange paramChange = getParamChange(changeParamProposal, chainHeight); + if (paramChange != null) { + String paramName = paramChange.getParamName(); + evaluatedProposalsByParam.putIfAbsent(paramName, new ArrayList<>()); + evaluatedProposalsByParam.get(paramName).add(evaluatedProposal); + } + } + }); + + evaluatedProposalsByParam.forEach((key, list) -> { + if (list.size() == 1) { + applyAcceptedChangeParamProposal((ChangeParamProposal) list.get(0).getProposal(), chainHeight); + } else if (list.size() > 1) { + log.warn("There have been multiple winning param change proposals with the same item. " + + "This is a sign of a social consensus failure. " + + "We treat all requests as failed in such a case."); + } + }); + } + + private void applyAcceptedChangeParamProposal(ChangeParamProposal changeParamProposal, int chainHeight) { + @SuppressWarnings("StringBufferReplaceableByString") + StringBuilder sb = new StringBuilder(); + sb.append("\n################################################################################\n"); + sb.append("We changed a parameter. ProposalTxId=").append(changeParamProposal.getTxId()) + .append("\nParam: ").append(changeParamProposal.getParam().name()) + .append(" new value: ").append(changeParamProposal.getParamValue()) + .append("\n################################################################################\n"); + log.info(sb.toString()); + + daoStateService.setNewParam(chainHeight, changeParamProposal.getParam(), changeParamProposal.getParamValue()); + } + + private ParamChange getParamChange(ChangeParamProposal changeParamProposal, int chainHeight) { + return daoStateService.getStartHeightOfNextCycle(chainHeight) + .map(heightOfNewCycle -> new ParamChange(changeParamProposal.getParam().name(), + changeParamProposal.getParamValue(), + heightOfNewCycle)) + .orElse(null); + } + + private void applyBondedRole(Set acceptedEvaluatedProposals) { + acceptedEvaluatedProposals.forEach(evaluatedProposal -> { + if (evaluatedProposal.getProposal() instanceof RoleProposal) { + RoleProposal roleProposal = (RoleProposal) evaluatedProposal.getProposal(); + Role role = roleProposal.getRole(); + @SuppressWarnings("StringBufferReplaceableByString") + StringBuilder sb = new StringBuilder(); + sb.append("\n################################################################################\n"); + sb.append("We added a bonded role. ProposalTxId=").append(roleProposal.getTxId()) + .append("\nRole: ").append(role.getDisplayString()) + .append("\n################################################################################\n"); + log.info(sb.toString()); + } + }); + } + + private void applyConfiscateBond(Set acceptedEvaluatedProposals) { + acceptedEvaluatedProposals.forEach(evaluatedProposal -> { + if (evaluatedProposal.getProposal() instanceof ConfiscateBondProposal) { + ConfiscateBondProposal confiscateBondProposal = (ConfiscateBondProposal) evaluatedProposal.getProposal(); + daoStateService.confiscateBond(confiscateBondProposal.getLockupTxId()); + + @SuppressWarnings("StringBufferReplaceableByString") + StringBuilder sb = new StringBuilder(); + sb.append("\n################################################################################\n"); + sb.append("We confiscated a bond. ProposalTxId=").append(confiscateBondProposal.getTxId()) + .append("\nLockupTxId: ").append(confiscateBondProposal.getLockupTxId()) + .append("\n################################################################################\n"); + log.info(sb.toString()); + } + }); + } + + private void applyRemoveAsset(Set acceptedEvaluatedProposals) { + acceptedEvaluatedProposals.forEach(evaluatedProposal -> { + if (evaluatedProposal.getProposal() instanceof RemoveAssetProposal) { + RemoveAssetProposal removeAssetProposal = (RemoveAssetProposal) evaluatedProposal.getProposal(); + String tickerSymbol = removeAssetProposal.getTickerSymbol(); + @SuppressWarnings("StringBufferReplaceableByString") + StringBuilder sb = new StringBuilder(); + sb.append("\n################################################################################\n"); + sb.append("We removed an asset. ProposalTxId=").append(removeAssetProposal.getTxId()) + .append("\nAsset: ").append(CurrencyUtil.getNameByCode(tickerSymbol)) + .append("\n################################################################################\n"); + log.info(sb.toString()); + } + }); + } + + private Set getAcceptedEvaluatedProposals(Set evaluatedProposals) { + return evaluatedProposals.stream() + .filter(EvaluatedProposal::isAccepted) + .collect(Collectors.toSet()); + } + + private boolean isInVoteResultPhase(int chainHeight) { + return periodService.getFirstBlockOfPhase(chainHeight, DaoPhase.Phase.RESULT) == chainHeight; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Inner classes + /////////////////////////////////////////////////////////////////////////////////////////// + + @Value + public static class HashWithStake { + private final byte[] hash; + private final long stake; + + HashWithStake(byte[] hash, long stake) { + this.hash = hash; + this.stake = stake; + } + + @Override + public String toString() { + return "HashWithStake{" + + "\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n stake=" + stake + + "\n}"; + } + } + + @Value + private static class VoteWithStake { + @Nullable + private final Vote vote; + private final long stake; + private final long sumOfAllMerits; + + VoteWithStake(@Nullable Vote vote, long stake, long sumOfAllMerits) { + this.vote = vote; + this.stake = stake; + this.sumOfAllMerits = sumOfAllMerits; + } + + @Override + public String toString() { + return "VoteWithStake{" + + "\n vote=" + vote + + ",\n stake=" + stake + + ",\n sumOfAllMerits=" + sumOfAllMerits + + "\n}"; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java new file mode 100644 index 0000000000..be93a447a5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.dao.governance.voteresult.issuance; + +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.IssuanceProposal; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.ReimbursementProposal; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + + +@Slf4j +public class IssuanceService { + private final DaoStateService daoStateService; + private final PeriodService periodService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public IssuanceService(DaoStateService daoStateService, PeriodService periodService) { + this.daoStateService = daoStateService; + this.periodService = periodService; + } + + public void issueBsq(IssuanceProposal issuanceProposal, int chainHeight) { + daoStateService.getIssuanceCandidateTxOutputs().stream() + .filter(txOutput -> isValid(txOutput, issuanceProposal, periodService, chainHeight)) + .forEach(txOutput -> { + IssuanceType issuanceType = IssuanceType.UNDEFINED; + if (issuanceProposal instanceof CompensationProposal) { + issuanceType = IssuanceType.COMPENSATION; + } else if (issuanceProposal instanceof ReimbursementProposal) { + issuanceType = IssuanceType.REIMBURSEMENT; + } + checkArgument(issuanceType != IssuanceType.UNDEFINED, "issuanceType must not be undefined"); + + // We don't check atm if the output is unspent. We cannot use the bsqWallet as that would not + // reflect our current block state (could have been spent at later block which is valid and + // bsqWallet would show that spent state). We would need to support a spent status for the outputs + // which are interpreted as BTC (as a not yet accepted comp. request). + Optional optionalTx = daoStateService.getTx(issuanceProposal.getTxId()); + checkArgument(optionalTx.isPresent(), "optionalTx must be present"); + long amount = issuanceProposal.getRequestedBsq().value; + Tx tx = optionalTx.get(); + // We use key from first input + TxInput txInput = tx.getTxInputs().get(0); + String pubKey = txInput.getPubKey(); + Issuance issuance = new Issuance(tx.getId(), chainHeight, amount, pubKey, issuanceType); + daoStateService.addIssuance(issuance); + daoStateService.addUnspentTxOutput(txOutput); + + @SuppressWarnings("StringBufferReplaceableByString") + StringBuilder sb = new StringBuilder(); + sb.append("\n################################################################################\n"); + sb.append("We issued new BSQ to tx with ID ").append(txOutput.getTxId()) + .append("\nIssued BSQ: ").append(MathUtils.scaleDownByPowerOf10(amount, 2)) + .append("\nIssuance type: ").append(issuanceType.name()) + .append("\n################################################################################\n"); + log.info(sb.toString()); + }); + } + + private boolean isValid(TxOutput txOutput, IssuanceProposal issuanceProposal, PeriodService periodService, int chainHeight) { + return txOutput.getTxId().equals(issuanceProposal.getTxId()) + && issuanceProposal.getRequestedBsq().value == txOutput.getValue() + && issuanceProposal.getBsqAddress().substring(1).equals(txOutput.getAddress()) + && periodService.isTxInPhaseAndCycle(txOutput.getTxId(), DaoPhase.Phase.PROPOSAL, chainHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java new file mode 100644 index 0000000000..19993205fe --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.core.dao.governance.votereveal; + +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.crypto.Hash; + +import javax.crypto.SecretKey; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * All consensus critical aspects are handled here. + */ +@Slf4j +public class VoteRevealConsensus { + + public static byte[] getHashOfBlindVoteList(List blindVotes) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + blindVotes.forEach(blindVote -> { + byte[] data = blindVote.toProtoMessage().toByteArray(); + try { + outputStream.write(data); + } catch (IOException e) { + e.printStackTrace(); + } + }); + return Hash.getSha256Ripemd160hash(outputStream.toByteArray()); + } + + public static byte[] getOpReturnData(byte[] hashOfBlindVoteList, SecretKey secretKey) throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.VOTE_REVEAL.getType()); + outputStream.write(Version.VOTE_REVEAL); + outputStream.write(hashOfBlindVoteList); // hash is 20 bytes + outputStream.write(secretKey.getEncoded()); // encoded secretKey has 16 bytes + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + throw e; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealException.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealException.java new file mode 100644 index 0000000000..673df1f28e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealException.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.dao.governance.votereveal; + +import bisq.core.dao.governance.myvote.MyVote; + +import org.bitcoinj.core.Transaction; + +import lombok.Getter; + +import javax.annotation.Nullable; + +@SuppressWarnings("SameParameterValue") +public class VoteRevealException extends Exception { + @Getter + @Nullable + private Transaction voteRevealTx; + @Getter + @Nullable + private String blindVoteTxId; + @Getter + @Nullable + private MyVote myVote; + + VoteRevealException(String message, Throwable cause, @SuppressWarnings("NullableProblems") String blindVoteTxId) { + super(message, cause); + this.blindVoteTxId = blindVoteTxId; + } + + VoteRevealException(String message, @SuppressWarnings("NullableProblems") MyVote myVote) { + super(message); + this.myVote = myVote; + } + + VoteRevealException(String message, Throwable cause, @SuppressWarnings("NullableProblems") Transaction voteRevealTx) { + super(message, cause); + this.voteRevealTx = voteRevealTx; + } + + + @Override + public String toString() { + return "VoteRevealException{" + + "\n voteRevealTx=" + voteRevealTx + + ",\n blindVoteTxId='" + blindVoteTxId + '\'' + + ",\n myVote=" + myVote + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java new file mode 100644 index 0000000000..94b4f0b9a6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java @@ -0,0 +1,275 @@ +/* + * 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 bisq.core.dao.governance.votereveal; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.myvote.MyVote; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +// TODO Broadcast the winning list at the moment the reveal period is over and have the break +// interval as time buffer for all nodes to receive that winning list. All nodes which are in sync with the +// majority data view can broadcast. That way it will become a very unlikely case that a node is missing +// data. + +/** + * Publishes voteRevealTx with the secret key used for encryption at blind vote and the hash of the list of + * the blind vote payloads. Republishes also all blindVotes of that cycle to add more resilience. + */ +@Slf4j +public class VoteRevealService implements DaoStateListener, DaoSetupService { + + public interface VoteRevealTxPublishedListener { + void onVoteRevealTxPublished(String txId); + } + + private final DaoStateService daoStateService; + private final BlindVoteListService blindVoteListService; + private final PeriodService periodService; + private final MyVoteListService myVoteListService; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + + @Getter + private final ObservableList voteRevealExceptions = FXCollections.observableArrayList(); + private final List voteRevealTxPublishedListeners = new ArrayList<>(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public VoteRevealService(DaoStateService daoStateService, + BlindVoteListService blindVoteListService, + PeriodService periodService, + MyVoteListService myVoteListService, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager) { + this.daoStateService = daoStateService; + this.blindVoteListService = blindVoteListService; + this.periodService = periodService; + this.myVoteListService = myVoteListService; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + voteRevealExceptions.addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) + c.getAddedSubList().forEach(exception -> log.error(exception.toString())); + }); + daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + private byte[] getHashOfBlindVoteList() { + List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + byte[] hashOfBlindVoteList = VoteRevealConsensus.getHashOfBlindVoteList(blindVotes); + log.debug("blindVoteList for creating hash: {}", blindVotes); + log.info("Sha256Ripemd160 hash of hashOfBlindVoteList {}", Utilities.bytesAsHexString(hashOfBlindVoteList)); + return hashOfBlindVoteList; + } + + public void addVoteRevealTxPublishedListener(VoteRevealTxPublishedListener voteRevealTxPublishedListener) { + voteRevealTxPublishedListeners.add(voteRevealTxPublishedListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + maybeRevealVotes(block.getHeight()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // Creation of vote reveal tx is done without user activity! + // We create automatically the vote reveal tx when we are in the reveal phase of the current cycle when + // the blind vote was created in case we have not done it already. + // The voter needs to be at least once online in the reveal phase when he has a blind vote created, + // otherwise his vote becomes invalid. + // In case the user misses the vote reveal phase an (invalid) vote reveal tx will be created the next time the user is + // online. That tx only serves the purpose to unlock the stake from the blind vote but it will be ignored for voting. + // A blind vote which did not get revealed might still be part of the majority hash calculation as we cannot know + // which blind votes might be revealed until the phase is over at the moment when we publish the vote reveal tx. + private void maybeRevealVotes(int chainHeight) { + myVoteListService.getMyVoteList().stream() + .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed + .forEach(myVote -> { + boolean isInVoteRevealPhase = periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL; + // If we would create the tx in the last block it would be confirmed in the best case in th next + // block which would be already the break and would invalidate the vote reveal. + boolean isLastBlockInPhase = chainHeight == periodService.getLastBlockOfPhase(chainHeight, DaoPhase.Phase.VOTE_REVEAL); + String blindVoteTxId = myVote.getBlindVoteTxId(); + boolean isBlindVoteTxInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight); + if (isInVoteRevealPhase && !isLastBlockInPhase && isBlindVoteTxInCorrectPhaseAndCycle) { + log.info("We call revealVote at blockHeight {} for blindVoteTxId {}", chainHeight, blindVoteTxId); + // Standard case that we are in the correct phase and cycle and create the reveal tx. + revealVote(myVote, true); + } else { + // We missed the vote reveal phase but publish a vote reveal tx to unlock the blind vote stake. + boolean isAfterVoteRevealPhase = periodService.getPhaseForHeight(chainHeight).ordinal() > DaoPhase.Phase.VOTE_REVEAL.ordinal(); + + // We missed the reveal phase but we are in the correct cycle + boolean missedPhaseSameCycle = isAfterVoteRevealPhase && isBlindVoteTxInCorrectPhaseAndCycle; + + // If we missed the cycle we don't care about the phase anymore. + boolean isBlindVoteTxInPastCycle = periodService.isTxInPastCycle(blindVoteTxId, chainHeight); + + if (missedPhaseSameCycle || isBlindVoteTxInPastCycle) { + // Exceptional case that the user missed the vote reveal phase. We still publish the vote + // reveal tx to unlock the vote stake. + + // We cannot handle that case in the parser directly to avoid that reveal tx and unlock the + // BSQ because the blind vote tx is already in the snapshot and does not get parsed + // again. It would require a reset of the snapshot and parse all blocks again. + // As this is an exceptional case we prefer to have a simple solution instead and just + // publish the vote reveal tx but are aware that it is invalid. + log.warn("We missed the vote reveal phase but publish now the tx to unlock our locked " + + "BSQ from the blind vote tx. BlindVoteTxId={}, blockHeight={}", + blindVoteTxId, chainHeight); + + // We handle the exception here inside the stream iteration as we have not get triggered from an + // outside user intent anyway. We keep errors in a observable list so clients can observe that to + // get notified if anything went wrong. + revealVote(myVote, false); + } + } + }); + } + + private void revealVote(MyVote myVote, boolean isInVoteRevealPhase) { + try { + // We collect all valid blind vote items we received via the p2p network. + // It might be that different nodes have a different collection of those items. + // To ensure we get a consensus of the data for later calculating the result we will put a hash of each + // voter's blind vote collection into the opReturn data and check for a majority in the vote result phase. + // The voters "vote" with their stake at the reveal tx for their version of the blind vote collection. + + // If we are not in the right phase we just add an empty hash (still need to have the hash as otherwise we + // would not recognize the tx as vote reveal tx) + byte[] hashOfBlindVoteList = isInVoteRevealPhase ? getHashOfBlindVoteList() : new byte[20]; + byte[] opReturnData = VoteRevealConsensus.getOpReturnData(hashOfBlindVoteList, myVote.getSecretKey()); + + // We search for my unspent stake output. + // myVote is already tested if it is in current cycle at maybeRevealVotes + // We expect that the blind vote tx and stake output is available. If not we throw an exception. + TxOutput stakeTxOutput = daoStateService.getUnspentBlindVoteStakeTxOutputs().stream() + .filter(txOutput -> txOutput.getTxId().equals(myVote.getBlindVoteTxId())) + .findFirst() + .orElseThrow(() -> new VoteRevealException("stakeTxOutput is not found for myVote.", myVote)); + + Transaction voteRevealTx = getVoteRevealTx(stakeTxOutput, opReturnData); + log.info("voteRevealTx={}", voteRevealTx); + publishTx(voteRevealTx); + + // We don't want to wait for a successful broadcast to avoid issues if the broadcast succeeds delayed or at + // next startup but the tx was actually broadcast. + myVoteListService.applyRevealTxId(myVote, voteRevealTx.getTxId().toString()); + } catch (IOException | WalletException | TransactionVerificationException + | InsufficientMoneyException e) { + voteRevealExceptions.add(new VoteRevealException("Exception at calling revealVote.", + e, myVote.getBlindVoteTxId())); + } catch (VoteRevealException e) { + voteRevealExceptions.add(e); + } + } + + private void publishTx(Transaction voteRevealTx) { + walletsManager.publishAndCommitBsqTx(voteRevealTx, TxType.VOTE_REVEAL, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("voteRevealTx successfully broadcast."); + voteRevealTxPublishedListeners.forEach(l -> l.onVoteRevealTxPublished(transaction.getTxId().toString())); + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error(exception.toString()); + voteRevealExceptions.add(new VoteRevealException("Publishing of voteRevealTx failed.", + exception, voteRevealTx)); + } + }); + } + + private Transaction getVoteRevealTx(TxOutput stakeTxOutput, byte[] opReturnData) + throws InsufficientMoneyException, WalletException, TransactionVerificationException { + Transaction preparedTx = bsqWalletService.getPreparedVoteRevealTx(stakeTxOutput); + Transaction txWithBtcFee = btcWalletService.completePreparedVoteRevealTx(preparedTx, opReturnData); + return bsqWalletService.signTx(txWithBtcFee); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java new file mode 100644 index 0000000000..530b197d72 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java @@ -0,0 +1,347 @@ +/* + * 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 bisq.core.dao.monitoring; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.MyBlindVoteList; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.BlindVoteStateHash; +import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.GenesisTxInfo; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.UserThread; +import bisq.common.crypto.Hash; + +import javax.inject.Inject; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Monitors the BlindVote P2P network payloads with using a hash of a sorted list of BlindVotes from one cycle and + * make it accessible to the network so we can detect quickly if any consensus issue arises. + * We create that hash at the first block of the VoteReveal phase. There is one hash created per cycle. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. + * + * We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low. + */ +@Slf4j +public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStateListener, BlindVoteStateNetworkService.Listener { + public interface Listener { + void onBlindVoteStateBlockChainChanged(); + } + + private final DaoStateService daoStateService; + private final BlindVoteStateNetworkService blindVoteStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + private final PeriodService periodService; + private final BlindVoteListService blindVoteListService; + private final Set seedNodeAddresses; + + @Getter + private final LinkedList blindVoteStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList blindVoteStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + @Getter + private boolean isInConflictWithNonSeedNode; + @Getter + private boolean isInConflictWithSeedNode; + private boolean parseBlockChainComplete; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BlindVoteStateMonitoringService(DaoStateService daoStateService, + BlindVoteStateNetworkService blindVoteStateNetworkService, + GenesisTxInfo genesisTxInfo, + PeriodService periodService, + BlindVoteListService blindVoteListService, + SeedNodeRepository seedNodeRepository) { + this.daoStateService = daoStateService; + this.blindVoteStateNetworkService = blindVoteStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + this.periodService = periodService; + this.blindVoteListService = blindVoteListService; + seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream() + .map(NodeAddress::getFullAddress) + .collect(Collectors.toSet()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + blindVoteStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("Duplicates") + @Override + public void onDaoStateChanged(Block block) { + int blockHeight = block.getHeight(); + + int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); + + if (blindVoteStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) { + // Takes about 150 ms for dao testnet data + long ts = System.currentTimeMillis(); + for (int i = genesisBlockHeight; i < blockHeight; i++) { + maybeUpdateHashChain(i); + } + if (!blindVoteStateBlockChain.isEmpty()) { + log.info("updateHashChain for {} blocks took {} ms", + blockHeight - genesisBlockHeight, + System.currentTimeMillis() - ts); + } + } + + long ts = System.currentTimeMillis(); + boolean updated = maybeUpdateHashChain(blockHeight); + if (updated) { + log.info("updateHashChain for block {} took {} ms", + blockHeight, + System.currentTimeMillis() - ts); + } + } + + @SuppressWarnings("Duplicates") + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + blindVoteStateNetworkService.addListeners(); + + // We wait for processing messages until we have completed batch processing + + // We request data from last 5 cycles. We ignore possible duration changes done by voting. + // period is arbitrary anyway... + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5); + + blindVoteStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewBlindVoteStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersBlindVoteStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetBlindVoteStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List blindVoteStateHashes = blindVoteStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(BlindVoteStateBlock::getMyStateHash) + .collect(Collectors.toList()); + blindVoteStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), blindVoteStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersBlindVoteStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + blindVoteStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean maybeUpdateHashChain(int blockHeight) { + // We use first block in blind vote phase to create the hash of our blindVotes. We prefer to wait as long as + // possible to increase the chance that we have received all blindVotes. + if (!isFirstBlockOfBlindVotePhase(blockHeight)) { + return false; + } + + periodService.getCycle(blockHeight).ifPresent(cycle -> { + List blindVotes = blindVoteListService.getConfirmedBlindVotes().stream() + .filter(e -> periodService.isTxInCorrectCycle(e.getTxId(), blockHeight)) + .sorted(Comparator.comparing(BlindVote::getTxId)).collect(Collectors.toList()); + + // We use MyBlindVoteList to get the serialized bytes from the blindVotes list + byte[] serializedBlindVotes = new MyBlindVoteList(blindVotes).toProtoMessage().toByteArray(); + + byte[] prevHash; + if (blindVoteStateBlockChain.isEmpty()) { + prevHash = new byte[0]; + } else { + prevHash = blindVoteStateBlockChain.getLast().getHash(); + } + byte[] combined = ArrayUtils.addAll(prevHash, serializedBlindVotes); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + + BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, prevHash, blindVotes.size()); + BlindVoteStateBlock blindVoteStateBlock = new BlindVoteStateBlock(myBlindVoteStateHash); + blindVoteStateBlockChain.add(blindVoteStateBlock); + blindVoteStateHashChain.add(myBlindVoteStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> blindVoteStateNetworkService.broadcastMyStateHash(myBlindVoteStateHash), delayInSec); + } + }); + return true; + } + + private boolean processPeersBlindVoteStateHash(BlindVoteStateHash blindVoteStateHash, + Optional peersNodeAddress, + boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode); + AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode); + StringBuilder sb = new StringBuilder(); + blindVoteStateBlockChain.stream() + .filter(e -> e.getHeight() == blindVoteStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, blindVoteStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(blindVoteStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, blindVoteStateHash); + if (seedNodeAddresses.contains(peersNodeAddressAsString)) { + inConflictWithSeedNode.set(true); + } else { + inConflictWithNonSeedNode.set(true); + } + + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my blindVoteStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers blindVoteStateHash=") + .append(blindVoteStateHash); + } + changed.set(true); + }); + + this.isInConflictWithNonSeedNode = inConflictWithNonSeedNode.get(); + this.isInConflictWithSeedNode = inConflictWithSeedNode.get(); + + String conflictMsg = sb.toString(); + if (!conflictMsg.isEmpty()) { + if (this.isInConflictWithSeedNode) + log.warn("Conflict with seed nodes: {}", conflictMsg); + else if (this.isInConflictWithNonSeedNode) + log.info("Conflict with non-seed nodes: {}", conflictMsg); + } + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + } + + return changed.get(); + } + + private boolean isFirstBlockOfBlindVotePhase(int blockHeight) { + return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.VOTE_REVEAL); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java new file mode 100644 index 0000000000..18eb1f9683 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -0,0 +1,429 @@ +/* + * 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 bisq.core.dao.monitoring; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.monitoring.model.UtxoMismatch; +import bisq.core.dao.monitoring.network.Checkpoint; +import bisq.core.dao.monitoring.network.DaoStateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.GenesisTxInfo; +import bisq.core.dao.state.model.blockchain.BaseTxOutput; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.IssuanceType; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.crypto.Hash; +import bisq.common.file.FileUtil; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang3.ArrayUtils; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.io.File; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Monitors the DaoState by using a hash for the complete daoState and make it accessible to the network + * so we can detect quickly if any consensus issue arise. + * We create that hash after parsing and processing of a block is completed. There is one hash created per block. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. It could be that the received dao + * state from the peers is already covering the next block we have not received yet. So we only take data in account + * which are inside the block height we have already. To avoid such race conditions we delay the broadcasting of our + * state to the peers to not get ignored it in case they have not received the block yet. + * + * We do persist that chain of hashes with the snapshot. + */ +@Slf4j +public class DaoStateMonitoringService implements DaoSetupService, DaoStateListener, + DaoStateNetworkService.Listener { + + public interface Listener { + void onChangeAfterBatchProcessing(); + + void onCheckpointFail(); + } + + private final DaoStateService daoStateService; + private final DaoStateNetworkService daoStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + private final Set seedNodeAddresses; + + + @Getter + private final LinkedList daoStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList daoStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + private boolean parseBlockChainComplete; + @Getter + private boolean isInConflictWithNonSeedNode; + @Getter + private boolean isInConflictWithSeedNode; + @Getter + private final ObservableList utxoMismatches = FXCollections.observableArrayList(); + + private final List checkpoints = Arrays.asList( + new Checkpoint(586920, Utilities.decodeFromHex("523aaad4e760f6ac6196fec1b3ec9a2f42e5b272")) + ); + private boolean checkpointFailed; + private final boolean ignoreDevMsg; + private int numCalls; + private long accumulatedDuration; + + private final File storageDir; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoStateMonitoringService(DaoStateService daoStateService, + DaoStateNetworkService daoStateNetworkService, + GenesisTxInfo genesisTxInfo, + SeedNodeRepository seedNodeRepository, + @Named(Config.STORAGE_DIR) File storageDir, + @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg) { + this.daoStateService = daoStateService; + this.daoStateNetworkService = daoStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + this.storageDir = storageDir; + this.ignoreDevMsg = ignoreDevMsg; + seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream() + .map(NodeAddress::getFullAddress) + .collect(Collectors.toSet()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + daoStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + // We do not use onDaoStateChanged but let the DaoEventCoordinator call createHashFromBlock to ensure the + // correct order of execution. + + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + daoStateNetworkService.addListeners(); + + // We wait for processing messages until we have completed batch processing + int fromHeight = daoStateService.getChainHeight() - 10; + daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + + if (!ignoreDevMsg) { + verifyCheckpoints(); + } + + log.info("ParseBlockChainComplete: Accumulated updateHashChain() calls for {} block took {} ms " + + "({} ms in average / block)", + numCalls, + accumulatedDuration, + (int) ((double) accumulatedDuration / (double) numCalls)); + } + + @Override + public void onDaoStateChanged(Block block) { + long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value; + long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION); + long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT); + long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq(); + // confiscated funds are still in the utxo set + long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum(); + long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq; + + if (sumBsq != sumUtxo) { + utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewDaoStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersDaoStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetDaoStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List daoStateHashes = daoStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(DaoStateBlock::getMyStateHash) + .collect(Collectors.toList()); + daoStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), daoStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersDaoStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onChangeAfterBatchProcessing); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void createHashFromBlock(Block block) { + updateHashChain(block); + } + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + daoStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + public void applySnapshot(LinkedList persistedDaoStateHashChain) { + // We could get a reset from a reorg, so we clear all and start over from the genesis block. + daoStateHashChain.clear(); + daoStateBlockChain.clear(); + daoStateNetworkService.reset(); + + if (!persistedDaoStateHashChain.isEmpty()) { + log.info("Apply snapshot with {} daoStateHashes. Last daoStateHash={}", + persistedDaoStateHashChain.size(), persistedDaoStateHashChain.getLast()); + } + daoStateHashChain.addAll(persistedDaoStateHashChain); + daoStateHashChain.forEach(e -> daoStateBlockChain.add(new DaoStateBlock(e))); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateHashChain(Block block) { + long ts = System.currentTimeMillis(); + byte[] prevHash; + int height = block.getHeight(); + if (daoStateBlockChain.isEmpty()) { + // Only at genesis we allow an empty prevHash + if (height == genesisTxInfo.getGenesisBlockHeight()) { + prevHash = new byte[0]; + } else { + log.warn("DaoStateBlockchain is empty but we received the block which was not the genesis block. " + + "We stop execution here."); + return; + } + } else { + checkArgument(height == daoStateBlockChain.getLast().getHeight() + 1, + "New block must be 1 block above previous block. height={}, " + + "daoStateBlockChain.getLast().getHeight()={}", + height, daoStateBlockChain.getLast().getHeight()); + prevHash = daoStateBlockChain.getLast().getHash(); + } + byte[] stateHash = daoStateService.getSerializedStateForHashChain(); + // We include the prev. hash in our new hash so we can be sure that if one hash is matching all the past would + // match as well. + byte[] combined = ArrayUtils.addAll(prevHash, stateHash); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + + DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, prevHash); + DaoStateBlock daoStateBlock = new DaoStateBlock(myDaoStateHash); + daoStateBlockChain.add(daoStateBlock); + daoStateHashChain.add(myDaoStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onChangeAfterBatchProcessing); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec); + } + long duration = System.currentTimeMillis() - ts; + // We don't want to spam the output. We log accumulated time after parsing is completed. + log.trace("updateHashChain for block {} took {} ms", + block.getHeight(), + duration); + accumulatedDuration += duration; + numCalls++; + } + + private boolean processPeersDaoStateHash(DaoStateHash daoStateHash, Optional peersNodeAddress, + boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode); + AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode); + StringBuilder sb = new StringBuilder(); + daoStateBlockChain.stream() + .filter(e -> e.getHeight() == daoStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, daoStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(daoStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, daoStateHash); + if (seedNodeAddresses.contains(peersNodeAddressAsString)) { + inConflictWithSeedNode.set(true); + } else { + inConflictWithNonSeedNode.set(true); + } + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my daoStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers daoStateHash=") + .append(daoStateHash); + } + changed.set(true); + }); + + this.isInConflictWithNonSeedNode = inConflictWithNonSeedNode.get(); + this.isInConflictWithSeedNode = inConflictWithSeedNode.get(); + + String conflictMsg = sb.toString(); + if (!conflictMsg.isEmpty()) { + if (this.isInConflictWithSeedNode) + log.warn("Conflict with seed nodes: {}", conflictMsg); + else if (this.isInConflictWithNonSeedNode) + log.debug("Conflict with non-seed nodes: {}", conflictMsg); + } + + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onChangeAfterBatchProcessing); + } + + return changed.get(); + } + + private void verifyCheckpoints() { + // Checkpoint + checkpoints.forEach(checkpoint -> daoStateHashChain.stream() + .filter(daoStateHash -> daoStateHash.getHeight() == checkpoint.getHeight()) + .findAny() + .ifPresent(daoStateHash -> { + if (Arrays.equals(daoStateHash.getHash(), checkpoint.getHash())) { + log.info("Passed checkpoint {}", checkpoint.toString()); + } else { + if (checkpointFailed) { + return; + } + checkpointFailed = true; + try { + // Delete state and stop + removeFile("DaoStateStore"); + removeFile("BlindVoteStore"); + removeFile("ProposalStore"); + removeFile("TempProposalStore"); + + listeners.forEach(Listener::onCheckpointFail); + log.error("Failed checkpoint {}", checkpoint.toString()); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + } + } + })); + } + + private void removeFile(String storeName) { + long currentTime = System.currentTimeMillis(); + String newFileName = storeName + "_" + currentTime; + String backupDirName = "out_of_sync_dao_data"; + File corrupted = new File(storageDir, storeName); + try { + if (corrupted.exists()) { + FileUtil.removeAndBackupFile(storageDir, corrupted, newFileName, backupDirName); + } + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java new file mode 100644 index 0000000000..56581168c0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java @@ -0,0 +1,347 @@ +/* + * 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 bisq.core.dao.monitoring; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.MyProposalList; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateHash; +import bisq.core.dao.monitoring.network.ProposalStateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.GenesisTxInfo; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.UserThread; +import bisq.common.crypto.Hash; + +import javax.inject.Inject; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Monitors the Proposal P2P network payloads with using a hash of a sorted list of Proposals from one cycle and + * make it accessible to the network so we can detect quickly if any consensus issue arises. + * We create that hash at the first block of the BlindVote phase. There is one hash created per cycle. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. + * + * We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low. + */ +@Slf4j +public class ProposalStateMonitoringService implements DaoSetupService, DaoStateListener, ProposalStateNetworkService.Listener { + public interface Listener { + void onProposalStateBlockChainChanged(); + } + + private final DaoStateService daoStateService; + private final ProposalStateNetworkService proposalStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + private final PeriodService periodService; + private final ProposalService proposalService; + private final Set seedNodeAddresses; + + + @Getter + private final LinkedList proposalStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList proposalStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + @Getter + private boolean isInConflictWithNonSeedNode; + @Getter + private boolean isInConflictWithSeedNode; + private boolean parseBlockChainComplete; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalStateMonitoringService(DaoStateService daoStateService, + ProposalStateNetworkService proposalStateNetworkService, + GenesisTxInfo genesisTxInfo, + PeriodService periodService, + ProposalService proposalService, + SeedNodeRepository seedNodeRepository) { + this.daoStateService = daoStateService; + this.proposalStateNetworkService = proposalStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + this.periodService = periodService; + this.proposalService = proposalService; + seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream() + .map(NodeAddress::getFullAddress) + .collect(Collectors.toSet()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + proposalStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("Duplicates") + public void onDaoStateChanged(Block block) { + int blockHeight = block.getHeight(); + int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); + + boolean hashChainUpdated = false; + if (proposalStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) { + // Takes about 150 ms for dao testnet data + long ts = System.currentTimeMillis(); + for (int i = genesisBlockHeight; i < blockHeight; i++) { + boolean isHashChainUpdated = maybeUpdateHashChain(i); + if (isHashChainUpdated) { + hashChainUpdated = true; + } + } + if (hashChainUpdated) { + log.info("updateHashChain for {} blocks took {} ms", + blockHeight - genesisBlockHeight, + System.currentTimeMillis() - ts); + } + } + long ts = System.currentTimeMillis(); + boolean updated = maybeUpdateHashChain(blockHeight); + if (updated) { + log.info("updateHashChain for block {} took {} ms", + blockHeight, + System.currentTimeMillis() - ts); + } + } + + @SuppressWarnings("Duplicates") + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + proposalStateNetworkService.addListeners(); + + // We wait for processing messages until we have completed batch processing + + // We request data from last 5 cycles. We ignore possible duration changes done by voting. + // period is arbitrary anyway... + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5); + + proposalStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewProposalStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersProposalStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetProposalStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List proposalStateHashes = proposalStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(ProposalStateBlock::getMyStateHash) + .collect(Collectors.toList()); + proposalStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), proposalStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersProposalStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onProposalStateBlockChainChanged); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + proposalStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean maybeUpdateHashChain(int blockHeight) { + // We use first block in blind vote phase to create the hash of our proposals. We prefer to wait as long as + // possible to increase the chance that we have received all proposals. + if (!isFirstBlockOfBlindVotePhase(blockHeight)) { + return false; + } + + periodService.getCycle(blockHeight).ifPresent(cycle -> { + List proposals = proposalService.getValidatedProposals().stream() + .filter(e -> periodService.isTxInPhaseAndCycle(e.getTxId(), DaoPhase.Phase.PROPOSAL, blockHeight)) + .filter(e -> e.getTxId() != null) + .sorted(Comparator.comparing(Proposal::getTxId)) + .collect(Collectors.toList()); + + // We use MyProposalList to get the serialized bytes from the proposals list + byte[] serializedProposals = new MyProposalList(proposals).toProtoMessage().toByteArray(); + + byte[] prevHash; + if (proposalStateBlockChain.isEmpty()) { + prevHash = new byte[0]; + } else { + prevHash = proposalStateBlockChain.getLast().getHash(); + } + byte[] combined = ArrayUtils.addAll(prevHash, serializedProposals); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, prevHash, proposals.size()); + ProposalStateBlock proposalStateBlock = new ProposalStateBlock(myProposalStateHash); + proposalStateBlockChain.add(proposalStateBlock); + proposalStateHashChain.add(myProposalStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onProposalStateBlockChainChanged); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> proposalStateNetworkService.broadcastMyStateHash(myProposalStateHash), delayInSec); + } + }); + return true; + } + + private boolean processPeersProposalStateHash(ProposalStateHash proposalStateHash, Optional peersNodeAddress, boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode); + AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode); + StringBuilder sb = new StringBuilder(); + proposalStateBlockChain.stream() + .filter(e -> e.getHeight() == proposalStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, proposalStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(proposalStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, proposalStateHash); + if (seedNodeAddresses.contains(peersNodeAddressAsString)) { + inConflictWithSeedNode.set(true); + } else { + inConflictWithNonSeedNode.set(true); + } + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my proposalStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers proposalStateHash=") + .append(proposalStateHash); + } + changed.set(true); + }); + + this.isInConflictWithNonSeedNode = inConflictWithNonSeedNode.get(); + this.isInConflictWithSeedNode = inConflictWithSeedNode.get(); + + String conflictMsg = sb.toString(); + if (!conflictMsg.isEmpty()) { + if (this.isInConflictWithSeedNode) + log.warn("Conflict with seed nodes: {}", conflictMsg); + else if (this.isInConflictWithNonSeedNode) + log.info("Conflict with non-seed nodes: {}", conflictMsg); + } + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onProposalStateBlockChainChanged); + } + + return changed.get(); + } + + private boolean isFirstBlockOfBlindVotePhase(int blockHeight) { + return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.BLIND_VOTE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.java new file mode 100644 index 0000000000..bc527eeb6e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.dao.monitoring.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class BlindVoteStateBlock extends StateBlock { + public BlindVoteStateBlock(BlindVoteStateHash myBlindVoteStateHash) { + super(myBlindVoteStateHash); + } + + public int getNumBlindVotes() { + return myStateHash.getNumBlindVotes(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java new file mode 100644 index 0000000000..79a59032c7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.dao.monitoring.model; + + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) + +public final class BlindVoteStateHash extends StateHash { + @Getter + private final int numBlindVotes; + + public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numBlindVotes) { + super(cycleStartBlockHeight, hash, prevHash); + this.numBlindVotes = numBlindVotes; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.BlindVoteStateHash toProtoMessage() { + return protobuf.BlindVoteStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)) + .setNumBlindVotes(numBlindVotes).build(); + } + + public static BlindVoteStateHash fromProto(protobuf.BlindVoteStateHash proto) { + return new BlindVoteStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray(), + proto.getNumBlindVotes()); + } + + @Override + public String toString() { + return "BlindVoteStateHash{" + + "\n numBlindVotes=" + numBlindVotes + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java new file mode 100644 index 0000000000..8bd91e67fa --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.core.dao.monitoring.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class DaoStateBlock extends StateBlock { + public DaoStateBlock(DaoStateHash myDaoStateHash) { + super(myDaoStateHash); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java new file mode 100644 index 0000000000..e0e7f5e403 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.dao.monitoring.model; + + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class DaoStateHash extends StateHash { + public DaoStateHash(int height, byte[] hash, byte[] prevHash) { + super(height, hash, prevHash); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.DaoStateHash toProtoMessage() { + return protobuf.DaoStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)).build(); + } + + public static DaoStateHash fromProto(protobuf.DaoStateHash proto) { + return new DaoStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray()); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.java new file mode 100644 index 0000000000..ddfc83dbb6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.dao.monitoring.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class ProposalStateBlock extends StateBlock { + public ProposalStateBlock(ProposalStateHash myProposalStateHash) { + super(myProposalStateHash); + } + + public int getNumProposals() { + return myStateHash.getNumProposals(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java new file mode 100644 index 0000000000..dc2ccdeabf --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.dao.monitoring.model; + + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) + +public final class ProposalStateHash extends StateHash { + @Getter + private final int numProposals; + + public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numProposals) { + super(cycleStartBlockHeight, hash, prevHash); + this.numProposals = numProposals; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.ProposalStateHash toProtoMessage() { + return protobuf.ProposalStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)) + .setNumProposals(numProposals).build(); + } + + public static ProposalStateHash fromProto(protobuf.ProposalStateHash proto) { + return new ProposalStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray(), + proto.getNumProposals()); + } + + + @Override + public String toString() { + return "ProposalStateHash{" + + "\n numProposals=" + numProposals + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java new file mode 100644 index 0000000000..b41ddc2296 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.dao.monitoring.model; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Contains my StateHash at a particular block height and the received stateHash from our peers. + * The maps get updated over time, this is not an immutable class. + */ +@Getter +@EqualsAndHashCode +public abstract class StateBlock { + protected final T myStateHash; + + private final Map peersMap = new HashMap<>(); + private final Map inConflictMap = new HashMap<>(); + + StateBlock(T myStateHash) { + this.myStateHash = myStateHash; + } + + public void putInPeersMap(String peersNodeAddress, T stateHash) { + peersMap.putIfAbsent(peersNodeAddress, stateHash); + } + + public void putInConflictMap(String peersNodeAddress, T stateHash) { + inConflictMap.putIfAbsent(peersNodeAddress, stateHash); + } + + // Delegates + public int getHeight() { + return myStateHash.getHeight(); + } + + public byte[] getHash() { + return myStateHash.getHash(); + } + + public byte[] getPrevHash() { + return myStateHash.getPrevHash(); + } + + @Override + public String toString() { + return "StateBlock{" + + "\n myStateHash=" + myStateHash + + ",\n peersMap=" + peersMap + + ",\n inConflictMap=" + inConflictMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java new file mode 100644 index 0000000000..12cb299a11 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.monitoring.model; + + +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import java.util.Arrays; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Contains the blockHeight, the hash and the previous hash of the state. + * As the hash is created from the state at the particular height including the previous hash we get the history of + * the full chain included and we know if the hash matches at a particular height that all the past blocks need to match + * as well. + */ +@EqualsAndHashCode +@Getter +@Slf4j +public abstract class StateHash implements PersistablePayload, NetworkPayload { + protected final int height; + protected final byte[] hash; + // For first block the prevHash is an empty byte array + protected final byte[] prevHash; + + StateHash(int height, byte[] hash, byte[] prevHash) { + this.height = height; + this.hash = hash; + this.prevHash = prevHash; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean hasEqualHash(StateHash other) { + return Arrays.equals(hash, other.getHash()); + } + + public byte[] getHash() { + return hash; + } + + @Override + public String toString() { + return "StateHash{" + + "\n height=" + height + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n prevHash=" + Utilities.bytesAsHexString(prevHash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java b/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java new file mode 100644 index 0000000000..1509023c92 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.dao.monitoring.model; + +import lombok.Value; + +@Value +public class UtxoMismatch { + private final int height; + private final long sumUtxo; + private final long sumBsq; + + public UtxoMismatch(int height, long sumUtxo, long sumBsq) { + this.height = height; + this.sumUtxo = sumUtxo; + this.sumBsq = sumBsq; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java new file mode 100644 index 0000000000..81bd46f8ee --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BlindVoteStateNetworkService extends StateNetworkService { + @Inject + public BlindVoteStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetBlindVoteStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetBlindVoteStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetBlindVoteStateHashesRequest; + } + + @Override + protected NewBlindVoteStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewBlindVoteStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewBlindVoteStateHashMessage; + } + + @Override + protected GetBlindVoteStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetBlindVoteStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewBlindVoteStateHashMessage getNewStateHashMessage(BlindVoteStateHash myStateHash) { + return new NewBlindVoteStateHashMessage(myStateHash); + } + + @Override + protected RequestBlindVoteStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestBlindVoteStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java b/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java new file mode 100644 index 0000000000..911db2e15a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.common.util.Utilities; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class Checkpoint { + final int height; + final byte[] hash; + @Setter + boolean passed; + + public Checkpoint(int height, byte[] hash) { + this.height = height; + this.hash = hash; + } + + @Override + public String toString() { + return "Checkpoint {" + + "\n height=" + height + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } + +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java new file mode 100644 index 0000000000..e394977b31 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoStateNetworkService extends StateNetworkService { + @Inject + public DaoStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetDaoStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetDaoStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetDaoStateHashesRequest; + } + + @Override + protected NewDaoStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewDaoStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewDaoStateHashMessage; + } + + @Override + protected GetDaoStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetDaoStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewDaoStateHashMessage getNewStateHashMessage(DaoStateHash myStateHash) { + return new NewDaoStateHashMessage(myStateHash); + } + + @Override + protected RequestDaoStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestDaoStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } + +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java new file mode 100644 index 0000000000..76e6f33bd1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.model.ProposalStateHash; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProposalStateNetworkService extends StateNetworkService { + @Inject + public ProposalStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetProposalStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetProposalStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetProposalStateHashesRequest; + } + + @Override + protected NewProposalStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewProposalStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewProposalStateHashMessage; + } + + @Override + protected GetProposalStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetProposalStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewProposalStateHashMessage getNewStateHashMessage(ProposalStateHash myStateHash) { + return new NewProposalStateHashMessage(myStateHash); + } + + @Override + protected RequestProposalStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestProposalStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } + +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java new file mode 100644 index 0000000000..5f33b05a48 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestBlindVoteStateHashesHandler extends RequestStateHashesHandler { + RequestBlindVoteStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetBlindVoteStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetBlindVoteStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetBlindVoteStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetBlindVoteStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetBlindVoteStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java new file mode 100644 index 0000000000..6a7b7bdcc7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestDaoStateHashesHandler extends RequestStateHashesHandler { + RequestDaoStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetDaoStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetDaoStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetDaoStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetDaoStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetDaoStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java new file mode 100644 index 0000000000..0e4a20502e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestProposalStateHashesHandler extends RequestStateHashesHandler { + RequestProposalStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetProposalStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetProposalStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetProposalStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetProposalStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetProposalStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java new file mode 100644 index 0000000000..602677121b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java @@ -0,0 +1,220 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Optional; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Slf4j +abstract class RequestStateHashesHandler implements MessageListener { + private static final long TIMEOUT = 120; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onComplete(Res getStateHashesResponse, Optional peersNodeAddress); + + @SuppressWarnings("UnusedParameters") + void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final NodeAddress nodeAddress; + private final Listener listener; + private Timer timeoutTimer; + final int nonce = new Random().nextInt(); + private boolean stopped; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + RequestStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.nodeAddress = nodeAddress; + this.listener = listener; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract Req getGetStateHashesRequest(int fromHeight); + + protected abstract Res castToGetStateHashesResponse(NetworkEnvelope networkEnvelope); + + protected abstract boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestStateHashes(int fromHeight) { + if (!stopped) { + Req getStateHashesRequest = getGetStateHashesRequest(fromHeight); + if (timeoutTimer == null) { + timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + if (!stopped) { + String errorMessage = "A timeout occurred at sending getStateHashesRequest:" + getStateHashesRequest + + " on peersNodeAddress:" + nodeAddress; + log.debug(errorMessage + " / RequestStateHashesHandler=" + RequestStateHashesHandler.this); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT); + } else { + log.trace("We have stopped already. We ignore that timeoutTimer.run call. " + + "Might be caused by a previous networkNode.sendMessage.onFailure."); + } + }, + TIMEOUT); + } + + log.debug("We send to peer {} a {}.", nodeAddress, getStateHashesRequest); + networkNode.addMessageListener(this); + SettableFuture future = networkNode.sendMessage(nodeAddress, getStateHashesRequest); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.info("Sending of {} message to peer {} succeeded.", + getStateHashesRequest.getClass().getSimpleName(), + nodeAddress.getFullAddress()); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + + "Might be caused by a previous timeout."); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getStateHashesRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "getStateHashesRequest=" + getStateHashesRequest + "." + + "\n\tException=" + throwable.getMessage(); + log.error(errorMessage); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + + "Might be caused by a previous timeout."); + } + } + }, MoreExecutors.directExecutor()); + } else { + log.warn("We have stopped already. We ignore that requestProposalsHash call."); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (isGetStateHashesResponse(networkEnvelope)) { + if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(nodeAddress)) { + if (!stopped) { + Res getStateHashesResponse = castToGetStateHashesResponse(networkEnvelope); + if (getStateHashesResponse.getRequestNonce() == nonce) { + stopTimeoutTimer(); + cleanup(); + log.info("We received from peer {} a {} with {} stateHashes", + nodeAddress.getFullAddress(), getStateHashesResponse.getClass().getSimpleName(), + getStateHashesResponse.getStateHashes().size()); + listener.onComplete(getStateHashesResponse, connection.getPeersNodeAddressOptional()); + } else { + log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " + + "handshake (timeout causes connection close but peer might have sent a msg before " + + "connection was closed).\n\t" + + "We drop that message. nonce={} / requestNonce={}", + nonce, getStateHashesResponse.getRequestNonce()); + } + } else { + log.warn("We have stopped already."); + } + } else if (connection.getPeersNodeAddressOptional().isPresent()) { + log.debug("{}: We got a message from another node. We ignore that.", + this.getClass().getSimpleName()); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("UnusedParameters") + private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) { + cleanup(); + peerManager.handleConnectionFault(nodeAddress); + listener.onFault(errorMessage, null); + } + + private void cleanup() { + stopped = true; + networkNode.removeMessageListener(this); + stopTimeoutTimer(); + } + + private void stopTimeoutTimer() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java new file mode 100644 index 0000000000..8300c76b23 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java @@ -0,0 +1,204 @@ +/* + * 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 bisq.core.dao.monitoring.network; + +import bisq.core.dao.monitoring.model.StateHash; +import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class StateNetworkService, + Han extends RequestStateHashesHandler, + StH extends StateHash> implements MessageListener { + + public interface Listener { + void onNewStateHashMessage(Msg newStateHashMessage, Connection connection); + + void onGetStateHashRequest(Connection connection, Req getStateHashRequest); + + void onPeersStateHashes(List stateHashes, Optional peersNodeAddress); + } + + protected final NetworkNode networkNode; + protected final PeerManager peerManager; + private final Broadcaster broadcaster; + + @Getter + private final Map requestStateHashHandlerMap = new HashMap<>(); + private final List> listeners = new CopyOnWriteArrayList<>(); + private boolean messageListenerAdded; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public StateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.broadcaster = broadcaster; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract Req castToGetStateHashRequest(NetworkEnvelope networkEnvelope); + + + protected abstract boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope); + + + protected abstract Msg castToNewStateHashMessage(NetworkEnvelope networkEnvelope); + + + protected abstract boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope); + + protected abstract Res getGetStateHashesResponse(int nonce, List stateHashes); + + protected abstract Msg getNewStateHashMessage(StH myStateHash); + + protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (isNewStateHashMessage(networkEnvelope)) { + Msg newStateHashMessage = castToNewStateHashMessage(networkEnvelope); + log.debug("We received a {} from peer {} with stateHash={} ", + newStateHashMessage.getClass().getSimpleName(), + connection.getPeersNodeAddressOptional(), + newStateHashMessage.getStateHash()); + listeners.forEach(e -> e.onNewStateHashMessage(newStateHashMessage, connection)); + } else if (isGetStateHashesRequest(networkEnvelope)) { + Req getStateHashRequest = castToGetStateHashRequest(networkEnvelope); + log.debug("We received a {} from peer {} for height={} ", + getStateHashRequest.getClass().getSimpleName(), + connection.getPeersNodeAddressOptional(), + getStateHashRequest.getHeight()); + listeners.forEach(e -> e.onGetStateHashRequest(connection, getStateHashRequest)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListeners() { + if (!messageListenerAdded) { + networkNode.addMessageListener(this); + messageListenerAdded = true; + } + } + + public void sendGetStateHashesResponse(Connection connection, int nonce, List stateHashes) { + Res getStateHashesResponse = getGetStateHashesResponse(nonce, stateHashes); + log.info("Send {} with {} stateHashes to peer {}", getStateHashesResponse.getClass().getSimpleName(), + stateHashes.size(), connection.getPeersNodeAddressOptional()); + connection.sendMessage(getStateHashesResponse); + } + + public void requestHashesFromAllConnectedSeedNodes(int fromHeight) { + networkNode.getConfirmedConnections().stream() + .filter(peerManager::isSeedNode) + .forEach(connection -> connection.getPeersNodeAddressOptional() + .ifPresent(e -> requestHashesFromSeedNode(fromHeight, e))); + } + + public void broadcastMyStateHash(StH myStateHash) { + NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash); + broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress()); + } + + public void requestHashes(int fromHeight, String peersAddress) { + requestHashesFromSeedNode(fromHeight, new NodeAddress(peersAddress)); + } + + public void reset() { + requestStateHashHandlerMap.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestHashesFromSeedNode(int fromHeight, NodeAddress nodeAddress) { + RequestStateHashesHandler.Listener listener = new RequestStateHashesHandler.Listener<>() { + @Override + public void onComplete(Res getStateHashesResponse, Optional peersNodeAddress) { + requestStateHashHandlerMap.remove(nodeAddress); + List stateHashes = getStateHashesResponse.getStateHashes(); + listeners.forEach(e -> e.onPeersStateHashes(stateHashes, peersNodeAddress)); + } + + @Override + public void onFault(String errorMessage, @Nullable Connection connection) { + log.warn("requestDaoStateHashesHandler with outbound connection failed.\n\tnodeAddress={}\n\t" + + "ErrorMessage={}", nodeAddress, errorMessage); + requestStateHashHandlerMap.remove(nodeAddress); + } + }; + Han requestStateHashesHandler = getRequestStateHashesHandler(nodeAddress, listener); + requestStateHashHandlerMap.put(nodeAddress, requestStateHashesHandler); + requestStateHashesHandler.requestStateHashes(fromHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java new file mode 100644 index 0000000000..7505c3f9c4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetBlindVoteStateHashesRequest extends GetStateHashesRequest { + public GetBlindVoteStateHashesRequest(int fromCycleStartHeight, int nonce) { + super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlindVoteStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetBlindVoteStateHashesRequest(protobuf.GetBlindVoteStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetBlindVoteStateHashesRequest proto, int messageVersion) { + return new GetBlindVoteStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java new file mode 100644 index 0000000000..ead682dd1d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import bisq.network.p2p.InitialDataRequest; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetBlindVoteStateHashesResponse extends GetStateHashesResponse { + public GetBlindVoteStateHashesResponse(List stateHashes, int requestNonce) { + super(stateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlindVoteStateHashesResponse(List stateHashes, + int requestNonce, + int messageVersion) { + super(stateHashes, requestNonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetBlindVoteStateHashesResponse(protobuf.GetBlindVoteStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(BlindVoteStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetBlindVoteStateHashesResponse proto, int messageVersion) { + return new GetBlindVoteStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(BlindVoteStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } + + @Override + public Class associatedRequest() { + return GetBlindVoteStateHashesRequest.class; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java new file mode 100644 index 0000000000..c9dc3af482 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetDaoStateHashesRequest extends GetStateHashesRequest { + public GetDaoStateHashesRequest(int height, int nonce) { + super(height, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetDaoStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetDaoStateHashesRequest(protobuf.GetDaoStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetDaoStateHashesRequest proto, int messageVersion) { + return new GetDaoStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java new file mode 100644 index 0000000000..899f10c612 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.DaoStateHash; + +import bisq.network.p2p.InitialDataRequest; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetDaoStateHashesResponse extends GetStateHashesResponse { + public GetDaoStateHashesResponse(List daoStateHashes, int requestNonce) { + super(daoStateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetDaoStateHashesResponse(List daoStateHashes, + int requestNonce, + int messageVersion) { + super(daoStateHashes, requestNonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetDaoStateHashesResponse(protobuf.GetDaoStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(DaoStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetDaoStateHashesResponse proto, int messageVersion) { + return new GetDaoStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(DaoStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } + + @Override + public Class associatedRequest() { + return GetDaoStateHashesRequest.class; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java new file mode 100644 index 0000000000..34f80ad39b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetProposalStateHashesRequest extends GetStateHashesRequest { + public GetProposalStateHashesRequest(int fromCycleStartHeight, int nonce) { + super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetProposalStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetProposalStateHashesRequest(protobuf.GetProposalStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetProposalStateHashesRequest proto, int messageVersion) { + return new GetProposalStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java new file mode 100644 index 0000000000..1f5ce21c20 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import bisq.network.p2p.InitialDataRequest; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetProposalStateHashesResponse extends GetStateHashesResponse { + public GetProposalStateHashesResponse(List proposalStateHashes, int requestNonce) { + super(proposalStateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetProposalStateHashesResponse(List proposalStateHashes, + int requestNonce, + int messageVersion) { + super(proposalStateHashes, requestNonce, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetProposalStateHashesResponse(protobuf.GetProposalStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(ProposalStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetProposalStateHashesResponse proto, int messageVersion) { + return new GetProposalStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(ProposalStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } + + @Override + public Class associatedRequest() { + return GetProposalStateHashesRequest.class; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java new file mode 100644 index 0000000000..f62926040f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.InitialDataRequest; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class GetStateHashesRequest extends NetworkEnvelope implements DirectMessage, + CapabilityRequiringPayload, InitialDataRequest { + protected final int height; + protected final int nonce; + + protected GetStateHashesRequest(int height, int nonce, int messageVersion) { + super(messageVersion); + this.height = height; + this.nonce = nonce; + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } + + @Override + public String toString() { + return "GetStateHashesRequest{" + + ",\n height=" + height + + ",\n nonce=" + nonce + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java new file mode 100644 index 0000000000..b33cda4b4d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.StateHash; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.ExtendedDataSizePermission; +import bisq.network.p2p.InitialDataResponse; + +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class GetStateHashesResponse extends NetworkEnvelope implements DirectMessage, + ExtendedDataSizePermission, InitialDataResponse { + protected final List stateHashes; + protected final int requestNonce; + + protected GetStateHashesResponse(List stateHashes, + int requestNonce, + int messageVersion) { + super(messageVersion); + this.stateHashes = stateHashes; + this.requestNonce = requestNonce; + } + + @Override + public String toString() { + return "GetStateHashesResponse{" + + "\n stateHashes=" + stateHashes + + ",\n requestNonce=" + requestNonce + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java new file mode 100644 index 0000000000..986371ce58 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewBlindVoteStateHashMessage extends NewStateHashMessage { + public NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash) { + super(stateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash, int messageVersion) { + super(stateHash, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewBlindVoteStateHashMessage(protobuf.NewBlindVoteStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.NewBlindVoteStateHashMessage proto, int messageVersion) { + return new NewBlindVoteStateHashMessage(BlindVoteStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java new file mode 100644 index 0000000000..adc3523e44 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.DaoStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewDaoStateHashMessage extends NewStateHashMessage { + public NewDaoStateHashMessage(DaoStateHash daoStateHash) { + super(daoStateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewDaoStateHashMessage(DaoStateHash daoStateHash, int messageVersion) { + super(daoStateHash, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewDaoStateHashMessage(protobuf.NewDaoStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.NewDaoStateHashMessage proto, int messageVersion) { + return new NewDaoStateHashMessage(DaoStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java new file mode 100644 index 0000000000..eaf38180a2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewProposalStateHashMessage extends NewStateHashMessage { + public NewProposalStateHashMessage(ProposalStateHash proposalStateHash) { + super(proposalStateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewProposalStateHashMessage(ProposalStateHash proposalStateHash, int messageVersion) { + super(proposalStateHash, messageVersion); + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewProposalStateHashMessage(protobuf.NewProposalStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.NewProposalStateHashMessage proto, int messageVersion) { + return new NewProposalStateHashMessage(ProposalStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java new file mode 100644 index 0000000000..4d2f05d235 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.dao.monitoring.network.messages; + +import bisq.core.dao.monitoring.model.StateHash; + +import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class NewStateHashMessage extends BroadcastMessage implements CapabilityRequiringPayload { + protected final T stateHash; + + protected NewStateHashMessage(T stateHash, int messageVersion) { + super(messageVersion); + this.stateHash = stateHash; + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } + + @Override + public String toString() { + return "NewStateHashMessage{" + + "\n stateHash=" + stateHash + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java new file mode 100644 index 0000000000..2e0bdfef4d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -0,0 +1,296 @@ +/* + * 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 bisq.core.dao.node; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.node.explorer.ExportJsonFilesService; +import bisq.core.dao.node.full.RawBlock; +import bisq.core.dao.node.parser.BlockParser; +import bisq.core.dao.node.parser.exceptions.BlockHashNotConnectingException; +import bisq.core.dao.node.parser.exceptions.BlockHeightNotConnectingException; +import bisq.core.dao.node.parser.exceptions.RequiredReorgFromSnapshotException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.P2PServiceListener; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Base class for the lite and full node. + * It is responsible or the setup of the parser and snapshot management. + */ +@Slf4j +public abstract class BsqNode implements DaoSetupService { + private final BlockParser blockParser; + private final P2PService p2PService; + protected final DaoStateService daoStateService; + private final String genesisTxId; + private final int genesisBlockHeight; + private final ExportJsonFilesService exportJsonFilesService; + private final DaoStateSnapshotService daoStateSnapshotService; + private final P2PServiceListener p2PServiceListener; + protected boolean parseBlockchainComplete; + protected boolean p2pNetworkReady; + @Nullable + protected Consumer errorMessageHandler; + @Nullable + protected Consumer warnMessageHandler; + private final List pendingBlocks = new ArrayList<>(); + + // The chain height of the latest Block we either get reported by Bitcoin Core or from the seed node + // This property should not be used in consensus code but only for retrieving blocks as it is not in sync with the + // parsing and the daoState. It also does not represent the latest blockHeight but the currently received + // (not parsed) block. + @Getter + protected int chainTipHeight; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BsqNode(BlockParser blockParser, + DaoStateService daoStateService, + DaoStateSnapshotService daoStateSnapshotService, + P2PService p2PService, + ExportJsonFilesService exportJsonFilesService) { + this.blockParser = blockParser; + this.daoStateService = daoStateService; + this.daoStateSnapshotService = daoStateSnapshotService; + this.p2PService = p2PService; + this.exportJsonFilesService = exportJsonFilesService; + + genesisTxId = daoStateService.getGenesisTxId(); + genesisBlockHeight = daoStateService.getGenesisBlockHeight(); + + p2PServiceListener = new P2PServiceListener() { + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } + + @Override + public void onDataReceived() { + } + + @Override + public void onNoSeedNodeAvailable() { + onP2PNetworkReady(); + } + + @Override + public void onNoPeersAvailable() { + } + + @Override + public void onUpdatedDataReceived() { + onP2PNetworkReady(); + } + }; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public abstract void start(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setErrorMessageHandler(@SuppressWarnings("NullableProblems") Consumer errorMessageHandler) { + this.errorMessageHandler = errorMessageHandler; + } + + public void setWarnMessageHandler(@SuppressWarnings("NullableProblems") Consumer warnMessageHandler) { + this.warnMessageHandler = warnMessageHandler; + } + + public void shutDown() { + exportJsonFilesService.shutDown(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess") + protected void onInitialized() { + daoStateSnapshotService.applySnapshot(false); + + if (p2PService.isBootstrapped()) { + log.info("onAllServicesInitialized: isBootstrapped"); + onP2PNetworkReady(); + } else { + p2PService.addP2PServiceListener(p2PServiceListener); + } + } + + @SuppressWarnings("WeakerAccess") + protected void onP2PNetworkReady() { + p2pNetworkReady = true; + p2PService.removeP2PServiceListener(p2PServiceListener); + } + + @SuppressWarnings("WeakerAccess") + protected int getStartBlockHeight() { + int chainHeight = daoStateService.getChainHeight(); + int startBlockHeight = chainHeight; + if (chainHeight > genesisBlockHeight) + startBlockHeight = chainHeight + 1; + + log.info("getStartBlockHeight:\n" + + " Start block height={}\n" + + " Genesis txId={}\n" + + " Genesis block height={}\n" + + " Block height={}\n", + startBlockHeight, + genesisTxId, + genesisBlockHeight, + chainHeight); + + return startBlockHeight; + } + + protected abstract void startParseBlocks(); + + protected void onParseBlockChainComplete() { + log.info("onParseBlockChainComplete"); + parseBlockchainComplete = true; + daoStateService.onParseBlockChainComplete(); + + maybeExportToJson(); + } + + @SuppressWarnings("WeakerAccess") + protected void startReOrgFromLastSnapshot() { + daoStateSnapshotService.applySnapshot(true); + } + + + protected Optional doParseBlock(RawBlock rawBlock) throws RequiredReorgFromSnapshotException { + // We check if we have a block with that height. If so we return. We do not use the chainHeight as with genesis + // height we have no block but chainHeight is initially set to genesis height (bad design ;-( but a bit tricky + // to change now as it used in many areas.) + if (daoStateService.getBlockAtHeight(rawBlock.getHeight()).isPresent()) { + log.info("We have already a block with the height of the new block. Height of new block={}", rawBlock.getHeight()); + return Optional.empty(); + } + + try { + Block block = blockParser.parseBlock(rawBlock); + + pendingBlocks.remove(rawBlock); + + // After parsing we check if we have pending blocks we might have received earlier but which have been + // not connecting from the latest height we had. The list is sorted by height + if (!pendingBlocks.isEmpty()) { + // We take only first element after sorting (so it is the block with the next height) to avoid that + // we would repeat calls in recursions in case we would iterate the list. + pendingBlocks.sort(Comparator.comparing(RawBlock::getHeight)); + RawBlock nextPending = pendingBlocks.get(0); + if (nextPending.getHeight() == daoStateService.getChainHeight() + 1) + doParseBlock(nextPending); + } + + return Optional.of(block); + } catch (BlockHeightNotConnectingException e) { + // There is no guaranteed order how we receive blocks. We could have received block 102 before 101. + // If block is in the future we move the block to the pendingBlocks list. At next block we look up the + // list if there is any potential candidate with the correct height and if so we remove that from that list. + + int heightForNextBlock = daoStateService.getChainHeight() + 1; + if (rawBlock.getHeight() > heightForNextBlock) { + if (!pendingBlocks.contains(rawBlock)) { + pendingBlocks.add(rawBlock); + log.info("We received a block with a future block height. We store it as pending and try to apply " + + "it at the next block. rawBlock: height/hash={}/{}", rawBlock.getHeight(), rawBlock.getHash()); + } else { + log.warn("We received a block with a future block height but we had it already added to our pendingBlocks."); + } + } else if (rawBlock.getHeight() >= daoStateService.getGenesisBlockHeight()) { + // We received an older block. We compare if we have it in our chain. + Optional optionalBlock = daoStateService.getBlockAtHeight(rawBlock.getHeight()); + if (optionalBlock.isPresent()) { + if (optionalBlock.get().getHash().equals(rawBlock.getPreviousBlockHash())) { + log.info("We received an old block we have already parsed and added. We ignore it."); + } else { + log.info("We received an old block with a different hash. We ignore it. Hash={}", rawBlock.getHash()); + } + } else { + log.info("In case we have reset from genesis height we would not find the block"); + } + } else { + log.info("We ignore it as it was before genesis height"); + } + } catch (BlockHashNotConnectingException throwable) { + Optional lastBlock = daoStateService.getLastBlock(); + log.warn("Block not connecting:\n" + + "New block height/hash/previousBlockHash={}/{}/{}, latest block height/hash={}/{}", + rawBlock.getHeight(), + rawBlock.getHash(), + rawBlock.getPreviousBlockHash(), + lastBlock.isPresent() ? lastBlock.get().getHeight() : "lastBlock not present", + lastBlock.isPresent() ? lastBlock.get().getHash() : "lastBlock not present"); + + pendingBlocks.clear(); + startReOrgFromLastSnapshot(); + throw new RequiredReorgFromSnapshotException(rawBlock); + } + return Optional.empty(); + } + + protected void maybeExportToJson() { + exportJsonFilesService.maybeExportToJson(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/BsqNodeProvider.java b/core/src/main/java/bisq/core/dao/node/BsqNodeProvider.java new file mode 100644 index 0000000000..933a941419 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/BsqNodeProvider.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.dao.node; + +import bisq.core.dao.node.full.FullNode; +import bisq.core.dao.node.lite.LiteNode; +import bisq.core.user.Preferences; + +import com.google.inject.Inject; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Returns a FullNode or LiteNode based on the Config.FULL_DAO_NODE option. + */ +@Slf4j +public class BsqNodeProvider { + @Getter + private final BsqNode bsqNode; + + @Inject + public BsqNodeProvider(LiteNode bsqLiteNode, + FullNode bsqFullNode, + Preferences preferences) { + + boolean rpcDataSet = preferences.getRpcUser() != null && + !preferences.getRpcUser().isEmpty() + && preferences.getRpcPw() != null && + !preferences.getRpcPw().isEmpty() && + preferences.getBlockNotifyPort() > 0; + boolean daoFullNode = preferences.isDaoFullNode(); + if (daoFullNode && !rpcDataSet) { + log.warn("daoFullNode is set in preferences but RPC user and pw are missing. We reset daoFullNode in preferences to false."); + preferences.setDaoFullNode(false); + } + bsqNode = daoFullNode && rpcDataSet ? bsqFullNode : bsqLiteNode; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java new file mode 100644 index 0000000000..2d56e784dc --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java @@ -0,0 +1,262 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.PubKeyScript; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.file.JsonFileManager; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Utils; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ExportJsonFilesService implements DaoSetupService { + private final DaoStateService daoStateService; + private final File storageDir; + private final boolean dumpBlockchainData; + + private final ListeningExecutorService executor = Utilities.getListeningExecutorService("JsonExporter", + 1, 1, 1200); + private JsonFileManager txFileManager, txOutputFileManager, bsqStateFileManager; + + @Inject + public ExportJsonFilesService(DaoStateService daoStateService, + @Named(Config.STORAGE_DIR) File storageDir, + @Named(Config.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) { + this.daoStateService = daoStateService; + this.storageDir = storageDir; + this.dumpBlockchainData = dumpBlockchainData; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + if (dumpBlockchainData) { + File jsonDir = new File(Paths.get(storageDir.getAbsolutePath(), "json").toString()); + File txDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "tx").toString()); + File txOutputDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "txo").toString()); + File bsqStateDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "all").toString()); + try { + if (txDir.exists()) + FileUtil.deleteDirectory(txDir); + if (txOutputDir.exists()) + FileUtil.deleteDirectory(txOutputDir); + if (bsqStateDir.exists()) + FileUtil.deleteDirectory(bsqStateDir); + if (jsonDir.exists()) + FileUtil.deleteDirectory(jsonDir); + } catch (IOException e) { + log.error(e.toString()); + e.printStackTrace(); + } + + if (!jsonDir.mkdir()) + log.warn("make jsonDir failed.\njsonDir=" + jsonDir.getAbsolutePath()); + + if (!txDir.mkdir()) + log.warn("make txDir failed.\ntxDir=" + txDir.getAbsolutePath()); + + if (!txOutputDir.mkdir()) + log.warn("make txOutputDir failed.\ntxOutputDir=" + txOutputDir.getAbsolutePath()); + + if (!bsqStateDir.mkdir()) + log.warn("make bsqStateDir failed.\nbsqStateDir=" + bsqStateDir.getAbsolutePath()); + + txFileManager = new JsonFileManager(txDir); + txOutputFileManager = new JsonFileManager(txOutputDir); + bsqStateFileManager = new JsonFileManager(bsqStateDir); + } + } + + public void shutDown() { + if (dumpBlockchainData && txFileManager != null) { + txFileManager.shutDown(); + txOutputFileManager.shutDown(); + bsqStateFileManager.shutDown(); + } + } + + public void maybeExportToJson() { + if (dumpBlockchainData && + daoStateService.isParseBlockChainComplete()) { + // We store the data we need once we write the data to disk (in the thread) locally. + // Access to daoStateService is single threaded, we must not access daoStateService from the thread. + List allJsonTxOutputs = new ArrayList<>(); + + List jsonTxs = daoStateService.getUnorderedTxStream() + .map(tx -> { + JsonTx jsonTx = getJsonTx(tx); + allJsonTxOutputs.addAll(jsonTx.getOutputs()); + return jsonTx; + }).collect(Collectors.toList()); + + DaoState daoState = daoStateService.getClone(); + List jsonBlockList = daoState.getBlocks().stream() + .map(this::getJsonBlock) + .collect(Collectors.toList()); + JsonBlocks jsonBlocks = new JsonBlocks(daoState.getChainHeight(), jsonBlockList); + + ListenableFuture future = executor.submit(() -> { + bsqStateFileManager.writeToDisc(Utilities.objectToJson(jsonBlocks), "blocks"); + allJsonTxOutputs.forEach(jsonTxOutput -> txOutputFileManager.writeToDisc(Utilities.objectToJson(jsonTxOutput), jsonTxOutput.getId())); + jsonTxs.forEach(jsonTx -> txFileManager.writeToDisc(Utilities.objectToJson(jsonTx), jsonTx.getId())); + return null; + }); + + Futures.addCallback(future, Utilities.failureCallback(throwable -> { + log.error(throwable.toString()); + throwable.printStackTrace(); + }), MoreExecutors.directExecutor()); + } + } + + private JsonBlock getJsonBlock(Block block) { + List jsonTxs = block.getTxs().stream() + .map(this::getJsonTx) + .collect(Collectors.toList()); + return new JsonBlock(block.getHeight(), + block.getTime(), + block.getHash(), + block.getPreviousBlockHash(), + jsonTxs); + } + + private JsonTx getJsonTx(Tx tx) { + JsonTxType jsonTxType = getJsonTxType(tx); + String jsonTxTypeDisplayString = getJsonTxTypeDisplayString(jsonTxType); + return new JsonTx(tx.getId(), + tx.getBlockHeight(), + tx.getBlockHash(), + tx.getTime(), + getJsonTxInputs(tx), + getJsonTxOutputs(tx), + jsonTxType, + jsonTxTypeDisplayString, + tx.getBurntFee(), + tx.getInvalidatedBsq(), + tx.getUnlockBlockHeight()); + } + + private List getJsonTxInputs(Tx tx) { + return tx.getTxInputs().stream() + .map(txInput -> { + Optional optionalTxOutput = daoStateService.getConnectedTxOutput(txInput); + if (optionalTxOutput.isPresent()) { + TxOutput connectedTxOutput = optionalTxOutput.get(); + boolean isBsqTxOutputType = daoStateService.isBsqTxOutputType(connectedTxOutput); + return new JsonTxInput(txInput.getConnectedTxOutputIndex(), + txInput.getConnectedTxOutputTxId(), + connectedTxOutput.getValue(), + isBsqTxOutputType, + connectedTxOutput.getAddress(), + tx.getTime()); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private List getJsonTxOutputs(Tx tx) { + JsonTxType jsonTxType = getJsonTxType(tx); + String jsonTxTypeDisplayString = getJsonTxTypeDisplayString(jsonTxType); + return tx.getTxOutputs().stream() + .map(txOutput -> { + boolean isBsqTxOutputType = daoStateService.isBsqTxOutputType(txOutput); + long bsqAmount = isBsqTxOutputType ? txOutput.getValue() : 0; + long btcAmount = !isBsqTxOutputType ? txOutput.getValue() : 0; + PubKeyScript pubKeyScript = txOutput.getPubKeyScript(); + JsonScriptPubKey scriptPubKey = pubKeyScript != null ? new JsonScriptPubKey(pubKeyScript) : null; + JsonSpentInfo spentInfo = daoStateService.getSpentInfo(txOutput).map(JsonSpentInfo::new).orElse(null); + JsonTxOutputType txOutputType = JsonTxOutputType.valueOf(txOutput.getTxOutputType().name()); + int lockTime = txOutput.getLockTime(); + String opReturn = txOutput.getOpReturnData() != null ? Utils.HEX.encode(txOutput.getOpReturnData()) : null; + boolean isUnspent = daoStateService.isUnspent(txOutput.getKey()); + return new JsonTxOutput(tx.getId(), + txOutput.getIndex(), + bsqAmount, + btcAmount, + tx.getBlockHeight(), + isBsqTxOutputType, + tx.getBurntFee(), + tx.getInvalidatedBsq(), + txOutput.getAddress(), + scriptPubKey, + spentInfo, + tx.getTime(), + jsonTxType, + jsonTxTypeDisplayString, + txOutputType, + txOutputType.getDisplayString(), + opReturn, + lockTime, + isUnspent + ); + }) + .collect(Collectors.toList()); + } + + private String getJsonTxTypeDisplayString(JsonTxType jsonTxType) { + return jsonTxType != null ? jsonTxType.getDisplayString() : ""; + } + + private JsonTxType getJsonTxType(Tx tx) { + TxType txType = tx.getTxType(); + return txType != null ? JsonTxType.valueOf(txType.name()) : null; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonBlock.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonBlock.java new file mode 100644 index 0000000000..da93f0c012 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonBlock.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import java.util.List; + +import lombok.Value; + +@Value +class JsonBlock { + protected final int height; + protected final long time; // in ms + protected final String hash; + protected final String previousBlockHash; + private final List txs; +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonBlocks.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonBlocks.java new file mode 100644 index 0000000000..f896b1f2fa --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonBlocks.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import java.util.List; + +import lombok.Value; + +@Value +class JsonBlocks { + private int chainHeight; + private final List blocks; +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonScriptPubKey.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonScriptPubKey.java new file mode 100644 index 0000000000..60cecb41dd --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonScriptPubKey.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 bisq.core.dao.node.explorer; + +import bisq.core.dao.state.model.blockchain.PubKeyScript; + +import java.util.List; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +class JsonScriptPubKey { + private final List addresses; + private final String asm; + private final String hex; + private final int reqSigs; + private final String type; + + JsonScriptPubKey(PubKeyScript pubKeyScript) { + addresses = pubKeyScript.getAddresses(); + asm = pubKeyScript.getAsm(); + hex = pubKeyScript.getHex(); + reqSigs = pubKeyScript.getReqSigs(); + type = pubKeyScript.getScriptType().toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonSpentInfo.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonSpentInfo.java new file mode 100644 index 0000000000..0624fa5ac3 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonSpentInfo.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import bisq.core.dao.state.model.blockchain.SpentInfo; + +import lombok.Value; + +@Value +class JsonSpentInfo { + private final long height; + private final int inputIndex; + private final String txId; + + JsonSpentInfo(SpentInfo spentInfo) { + height = spentInfo.getBlockHeight(); + inputIndex = spentInfo.getInputIndex(); + txId = spentInfo.getTxId(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java new file mode 100644 index 0000000000..145826bf60 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import bisq.common.app.Version; + +import java.util.List; +import java.util.Objects; + +import lombok.Value; + +@Value +class JsonTx { + private final String txVersion = Version.BSQ_TX_VERSION; + private final String id; + private final int blockHeight; + private final String blockHash; + private final long time; + private final List inputs; + private final List outputs; + private final JsonTxType txType; + private final String txTypeDisplayString; + private final long burntFee; + private final long invalidatedBsq; + // If not set it is -1. LockTime of 0 is a valid value. + private final int unlockBlockHeight; + + JsonTx(String id, int blockHeight, String blockHash, long time, List inputs, + List outputs, JsonTxType txType, String txTypeDisplayString, long burntFee, + long invalidatedBsq, int unlockBlockHeight) { + this.id = id; + this.blockHeight = blockHeight; + this.blockHash = blockHash; + this.time = time; + this.inputs = inputs; + this.outputs = outputs; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.unlockBlockHeight = unlockBlockHeight; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JsonTx)) return false; + if (!super.equals(o)) return false; + JsonTx jsonTx = (JsonTx) o; + return blockHeight == jsonTx.blockHeight && + time == jsonTx.time && + burntFee == jsonTx.burntFee && + invalidatedBsq == jsonTx.invalidatedBsq && + unlockBlockHeight == jsonTx.unlockBlockHeight && + Objects.equals(txVersion, jsonTx.txVersion) && + Objects.equals(id, jsonTx.id) && + Objects.equals(blockHash, jsonTx.blockHash) && + Objects.equals(inputs, jsonTx.inputs) && + Objects.equals(outputs, jsonTx.outputs) && + txType.name().equals(jsonTx.txType.name()) && + Objects.equals(txTypeDisplayString, jsonTx.txTypeDisplayString); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txVersion, id, blockHeight, blockHash, time, inputs, outputs, + txType.name(), txTypeDisplayString, burntFee, invalidatedBsq, unlockBlockHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxInput.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxInput.java new file mode 100644 index 0000000000..6c7ea14222 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxInput.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +@Value +@Immutable +class JsonTxInput { + private final int spendingTxOutputIndex; // connectedTxOutputIndex + private final String spendingTxId; // connectedTxOutputTxId + private final long bsqAmount; + private final boolean isVerified; // isBsqTxOutputType + private final String address; + private final long time; +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java new file mode 100644 index 0000000000..65ce85374f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java @@ -0,0 +1,119 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import bisq.common.app.Version; + +import java.util.Objects; + +import lombok.Value; + +import javax.annotation.Nullable; + +@Value +class JsonTxOutput { + private final String txVersion = Version.BSQ_TX_VERSION; + private final String txId; + private final int index; + private final long bsqAmount; + private final long btcAmount; + private final int height; + private final boolean isVerified; // isBsqTxOutputType + private final long burntFee; + private final long invalidatedBsq; + private final String address; + @Nullable + private final JsonScriptPubKey scriptPubKey; + @Nullable + private final JsonSpentInfo spentInfo; + private final long time; + private final JsonTxType txType; + private final String txTypeDisplayString; + private final JsonTxOutputType txOutputType; + private final String txOutputTypeDisplayString; + @Nullable + private final String opReturn; + private final int lockTime; + private final boolean isUnspent; + + JsonTxOutput(String txId, int index, long bsqAmount, long btcAmount, int height, boolean isVerified, long burntFee, + long invalidatedBsq, String address, JsonScriptPubKey scriptPubKey, JsonSpentInfo spentInfo, + long time, JsonTxType txType, String txTypeDisplayString, JsonTxOutputType txOutputType, + String txOutputTypeDisplayString, String opReturn, int lockTime, boolean isUnspent) { + this.txId = txId; + this.index = index; + this.bsqAmount = bsqAmount; + this.btcAmount = btcAmount; + this.height = height; + this.isVerified = isVerified; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.address = address; + this.scriptPubKey = scriptPubKey; + this.spentInfo = spentInfo; + this.time = time; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.txOutputType = txOutputType; + this.txOutputTypeDisplayString = txOutputTypeDisplayString; + this.opReturn = opReturn; + this.lockTime = lockTime; + this.isUnspent = isUnspent; + } + + String getId() { + return txId + ":" + index; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JsonTxOutput)) return false; + if (!super.equals(o)) return false; + JsonTxOutput that = (JsonTxOutput) o; + return index == that.index && + bsqAmount == that.bsqAmount && + btcAmount == that.btcAmount && + height == that.height && + isVerified == that.isVerified && + burntFee == that.burntFee && + invalidatedBsq == that.invalidatedBsq && + time == that.time && + lockTime == that.lockTime && + isUnspent == that.isUnspent && + Objects.equals(txVersion, that.txVersion) && + Objects.equals(txId, that.txId) && + Objects.equals(address, that.address) && + Objects.equals(scriptPubKey, that.scriptPubKey) && + Objects.equals(spentInfo, that.spentInfo) && + txType.name().equals(that.txType.name()) && + Objects.equals(txTypeDisplayString, that.txTypeDisplayString) && + txOutputType == that.txOutputType && + Objects.equals(txOutputTypeDisplayString, that.txOutputTypeDisplayString) && + Objects.equals(opReturn, that.opReturn); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txVersion, txId, index, bsqAmount, btcAmount, height, isVerified, + burntFee, invalidatedBsq, address, scriptPubKey, spentInfo, time, txType.name(), txTypeDisplayString, + txOutputType, txOutputTypeDisplayString, opReturn, lockTime, isUnspent); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java new file mode 100644 index 0000000000..a5657bfaf8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import lombok.Getter; + +// Need to be in sync with TxOutputType +enum JsonTxOutputType { + UNDEFINED("Undefined"), + UNDEFINED_OUTPUT("Undefined output"), + GENESIS_OUTPUT("Genesis"), + BSQ_OUTPUT("BSQ"), + BTC_OUTPUT("BTC"), + PROPOSAL_OP_RETURN_OUTPUT("Proposal opReturn"), + COMP_REQ_OP_RETURN_OUTPUT("Compensation request opReturn"), + REIMBURSEMENT_OP_RETURN_OUTPUT("Reimbursement request opReturn"), + CONFISCATE_BOND_OP_RETURN_OUTPUT("Confiscate bond opReturn"), + ISSUANCE_CANDIDATE_OUTPUT("Issuance candidate"), + BLIND_VOTE_LOCK_STAKE_OUTPUT("Blind vote lock stake"), + BLIND_VOTE_OP_RETURN_OUTPUT("Blind vote opReturn"), + VOTE_REVEAL_UNLOCK_STAKE_OUTPUT("Vote reveal unlock stake"), + VOTE_REVEAL_OP_RETURN_OUTPUT("Vote reveal opReturn"), + ASSET_LISTING_FEE_OP_RETURN_OUTPUT("Asset listing fee OpReturn"), + PROOF_OF_BURN_OP_RETURN_OUTPUT("Proof of burn opReturn"), + LOCKUP_OUTPUT("Lockup"), + LOCKUP_OP_RETURN_OUTPUT("Lockup opReturn"), + UNLOCK_OUTPUT("Unlock"), + INVALID_OUTPUT("Invalid"); + + @Getter + private final String displayString; + + JsonTxOutputType(String displayString) { + this.displayString = displayString; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java new file mode 100644 index 0000000000..ee8ffd2527 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.dao.node.explorer; + +import lombok.Getter; + +// Need to be in sync with TxOutputType +enum JsonTxType { + UNDEFINED("Undefined"), + UNDEFINED_TX_TYPE("Undefined tx type"), + UNVERIFIED("Unverified"), + INVALID("Invalid"), + GENESIS("Genesis"), + TRANSFER_BSQ("Transfer BSQ"), + PAY_TRADE_FEE("Pay trade fee"), + PROPOSAL("Proposal"), + COMPENSATION_REQUEST("Compensation request"), + REIMBURSEMENT_REQUEST("Reimbursement request"), + BLIND_VOTE("Blind vote"), + VOTE_REVEAL("Vote reveal"), + LOCKUP("Lockup"), + UNLOCK("Unlock"), + ASSET_LISTING_FEE("Asset listing fee"), + PROOF_OF_BURN("Proof of burn"), + IRREGULAR("Irregular"); + + @Getter + private final String displayString; + + JsonTxType(String displayString) { + this.displayString = displayString; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/package-info.java b/core/src/main/java/bisq/core/dao/node/explorer/package-info.java new file mode 100644 index 0000000000..30fc3a6f49 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/explorer/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +/** + * Contains classes which are only used for providing data to the BSQ explorer. + */ + +package bisq.core.dao.node.explorer; diff --git a/core/src/main/java/bisq/core/dao/node/full/FullNode.java b/core/src/main/java/bisq/core/dao/node/full/FullNode.java new file mode 100644 index 0000000000..1bdd6b1ffb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/FullNode.java @@ -0,0 +1,292 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.node.BsqNode; +import bisq.core.dao.node.explorer.ExportJsonFilesService; +import bisq.core.dao.node.full.network.FullNodeNetworkService; +import bisq.core.dao.node.full.rpc.NotificationHandlerException; +import bisq.core.dao.node.parser.BlockParser; +import bisq.core.dao.node.parser.exceptions.BlockHashNotConnectingException; +import bisq.core.dao.node.parser.exceptions.BlockHeightNotConnectingException; +import bisq.core.dao.node.parser.exceptions.RequiredReorgFromSnapshotException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.network.ConnectionState; + +import bisq.common.UserThread; +import bisq.common.handlers.ResultHandler; + +import javax.inject.Inject; + +import java.net.ConnectException; + +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +/** + * Main class for a full node which have Bitcoin Core with rpc running and does the blockchain lookup itself. + * It also provides the BSQ transactions to lite nodes on request and broadcasts new BSQ blocks. + *

    + * TODO request p2p network data again after parsing is complete to be sure that in case we missed data during parsing + * we get it added. + */ +@Slf4j +public class FullNode extends BsqNode { + + private final RpcService rpcService; + private final FullNodeNetworkService fullNodeNetworkService; + private boolean addBlockHandlerAdded; + private int blocksToParseInBatch; + private long parseInBatchStartTime; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private FullNode(BlockParser blockParser, + DaoStateService daoStateService, + DaoStateSnapshotService daoStateSnapshotService, + P2PService p2PService, + RpcService rpcService, + ExportJsonFilesService exportJsonFilesService, + FullNodeNetworkService fullNodeNetworkService) { + super(blockParser, daoStateService, daoStateSnapshotService, p2PService, exportJsonFilesService); + this.rpcService = rpcService; + + this.fullNodeNetworkService = fullNodeNetworkService; + ConnectionState.setExpectedRequests(5); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() { + fullNodeNetworkService.start(); + + rpcService.setup(() -> { + super.onInitialized(); + startParseBlocks(); + }, + this::handleError); + } + + public void shutDown() { + super.shutDown(); + fullNodeNetworkService.shutDown(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void startParseBlocks() { + requestChainHeadHeightAndParseBlocks(getStartBlockHeight()); + } + + @Override + protected void startReOrgFromLastSnapshot() { + super.startReOrgFromLastSnapshot(); + + int startBlockHeight = getStartBlockHeight(); + rpcService.requestChainHeadHeight(chainHeight -> parseBlocksOnHeadHeight(startBlockHeight, chainHeight), + this::handleError); + } + + @Override + protected void onP2PNetworkReady() { + super.onP2PNetworkReady(); + + if (parseBlockchainComplete) { + addBlockHandler(); + int blockHeightOfLastBlock = daoStateService.getBlockHeightOfLastBlock(); + log.info("onP2PNetworkReady: We run parseBlocksIfNewBlockAvailable with latest block height {}.", blockHeightOfLastBlock); + parseBlocksIfNewBlockAvailable(blockHeightOfLastBlock); + } + } + + @Override + protected void onParseBlockChainComplete() { + super.onParseBlockChainComplete(); + + if (p2pNetworkReady) + addBlockHandler(); + else + log.info("onParseBlockChainComplete but P2P network is not ready yet."); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBlockHandler() { + if (!addBlockHandlerAdded) { + addBlockHandlerAdded = true; + rpcService.addNewDtoBlockHandler(rawBlock -> { + try { + // We need to call that before parsing to have set the chain tip correctly for clients + // which might listen for new blocks on daoStateService. DaoStateListener.onNewBlockHeight + // is called before the doParseBlock returns. + + // We only update chainTipHeight if we get a newer block + int blockHeight = rawBlock.getHeight(); + if (blockHeight > chainTipHeight) + chainTipHeight = blockHeight; + + doParseBlock(rawBlock).ifPresent(this::onNewBlock); + } catch (RequiredReorgFromSnapshotException ignore) { + } + }, + this::handleError); + } + } + + private void onNewBlock(Block block) { + maybeExportToJson(); + + if (p2pNetworkReady && parseBlockchainComplete) + fullNodeNetworkService.publishNewBlock(block); + } + + + private void parseBlocksIfNewBlockAvailable(int chainHeight) { + rpcService.requestChainHeadHeight(newChainHeight -> { + if (newChainHeight > chainHeight) { + log.info("During parsing new blocks have arrived. We parse again with those missing blocks." + + "ChainHeadHeight={}, newChainHeadHeight={}", chainHeight, newChainHeight); + parseBlocksOnHeadHeight(chainHeight + 1, newChainHeight); + } else { + log.info("parseBlocksIfNewBlockAvailable did not result in a new block, so we complete."); + log.info("parse {} blocks took {} seconds", blocksToParseInBatch, (System.currentTimeMillis() - parseInBatchStartTime) / 1000d); + if (!parseBlockchainComplete) { + onParseBlockChainComplete(); + } + } + }, + this::handleError); + } + + private void requestChainHeadHeightAndParseBlocks(int startBlockHeight) { + log.info("requestChainHeadHeightAndParseBlocks with startBlockHeight={}", startBlockHeight); + rpcService.requestChainHeadHeight(chainHeight -> parseBlocksOnHeadHeight(startBlockHeight, chainHeight), + this::handleError); + } + + private void parseBlocksOnHeadHeight(int startBlockHeight, int chainHeight) { + if (startBlockHeight <= chainHeight) { + blocksToParseInBatch = chainHeight - startBlockHeight; + parseInBatchStartTime = System.currentTimeMillis(); + log.info("parse {} blocks with startBlockHeight={} and chainHeight={}", blocksToParseInBatch, startBlockHeight, chainHeight); + chainTipHeight = chainHeight; + parseBlocks(startBlockHeight, + chainHeight, + this::onNewBlock, + () -> { + // We are done but it might be that new blocks have arrived in the meantime, + // so we try again with startBlockHeight set to current chainHeight + // We also set up the listener in the else main branch where we check + // if we are at chainTip, so do not include here another check as it would + // not trigger the listener registration. + parseBlocksIfNewBlockAvailable(chainHeight); + }, this::handleError); + } else { + log.warn("We are trying to start with a block which is above the chain height of Bitcoin Core. " + + "We need probably wait longer until Bitcoin Core has fully synced. " + + "We try again after a delay of 1 min."); + UserThread.runAfter(() -> requestChainHeadHeightAndParseBlocks(startBlockHeight), 60); + } + } + + private void parseBlocks(int startBlockHeight, + int chainHeight, + Consumer newBlockHandler, + ResultHandler resultHandler, + Consumer errorHandler) { + parseBlockRecursively(startBlockHeight, chainHeight, newBlockHandler, resultHandler, errorHandler); + } + + private void parseBlockRecursively(int blockHeight, + int chainHeight, + Consumer newBlockHandler, + ResultHandler resultHandler, + Consumer errorHandler) { + rpcService.requestDtoBlock(blockHeight, + rawBlock -> { + try { + doParseBlock(rawBlock).ifPresent(newBlockHandler); + + // Increment blockHeight and recursively call parseBlockAsync until we reach chainHeight + if (blockHeight < chainHeight) { + int newBlockHeight = blockHeight + 1; + parseBlockRecursively(newBlockHeight, chainHeight, newBlockHandler, resultHandler, errorHandler); + } else { + // We are done + resultHandler.handleResult(); + } + } catch (RequiredReorgFromSnapshotException ignore) { + // If we get a reorg we don't continue to call parseBlockRecursively + } + }, + errorHandler); + } + + private void handleError(Throwable throwable) { + if (throwable instanceof BlockHashNotConnectingException || throwable instanceof BlockHeightNotConnectingException) { + // We do not escalate that exception as it is handled with the snapshot manager to recover its state. + log.warn(throwable.toString()); + } else { + String errorMessage = "An error occurred: Error=" + throwable.toString(); + log.error(errorMessage); + throwable.printStackTrace(); + + if (throwable instanceof RpcException) { + Throwable cause = throwable.getCause(); + if (cause != null) { + if (cause instanceof ConnectException) { + if (warnMessageHandler != null) + warnMessageHandler.accept("You have configured Bisq to run as DAO full node but there is no " + + "localhost Bitcoin Core node detected. You need to have Bitcoin Core started and synced before " + + "starting Bisq. Please restart Bisq with proper DAO full node setup or switch to lite node mode."); + return; + } else if (cause instanceof NotificationHandlerException) { + log.error("Error from within block notification daemon: {}", cause.getCause().toString()); + startReOrgFromLastSnapshot(); + return; + } else if (cause instanceof Error) { + throw (Error) cause; + } + } + } + + if (errorMessageHandler != null) + errorMessageHandler.accept(errorMessage); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RawBlock.java b/core/src/main/java/bisq/core/dao/node/full/RawBlock.java new file mode 100644 index 0000000000..686ecb2dbe --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/RawBlock.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.state.model.blockchain.BaseBlock; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.common.proto.network.NetworkPayload; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +/** + * A block derived from the BTC blockchain and filtered for BSQ relevant transactions, though the transactions are not + * verified at that stage. That block is passed to lite nodes over the P2P network. The validation is done by the lite + * nodes themselves but the transactions are already filtered for BSQ only transactions to keep bandwidth requirements + * low. + * Sent over wire. + */ +@Immutable +@EqualsAndHashCode(callSuper = true) +@Value +public final class RawBlock extends BaseBlock implements NetworkPayload { + // Used when a full node sends a block over the P2P network + public static RawBlock fromBlock(Block block) { + ImmutableList txs = ImmutableList.copyOf(block.getTxs().stream().map(RawTx::fromTx).collect(Collectors.toList())); + return new RawBlock(block.getHeight(), + block.getTime(), + block.getHash(), + block.getPreviousBlockHash(), + txs); + } + + private final ImmutableList rawTxs; + + RawBlock(int height, + long time, + String hash, + String previousBlockHash, + ImmutableList rawTxs) { + super(height, + time, + hash, + previousBlockHash); + this.rawTxs = rawTxs; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.BaseBlock toProtoMessage() { + protobuf.RawBlock.Builder builder = protobuf.RawBlock.newBuilder() + .addAllRawTxs(rawTxs.stream() + .map(RawTx::toProtoMessage) + .collect(Collectors.toList())); + return getBaseBlockBuilder().setRawBlock(builder).build(); + } + + public static RawBlock fromProto(protobuf.BaseBlock proto) { + protobuf.RawBlock rawBlockProto = proto.getRawBlock(); + ImmutableList rawTxs = rawBlockProto.getRawTxsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(rawBlockProto.getRawTxsList().stream() + .map(RawTx::fromProto) + .collect(Collectors.toList())); + return new RawBlock(proto.getHeight(), + proto.getTime(), + proto.getHash(), + proto.getPreviousBlockHash(), + rawTxs); + } + + + @Override + public String toString() { + return "RawBlock{" + + "\n rawTxs=" + rawTxs + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RawTx.java b/core/src/main/java/bisq/core/dao/node/full/RawTx.java new file mode 100644 index 0000000000..bd291a0cd4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/RawTx.java @@ -0,0 +1,138 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxInput; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkPayload; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * RawTx as we get it from the RPC service (full node) or from via the P2P network (lite node). + * It contains pure bitcoin blockchain data without any BSQ specific data. + * Sent over wire. + */ +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class RawTx extends BaseTx implements NetworkPayload { + // Used when a full node sends a block over the P2P network + public static RawTx fromTx(Tx tx) { + ImmutableList rawTxOutputs = ImmutableList.copyOf(tx.getTxOutputs().stream() + .map(RawTxOutput::fromTxOutput) + .collect(Collectors.toList())); + + return new RawTx(tx.getTxVersion(), + tx.getId(), + tx.getBlockHeight(), + tx.getBlockHash(), + tx.getTime(), + tx.getTxInputs(), + rawTxOutputs); + } + + private final ImmutableList rawTxOutputs; + + // The RPC service is creating a RawTx. + public RawTx(String id, + int blockHeight, + String blockHash, + long time, + ImmutableList txInputs, + ImmutableList rawTxOutputs) { + super(Version.BSQ_TX_VERSION, + id, + blockHeight, + blockHash, + time, + txInputs); + this.rawTxOutputs = rawTxOutputs; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RawTx(String txVersion, + String id, + int blockHeight, + String blockHash, + long time, + ImmutableList txInputs, + ImmutableList rawTxOutputs) { + super(txVersion, + id, + blockHeight, + blockHash, + time, + txInputs); + this.rawTxOutputs = rawTxOutputs; + } + + @Override + public protobuf.BaseTx toProtoMessage() { + final protobuf.RawTx.Builder builder = protobuf.RawTx.newBuilder() + .addAllRawTxOutputs(rawTxOutputs.stream() + .map(RawTxOutput::toProtoMessage) + .collect(Collectors.toList())); + return getBaseTxBuilder().setRawTx(builder).build(); + } + + public static RawTx fromProto(protobuf.BaseTx protoBaseTx) { + ImmutableList txInputs = protoBaseTx.getTxInputsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(protoBaseTx.getTxInputsList().stream() + .map(TxInput::fromProto) + .collect(Collectors.toList())); + protobuf.RawTx protoRawTx = protoBaseTx.getRawTx(); + ImmutableList outputs = protoRawTx.getRawTxOutputsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(protoRawTx.getRawTxOutputsList().stream() + .map(RawTxOutput::fromProto) + .collect(Collectors.toList())); + return new RawTx(protoBaseTx.getTxVersion(), + protoBaseTx.getId(), + protoBaseTx.getBlockHeight(), + protoBaseTx.getBlockHash(), + protoBaseTx.getTime(), + txInputs, + outputs); + } + + @Override + public String toString() { + return "RawTx{" + + "\n rawTxOutputs=" + rawTxOutputs + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java b/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java new file mode 100644 index 0000000000..2d5599b2fb --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.state.model.blockchain.BaseTxOutput; +import bisq.core.dao.state.model.blockchain.PubKeyScript; +import bisq.core.dao.state.model.blockchain.TxOutput; + +import bisq.common.proto.network.NetworkPayload; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * TxOutput used in RawTx. Containing only immutable bitcoin specific fields. + * Sent over wire. + */ +@Slf4j +@Immutable +@EqualsAndHashCode(callSuper = true) +@Value +public final class RawTxOutput extends BaseTxOutput implements NetworkPayload { + public static RawTxOutput fromTxOutput(TxOutput txOutput) { + return new RawTxOutput(txOutput.getIndex(), + txOutput.getValue(), + txOutput.getTxId(), + txOutput.getPubKeyScript(), + txOutput.getAddress(), + txOutput.getOpReturnData(), + txOutput.getBlockHeight()); + } + + public RawTxOutput(int index, + long value, + String txId, + @Nullable PubKeyScript pubKeyScript, + @Nullable String address, + @Nullable byte[] opReturnData, + int blockHeight) { + super(index, + value, + txId, + pubKeyScript, + address, + opReturnData, + blockHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.BaseTxOutput toProtoMessage() { + return getRawTxOutputBuilder().setRawTxOutput(protobuf.RawTxOutput.newBuilder()).build(); + } + + public static RawTxOutput fromProto(protobuf.BaseTxOutput proto) { + return new RawTxOutput(proto.getIndex(), + proto.getValue(), + proto.getTxId(), + proto.hasPubKeyScript() ? PubKeyScript.fromProto(proto.getPubKeyScript()) : null, + proto.getAddress().isEmpty() ? null : proto.getAddress(), + proto.getOpReturnData().isEmpty() ? null : proto.getOpReturnData().toByteArray(), + proto.getBlockHeight()); + } + + + @Override + public String toString() { + return "RawTxOutput{} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RpcException.java b/core/src/main/java/bisq/core/dao/node/full/RpcException.java new file mode 100644 index 0000000000..92defd15c5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/RpcException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.dao.node.full; + +class RpcException extends Exception { + RpcException(String message, Throwable cause) { + super(message, cause); + } + + RpcException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RpcService.java b/core/src/main/java/bisq/core/dao/node/full/RpcService.java new file mode 100644 index 0000000000..394f420de6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/RpcService.java @@ -0,0 +1,379 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.node.full.rpc.BitcoindClient; +import bisq.core.dao.node.full.rpc.BitcoindDaemon; +import bisq.core.dao.node.full.rpc.dto.DtoPubKeyScript; +import bisq.core.dao.node.full.rpc.dto.RawDtoBlock; +import bisq.core.dao.node.full.rpc.dto.RawDtoInput; +import bisq.core.dao.node.full.rpc.dto.RawDtoTransaction; +import bisq.core.dao.state.model.blockchain.PubKeyScript; +import bisq.core.dao.state.model.blockchain.ScriptType; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.user.Preferences; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Utils; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Range; +import com.google.common.primitives.Chars; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.io.IOException; + +import java.math.BigDecimal; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +/** + * Request blockchain data via RPC from Bitcoin Core for a FullNode. + * Runs in a custom thread. + * See the rpc.md file in the doc directory for more info about the setup. + */ +@Slf4j +public class RpcService { + private static final int ACTIVATE_HARD_FORK_2_HEIGHT_MAINNET = 680300; + private static final int ACTIVATE_HARD_FORK_2_HEIGHT_TESTNET = 1943000; + private static final int ACTIVATE_HARD_FORK_2_HEIGHT_REGTEST = 1; + private static final Range SUPPORTED_NODE_VERSION_RANGE = Range.closedOpen(180000, 210100); + + private final String rpcUser; + private final String rpcPassword; + private final String rpcHost; + private final int rpcPort; + private final int rpcBlockPort; + private final String rpcBlockHost; + + private BitcoindClient client; + private BitcoindDaemon daemon; + + // We could use multiple threads but then we need to support ordering of results in a queue + // Keep that for optimization after measuring performance differences + private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService"); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private RpcService(Preferences preferences, + @Named(Config.RPC_HOST) String rpcHost, + @Named(Config.RPC_PORT) int rpcPort, + @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockPort, + @Named(Config.RPC_BLOCK_NOTIFICATION_HOST) String rpcBlockHost) { + this.rpcUser = preferences.getRpcUser(); + this.rpcPassword = preferences.getRpcPw(); + + // mainnet is 8332, testnet 18332, regtest 18443 + boolean isHostSet = !rpcHost.isEmpty(); + boolean isPortSet = rpcPort != Config.UNSPECIFIED_PORT; + boolean isMainnet = Config.baseCurrencyNetwork().isMainnet(); + boolean isTestnet = Config.baseCurrencyNetwork().isTestnet(); + boolean isDaoBetaNet = Config.baseCurrencyNetwork().isDaoBetaNet(); + this.rpcHost = isHostSet ? rpcHost : "127.0.0.1"; + this.rpcPort = isPortSet ? rpcPort : + isMainnet || isDaoBetaNet ? 8332 : + isTestnet ? 18332 : + 18443; // regtest + boolean isBlockPortSet = rpcBlockPort != Config.UNSPECIFIED_PORT; + boolean isBlockHostSet = !rpcBlockHost.isEmpty(); + this.rpcBlockPort = isBlockPortSet ? rpcBlockPort : 5125; + this.rpcBlockHost = isBlockHostSet ? rpcBlockHost : "127.0.0.1"; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown() { + if (daemon != null) { + daemon.shutdown(); + log.info("daemon shut down"); + } + + executor.shutdown(); + } + + void setup(ResultHandler resultHandler, Consumer errorHandler) { + ListenableFuture future = executor.submit(() -> { + try { + log.info("Starting RpcService on {}:{} with user {}, listening for blocknotify on port {} from {}", + this.rpcHost, this.rpcPort, this.rpcUser, this.rpcBlockPort, this.rpcBlockHost); + + long startTs = System.currentTimeMillis(); + + client = BitcoindClient.builder() + .rpcHost(rpcHost) + .rpcPort(rpcPort) + .rpcUser(rpcUser) + .rpcPassword(rpcPassword) + .build(); + checkNodeVersionAndHealth(); + + daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> { + log.error(throwable.toString()); + throwable.printStackTrace(); + UserThread.execute(() -> errorHandler.accept(new RpcException(throwable))); + }); + + log.info("Setup took {} ms", System.currentTimeMillis() - startTs); + } catch (Throwable e) { + log.error(e.toString()); + e.printStackTrace(); + throw new RpcException(e.toString(), e); + } + return null; + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(Void ignore) { + UserThread.execute(resultHandler::handleResult); + } + + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> errorHandler.accept(throwable)); + } + }, MoreExecutors.directExecutor()); + } + + private String decodeNodeVersion(Integer encodedVersion) { + var paddedEncodedVersion = Strings.padStart(encodedVersion.toString(), 8, '0'); + + return Lists.partition(Chars.asList(paddedEncodedVersion.toCharArray()), 2).stream() + .map(chars -> new String(Chars.toArray(chars)).replaceAll("^0", "")) + .collect(Collectors.joining(".")) + .replaceAll("\\.0$", ""); + } + + private void checkNodeVersionAndHealth() throws IOException { + var networkInfo = client.getNetworkInfo(); + var nodeVersion = decodeNodeVersion(networkInfo.getVersion()); + + if (SUPPORTED_NODE_VERSION_RANGE.contains(networkInfo.getVersion())) { + log.info("Got Bitcoin Core version: {}", nodeVersion); + } else { + log.warn("Server version mismatch - client optimized for '[{} .. {})', node responded with '{}'", + decodeNodeVersion(SUPPORTED_NODE_VERSION_RANGE.lowerEndpoint()), + decodeNodeVersion(SUPPORTED_NODE_VERSION_RANGE.upperEndpoint()), nodeVersion); + } + + var bestRawBlock = client.getBlock(client.getBestBlockHash(), 1); + long currentTime = System.currentTimeMillis() / 1000; + if ((currentTime - bestRawBlock.getTime()) > TimeUnit.HOURS.toSeconds(6)) { + log.warn("Last available block was mined >{} hours ago; please check your network connection", + ((currentTime - bestRawBlock.getTime()) / 3600)); + } + } + + void addNewDtoBlockHandler(Consumer dtoBlockHandler, + Consumer errorHandler) { + daemon.setBlockListener(blockHash -> { + try { + var rawDtoBlock = client.getBlock(blockHash, 2); + log.info("New block received: height={}, id={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash()); + + var block = getBlockFromRawDtoBlock(rawDtoBlock); + UserThread.execute(() -> dtoBlockHandler.accept(block)); + } catch (Throwable t) { + errorHandler.accept(t); + } + }); + } + + void requestChainHeadHeight(Consumer resultHandler, Consumer errorHandler) { + ListenableFuture future = executor.submit(client::getBlockCount); + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(Integer chainHeight) { + UserThread.execute(() -> resultHandler.accept(chainHeight)); + } + + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> errorHandler.accept(throwable)); + } + }, MoreExecutors.directExecutor()); + } + + void requestDtoBlock(int blockHeight, + Consumer resultHandler, + Consumer errorHandler) { + ListenableFuture future = executor.submit(() -> { + long startTs = System.currentTimeMillis(); + String blockHash = client.getBlockHash(blockHeight); + var rawDtoBlock = client.getBlock(blockHash, 2); + var block = getBlockFromRawDtoBlock(rawDtoBlock); + log.info("requestDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms", + blockHeight, block.getRawTxs().size(), System.currentTimeMillis() - startTs); + return block; + }); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(RawBlock block) { + UserThread.execute(() -> resultHandler.accept(block)); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.error("Error at requestDtoBlock: blockHeight={}", blockHeight); + UserThread.execute(() -> errorHandler.accept(throwable)); + } + }, MoreExecutors.directExecutor()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private static RawBlock getBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) { + List txList = rawDtoBlock.getTx().stream() + .map(e -> getTxFromRawTransaction(e, rawDtoBlock)) + .collect(Collectors.toList()); + return new RawBlock(rawDtoBlock.getHeight(), + rawDtoBlock.getTime() * 1000, // rawDtoBlock.getTime() is in sec but we want ms + rawDtoBlock.getHash(), + rawDtoBlock.getPreviousBlockHash(), + ImmutableList.copyOf(txList)); + } + + private static RawTx getTxFromRawTransaction(RawDtoTransaction rawDtoTx, + RawDtoBlock rawDtoBlock) { + String txId = rawDtoTx.getTxId(); + long blockTime = rawDtoBlock.getTime() * 1000; // We convert block time from sec to ms + int blockHeight = rawDtoBlock.getHeight(); + String blockHash = rawDtoBlock.getHash(); + + // Extracting pubKeys for segwit (P2WPKH) inputs, instead of just P2PKH inputs as + // originally, changes the DAO state and thus represents a hard fork. We disallow + // it until the fork activates, which is determined by block height. + boolean allowSegwit = blockHeight >= getActivateHardFork2Height(); + + final List txInputs = rawDtoTx.getVIn() + .stream() + .filter(rawInput -> rawInput != null && rawInput.getVOut() != null && rawInput.getTxId() != null) + .map(rawInput -> { + String pubKeyAsHex = extractPubKeyAsHex(rawInput, allowSegwit); + if (pubKeyAsHex == null) { + log.debug("pubKeyAsHex is not set as we received a not supported sigScript. " + + "txId={}, asm={}, txInWitness={}", + rawDtoTx.getTxId(), rawInput.getScriptSig().getAsm(), rawInput.getTxInWitness()); + } + return new TxInput(rawInput.getTxId(), rawInput.getVOut(), pubKeyAsHex); + }) + .collect(Collectors.toList()); + + final List txOutputs = rawDtoTx.getVOut() + .stream() + .filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null) + .map(rawDtoTxOutput -> { + byte[] opReturnData = null; + DtoPubKeyScript scriptPubKey = rawDtoTxOutput.getScriptPubKey(); + if (ScriptType.NULL_DATA.equals(scriptPubKey.getType()) && scriptPubKey.getAsm() != null) { + String[] chunks = scriptPubKey.getAsm().split(" "); + // We get on testnet a lot of "OP_RETURN 0" data, so we filter those away + if (chunks.length == 2 && "OP_RETURN".equals(chunks[0]) && !"0".equals(chunks[1])) { + try { + opReturnData = Utils.HEX.decode(chunks[1]); + } catch (Throwable t) { + log.debug("Error at Utils.HEX.decode(chunks[1]): " + t.toString() + + " / chunks[1]=" + chunks[1] + + "\nWe get sometimes exceptions with opReturn data, seems BitcoinJ " + + "cannot handle all " + + "existing OP_RETURN data, but we ignore them anyway as the OP_RETURN " + + "data used for DAO transactions are all valid in BitcoinJ"); + } + } + } + // We don't support raw MS which are the only case where scriptPubKey.getAddresses()>1 + String address = scriptPubKey.getAddresses() != null && + scriptPubKey.getAddresses().size() == 1 ? scriptPubKey.getAddresses().get(0) : null; + PubKeyScript pubKeyScript = new PubKeyScript(scriptPubKey); + return new RawTxOutput(rawDtoTxOutput.getN(), + BigDecimal.valueOf(rawDtoTxOutput.getValue()).movePointRight(8).longValueExact(), + rawDtoTx.getTxId(), + pubKeyScript, + address, + opReturnData, + blockHeight); + } + ) + .collect(Collectors.toList()); + + return new RawTx(txId, + blockHeight, + blockHash, + blockTime, + ImmutableList.copyOf(txInputs), + ImmutableList.copyOf(txOutputs)); + } + + private static int getActivateHardFork2Height() { + return Config.baseCurrencyNetwork().isMainnet() ? ACTIVATE_HARD_FORK_2_HEIGHT_MAINNET : + Config.baseCurrencyNetwork().isTestnet() ? ACTIVATE_HARD_FORK_2_HEIGHT_TESTNET : + ACTIVATE_HARD_FORK_2_HEIGHT_REGTEST; + } + + @VisibleForTesting + static String extractPubKeyAsHex(RawDtoInput rawInput, boolean allowSegwit) { + // We only allow inputs with a single SIGHASH_ALL signature. That is, multisig or + // signing of only some of the tx inputs/outputs is intentionally disallowed... + if (rawInput.getScriptSig() == null) { + // coinbase input - no pubKey to extract + return null; + } + String[] split = rawInput.getScriptSig().getAsm().split(" "); + if (split.length == 2 && split[0].endsWith("[ALL]")) { + // P2PKH input + return split[1]; + } + List txInWitness = rawInput.getTxInWitness() != null ? rawInput.getTxInWitness() : List.of(); + if (allowSegwit && split.length < 2 && txInWitness.size() == 2 && txInWitness.get(0).endsWith("01")) { + // P2WPKH or P2SH-P2WPKH input + return txInWitness.get(1); + } + // If we receive a pay to pubkey tx, the pubKey is not included as it is in the + // output already. + return null; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java new file mode 100644 index 0000000000..e8b6a0483f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java @@ -0,0 +1,196 @@ +/* + * 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 bisq.core.dao.node.full.network; + +import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; +import bisq.core.dao.node.full.RawBlock; +import bisq.core.dao.node.messages.GetBlocksRequest; +import bisq.core.dao.node.messages.NewBlockBroadcastMessage; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +/** + * Responsible for handling requests for BSQ blocks from lite nodes and for broadcasting new blocks to the P2P network. + */ +@Slf4j +public class FullNodeNetworkService implements MessageListener, PeerManager.Listener { + + private static final long CLEANUP_TIMER = 120; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final Broadcaster broadcaster; + private final MissingDataRequestService missingDataRequestService; + private final DaoStateService daoStateService; + + // Key is connection UID + private final Map getBlocksRequestHandlers = new HashMap<>(); + private boolean stopped; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public FullNodeNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster, + MissingDataRequestService missingDataRequestService, + DaoStateService daoStateService) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.broadcaster = broadcaster; + this.missingDataRequestService = missingDataRequestService; + this.daoStateService = daoStateService; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void start() { + networkNode.addMessageListener(this); + peerManager.addListener(this); + } + + @SuppressWarnings("Duplicates") + public void shutDown() { + stopped = true; + networkNode.removeMessageListener(this); + peerManager.removeListener(this); + } + + public void publishNewBlock(Block block) { + log.info("Publish new block at height={} and block hash={}", block.getHeight(), block.getHash()); + RawBlock rawBlock = RawBlock.fromBlock(block); + NewBlockBroadcastMessage newBlockBroadcastMessage = new NewBlockBroadcastMessage(rawBlock); + broadcaster.broadcast(newBlockBroadcastMessage, networkNode.getNodeAddress()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PeerManager.Listener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAllConnectionsLost() { + stopped = true; + } + + @Override + public void onNewConnectionAfterAllConnectionsLost() { + stopped = false; + } + + @Override + public void onAwakeFromStandby() { + stopped = false; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetBlocksRequest) { + handleGetBlocksRequest((GetBlocksRequest) networkEnvelope, connection); + } else if (networkEnvelope instanceof RepublishGovernanceDataRequest) { + handleRepublishGovernanceDataRequest(); + } + } + + private void handleGetBlocksRequest(GetBlocksRequest getBlocksRequest, Connection connection) { + if (stopped) { + log.warn("We have stopped already. We ignore that onMessage call."); + return; + } + + String uid = connection.getUid(); + if (getBlocksRequestHandlers.containsKey(uid)) { + log.warn("We have already a GetDataRequestHandler for that connection started. " + + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes."); + + UserThread.runAfter(() -> { + if (getBlocksRequestHandlers.containsKey(uid)) { + GetBlocksRequestHandler handler = getBlocksRequestHandlers.get(uid); + handler.stop(); + getBlocksRequestHandlers.remove(uid); + } + }, CLEANUP_TIMER); + return; + } + + GetBlocksRequestHandler requestHandler = new GetBlocksRequestHandler(networkNode, + daoStateService, + new GetBlocksRequestHandler.Listener() { + @Override + public void onComplete() { + getBlocksRequestHandlers.remove(uid); + } + + @Override + public void onFault(String errorMessage, @Nullable Connection connection) { + getBlocksRequestHandlers.remove(uid); + if (!stopped) { + log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" + + "ErrorMessage={}", connection, errorMessage); + if (connection != null) { + peerManager.handleConnectionFault(connection); + } + } else { + log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call."); + } + } + }); + getBlocksRequestHandlers.put(uid, requestHandler); + requestHandler.onGetBlocksRequest(getBlocksRequest, connection); + } + + private void handleRepublishGovernanceDataRequest() { + log.warn("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " + + "blindVotePayloads to the P2P network."); + missingDataRequestService.reRepublishAllGovernanceData(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java new file mode 100644 index 0000000000..dd8d6e9203 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java @@ -0,0 +1,169 @@ +/* + * 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 bisq.core.dao.node.full.network; + +import bisq.core.dao.node.full.RawBlock; +import bisq.core.dao.node.messages.GetBlocksRequest; +import bisq.core.dao.node.messages.GetBlocksResponse; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.Timer; +import bisq.common.UserThread; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +/** + * Accepts a GetBlocksRequest from a lite node and sends back a corresponding GetBlocksResponse. + */ +@Slf4j +class GetBlocksRequestHandler { + private static final long TIMEOUT_MIN = 3; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onComplete(); + + void onFault(String errorMessage, Connection connection); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final DaoStateService daoStateService; + private final Listener listener; + private Timer timeoutTimer; + private boolean stopped; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public GetBlocksRequestHandler(NetworkNode networkNode, DaoStateService daoStateService, Listener listener) { + this.networkNode = networkNode; + this.daoStateService = daoStateService; + this.listener = listener; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onGetBlocksRequest(GetBlocksRequest getBlocksRequest, Connection connection) { + long ts = System.currentTimeMillis(); + // We limit number of blocks to 6000 which is about 1.5 month. + List blocks = new LinkedList<>(daoStateService.getBlocksFromBlockHeight(getBlocksRequest.getFromBlockHeight(), 6000)); + List rawBlocks = blocks.stream().map(RawBlock::fromBlock).collect(Collectors.toList()); + GetBlocksResponse getBlocksResponse = new GetBlocksResponse(rawBlocks, getBlocksRequest.getNonce()); + log.info("Received GetBlocksRequest from {} for blocks from height {}. " + + "Building GetBlocksResponse with {} blocks took {} ms.", + connection.getPeersNodeAddressOptional(), getBlocksRequest.getFromBlockHeight(), + rawBlocks.size(), System.currentTimeMillis() - ts); + + if (timeoutTimer != null) { + timeoutTimer.stop(); + log.warn("Timeout was already running. We stopped it."); + } + timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + String errorMessage = "A timeout occurred for getBlocksResponse.requestNonce:" + + getBlocksResponse.getRequestNonce() + + " on connection: " + connection; + handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, connection); + }, + TIMEOUT_MIN, TimeUnit.MINUTES); + + SettableFuture future = networkNode.sendMessage(connection, getBlocksResponse); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.info("Send DataResponse to {} succeeded. getBlocksResponse.getBlocks().size()={}", + connection.getPeersNodeAddressOptional(), getBlocksResponse.getBlocks().size()); + cleanup(); + listener.onComplete(); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getBlocksResponse to " + connection + + " failed. That is expected if the peer is offline. getBlocksResponse=" + getBlocksResponse + "." + + "Exception: " + throwable.getMessage(); + handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); + } + } + }, MoreExecutors.directExecutor()); + } + + public void stop() { + cleanup(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) { + if (!stopped) { + log.warn("{}, closeConnectionReason={}", errorMessage, closeConnectionReason); + cleanup(); + listener.onFault(errorMessage, connection); + } else { + log.warn("We have already stopped (handleFault)"); + } + } + + private void cleanup() { + stopped = true; + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindClient.java b/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindClient.java new file mode 100644 index 0000000000..dd9e97f9fc --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindClient.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.core.dao.node.full.rpc; + +import bisq.core.dao.node.full.rpc.dto.DtoNetworkInfo; +import bisq.core.dao.node.full.rpc.dto.RawDtoBlock; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLStreamHandler; + +import java.nio.charset.StandardCharsets; + +import java.io.IOException; + +import java.util.Base64; +import java.util.Collections; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + + + +import com.googlecode.jsonrpc4j.JsonRpcHttpClient; +import com.googlecode.jsonrpc4j.JsonRpcMethod; +import com.googlecode.jsonrpc4j.ProxyUtil; +import com.googlecode.jsonrpc4j.RequestIDGenerator; + +public interface BitcoindClient { + @JsonRpcMethod("getblock") + RawDtoBlock getBlock(String headerHash, int verbosity) throws IOException; + + @JsonRpcMethod("getblockcount") + Integer getBlockCount() throws IOException; + + @JsonRpcMethod("getblockhash") + String getBlockHash(Integer blockHeight) throws IOException; + + @JsonRpcMethod("getbestblockhash") + String getBestBlockHash() throws IOException; + + @JsonRpcMethod("getnetworkinfo") + DtoNetworkInfo getNetworkInfo() throws IOException; + + static Builder builder() { + return new Builder(); + } + + class Builder { + private String rpcHost; + private int rpcPort = -1; + private String rpcUser; + private String rpcPassword; + private URLStreamHandler urlStreamHandler; + private RequestIDGenerator requestIDGenerator; + + public Builder rpcHost(String rpcHost) { + this.rpcHost = rpcHost; + return this; + } + + public Builder rpcPort(int rpcPort) { + this.rpcPort = rpcPort; + return this; + } + + public Builder rpcUser(String rpcUser) { + this.rpcUser = rpcUser; + return this; + } + + public Builder rpcPassword(String rpcPassword) { + this.rpcPassword = rpcPassword; + return this; + } + + public Builder urlStreamHandler(URLStreamHandler urlStreamHandler) { + this.urlStreamHandler = urlStreamHandler; + return this; + } + + public Builder requestIDGenerator(RequestIDGenerator requestIDGenerator) { + this.requestIDGenerator = requestIDGenerator; + return this; + } + + public BitcoindClient build() throws MalformedURLException { + var userPass = checkNotNull(rpcUser, "rpcUser not set") + + ":" + checkNotNull(rpcPassword, "rpcPassword not set"); + + var headers = Collections.singletonMap("Authorization", "Basic " + + Base64.getEncoder().encodeToString(userPass.getBytes(StandardCharsets.US_ASCII))); + + var httpClient = new JsonRpcHttpClient( + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true), + new URL("http", rpcHost, rpcPort, "", urlStreamHandler), + headers); + Optional.ofNullable(requestIDGenerator).ifPresent(httpClient::setRequestIDGenerator); + return ProxyUtil.createClientProxy(getClass().getClassLoader(), BitcoindClient.class, httpClient); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindDaemon.java b/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindDaemon.java new file mode 100644 index 0000000000..b607ac4e2a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/BitcoindDaemon.java @@ -0,0 +1,125 @@ +/* + * 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 bisq.core.dao.node.full.rpc; + +import bisq.common.util.Utilities; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import org.apache.commons.io.IOUtils; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.SocketException; + +import java.nio.charset.StandardCharsets; + +import java.io.IOException; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BitcoindDaemon { + private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("block-notification-server"); + private final ListeningExecutorService workerPool = Utilities.getListeningExecutorService("block-notification-worker-%d", + 1, 10, 60, new ArrayBlockingQueue<>(100)); + private final ServerSocket serverSocket; + private final Consumer errorHandler; + private volatile boolean active; + private volatile BlockListener blockListener = blockHash -> { + }; + + public BitcoindDaemon(String host, int port, Consumer errorHandler) throws NotificationHandlerException { + this(newServerSocket(host, port), errorHandler); + } + + @VisibleForTesting + BitcoindDaemon(ServerSocket serverSocket, Consumer errorHandler) { + this.serverSocket = serverSocket; + this.errorHandler = errorHandler; + initialize(); + } + + private static ServerSocket newServerSocket(String host, int port) throws NotificationHandlerException { + try { + return new ServerSocket(port, 5, InetAddress.getByName(host)); + } catch (Exception e) { + throw new NotificationHandlerException(e); + } + } + + private void initialize() { + active = true; + var serverFuture = executor.submit((Callable) () -> { + try { + while (active) { + try (var socket = serverSocket.accept(); var is = socket.getInputStream()) { + var blockHash = IOUtils.toString(is, StandardCharsets.UTF_8).trim(); + var future = workerPool.submit((Callable) () -> { + try { + blockListener.blockDetected(blockHash); + return null; + } catch (RuntimeException e) { + throw new NotificationHandlerException(e); + } + }); + Futures.addCallback(future, Utilities.failureCallback(errorHandler), MoreExecutors.directExecutor()); + } + } + } catch (SocketException e) { + if (active) { + throw new NotificationHandlerException(e); + } + } catch (Exception e) { + throw new NotificationHandlerException(e); + } finally { + log.info("Shutting down block notification server"); + } + return null; + }); + Futures.addCallback(serverFuture, Utilities.failureCallback(errorHandler), MoreExecutors.directExecutor()); + } + + public void shutdown() { + active = false; + try { + serverSocket.close(); + } catch (IOException e) { + log.error("Error closing block notification server socket", e); + } finally { + Utilities.shutdownAndAwaitTermination(executor, 1, TimeUnit.SECONDS); + Utilities.shutdownAndAwaitTermination(workerPool, 5, TimeUnit.SECONDS); + } + } + + public void setBlockListener(BlockListener blockListener) { + this.blockListener = blockListener; + } + + public interface BlockListener { + void blockDetected(String blockHash); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/NotificationHandlerException.java b/core/src/main/java/bisq/core/dao/node/full/rpc/NotificationHandlerException.java new file mode 100644 index 0000000000..3f0278eee9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/NotificationHandlerException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.node.full.rpc; + +public class NotificationHandlerException extends Exception { + public NotificationHandlerException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoNetworkInfo.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoNetworkInfo.java new file mode 100644 index 0000000000..2b64073b93 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoNetworkInfo.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"version", "subversion", "protocolversion", "localservices", "localservicesnames", "localrelay", + "timeoffset", "networkactive", "connections", "connections_in", "connections_out", "networks", "relayfee", + "incrementalfee", "localaddresses", "warnings"}) +public class DtoNetworkInfo { + private Integer version; + @JsonProperty("subversion") + private String subVersion; + @JsonProperty("protocolversion") + private Integer protocolVersion; + @JsonProperty("localservices") + private String localServices; + @JsonProperty("localservicesnames") + private List localServicesNames; + @JsonProperty("localrelay") + private Boolean localRelay; + @JsonProperty("timeoffset") + private Integer timeOffset; + @JsonProperty("networkactive") + private Boolean networkActive; + private Integer connections; + @JsonProperty("connections_in") + private Integer connectionsIn; + @JsonProperty("connections_out") + private Integer connectionsOut; + private List networks; + @JsonProperty("relayfee") + private Double relayFee; + @JsonProperty("incrementalfee") + private Double incrementalFee; + @JsonProperty("localaddresses") + private List localAddresses; + private String warnings; + + @Data + @NoArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonPropertyOrder({"name", "limited", "reachable", "proxy", "proxy_randomize_credentials"}) + public static class Network { + private NetworkType name; + private Boolean limited; + private Boolean reachable; + private String proxy; + @JsonProperty("proxy_randomize_credentials") + private Boolean proxyRandomizeCredentials; + } + + @Data + @NoArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonPropertyOrder({"address", "port", "score"}) + public static class LocalAddress { + private String address; + private Integer port; + private Integer score; + } + + @RequiredArgsConstructor + public enum NetworkType { + IPV4("ipv4"), IPV6("ipv6"), ONION("onion"); + + @Getter(onMethod_ = @JsonValue) + private final String name; + } + + @RequiredArgsConstructor + public enum ServiceFlag { + @JsonEnumDefaultValue + UNKNOWN(0), + // Taken from https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h: + NETWORK(1), + BLOOM(1 << 2), + WITNESS(1 << 3), + COMPACT_FILTERS(1 << 6), + NETWORK_LIMITED(1 << 10); + + @Getter + private final int value; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoPubKeyScript.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoPubKeyScript.java new file mode 100644 index 0000000000..a9fd9331c3 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoPubKeyScript.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import bisq.core.dao.state.model.blockchain.ScriptType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"asm", "hex", "reqSigs", "type", "addresses"}) +public class DtoPubKeyScript { + private String asm; + private String hex; + private Integer reqSigs; + private ScriptType type; + private List addresses; +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoSignatureScript.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoSignatureScript.java new file mode 100644 index 0000000000..77edaaa6e2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/DtoSignatureScript.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"asm", "hex"}) +public class DtoSignatureScript { + private String asm; + private String hex; +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoBlock.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoBlock.java new file mode 100644 index 0000000000..91edb701b4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoBlock.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true, value = "ntx") +@JsonPropertyOrder({"hash", "confirmations", "strippedsize", "size", "weight", "height", "version", "versionHex", + "merkleroot", "tx", "time", "mediantime", "nonce", "bits", "difficulty", "chainwork", "nTx", + "previousblockhash", "nextblockhash"}) +public class RawDtoBlock { + private String hash; + private Integer confirmations; + @JsonProperty("strippedsize") + private Integer strippedSize; + private Integer size; + private Integer weight; + private Integer height; + private Integer version; + private String versionHex; + @JsonProperty("merkleroot") + private String merkleRoot; + private List tx; + private Long time; + @JsonProperty("mediantime") + private Long medianTime; + private Long nonce; + private String bits; + private Double difficulty; + @JsonProperty("chainwork") + private String chainWork; + // There seems to be a bug in Jackson where it misses and/or duplicates this field without + // an explicit @JsonProperty annotation plus the @JsonIgnoreProperties 'ntx' term above: + @JsonProperty("nTx") + private Integer nTx; + @JsonProperty("previousblockhash") + private String previousBlockHash; + @JsonProperty("nextblockhash") + private String nextBlockHash; + + @JsonCreator + public static Summarized summarized(String hex) { + var result = new Summarized(); + result.setHex(hex); + return result; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Summarized extends RawDtoBlock { + @Getter(onMethod_ = @JsonValue) + private String hex; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoInput.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoInput.java new file mode 100644 index 0000000000..a33c2b25e9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoInput.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"txid", "vout", "coinbase", "scriptSig", "txinwitness", "sequence"}) +public class RawDtoInput { + @JsonProperty("txid") + private String txId; + @JsonProperty("vout") + private Integer vOut; + private String coinbase; + private DtoSignatureScript scriptSig; + @JsonProperty("txinwitness") + private List txInWitness; + private Long sequence; +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoOutput.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoOutput.java new file mode 100644 index 0000000000..4bd4d60569 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoOutput.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"value", "n", "scriptPubKey"}) +public class RawDtoOutput { + private Double value; + private Integer n; + private DtoPubKeyScript scriptPubKey; +} diff --git a/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoTransaction.java b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoTransaction.java new file mode 100644 index 0000000000..e8e2c75206 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawDtoTransaction.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.dao.node.full.rpc.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"txid", "hash", "version", "size", "vsize", "weight", "locktime", "vin", "vout", "hex"}) +public class RawDtoTransaction { + @JsonProperty("in_active_chain") + private Boolean inActiveChain; + @JsonProperty("txid") + private String txId; + private String hash; + private Integer version; + private Integer size; + @JsonProperty("vsize") + private Integer vSize; + private Integer weight; + @JsonProperty("locktime") + private Long lockTime; + @JsonProperty("vin") + private List vIn; + @JsonProperty("vout") + private List vOut; + @JsonProperty("blockhash") + private String blockHash; + private Integer confirmations; + @JsonProperty("blocktime") + private Long blockTime; + private Long time; + private String hex; + + @JsonCreator + public static Summarized summarized(String hex) { + var result = new Summarized(); + result.setHex(hex); + return result; + } + + public static class Summarized extends RawDtoTransaction { + @Override + @JsonValue + public String getHex() { + return super.getHex(); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java b/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java new file mode 100644 index 0000000000..c0c8b64980 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java @@ -0,0 +1,274 @@ +/* + * 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 bisq.core.dao.node.lite; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.node.BsqNode; +import bisq.core.dao.node.explorer.ExportJsonFilesService; +import bisq.core.dao.node.full.RawBlock; +import bisq.core.dao.node.lite.network.LiteNodeNetworkService; +import bisq.core.dao.node.messages.GetBlocksResponse; +import bisq.core.dao.node.messages.NewBlockBroadcastMessage; +import bisq.core.dao.node.parser.BlockParser; +import bisq.core.dao.node.parser.exceptions.RequiredReorgFromSnapshotException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.network.Connection; + +import bisq.common.Timer; +import bisq.common.UserThread; + +import com.google.inject.Inject; + +import javafx.beans.value.ChangeListener; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +/** + * Main class for lite nodes which receive the BSQ transactions from a full node (e.g. seed nodes). + * Verification of BSQ transactions is done also by the lite node. + */ +@Slf4j +public class LiteNode extends BsqNode { + private static final int CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC = 10; + + private final LiteNodeNetworkService liteNodeNetworkService; + private final BsqWalletService bsqWalletService; + private final WalletsSetup walletsSetup; + private Timer checkForBlockReceivedTimer; + private final ChangeListener blockDownloadListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess") + @Inject + public LiteNode(BlockParser blockParser, + DaoStateService daoStateService, + DaoStateSnapshotService daoStateSnapshotService, + P2PService p2PService, + LiteNodeNetworkService liteNodeNetworkService, + BsqWalletService bsqWalletService, + WalletsSetup walletsSetup, + ExportJsonFilesService exportJsonFilesService) { + super(blockParser, daoStateService, daoStateSnapshotService, p2PService, exportJsonFilesService); + + this.liteNodeNetworkService = liteNodeNetworkService; + this.bsqWalletService = bsqWalletService; + this.walletsSetup = walletsSetup; + + blockDownloadListener = (observable, oldValue, newValue) -> { + if ((double) newValue == 1) { + setupWalletBestBlockListener(); + } + }; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start() { + super.onInitialized(); + + liteNodeNetworkService.start(); + + // We wait until the wallet is synced before using it for triggering requests + if (walletsSetup.isDownloadComplete()) { + setupWalletBestBlockListener(); + } else { + walletsSetup.downloadPercentageProperty().addListener(blockDownloadListener); + } + } + + private void setupWalletBestBlockListener() { + walletsSetup.downloadPercentageProperty().removeListener(blockDownloadListener); + + bsqWalletService.addNewBestBlockListener(blockFromWallet -> { + // Check if we are done with parsing + if (!daoStateService.isParseBlockChainComplete()) + return; + + if (checkForBlockReceivedTimer != null) { + // In case we received a new block before out timer gets called we stop the old timer + checkForBlockReceivedTimer.stop(); + } + + int walletBlockHeight = blockFromWallet.getHeight(); + log.info("New block at height {} from bsqWalletService", walletBlockHeight); + + // We expect to receive the new BSQ block from the network shortly after BitcoinJ has been aware of it. + // If we don't receive it we request it manually from seed nodes + checkForBlockReceivedTimer = UserThread.runAfter(() -> { + int daoChainHeight = daoStateService.getChainHeight(); + if (daoChainHeight < walletBlockHeight) { + log.warn("We did not receive a block from the network {} seconds after we saw the new block in BitcoinJ. " + + "We request from our seed nodes missing blocks from block height {}.", + CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC, daoChainHeight + 1); + liteNodeNetworkService.requestBlocks(daoChainHeight + 1); + } + }, CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC); + }); + } + + @Override + public void shutDown() { + super.shutDown(); + liteNodeNetworkService.shutDown(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onP2PNetworkReady() { + super.onP2PNetworkReady(); + + liteNodeNetworkService.addListener(new LiteNodeNetworkService.Listener() { + @Override + public void onRequestedBlocksReceived(GetBlocksResponse getBlocksResponse, Runnable onParsingComplete) { + LiteNode.this.onRequestedBlocksReceived(new ArrayList<>(getBlocksResponse.getBlocks()), + onParsingComplete); + } + + @Override + public void onNewBlockReceived(NewBlockBroadcastMessage newBlockBroadcastMessage) { + LiteNode.this.onNewBlockReceived(newBlockBroadcastMessage.getBlock()); + } + + @Override + public void onNoSeedNodeAvailable() { + } + + @Override + public void onFault(String errorMessage, @Nullable Connection connection) { + } + }); + + if (!parseBlockchainComplete) + startParseBlocks(); + } + + // First we request the blocks from a full node + @Override + protected void startParseBlocks() { + liteNodeNetworkService.requestBlocks(getStartBlockHeight()); + } + + @Override + protected void startReOrgFromLastSnapshot() { + super.startReOrgFromLastSnapshot(); + + int startBlockHeight = getStartBlockHeight(); + liteNodeNetworkService.reset(); + liteNodeNetworkService.requestBlocks(startBlockHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // We received the missing blocks + private void onRequestedBlocksReceived(List blockList, Runnable onParsingComplete) { + if (!blockList.isEmpty()) { + chainTipHeight = blockList.get(blockList.size() - 1).getHeight(); + log.info("We received blocks from height {} to {}", blockList.get(0).getHeight(), chainTipHeight); + } + + // We delay the parsing to next render frame to avoid that the UI get blocked in case we parse a lot of blocks. + // Parsing itself is very fast (3 sec. for 7000 blocks) but creating the hash chain slows down batch processing a lot + // (30 sec for 7000 blocks). + // The updates at block height change are not much optimized yet, so that can be for sure improved + // 144 blocks a day would result in about 4000 in a month, so if a user downloads the app after 1 months latest + // release it will be a bit of a performance hit. It is a one time event as the snapshots gets created and be + // used at next startup. New users will get the shipped snapshot. Users who have not used Bisq for longer might + // experience longer durations for batch processing. + long ts = System.currentTimeMillis(); + + if (blockList.isEmpty()) { + onParseBlockChainComplete(); + return; + } + + runDelayedBatchProcessing(new ArrayList<>(blockList), + () -> { + log.info("runDelayedBatchProcessing Parsing {} blocks took {} seconds.", blockList.size(), + (System.currentTimeMillis() - ts) / 1000d); + // We only request again if wallet is synced, otherwise we would get repeated calls we want to avoid. + // We deal with that case at the setupWalletBestBlockListener method above. + if (walletsSetup.isDownloadComplete() && + daoStateService.getChainHeight() < bsqWalletService.getBestChainHeight()) { + liteNodeNetworkService.requestBlocks(getStartBlockHeight()); + } else { + onParsingComplete.run(); + onParseBlockChainComplete(); + } + }); + } + + private void runDelayedBatchProcessing(List blocks, Runnable resultHandler) { + UserThread.execute(() -> { + if (blocks.isEmpty()) { + resultHandler.run(); + return; + } + + RawBlock block = blocks.remove(0); + try { + doParseBlock(block); + runDelayedBatchProcessing(blocks, resultHandler); + } catch (RequiredReorgFromSnapshotException e) { + resultHandler.run(); + } + }); + } + + // We received a new block + private void onNewBlockReceived(RawBlock block) { + int blockHeight = block.getHeight(); + log.info("onNewBlockReceived: block at height {}, hash={}. Our DAO chainHeight={}", + blockHeight, block.getHash(), chainTipHeight); + + // We only update chainTipHeight if we get a newer block + if (blockHeight > chainTipHeight) { + chainTipHeight = blockHeight; + } + + try { + doParseBlock(block); + } catch (RequiredReorgFromSnapshotException ignore) { + } + + maybeExportToJson(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/lite/network/LiteNodeNetworkService.java b/core/src/main/java/bisq/core/dao/node/lite/network/LiteNodeNetworkService.java new file mode 100644 index 0000000000..189afea334 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/lite/network/LiteNodeNetworkService.java @@ -0,0 +1,414 @@ +/* + * 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 bisq.core.dao.node.lite.network; + +import bisq.core.dao.node.messages.GetBlocksResponse; +import bisq.core.dao.node.messages.NewBlockBroadcastMessage; +import bisq.core.dao.state.model.blockchain.BaseTx; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.ConnectionListener; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Tuple2; + +import javax.inject.Inject; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +/** + * Responsible for requesting BSQ blocks from a full node and for listening to new blocks broadcasted by full nodes. + */ +@Slf4j +public class LiteNodeNetworkService implements MessageListener, ConnectionListener, PeerManager.Listener { + + private static final long RETRY_DELAY_SEC = 10; + private static final long CLEANUP_TIMER = 120; + private static final int MAX_RETRY = 3; + + private int retryCounter = 0; + private int lastRequestedBlockHeight; + private int lastReceivedBlockHeight; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onNoSeedNodeAvailable(); + + void onRequestedBlocksReceived(GetBlocksResponse getBlocksResponse, Runnable onParsingComplete); + + void onNewBlockReceived(NewBlockBroadcastMessage newBlockBroadcastMessage); + + void onFault(String errorMessage, @Nullable Connection connection); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final Broadcaster broadcaster; + private final Collection seedNodeAddresses; + + private final List listeners = new CopyOnWriteArrayList<>(); + + // Key is tuple of seedNode address and requested blockHeight + private final Map, RequestBlocksHandler> requestBlocksHandlerMap = new HashMap<>(); + private Timer retryTimer; + private boolean stopped; + private final Set receivedBlocks = new HashSet<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public LiteNodeNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster, + SeedNodeRepository seedNodesRepository) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.broadcaster = broadcaster; + // seedNodeAddresses can be empty (in case there is only 1 seed node, the seed node starting up has no other seed nodes) + this.seedNodeAddresses = new HashSet<>(seedNodesRepository.getSeedNodeAddresses()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void start() { + networkNode.addMessageListener(this); + networkNode.addConnectionListener(this); + peerManager.addListener(this); + } + + public void shutDown() { + stopped = true; + stopRetryTimer(); + networkNode.removeMessageListener(this); + networkNode.removeConnectionListener(this); + peerManager.removeListener(this); + closeAllHandlers(); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + /** + * + * @param startBlockHeight Block height from where we expect new blocks (current block height in bsqState + 1) + */ + public void requestBlocks(int startBlockHeight) { + lastRequestedBlockHeight = startBlockHeight; + Optional connectionToSeedNodeOptional = networkNode.getConfirmedConnections().stream() + .filter(peerManager::isSeedNode) + .findAny(); + + connectionToSeedNodeOptional.flatMap(Connection::getPeersNodeAddressOptional) + .ifPresentOrElse(candidate -> { + seedNodeAddresses.remove(candidate); + requestBlocks(candidate, startBlockHeight); + }, () -> { + tryWithNewSeedNode(startBlockHeight); + }); + } + + public void reset() { + lastRequestedBlockHeight = 0; + lastReceivedBlockHeight = 0; + retryCounter = 0; + requestBlocksHandlerMap.values().forEach(RequestBlocksHandler::terminate); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ConnectionListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + closeHandler(connection); + + if (peerManager.isPeerBanned(closeConnectionReason, connection)) { + connection.getPeersNodeAddressOptional().ifPresent(nodeAddress -> { + seedNodeAddresses.remove(nodeAddress); + removeFromRequestBlocksHandlerMap(nodeAddress); + }); + } + } + + @Override + public void onError(Throwable throwable) { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PeerManager.Listener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAllConnectionsLost() { + closeAllHandlers(); + stopRetryTimer(); + stopped = true; + tryWithNewSeedNode(lastRequestedBlockHeight); + } + + @Override + public void onNewConnectionAfterAllConnectionsLost() { + closeAllHandlers(); + stopped = false; + tryWithNewSeedNode(lastRequestedBlockHeight); + } + + @Override + public void onAwakeFromStandby() { + log.info("onAwakeFromStandby"); + closeAllHandlers(); + stopped = false; + tryWithNewSeedNode(lastRequestedBlockHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof NewBlockBroadcastMessage) { + NewBlockBroadcastMessage newBlockBroadcastMessage = (NewBlockBroadcastMessage) networkEnvelope; + // We combine blockHash and txId list in case we receive blocks with different transactions. + List txIds = newBlockBroadcastMessage.getBlock().getRawTxs().stream() + .map(BaseTx::getId) + .collect(Collectors.toList()); + String blockUid = newBlockBroadcastMessage.getBlock().getHash() + ":" + txIds; + if (receivedBlocks.contains(blockUid)) { + log.debug("We had that message already and do not further broadcast it. blockUid={}", blockUid); + return; + } + + log.info("We received a NewBlockBroadcastMessage from peer {} and broadcast it to our peers. blockUid={}", + connection.getPeersNodeAddressOptional().orElse(null), blockUid); + receivedBlocks.add(blockUid); + broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null)); + listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // RequestData + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestBlocks(NodeAddress peersNodeAddress, int startBlockHeight) { + if (stopped) { + log.warn("We have stopped already. We ignore that requestData call."); + return; + } + + Tuple2 key = new Tuple2<>(peersNodeAddress, startBlockHeight); + if (requestBlocksHandlerMap.containsKey(key)) { + log.warn("We have started already a requestDataHandshake for startBlockHeight {} to peer. nodeAddress={}\n" + + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes.", + peersNodeAddress, startBlockHeight); + + UserThread.runAfter(() -> { + if (requestBlocksHandlerMap.containsKey(key)) { + RequestBlocksHandler handler = requestBlocksHandlerMap.get(key); + handler.terminate(); + requestBlocksHandlerMap.remove(key); + } + }, CLEANUP_TIMER); + return; + } + + if (startBlockHeight < lastReceivedBlockHeight) { + log.warn("startBlockHeight must not be smaller than lastReceivedBlockHeight. That should never happen." + + "startBlockHeight={},lastReceivedBlockHeight={}", startBlockHeight, lastReceivedBlockHeight); + DevEnv.logErrorAndThrowIfDevMode("startBlockHeight must be larger than lastReceivedBlockHeight. startBlockHeight=" + + startBlockHeight + " / lastReceivedBlockHeight=" + lastReceivedBlockHeight); + return; + } + + RequestBlocksHandler requestBlocksHandler = new RequestBlocksHandler(networkNode, + peerManager, + peersNodeAddress, + startBlockHeight, + new RequestBlocksHandler.Listener() { + @Override + public void onComplete(GetBlocksResponse getBlocksResponse) { + log.info("requestBlocksHandler to {} completed", peersNodeAddress); + stopRetryTimer(); + + // need to remove before listeners are notified as they cause the update call + requestBlocksHandlerMap.remove(key); + // we only notify if our request was latest + if (startBlockHeight >= lastReceivedBlockHeight) { + lastReceivedBlockHeight = startBlockHeight; + + listeners.forEach(listener -> listener.onRequestedBlocksReceived(getBlocksResponse, + () -> { + })); + } else { + log.warn("We got a response which is already obsolete because we received a " + + "response from a request with a higher block height. " + + "This could theoretically happen, but is very unlikely."); + } + } + + @Override + public void onFault(String errorMessage, @Nullable Connection connection) { + log.warn("requestBlocksHandler with outbound connection failed.\n\tnodeAddress={}\n\t" + + "ErrorMessage={}", peersNodeAddress, errorMessage); + + peerManager.handleConnectionFault(peersNodeAddress); + requestBlocksHandlerMap.remove(key); + + listeners.forEach(listener -> listener.onFault(errorMessage, connection)); + + tryWithNewSeedNode(startBlockHeight); + } + }); + requestBlocksHandlerMap.put(key, requestBlocksHandler); + requestBlocksHandler.requestBlocks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void tryWithNewSeedNode(int startBlockHeight) { + if (networkNode.getAllConnections().isEmpty()) { + return; + } + + if (lastRequestedBlockHeight == 0) { + return; + } + + if (stopped) { + return; + } + + if (retryTimer != null) { + log.warn("We have a retry timer already running."); + return; + } + + retryCounter++; + + if (retryCounter > MAX_RETRY) { + log.warn("We tried {} times but could not connect to a seed node.", retryCounter); + listeners.forEach(Listener::onNoSeedNodeAvailable); + return; + } + + retryTimer = UserThread.runAfter(() -> { + stopped = false; + + stopRetryTimer(); + + List list = seedNodeAddresses.stream() + .filter(e -> peerManager.isSeedNode(e) && !peerManager.isSelf(e)) + .collect(Collectors.toList()); + Collections.shuffle(list); + + if (!list.isEmpty()) { + NodeAddress nextCandidate = list.get(0); + seedNodeAddresses.remove(nextCandidate); + log.info("We try requestBlocks from {} with startBlockHeight={}", nextCandidate, startBlockHeight); + requestBlocks(nextCandidate, startBlockHeight); + } else { + log.warn("No more seed nodes available we could try."); + listeners.forEach(Listener::onNoSeedNodeAvailable); + } + }, + RETRY_DELAY_SEC); + } + + private void stopRetryTimer() { + if (retryTimer != null) { + retryTimer.stop(); + retryTimer = null; + } + } + + private void closeHandler(Connection connection) { + Optional peersNodeAddressOptional = connection.getPeersNodeAddressOptional(); + if (peersNodeAddressOptional.isPresent()) { + NodeAddress nodeAddress = peersNodeAddressOptional.get(); + removeFromRequestBlocksHandlerMap(nodeAddress); + } else { + log.trace("closeHandler: nodeAddress not set in connection {}", connection); + } + } + + private void removeFromRequestBlocksHandlerMap(NodeAddress nodeAddress) { + requestBlocksHandlerMap.entrySet().stream() + .filter(e -> e.getKey().first.equals(nodeAddress)) + .findAny() + .ifPresent(e -> { + e.getValue().terminate(); + requestBlocksHandlerMap.remove(e.getKey()); + }); + } + + private void closeAllHandlers() { + requestBlocksHandlerMap.values().forEach(RequestBlocksHandler::terminate); + requestBlocksHandlerMap.clear(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java new file mode 100644 index 0000000000..752e4829f8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java @@ -0,0 +1,226 @@ +/* + * 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 bisq.core.dao.node.lite.network; + +import bisq.core.dao.node.messages.GetBlocksRequest; +import bisq.core.dao.node.messages.GetBlocksResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Sends a GetBlocksRequest to a full node and listens on corresponding GetBlocksResponse from the full node. + */ +@Slf4j +public class RequestBlocksHandler implements MessageListener { + private static final long TIMEOUT_MIN = 3; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onComplete(GetBlocksResponse getBlocksResponse); + + @SuppressWarnings("UnusedParameters") + void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final PeerManager peerManager; + @Getter + private final NodeAddress nodeAddress; + @Getter + private final int startBlockHeight; + private final Listener listener; + private Timer timeoutTimer; + private final int nonce = new Random().nextInt(); + private boolean stopped; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public RequestBlocksHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + int startBlockHeight, + Listener listener) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.nodeAddress = nodeAddress; + this.startBlockHeight = startBlockHeight; + this.listener = listener; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestBlocks() { + if (stopped) { + log.warn("We have stopped already. We ignore that requestData call."); + return; + } + + GetBlocksRequest getBlocksRequest = new GetBlocksRequest(startBlockHeight, nonce, networkNode.getNodeAddress()); + + if (timeoutTimer != null) { + log.warn("We had a timer already running and stop it."); + timeoutTimer.stop(); + } + timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + if (!stopped) { + String errorMessage = "A timeout occurred when sending getBlocksRequest:" + getBlocksRequest + + " on peersNodeAddress:" + nodeAddress; + log.debug("{} / RequestDataHandler={}", errorMessage, RequestBlocksHandler.this); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT); + } else { + log.warn("We have stopped already. We ignore that timeoutTimer.run call. " + + "Might be caused by a previous networkNode.sendMessage.onFailure."); + } + }, + TIMEOUT_MIN, TimeUnit.MINUTES); + + log.info("We request blocks from peer {} from block height {}.", nodeAddress, getBlocksRequest.getFromBlockHeight()); + + networkNode.addMessageListener(this); + + SettableFuture future = networkNode.sendMessage(nodeAddress, getBlocksRequest); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + log.info("Sending of GetBlocksRequest message to peer {} succeeded.", nodeAddress.getFullAddress()); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getBlocksRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "getBlocksRequest=" + getBlocksRequest + "." + + "\n\tException=" + throwable.getMessage(); + log.error(errorMessage); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); + } else { + log.warn("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); + } + } + }, MoreExecutors.directExecutor()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetBlocksResponse) { + if (stopped) { + log.warn("We have stopped already. We ignore that onDataRequest call."); + return; + } + + Optional optionalNodeAddress = connection.getPeersNodeAddressOptional(); + if (!optionalNodeAddress.isPresent()) { + log.warn("Peers node address is not present, that is not expected."); + // We do not return here as in case the connection has been created from the peers side we might not + // have the address set. As we check the nonce later we do not care that much for the check if the + // connection address is the same as the one we used. + } else if (!optionalNodeAddress.get().equals(nodeAddress)) { + log.warn("Peers node address is not the same we used for the request. This is not expected. We ignore that message."); + return; + } + + GetBlocksResponse getBlocksResponse = (GetBlocksResponse) networkEnvelope; + if (getBlocksResponse.getRequestNonce() != nonce) { + log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " + + "handshake (timeout causes connection close but peer might have sent a msg before " + + "connection was closed).\n\t" + + "We drop that message. nonce={} / requestNonce={}", + nonce, getBlocksResponse.getRequestNonce()); + return; + } + + terminate(); + log.info("We received from peer {} a BlocksResponse with {} blocks", + nodeAddress.getFullAddress(), getBlocksResponse.getBlocks().size()); + listener.onComplete(getBlocksResponse); + } + } + + public void terminate() { + stopped = true; + networkNode.removeMessageListener(this); + stopTimeoutTimer(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + + @SuppressWarnings("UnusedParameters") + private void handleFault(String errorMessage, + NodeAddress nodeAddress, + CloseConnectionReason closeConnectionReason) { + terminate(); + peerManager.handleConnectionFault(nodeAddress); + listener.onFault(errorMessage, null); + } + + private void stopTimeoutTimer() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/messages/GetBlocksRequest.java b/core/src/main/java/bisq/core/dao/node/messages/GetBlocksRequest.java new file mode 100644 index 0000000000..f1d216e94c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/messages/GetBlocksRequest.java @@ -0,0 +1,126 @@ +/* + * 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 bisq.core.dao.node.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.InitialDataRequest; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendersNodeAddressMessage; +import bisq.network.p2p.SupportedCapabilitiesMessage; + +import bisq.common.app.Capabilities; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +// TODO We remove CapabilityRequiringPayload as it would cause problems if the lite node connects to a new seed node and +// they have not exchanged capabilities already. We need to improve capability handling the we can re-enable it again. +// As this message is sent any only to seed nodes it does not has any effect. Even if a lite node receives it it will be +// simply ignored. + +// This message is sent only to full DAO nodes +@EqualsAndHashCode(callSuper = true) +@Getter +@Slf4j +public final class GetBlocksRequest extends NetworkEnvelope implements DirectMessage, SendersNodeAddressMessage, + SupportedCapabilitiesMessage, InitialDataRequest { + private final int fromBlockHeight; + private final int nonce; + + // Added after version 1.0.1. Can be null if received from older clients. + @Nullable + private final NodeAddress senderNodeAddress; + + // Added after version 1.0.1. Can be null if received from older clients. + @Nullable + private final Capabilities supportedCapabilities; + + public GetBlocksRequest(int fromBlockHeight, + int nonce, + @Nullable NodeAddress senderNodeAddress) { + this(fromBlockHeight, + nonce, + senderNodeAddress, + Capabilities.app, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlocksRequest(int fromBlockHeight, + int nonce, + @Nullable NodeAddress senderNodeAddress, + @Nullable Capabilities supportedCapabilities, + int messageVersion) { + super(messageVersion); + this.fromBlockHeight = fromBlockHeight; + this.nonce = nonce; + this.senderNodeAddress = senderNodeAddress; + this.supportedCapabilities = supportedCapabilities; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.GetBlocksRequest.Builder builder = protobuf.GetBlocksRequest.newBuilder() + .setFromBlockHeight(fromBlockHeight) + .setNonce(nonce); + Optional.ofNullable(senderNodeAddress).ifPresent(e -> builder.setSenderNodeAddress(e.toProtoMessage())); + Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); + return getNetworkEnvelopeBuilder().setGetBlocksRequest(builder).build(); + } + + public static NetworkEnvelope fromProto(protobuf.GetBlocksRequest proto, int messageVersion) { + protobuf.NodeAddress protoNodeAddress = proto.getSenderNodeAddress(); + NodeAddress senderNodeAddress = protoNodeAddress.getHostName().isEmpty() ? + null : + NodeAddress.fromProto(protoNodeAddress); + Capabilities supportedCapabilities = proto.getSupportedCapabilitiesList().isEmpty() ? + null : + Capabilities.fromIntList(proto.getSupportedCapabilitiesList()); + return new GetBlocksRequest(proto.getFromBlockHeight(), + proto.getNonce(), + senderNodeAddress, + supportedCapabilities, + messageVersion); + } + +// @Override +// public Capabilities getRequiredCapabilities() { +// return new Capabilities(Capability.DAO_FULL_NODE); +// } + + @Override + public String toString() { + return "GetBlocksRequest{" + + "\n fromBlockHeight=" + fromBlockHeight + + ",\n nonce=" + nonce + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n supportedCapabilities=" + supportedCapabilities + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/messages/GetBlocksResponse.java b/core/src/main/java/bisq/core/dao/node/messages/GetBlocksResponse.java new file mode 100644 index 0000000000..4d9876c09c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/messages/GetBlocksResponse.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.dao.node.messages; + +import bisq.core.dao.node.full.RawBlock; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.ExtendedDataSizePermission; +import bisq.network.p2p.InitialDataRequest; +import bisq.network.p2p.InitialDataResponse; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@Getter +@Slf4j +public final class GetBlocksResponse extends NetworkEnvelope implements DirectMessage, + ExtendedDataSizePermission, InitialDataResponse { + private final List blocks; + private final int requestNonce; + + public GetBlocksResponse(List blocks, int requestNonce) { + this(blocks, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlocksResponse(List blocks, int requestNonce, int messageVersion) { + super(messageVersion); + this.blocks = blocks; + this.requestNonce = requestNonce; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.NetworkEnvelope proto = getNetworkEnvelopeBuilder() + .setGetBlocksResponse(protobuf.GetBlocksResponse.newBuilder() + .addAllRawBlocks(blocks.stream() + .map(RawBlock::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + log.info("Sending a GetBlocksResponse with {} kB", proto.getSerializedSize() / 1000d); + return proto; + } + + public static NetworkEnvelope fromProto(protobuf.GetBlocksResponse proto, int messageVersion) { + List list = proto.getRawBlocksList().stream() + .map(RawBlock::fromProto) + .collect(Collectors.toList()); + log.info("Received a GetBlocksResponse with {} blocks and {} kB size", list.size(), proto.getSerializedSize() / 1000d); + return new GetBlocksResponse(proto.getRawBlocksList().isEmpty() ? + new ArrayList<>() : + list, + proto.getRequestNonce(), + messageVersion); + } + + + @Override + public String toString() { + return "GetBlocksResponse{" + + "\n blocks=" + blocks + + ",\n requestNonce=" + requestNonce + + "\n} " + super.toString(); + } + + @Override + public Class associatedRequest() { + return GetBlocksRequest.class; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/messages/NewBlockBroadcastMessage.java b/core/src/main/java/bisq/core/dao/node/messages/NewBlockBroadcastMessage.java new file mode 100644 index 0000000000..658ace48e0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/messages/NewBlockBroadcastMessage.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.dao.node.messages; + +import bisq.core.dao.node.full.RawBlock; + +import bisq.network.p2p.storage.messages.BroadcastMessage; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +// We remove the CapabilityRequiringPayload interface to avoid risks that new BSQ blocks are not well distributed in +// case the capability is not exchanged at the time when the message is sent. We need to improve the capability handling +// so that we can be sure that we know the actual capability of the peer. + +// This message is sent only to lite DAO nodes (full nodes get block from their local bitcoind) +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewBlockBroadcastMessage extends BroadcastMessage /*implements CapabilityRequiringPayload*/ { + private final RawBlock block; + + public NewBlockBroadcastMessage(RawBlock block) { + this(block, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewBlockBroadcastMessage(RawBlock block, int messageVersion) { + super(messageVersion); + this.block = block; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewBlockBroadcastMessage(protobuf.NewBlockBroadcastMessage.newBuilder() + .setRawBlock(block.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.NewBlockBroadcastMessage proto, int messageVersion) { + return new NewBlockBroadcastMessage(RawBlock.fromProto(proto.getRawBlock()), + messageVersion); + } + +// @Override +// public Capabilities getRequiredCapabilities() { +// return new Capabilities(Capability.RECEIVE_BSQ_BLOCK); +// } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java new file mode 100644 index 0000000000..900634bfb0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java @@ -0,0 +1,138 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.node.full.RawBlock; +import bisq.core.dao.node.parser.exceptions.BlockHashNotConnectingException; +import bisq.core.dao.node.parser.exceptions.BlockHeightNotConnectingException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.LinkedList; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Parse a rawBlock and creates a block from it with an empty tx list. + * Iterates all rawTx and if the tx is a BSQ tx it gets added to the tx list. + */ +@Slf4j +@Immutable +public class BlockParser { + private final TxParser txParser; + private final DaoStateService daoStateService; + private final String genesisTxId; + private final int genesisBlockHeight; + private final Coin genesisTotalSupply; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BlockParser(TxParser txParser, + DaoStateService daoStateService) { + this.txParser = txParser; + this.daoStateService = daoStateService; + this.genesisTxId = daoStateService.getGenesisTxId(); + this.genesisBlockHeight = daoStateService.getGenesisBlockHeight(); + this.genesisTotalSupply = daoStateService.getGenesisTotalSupply(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * + * @param rawBlock Contains all transactions of a bitcoin block without any BSQ specific data + * @return Block: Gets created from the rawBlock but contains only BSQ specific transactions. + * @throws BlockHashNotConnectingException If new block does not connect to previous block + * @throws BlockHeightNotConnectingException If new block height is not current chain Height + 1 + */ + public Block parseBlock(RawBlock rawBlock) throws BlockHashNotConnectingException, BlockHeightNotConnectingException { + int blockHeight = rawBlock.getHeight(); + log.trace("Parse block at height={} ", blockHeight); + + validateIfBlockIsConnecting(rawBlock); + + daoStateService.onNewBlockHeight(blockHeight); + + // We create a block from the rawBlock but the transaction list is not set yet (is empty) + final Block block = new Block(blockHeight, + rawBlock.getTime(), + rawBlock.getHash(), + rawBlock.getPreviousBlockHash()); + + if (isBlockAlreadyAdded(rawBlock)) { + log.warn("Block was already added."); + DevEnv.logErrorAndThrowIfDevMode("Block was already added. rawBlock=" + rawBlock); + } else { + daoStateService.onNewBlockWithEmptyTxs(block); + } + + // Worst case is that all txs in a block are depending on another, so only one get resolved at each iteration. + // Min tx size is 189 bytes (normally about 240 bytes), 1 MB can contain max. about 5300 txs (usually 2000). + // Realistically we don't expect more than a few recursive calls. + // There are some blocks with testing such dependency chains like block 130768 where at each iteration only + // one get resolved. + // Lately there is a patter with 24 iterations observed + long startTs = System.currentTimeMillis(); + + rawBlock.getRawTxs().forEach(rawTx -> + txParser.findTx(rawTx, + genesisTxId, + genesisBlockHeight, + genesisTotalSupply) + .ifPresent(tx -> daoStateService.onNewTxForLastBlock(block, tx))); + + log.info("Parsing {} transactions at block height {} took {} ms", rawBlock.getRawTxs().size(), + blockHeight, System.currentTimeMillis() - startTs); + + daoStateService.onParseBlockComplete(block); + return block; + } + + private void validateIfBlockIsConnecting(RawBlock rawBlock) throws BlockHashNotConnectingException, BlockHeightNotConnectingException { + LinkedList blocks = daoStateService.getBlocks(); + + if (blocks.isEmpty()) + return; + + Block last = blocks.getLast(); + if (last.getHeight() + 1 != rawBlock.getHeight()) + throw new BlockHeightNotConnectingException(rawBlock); + + if (!last.getHash().equals(rawBlock.getPreviousBlockHash())) + throw new BlockHashNotConnectingException(rawBlock); + } + + private boolean isBlockAlreadyAdded(RawBlock rawBlock) { + return daoStateService.isBlockHashKnown(rawBlock.getHash()); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java b/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java new file mode 100644 index 0000000000..7b7a054bde --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.node.full.RawTx; +import bisq.core.dao.node.parser.exceptions.InvalidGenesisTxException; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputType; +import bisq.core.dao.state.model.blockchain.TxType; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class GenesisTxParser { + public static boolean isGenesis(RawTx rawTx, String genesisTxId, int genesisBlockHeight) { + return rawTx.getBlockHeight() == genesisBlockHeight && rawTx.getId().equals(genesisTxId); + } + + public static Tx getGenesisTx(RawTx rawTx, Coin genesisTotalSupply, DaoStateService daoStateService) { + TempTx genesisTx = getGenesisTempTx(rawTx, genesisTotalSupply); + commitUTXOs(daoStateService, genesisTx); + return Tx.fromTempTx(genesisTx); + } + + private static void commitUTXOs(DaoStateService daoStateService, TempTx genesisTx) { + ImmutableList outputs = genesisTx.getTempTxOutputs(); + for (int i = 0; i < outputs.size(); ++i) { + TempTxOutput tempTxOutput = outputs.get(i); + daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(tempTxOutput)); + } + } + + /** + * Parse and return the genesis transaction for bisq, if applicable. + * + * @param rawTx The candidate transaction. + * @param genesisTotalSupply The total supply of the genesis issuance for bisq. + * @return The genesis transaction. + */ + @VisibleForTesting + static TempTx getGenesisTempTx(RawTx rawTx, Coin genesisTotalSupply) { + TempTx tempTx = TempTx.fromRawTx(rawTx); + tempTx.setTxType(TxType.GENESIS); + long remainingInputValue = genesisTotalSupply.getValue(); + List tempTxOutputs = tempTx.getTempTxOutputs(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < tempTxOutputs.size(); ++i) { + TempTxOutput txOutput = tempTxOutputs.get(i); + long value = txOutput.getValue(); + boolean isValid = value <= remainingInputValue; + if (!isValid) + throw new InvalidGenesisTxException("Genesis tx is invalid; using more than available inputs. " + + "Remaining input value is " + remainingInputValue + " sat; tx info: " + tempTx.toString()); + + remainingInputValue -= value; + txOutput.setTxOutputType(TxOutputType.GENESIS_OUTPUT); + } + + if (remainingInputValue > 0) { + throw new InvalidGenesisTxException("Genesis tx is invalid; not using all available inputs. " + + "Remaining input value is " + remainingInputValue + " sat, tx info: " + tempTx.toString()); + } + + return tempTx; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java b/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java new file mode 100644 index 0000000000..3b15bf322c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java @@ -0,0 +1,131 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.governance.asset.AssetConsensus; +import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.bond.lockup.LockupReason; +import bisq.core.dao.governance.proofofburn.ProofOfBurnConsensus; +import bisq.core.dao.governance.proposal.ProposalConsensus; +import bisq.core.dao.governance.voteresult.VoteResultConsensus; +import bisq.core.dao.node.parser.exceptions.InvalidParsingConditionException; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.blockchain.TxOutputType; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Utils; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Processes OpReturn output if valid and delegates validation to specific validators. + */ +@Slf4j +class OpReturnParser { + + /** + * Parse the type of OP_RETURN data and validate it. + * + * @param tempTxOutput The temporary transaction output to parse. + * @return The type of the transaction output, which will be either one of the + * {@code *_OP_RETURN_OUTPUT} values, or {@code UNDEFINED} in case of + * unexpected state. + */ + static TxOutputType getTxOutputType(TempTxOutput tempTxOutput) { + boolean nonZeroOutput = tempTxOutput.getValue() != 0; + byte[] opReturnData = tempTxOutput.getOpReturnData(); + checkNotNull(opReturnData, "opReturnData must not be null"); + + if (nonZeroOutput || opReturnData.length < 22) { + log.warn("OP_RETURN data does not match our rules. opReturnData={}", + Utils.HEX.encode(opReturnData)); + return TxOutputType.INVALID_OUTPUT; + } + Optional optionalOpReturnType = OpReturnType.getOpReturnType(opReturnData[0]); + if (!optionalOpReturnType.isPresent()) { + log.warn("OP_RETURN data does not match our defined types. opReturnData={}", + Utils.HEX.encode(opReturnData)); + return TxOutputType.INVALID_OUTPUT; + } + + switch (optionalOpReturnType.get()) { + case PROPOSAL: + if (ProposalConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.PROPOSAL_OP_RETURN_OUTPUT; + else + break; + case COMPENSATION_REQUEST: + if (ProposalConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.COMP_REQ_OP_RETURN_OUTPUT; + else + break; + case REIMBURSEMENT_REQUEST: + if (ProposalConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.REIMBURSEMENT_OP_RETURN_OUTPUT; + else + break; + case BLIND_VOTE: + if (BlindVoteConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.BLIND_VOTE_OP_RETURN_OUTPUT; + else + break; + case VOTE_REVEAL: + if (VoteResultConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.VOTE_REVEAL_OP_RETURN_OUTPUT; + else + break; + case LOCKUP: + if (!BondConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.INVALID_OUTPUT; + Optional optionalLockupReason = BondConsensus.getLockupReason(opReturnData); + if (!optionalLockupReason.isPresent()) { + log.warn("No lockupReason found for lockup tx, opReturnData=" + Utilities.encodeToHex(opReturnData)); + return TxOutputType.INVALID_OUTPUT; + } + + int lockTime = BondConsensus.getLockTime(opReturnData); + if (BondConsensus.isLockTimeInValidRange(lockTime)) { + return TxOutputType.LOCKUP_OP_RETURN_OUTPUT; + } else { + break; + } + case ASSET_LISTING_FEE: + if (AssetConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.ASSET_LISTING_FEE_OP_RETURN_OUTPUT; + else + break; + case PROOF_OF_BURN: + if (ProofOfBurnConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT; + else + break; + default: + throw new InvalidParsingConditionException("We must have a defined opReturnType as it was checked earlier in the caller."); + } + + log.info("We expected a compensation request op_return data but it did not " + + "match our rules."); + return TxOutputType.INVALID_OUTPUT; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TempTx.java b/core/src/main/java/bisq/core/dao/node/parser/TempTx.java new file mode 100644 index 0000000000..b98d2f4579 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/TempTx.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.node.full.RawTx; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxType; + +import com.google.common.collect.ImmutableList; + +import java.util.Objects; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +/** + * Used only temporary during the transaction parsing process to support mutable data while parsing. + * After parsing it will get cloned to the immutable Tx. + * We don't need to implement the ProtoBuffer methods as it is not persisted or sent over the wire. + */ +@Getter +@Setter +public class TempTx extends BaseTx { + static TempTx fromRawTx(RawTx rawTx) { + return new TempTx(rawTx.getTxVersion(), + rawTx.getId(), + rawTx.getBlockHeight(), + rawTx.getBlockHash(), + rawTx.getTime(), + rawTx.getTxInputs(), + ImmutableList.copyOf(rawTx.getRawTxOutputs().stream().map(TempTxOutput::fromRawTxOutput).collect(Collectors.toList())), + null, + 0); + } + + private final ImmutableList tempTxOutputs; + + // Mutable data + @Nullable + private TxType txType; + private long burntBsq; + + private TempTx(String txVersion, + String id, + int blockHeight, + String blockHash, + long time, + ImmutableList txInputs, + ImmutableList tempTxOutputs, + @Nullable TxType txType, + long burntBsq) { + super(txVersion, + id, + blockHeight, + blockHash, + time, + txInputs); + this.tempTxOutputs = tempTxOutputs; + this.txType = txType; + this.burntBsq = burntBsq; + } + + @Override + public String toString() { + return "TempTx{" + + "\n txOutputs=" + tempTxOutputs + + ",\n txType=" + txType + + ",\n burntBsq=" + burntBsq + + "\n} " + super.toString(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TempTx)) return false; + if (!super.equals(o)) return false; + TempTx tempTx = (TempTx) o; + + String name = txType != null ? txType.name() : ""; + String name1 = tempTx.txType != null ? tempTx.txType.name() : ""; + boolean isTxTypeEquals = name.equals(name1); + return burntBsq == tempTx.burntBsq && + Objects.equals(tempTxOutputs, tempTx.tempTxOutputs) && + isTxTypeEquals; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), tempTxOutputs, txType, burntBsq); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TempTxOutput.java b/core/src/main/java/bisq/core/dao/node/parser/TempTxOutput.java new file mode 100644 index 0000000000..8aa78c2b08 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/TempTxOutput.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.node.full.RawTxOutput; +import bisq.core.dao.state.model.blockchain.BaseTxOutput; +import bisq.core.dao.state.model.blockchain.PubKeyScript; +import bisq.core.dao.state.model.blockchain.TxOutputType; + +import java.util.Objects; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +/** + * Contains mutable BSQ specific data (TxOutputType) and used only during tx parsing. + * Will get converted to immutable TxOutput after tx parsing is completed. + */ +@Getter +@Setter +public class TempTxOutput extends BaseTxOutput { + static TempTxOutput fromRawTxOutput(RawTxOutput txOutput) { + return new TempTxOutput(txOutput.getIndex(), + txOutput.getValue(), + txOutput.getTxId(), + txOutput.getPubKeyScript(), + txOutput.getAddress(), + txOutput.getOpReturnData(), + txOutput.getBlockHeight(), + TxOutputType.UNDEFINED_OUTPUT, + -1, + 0); + } + + private TxOutputType txOutputType; + + // The lockTime is stored in the first output of the LOCKUP tx. + // If not set it is -1, 0 is a valid value. + private int lockTime; + // The unlockBlockHeight is stored in the first output of the UNLOCK tx. + private int unlockBlockHeight; + + private TempTxOutput(int index, + long value, + String txId, + @Nullable PubKeyScript pubKeyScript, + @Nullable String address, + @Nullable byte[] opReturnData, + int blockHeight, + TxOutputType txOutputType, + int lockTime, + int unlockBlockHeight) { + super(index, + value, + txId, + pubKeyScript, + address, + opReturnData, + blockHeight); + + this.txOutputType = txOutputType; + this.lockTime = lockTime; + this.unlockBlockHeight = unlockBlockHeight; + } + + public boolean isOpReturnOutput() { + // We do not check for pubKeyScript.scriptType.NULL_DATA because that is only set if dumpBlockchainData is true + return getOpReturnData() != null; + } + + @Override + public String toString() { + return "TempTxOutput{" + + "\n txOutputType=" + txOutputType + + "\n lockTime=" + lockTime + + "\n unlockBlockHeight=" + unlockBlockHeight + + "\n} " + super.toString(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TempTxOutput)) return false; + if (!super.equals(o)) return false; + TempTxOutput that = (TempTxOutput) o; + return lockTime == that.lockTime && + unlockBlockHeight == that.unlockBlockHeight && + txOutputType.name().equals(that.txOutputType.name()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txOutputType.name(), lockTime, unlockBlockHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java new file mode 100644 index 0000000000..d5b9b58a53 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java @@ -0,0 +1,149 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.SpentInfo; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.model.blockchain.TxOutputType; + +import javax.inject.Inject; + +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Processes TxInput and add input value to available balance if the input is a valid BSQ input. + */ +@Slf4j +public class TxInputParser { + private final DaoStateService daoStateService; + + // Getters + @Getter + private long accumulatedInputValue = 0; + @Getter + private long burntBondValue = 0; + @Getter + private int unlockBlockHeight; + @Getter + private Optional optionalSpentLockupTxOutput = Optional.empty(); + @Getter + private boolean isUnLockInputValid = true; + + // Private + private int numVoteRevealInputs = 0; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TxInputParser(DaoStateService daoStateService) { + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + void process(TxOutputKey txOutputKey, int blockHeight, String txId, int inputIndex) { + if (!daoStateService.isConfiscatedOutput(txOutputKey)) { + daoStateService.getUnspentTxOutput(txOutputKey) + .ifPresent(connectedTxOutput -> { + long inputValue = connectedTxOutput.getValue(); + accumulatedInputValue += inputValue; + + // If we are spending an output from a blind vote tx marked as VOTE_STAKE_OUTPUT we save it in our parsingModel + // for later verification at the outputs of a reveal tx. + TxOutputType connectedTxOutputType = connectedTxOutput.getTxOutputType(); + switch (connectedTxOutputType) { + case UNDEFINED_OUTPUT: + case GENESIS_OUTPUT: + case BSQ_OUTPUT: + case BTC_OUTPUT: + case PROPOSAL_OP_RETURN_OUTPUT: + case COMP_REQ_OP_RETURN_OUTPUT: + case REIMBURSEMENT_OP_RETURN_OUTPUT: + case CONFISCATE_BOND_OP_RETURN_OUTPUT: + case ISSUANCE_CANDIDATE_OUTPUT: + break; + case BLIND_VOTE_LOCK_STAKE_OUTPUT: + numVoteRevealInputs++; + // The connected tx output of the blind vote tx is our input for the reveal tx. + // We allow only one input from any blind vote tx otherwise the vote reveal tx is invalid. + if (!isVoteRevealInputValid()) { + log.warn("We have a tx which has >1 connected txOutputs marked as BLIND_VOTE_LOCK_STAKE_OUTPUT. " + + "This is not a valid BSQ tx."); + } + break; + case BLIND_VOTE_OP_RETURN_OUTPUT: + case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: + case VOTE_REVEAL_OP_RETURN_OUTPUT: + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + break; + case LOCKUP_OUTPUT: + // A LOCKUP BSQ txOutput is spent to a corresponding UNLOCK + // txInput. The UNLOCK can only be spent after lockTime blocks has passed. + isUnLockInputValid = !optionalSpentLockupTxOutput.isPresent(); + if (isUnLockInputValid) { + optionalSpentLockupTxOutput = Optional.of(connectedTxOutput); + unlockBlockHeight = blockHeight + connectedTxOutput.getLockTime(); + } else { + log.warn("We have a tx which has >1 connected txOutputs marked as LOCKUP_OUTPUT. " + + "This is not a valid BSQ tx."); + } + break; + case LOCKUP_OP_RETURN_OUTPUT: + // Cannot happen + break; + case UNLOCK_OUTPUT: + // This txInput is Spending an UNLOCK txOutput + int unlockBlockHeight = connectedTxOutput.getUnlockBlockHeight(); + if (blockHeight < unlockBlockHeight) { + accumulatedInputValue -= inputValue; + burntBondValue += inputValue; + + log.warn("We got a tx which spends the output from an unlock tx but before the " + + "unlockTime has passed. That leads to burned BSQ! " + + "blockHeight={}, unLockHeight={}", blockHeight, unlockBlockHeight); + } + break; + case INVALID_OUTPUT: + default: + break; + } + + daoStateService.setSpentInfo(connectedTxOutput.getKey(), new SpentInfo(blockHeight, txId, inputIndex)); + daoStateService.removeUnspentTxOutput(connectedTxOutput); + }); + } else { + log.warn("Connected txOutput {} at input {} of txId {} is confiscated ", txOutputKey, inputIndex, txId); + } + } + + private boolean isVoteRevealInputValid() { + return numVoteRevealInputs == 1; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java new file mode 100644 index 0000000000..7f311d6406 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -0,0 +1,450 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputType; + +import bisq.common.config.Config; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Checks if an output is a BSQ output and apply state change. + * + * With block 602500 (about 4 weeks after v1.2.0 release) we enforce a new rule which represents a + * hard fork. Not updated nodes would see an out of sync dao state hash if a relevant transaction would + * happen again. + * Further (highly unlikely) consequences could be: + * If the BSQ output would be sent to a BSQ address the old client would accept that even it is + * invalid according to the new rules. But sending such an output would require a manually crafted tx + * (not possible in the UI). Worst case a not updated user would buy invalid BSQ but that is not possible as we + * enforce update to 1.2.0 for trading a few days after release as that release introduced the new trade protocol + * and protection tool. Only if both traders would have deactivated filter messages they could trade. + * + * Problem description: + * We did not apply the check to not allow BSQ outputs after we had detected a BTC output. + * The supported BSQ transactions did not support such cases anyway but we missed an edge case: + * A trade fee tx in case when the BTC input matches exactly the BTC output + * (or BTC change was <= the miner fee) and the BSQ fee was > the miner fee. Then we + * create a change output after the BTC output (using an address from the BTC wallet) and as + * available BSQ was >= as spent BSQ it was considered a valid BSQ output. + * There have been observed 5 such transactions where 4 got spent later to a BTC address and by that burned + * the pending BSQ (spending amount was higher than sending amount). One was still unspent. + * The BSQ was sitting in the BTC wallet so not even visible as BSQ to the user. + * If the user would have crafted a custom BSQ tx he could have avoided that the full trade fee was burned. + * + * Not a universal rule: + * We cannot enforce the rule that no BSQ output is permitted to all possible transactions because there can be cases + * where we need to permit this case. + * For instance in case we confiscate a lockupTx we have usually 2 BSQ outputs: The first one is the bond which + * should be confiscated and the second one is the BSQ change output. + * At confiscating we set the first to TxOutputType.BTC_OUTPUT but we do not want to confiscate + * the second BSQ change output as well. So we do not apply the rule that no BSQ is allowed once a BTC output is + * found. Theoretically other transactions could be confiscated as well and all BSQ tx which allow > 1 BSQ outputs + * would have the same issue as well if the first output gets confiscated. + * We also don't enforce the rule for irregular or invalid txs which are usually set and detected at the end of + * the tx parsing which is done in the TxParser. Blind vote and LockupTx with invalid OpReturn would be such cases + * where we don't want to invalidate the change output (See comments in TxParser). + * + * Most transactions created in Bisq (Proposal, blind vote and lockup,...) have only 1 or 2 BSQ + * outputs but we do not enforce a limit of max. 2 transactions in the parser. + * We leave for now that flexibility but it should not be considered as a rule. We might strengthen + * it any time if we find a reason for that (e.g. attack risk) and add checks that no more + * BSQ outputs are permitted for those txs. + * Some transactions like issuance, vote reveal and unlock have exactly 1 BSQ output and that rule + * is enforced. + */ +@Slf4j +class TxOutputParser { + private static final int ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET = 605000; + private static final int ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET = 1583054; + private static final int ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST = 1; + + private final DaoStateService daoStateService; + // Setters + @Getter + @Setter + private long availableInputValue = 0; + @Setter + private int unlockBlockHeight; + @Setter + @Getter + private Optional optionalSpentLockupTxOutput = Optional.empty(); + + // Getters + @Getter + private boolean bsqOutputFound; + @Getter + private Optional optionalOpReturnType = Optional.empty(); + @Getter + private Optional optionalIssuanceCandidate = Optional.empty(); + @Getter + private Optional optionalBlindVoteLockStakeOutput = Optional.empty(); + @Getter + private Optional optionalVoteRevealUnlockStakeOutput = Optional.empty(); + @Getter + private Optional optionalLockupOutput = Optional.empty(); + private Optional optionalOpReturnIndex = Optional.empty(); + + // Private + private int lockTime; + private final List utxoCandidates = new ArrayList<>(); + private boolean prohibitMoreBsqOutputs = false; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + TxOutputParser(DaoStateService daoStateService) { + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + void processOpReturnOutput(TempTxOutput tempTxOutput) { + byte[] opReturnData = tempTxOutput.getOpReturnData(); + checkNotNull(opReturnData, "opReturnData must not be null"); + TxOutputType txOutputType = OpReturnParser.getTxOutputType(tempTxOutput); + tempTxOutput.setTxOutputType(txOutputType); + + optionalOpReturnType = getMappedOpReturnType(txOutputType); + + optionalOpReturnType.ifPresent(e -> optionalOpReturnIndex = Optional.of(tempTxOutput.getIndex())); + + // If we have a LOCKUP opReturn output we save the lockTime to apply it later to the LOCKUP output. + // We keep that data in that other output as it makes parsing of the UNLOCK tx easier. + optionalOpReturnType.filter(opReturnType -> opReturnType == OpReturnType.LOCKUP) + .ifPresent(opReturnType -> lockTime = BondConsensus.getLockTime(opReturnData)); + } + + void processTxOutput(TempTxOutput tempTxOutput) { + // We don not expect here an opReturn output as we do not get called on the last output. Any opReturn at + // another output index is invalid. + if (tempTxOutput.isOpReturnOutput()) { + tempTxOutput.setTxOutputType(TxOutputType.INVALID_OUTPUT); + return; + } + + if (!daoStateService.isConfiscatedOutput(tempTxOutput.getKey())) { + long txOutputValue = tempTxOutput.getValue(); + int index = tempTxOutput.getIndex(); + if (isUnlockBondTx(tempTxOutput.getValue(), index)) { + // We need to handle UNLOCK transactions separately as they don't follow the pattern on spending BSQ + // The LOCKUP BSQ is burnt unless the output exactly matches the input, that would cause the + // output to not be BSQ output at all + handleUnlockBondTx(tempTxOutput); + } else if (isBtcOutputOfBurnFeeTx(tempTxOutput)) { + // In case we have the opReturn for a burn fee tx all outputs after 1st output are considered BTC + handleBtcOutput(tempTxOutput, index); + } else if (isHardForkActivated(tempTxOutput) && isIssuanceCandidateTxOutput(tempTxOutput)) { + // After the hard fork activation we fix a bug with a transaction which would have interpreted the + // issuance output as BSQ if the availableInputValue was >= issuance amount. + // Such a tx was never created but as we don't know if it will happen before activation date we cannot + // enforce the bug fix which represents a rule change before the activation date. + handleIssuanceCandidateOutput(tempTxOutput); + } else if (availableInputValue > 0 && availableInputValue >= txOutputValue) { + if (isHardForkActivated(tempTxOutput) && prohibitMoreBsqOutputs) { + handleBtcOutput(tempTxOutput, index); + } else { + handleBsqOutput(tempTxOutput, index, txOutputValue); + } + } else { + handleBtcOutput(tempTxOutput, index); + } + } else { + log.warn("TxOutput {} is confiscated ", tempTxOutput.getKey()); + // We only burn that output + availableInputValue -= tempTxOutput.getValue(); + + // We must not set prohibitMoreBsqOutputs at confiscation transactions as optional + // BSQ change output (output 2) must not be confiscated. + tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + } + } + + void commitUTXOCandidates() { + utxoCandidates.forEach(output -> daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(output))); + } + + /** + * This sets all outputs to BTC_OUTPUT and doesn't add any txOutputs to the unspentTxOutput map in daoStateService + */ + void invalidateUTXOCandidates() { + // We do not need to apply prohibitMoreBsqOutputs as all spendable outputs are set to BTC_OUTPUT anyway. + utxoCandidates.forEach(output -> output.setTxOutputType(TxOutputType.BTC_OUTPUT)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Whether a transaction is a valid unlock bond transaction or not. + * + * @param txOutputValue The value of the current output, in satoshis. + * @param index The index of the output. + * @return True if the transaction is an unlock transaction, false otherwise. + */ + private boolean isUnlockBondTx(long txOutputValue, int index) { + // We require that the input value is exact the available value and the output value + return index == 0 && + availableInputValue == txOutputValue && + optionalSpentLockupTxOutput.isPresent() && + optionalSpentLockupTxOutput.get().getValue() == txOutputValue; + } + + private void handleUnlockBondTx(TempTxOutput txOutput) { + checkArgument(optionalSpentLockupTxOutput.isPresent(), "optionalSpentLockupTxOutput must be present"); + availableInputValue -= optionalSpentLockupTxOutput.get().getValue(); + + txOutput.setTxOutputType(TxOutputType.UNLOCK_OUTPUT); + txOutput.setUnlockBlockHeight(unlockBlockHeight); + utxoCandidates.add(txOutput); + + bsqOutputFound = true; + + // We do not permit more BSQ outputs after the unlock txo as we don't expect additional BSQ outputs. + prohibitMoreBsqOutputs = true; + } + + private boolean isBtcOutputOfBurnFeeTx(TempTxOutput tempTxOutput) { + if (optionalOpReturnType.isPresent()) { + int index = tempTxOutput.getIndex(); + switch (optionalOpReturnType.get()) { + case UNDEFINED: + break; + case PROPOSAL: + if (isHardForkActivated(tempTxOutput)) { + // We enforce a mandatory BSQ change output. + // We need that as similar to ASSET_LISTING_FEE and PROOF_OF_BURN + // we could not distinguish between 2 structurally same transactions otherwise (only way here + // would be to check the proposal fee as that is known from the params). + return index >= 1; + } + break; + case COMPENSATION_REQUEST: + break; + case REIMBURSEMENT_REQUEST: + break; + case BLIND_VOTE: + if (isHardForkActivated(tempTxOutput)) { + // After the hard fork activation we fix a bug with a transaction which would have interpreted the + // burned vote fee output as BSQ if the vote fee was >= miner fee. + // Such a tx was never created but as we don't know if it will happen before activation date we cannot + // enforce the bug fix which represents a rule change before the activation date. + + // If it is the vote stake output we return false. + if (index == 0) { + return false; + } + + // There must be a vote fee left + if (availableInputValue <= 0) { + return false; + } + + // Burned BSQ output is last output before opReturn. + // We could have also a BSQ change output as last output before opReturn but that will + // be detected at blindVoteFee check. + // We always have the BSQ change before the burned BSQ output if both are present. + checkArgument(optionalOpReturnIndex.isPresent()); + if (index != optionalOpReturnIndex.get() - 1) { + return false; + } + + // Without checking the fee we would not be able to distinguish between 2 structurally same transactions, one + // where the output is burned BSQ and one where it is a BSQ change output. + long blindVoteFee = daoStateService.getParamValueAsCoin(Param.BLIND_VOTE_FEE, tempTxOutput.getBlockHeight()).value; + return availableInputValue == blindVoteFee; + } + break; + case VOTE_REVEAL: + break; + case LOCKUP: + break; + case ASSET_LISTING_FEE: + case PROOF_OF_BURN: + // Asset listing fee and proof of burn tx are structurally the same. + + // We need to require one BSQ change output as we could otherwise not be able to distinguish between 2 + // structurally same transactions where only the BSQ fee is different. In case of asset listing fee and proof of + // burn it is a user input, so it is not known to the parser, instead we derive the burned fee from the parser. + // In case of proposal fee we could derive it from the params. + + // Case 1: 10 BSQ fee to burn + // In: 17 BSQ + // Out: BSQ change 7 BSQ -> valid BSQ + // Out: OpReturn + // Miner fee: 1000 sat (10 BSQ burned) + + + // Case 2: 17 BSQ fee to burn + // In: 17 BSQ + // Out: burned BSQ change 7 BSQ -> BTC (7 BSQ burned) + // Out: OpReturn + // Miner fee: 1000 sat (10 BSQ burned) + return index >= 1; + } + } + return false; + } + + private boolean isIssuanceCandidateTxOutput(TempTxOutput tempTxOutput) { + // If we have BSQ left as fee and we are at the second output we interpret it as a compensation request output. + return availableInputValue > 0 && + tempTxOutput.getIndex() == 1 && + optionalOpReturnType.isPresent() && + (optionalOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST || + optionalOpReturnType.get() == OpReturnType.REIMBURSEMENT_REQUEST); + } + + private void handleIssuanceCandidateOutput(TempTxOutput tempTxOutput) { + // We do not permit more BSQ outputs after the issuance candidate. + prohibitMoreBsqOutputs = true; + + // We store the candidate but we don't apply the TxOutputType yet as we need to verify the fee after all + // outputs are parsed and check the phase. The TxParser will do that.... + optionalIssuanceCandidate = Optional.of(tempTxOutput); + } + + private void handleBsqOutput(TempTxOutput txOutput, int index, long txOutputValue) { + // Update the input balance. + availableInputValue -= txOutputValue; + + boolean isFirstOutput = index == 0; + + OpReturnType opReturnTypeCandidate = null; + if (optionalOpReturnType.isPresent()) + opReturnTypeCandidate = optionalOpReturnType.get(); + + TxOutputType txOutputType; + if (isFirstOutput && opReturnTypeCandidate == OpReturnType.BLIND_VOTE) { + txOutputType = TxOutputType.BLIND_VOTE_LOCK_STAKE_OUTPUT; + optionalBlindVoteLockStakeOutput = Optional.of(txOutput); + } else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.VOTE_REVEAL) { + txOutputType = TxOutputType.VOTE_REVEAL_UNLOCK_STAKE_OUTPUT; + optionalVoteRevealUnlockStakeOutput = Optional.of(txOutput); + + // We do not permit more BSQ outputs after the VOTE_REVEAL_UNLOCK_STAKE_OUTPUT. + prohibitMoreBsqOutputs = true; + } else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.LOCKUP) { + txOutputType = TxOutputType.LOCKUP_OUTPUT; + + // We store the lockTime in the output which will be used as input for a unlock tx. + // That makes parsing of that data easier as if we would need to access it from the opReturn output of + // that tx. + txOutput.setLockTime(lockTime); + optionalLockupOutput = Optional.of(txOutput); + } else { + txOutputType = TxOutputType.BSQ_OUTPUT; + } + txOutput.setTxOutputType(txOutputType); + utxoCandidates.add(txOutput); + + bsqOutputFound = true; + } + + private void handleBtcOutput(TempTxOutput txOutput, int index) { + if (isHardForkActivated(txOutput)) { + txOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + + // For regular transactions we don't permit BSQ outputs after a BTC output was detected. + prohibitMoreBsqOutputs = true; + } else { + // If we have BSQ left as fee and we are at the second output it might be a compensation request output. + // We store the candidate but we don't apply the TxOutputType yet as we need to verify the fee after all + // outputs are parsed and check the phase. The TxParser will do that.... + if (availableInputValue > 0 && + index == 1 && + optionalOpReturnType.isPresent() && + (optionalOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST || + optionalOpReturnType.get() == OpReturnType.REIMBURSEMENT_REQUEST)) { + optionalIssuanceCandidate = Optional.of(txOutput); + + // We do not permit more BSQ outputs after the issuance candidate. + prohibitMoreBsqOutputs = true; + } else { + txOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + + // For regular transactions we don't permit BSQ outputs after a BTC output was detected. + prohibitMoreBsqOutputs = true; + } + } + } + + private boolean isHardForkActivated(TempTxOutput tempTxOutput) { + return tempTxOutput.getBlockHeight() >= getActivateHardFork1Height(); + } + + private int getActivateHardFork1Height() { + return Config.baseCurrencyNetwork().isMainnet() ? ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET : + Config.baseCurrencyNetwork().isTestnet() ? ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET : + ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + static Optional getMappedOpReturnType(TxOutputType outputType) { + switch (outputType) { + case PROPOSAL_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.PROPOSAL); + case COMP_REQ_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.COMPENSATION_REQUEST); + case REIMBURSEMENT_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.REIMBURSEMENT_REQUEST); + case BLIND_VOTE_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.BLIND_VOTE); + case VOTE_REVEAL_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.VOTE_REVEAL); + case LOCKUP_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.LOCKUP); + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.ASSET_LISTING_FEE); + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.PROOF_OF_BURN); + default: + return Optional.empty(); + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java new file mode 100644 index 0000000000..c2d38ebebc --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -0,0 +1,451 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.node.full.RawTx; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.model.blockchain.TxOutputType; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.DaoPhase; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +/** + * Verifies if a given transaction is a BSQ transaction. + */ +@Slf4j +public class TxParser { + private final PeriodService periodService; + private final DaoStateService daoStateService; + private TxOutputParser txOutputParser; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TxParser(PeriodService periodService, + DaoStateService daoStateService) { + this.periodService = periodService; + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeight, Coin genesisTotalSupply) { + if (GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight)) + return Optional.of(GenesisTxParser.getGenesisTx(rawTx, genesisTotalSupply, daoStateService)); + else + return findTx(rawTx); + } + + // Apply state changes to tx, inputs and outputs + // return Tx if any input contained BSQ + // Any tx with BSQ input is a BSQ tx. + // There might be txs without any valid BSQ txOutput but we still keep track of it, + // for instance to calculate the total burned BSQ. + private Optional findTx(RawTx rawTx) { + int blockHeight = rawTx.getBlockHeight(); + TempTx tempTx = TempTx.fromRawTx(rawTx); + + //**************************************************************************************** + // Parse Inputs + //**************************************************************************************** + + TxInputParser txInputParser = new TxInputParser(daoStateService); + for (int inputIndex = 0; inputIndex < tempTx.getTxInputs().size(); inputIndex++) { + TxInput input = tempTx.getTxInputs().get(inputIndex); + TxOutputKey outputKey = input.getConnectedTxOutputKey(); + txInputParser.process(outputKey, blockHeight, rawTx.getId(), inputIndex); + } + + // Results from txInputParser + long accumulatedInputValue = txInputParser.getAccumulatedInputValue(); + long burntBondValue = txInputParser.getBurntBondValue(); + boolean unLockInputValid = txInputParser.isUnLockInputValid(); + int unlockBlockHeight = txInputParser.getUnlockBlockHeight(); + Optional optionalSpentLockupTxOutput = txInputParser.getOptionalSpentLockupTxOutput(); + + boolean hasBsqInputs = accumulatedInputValue > 0; + boolean hasBurntBond = burntBondValue > 0; + + // If we don't have any BSQ in our input and we don't have burnt bonds we do not consider the tx as a BSQ tx. + if (!hasBsqInputs && !hasBurntBond) + return Optional.empty(); + + + //**************************************************************************************** + // Parse Outputs + //**************************************************************************************** + + txOutputParser = new TxOutputParser(daoStateService); + txOutputParser.setAvailableInputValue(accumulatedInputValue); + txOutputParser.setUnlockBlockHeight(unlockBlockHeight); + txOutputParser.setOptionalSpentLockupTxOutput(optionalSpentLockupTxOutput); + + List outputs = tempTx.getTempTxOutputs(); + // We start with last output as that might be an OP_RETURN output and gives us the specific tx type, so it is + // easier and cleaner at parsing the other outputs to detect which kind of tx we deal with. + int lastIndex = outputs.size() - 1; + int lastNonOpReturnIndex = lastIndex; + if (outputs.get(lastIndex).isOpReturnOutput()) { + txOutputParser.processOpReturnOutput(outputs.get(lastIndex)); + lastNonOpReturnIndex -= 1; + } + + // We need to consider the order of the outputs. An output is a BSQ utxo as long there is enough input value + // We iterate all outputs (excluding an optional opReturn). + for (int index = 0; index <= lastNonOpReturnIndex; index++) { + txOutputParser.processTxOutput(outputs.get(index)); + } + + // Results from txOutputParser + long remainingInputValue = txOutputParser.getAvailableInputValue(); + Optional optionalOpReturnType = txOutputParser.getOptionalOpReturnType(); + boolean bsqOutputFound = txOutputParser.isBsqOutputFound(); + + long burntBsq = remainingInputValue + burntBondValue; + boolean hasBurntBsq = burntBsq > 0; + if (hasBurntBsq) + tempTx.setBurntBsq(burntBsq); + + + //**************************************************************************************** + // Verify and apply txType and txOutputTypes after we have all outputs parsed + //**************************************************************************************** + + applyTxTypeAndTxOutputType(blockHeight, tempTx, remainingInputValue); + TxType txType; + if (tempTx.getTxType() != TxType.IRREGULAR && tempTx.getTxType() != TxType.INVALID) { + txType = evaluateTxType(tempTx, optionalOpReturnType, hasBurntBsq, unLockInputValid); + tempTx.setTxType(txType); + } else { + txType = tempTx.getTxType(); + } + + if (isTxInvalid(tempTx, bsqOutputFound, hasBurntBond)) { + tempTx.setTxType(TxType.INVALID); + // We consider all BSQ inputs as burned if the tx is invalid. + tempTx.setBurntBsq(accumulatedInputValue); + txOutputParser.invalidateUTXOCandidates(); + log.warn("We have destroyed BSQ because of an invalid tx. Burned BSQ={}. tx={}", accumulatedInputValue / 100D, tempTx); + } else if (txType == TxType.IRREGULAR) { + log.warn("We have an irregular tx {}", tempTx); + txOutputParser.commitUTXOCandidates(); + } else { + txOutputParser.commitUTXOCandidates(); + } + + return Optional.of(Tx.fromTempTx(tempTx)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * This method verifies after all outputs are parsed if the opReturn type and the optional txOutputs required for + * certain use cases are valid. + * It verifies also if the fee is correct (if required) and if the phase is correct (if relevant). + * We set the txType as well as the txOutputType of the relevant outputs. + */ + private void applyTxTypeAndTxOutputType(int blockHeight, TempTx tempTx, long bsqFee) { + OpReturnType opReturnType = null; + Optional optionalOpReturnType = txOutputParser.getOptionalOpReturnType(); + if (optionalOpReturnType.isPresent()) { + opReturnType = optionalOpReturnType.get(); + + switch (opReturnType) { + case PROPOSAL: + processProposal(blockHeight, tempTx, bsqFee); + break; + case COMPENSATION_REQUEST: + case REIMBURSEMENT_REQUEST: + processIssuance(blockHeight, tempTx, bsqFee); + break; + case BLIND_VOTE: + processBlindVote(blockHeight, tempTx, bsqFee); + break; + case VOTE_REVEAL: + // We do not check phase or cycle as a late voteReveal tx is considered a valid BSQ tx. + // The vote result though will ignore such votes. + break; + case LOCKUP: + case ASSET_LISTING_FEE: + case PROOF_OF_BURN: + // do nothing + break; + } + } + + // We need to check if any tempTxOutput is available and if so and the OpReturn data is invalid we + // set the output to a BTC output. We must not use `if else` cases here! + if (opReturnType != OpReturnType.COMPENSATION_REQUEST && opReturnType != OpReturnType.REIMBURSEMENT_REQUEST) { + // We applied already the check to not permit further BSQ outputs after the issuanceCandidate in the + // txOutputParser so we don't need to do any additional check here when we change to BTC_OUTPUT. + txOutputParser.getOptionalIssuanceCandidate().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT)); + } + + if (opReturnType != OpReturnType.BLIND_VOTE) { + txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> { + // We cannot apply the rule to not allow BSQ outputs after a BTC output as the 2nd output is an + // optional BSQ change output and we don't want to burn that in case the opReturn is invalid. + tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + }); + } + + if (opReturnType != OpReturnType.VOTE_REVEAL) { + txOutputParser.getOptionalVoteRevealUnlockStakeOutput().ifPresent(tempTxOutput -> { + // We do not apply the rule to not allow BSQ outputs after a BTC output here because we expect only + // one BSQ output anyway. + tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + }); + } + + if (opReturnType != OpReturnType.LOCKUP) { + txOutputParser.getOptionalLockupOutput().ifPresent(tempTxOutput -> { + // We cannot apply the rule to not allow BSQ outputs after a BTC output as the 2nd output is an + // optional BSQ change output and we don't want to burn that in case the opReturn is invalid. + tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); + }); + } + } + + private void processProposal(int blockHeight, TempTx tempTx, long bsqFee) { + boolean isFeeAndPhaseValid = isFeeAndPhaseValid(tempTx.getId(), blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE); + if (!isFeeAndPhaseValid) { + // We tolerate such an incorrect tx and do not burn the BSQ + tempTx.setTxType(TxType.IRREGULAR); + } + } + + private void processIssuance(int blockHeight, TempTx tempTx, long bsqFee) { + boolean isFeeAndPhaseValid = isFeeAndPhaseValid(tempTx.getId(), blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE); + Optional optionalIssuanceCandidate = txOutputParser.getOptionalIssuanceCandidate(); + if (isFeeAndPhaseValid) { + if (optionalIssuanceCandidate.isPresent()) { + // Now after we have validated the fee and phase we will apply the TxOutputType + optionalIssuanceCandidate.get().setTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT); + } else { + log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " + + "in not valid as the issuanceCandidate in not there. " + + "As the BSQ fee is set it must be either a buggy tx or a manually crafted invalid tx."); + // Even though the request part if invalid the BSQ transfer and change output should still be valid + // as long as the BSQ change <= BSQ inputs. + // We tolerate such an incorrect tx and do not burn the BSQ + tempTx.setTxType(TxType.IRREGULAR); + } + } else { + // This could be a valid compensation request that failed to be included in a block during the + // correct phase due to no fault of the user. We must not burn the change as long as the BSQ inputs + // cover the value of the outputs. + // We tolerate such an incorrect tx and do not burn the BSQ + tempTx.setTxType(TxType.IRREGULAR); + + // Make sure the optionalIssuanceCandidate is set to BTC + // We applied already the check to not permit further BSQ outputs after the issuanceCandidate in the + // txOutputParser so we don't need to do any additional check here when we change to BTC_OUTPUT. + optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT)); + // Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a + // valid BSQ tx. + } + } + + private void processBlindVote(int blockHeight, TempTx tempTx, long bsqFee) { + boolean isFeeAndPhaseValid = isFeeAndPhaseValid(tempTx.getId(), blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE); + if (!isFeeAndPhaseValid) { + // We tolerate such an incorrect tx and do not burn the BSQ + tempTx.setTxType(TxType.IRREGULAR); + + // Set the stake output from BLIND_VOTE_LOCK_STAKE_OUTPUT to BSQ + txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BSQ_OUTPUT)); + // Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a + // valid BSQ tx. + } + } + + /** + * Whether the BSQ fee and phase is valid for a transaction. + * + * @param blockHeight The height of the block that the transaction is in. + * @param bsqFee The fee in BSQ, in satoshi. + * @param phase The current phase of the DAO, e.g {@code DaoPhase.Phase.PROPOSAL}. + * @param param The parameter for the fee, e.g {@code Param.PROPOSAL_FEE}. + * @return True if the fee and phase was valid, false otherwise. + */ + private boolean isFeeAndPhaseValid(String txId, int blockHeight, long bsqFee, DaoPhase.Phase phase, Param param) { + // The leftover BSQ balance from the inputs is the BSQ fee in case we are in an OP_RETURN output + + if (!periodService.isInPhase(blockHeight, phase)) { + log.warn("Tx with ID {} is not in required phase ({}). blockHeight={}", txId, phase, blockHeight); + return false; + } + long paramValue = daoStateService.getParamValueAsCoin(param, blockHeight).value; + boolean isFeeCorrect = bsqFee == paramValue; + if (!isFeeCorrect) { + log.warn("Invalid fee. used fee={}, required fee={}, txId={}", bsqFee, paramValue, txId); + } + return isFeeCorrect; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + // Performs various checks for an invalid tx + static boolean isTxInvalid(TempTx tempTx, boolean bsqOutputFound, boolean burntBondValue) { + if (tempTx.getTxType() == TxType.INVALID) { + // We got already set the invalid type in earlier checks and return early. + return true; + } + + // We don't allow multiple opReturn outputs (they are non-standard but to be safe lets check it) + long numOpReturnOutputs = tempTx.getTempTxOutputs().stream() + .filter(TempTxOutput::isOpReturnOutput) + .count(); + if (numOpReturnOutputs > 1) { + log.warn("Invalid tx. We have multiple opReturn outputs. tx=" + tempTx); + return true; + } + + if ((tempTx.getTxType() == TxType.COMPENSATION_REQUEST || + tempTx.getTxType() == TxType.REIMBURSEMENT_REQUEST) + && !bsqOutputFound) { + log.warn("Invalid Tx: A compensation or reimbursement tx requires 1 BSQ output. Tx=" + tempTx); + return true; + } + + if (burntBondValue) { + log.warn("Invalid Tx: Bond value was burnt. tx=" + tempTx); + return true; + } + + if (tempTx.getTempTxOutputs().stream() + .anyMatch(txOutput -> TxOutputType.UNDEFINED_OUTPUT == txOutput.getTxOutputType() || + TxOutputType.INVALID_OUTPUT == txOutput.getTxOutputType())) { + log.warn("Invalid Tx: We have undefined or invalid txOutput types. tx=" + tempTx); + return true; + } + + return false; + } + + /** + * Retrieve the type of the transaction, assuming it is relevant to bisq. + * + * @param tempTx The temporary transaction. + * @param optionalOpReturnType The optional OP_RETURN type of the transaction. + * @param hasBurntBSQ If there have been remaining value from the inputs which got not spent in outputs. + * Might be valid BSQ fees or burned BSQ from an invalid tx. + * @return The type of the transaction, if it is relevant to bisq. + */ + @VisibleForTesting + static TxType evaluateTxType(TempTx tempTx, Optional optionalOpReturnType, + boolean hasBurntBSQ, boolean isUnLockInputValid) { + if (optionalOpReturnType.isPresent()) { + // We use the opReturnType to find the txType + return evaluateTxTypeFromOpReturnType(tempTx, optionalOpReturnType.get()); + } + + // No opReturnType, so we check for the remaining possible cases + if (hasBurntBSQ) { + // PAY_TRADE_FEE tx has a fee and no opReturn + return TxType.PAY_TRADE_FEE; + } + + // UNLOCK tx has no fee, no opReturn but an UNLOCK_OUTPUT at first output. + if (tempTx.getTempTxOutputs().get(0).getTxOutputType() == TxOutputType.UNLOCK_OUTPUT) { + // We check if there have been invalid inputs + if (!isUnLockInputValid) + return TxType.INVALID; + + return TxType.UNLOCK; + } + + // TRANSFER_BSQ has no fee, no opReturn and no UNLOCK_OUTPUT at first output + log.trace("No burned fee and no OP_RETURN, so this is a TRANSFER_BSQ tx."); + return TxType.TRANSFER_BSQ; + } + + @VisibleForTesting + static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opReturnType) { + switch (opReturnType) { + case PROPOSAL: + return TxType.PROPOSAL; + case COMPENSATION_REQUEST: + case REIMBURSEMENT_REQUEST: + boolean hasCorrectNumOutputs = tempTx.getTempTxOutputs().size() >= 3; + if (!hasCorrectNumOutputs) { + log.warn("Compensation/reimbursement request tx need to have at least 3 outputs"); + // Such a transaction cannot be created by the Bisq client and is considered invalid. + return TxType.INVALID; + } + + TempTxOutput issuanceTxOutput = tempTx.getTempTxOutputs().get(1); + boolean hasIssuanceOutput = issuanceTxOutput.getTxOutputType() == TxOutputType.ISSUANCE_CANDIDATE_OUTPUT; + if (!hasIssuanceOutput) { + log.warn("Compensation/reimbursement request txOutput type of output at index 1 need to be ISSUANCE_CANDIDATE_OUTPUT. " + + "TxOutputType={}", issuanceTxOutput.getTxOutputType()); + // Such a transaction cannot be created by the Bisq client and is considered invalid. + return TxType.INVALID; + } + + return opReturnType == OpReturnType.COMPENSATION_REQUEST ? + TxType.COMPENSATION_REQUEST : + TxType.REIMBURSEMENT_REQUEST; + case BLIND_VOTE: + return TxType.BLIND_VOTE; + case VOTE_REVEAL: + return TxType.VOTE_REVEAL; + case LOCKUP: + return TxType.LOCKUP; + case ASSET_LISTING_FEE: + return TxType.ASSET_LISTING_FEE; + case PROOF_OF_BURN: + return TxType.PROOF_OF_BURN; + default: + log.warn("We got a BSQ tx with an unknown OP_RETURN. tx={}, opReturnType={}", tempTx, opReturnType); + // We tolerate such an incorrect tx and do not burn the BSQ. We might need that in case we add new + // opReturn types in future. + return TxType.IRREGULAR; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHashNotConnectingException.java b/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHashNotConnectingException.java new file mode 100644 index 0000000000..c97ee28e11 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHashNotConnectingException.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.dao.node.parser.exceptions; + +import bisq.core.dao.node.full.RawBlock; + +import lombok.Getter; + +@Getter +public class BlockHashNotConnectingException extends Exception { + + private final RawBlock rawBlock; + + public BlockHashNotConnectingException(RawBlock rawBlock) { + this.rawBlock = rawBlock; + } + + @Override + public String toString() { + return "BlockHashNotConnectingException{" + + "\n rawBlock.getHash=" + rawBlock.getHash() + + "\n rawBlock.getHeight=" + rawBlock.getHeight() + + "\n rawBlock.getPreviousBlockHash=" + rawBlock.getPreviousBlockHash() + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHeightNotConnectingException.java b/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHeightNotConnectingException.java new file mode 100644 index 0000000000..09b39be647 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/exceptions/BlockHeightNotConnectingException.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.dao.node.parser.exceptions; + +import bisq.core.dao.node.full.RawBlock; + +import lombok.Getter; + +@Getter +public class BlockHeightNotConnectingException extends Exception { + + private final RawBlock rawBlock; + + public BlockHeightNotConnectingException(RawBlock rawBlock) { + this.rawBlock = rawBlock; + } + + @Override + public String toString() { + return "BlockHeightNotConnectingException{" + + "\n rawBlock.getHash=" + rawBlock.getHash() + + "\n rawBlock.getHeight=" + rawBlock.getHeight() + + "\n rawBlock.getPreviousBlockHash=" + rawBlock.getPreviousBlockHash() + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidGenesisTxException.java b/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidGenesisTxException.java new file mode 100644 index 0000000000..e99d6e2977 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidGenesisTxException.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.dao.node.parser.exceptions; + +import lombok.Getter; + +@Getter +public class InvalidGenesisTxException extends RuntimeException { + + public InvalidGenesisTxException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidParsingConditionException.java b/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidParsingConditionException.java new file mode 100644 index 0000000000..81e2aff020 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/exceptions/InvalidParsingConditionException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.node.parser.exceptions; + +public class InvalidParsingConditionException extends RuntimeException { + public InvalidParsingConditionException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/exceptions/RequiredReorgFromSnapshotException.java b/core/src/main/java/bisq/core/dao/node/parser/exceptions/RequiredReorgFromSnapshotException.java new file mode 100644 index 0000000000..184676078c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/node/parser/exceptions/RequiredReorgFromSnapshotException.java @@ -0,0 +1,32 @@ +/* + * 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 bisq.core.dao.node.parser.exceptions; + +import bisq.core.dao.node.full.RawBlock; + +import lombok.Getter; + +@Getter +public class RequiredReorgFromSnapshotException extends Exception { + + private final RawBlock rawBlock; + + public RequiredReorgFromSnapshotException(RawBlock rawBlock) { + this.rawBlock = rawBlock; + } +} diff --git a/core/src/main/java/bisq/core/dao/presentation/DaoUtil.java b/core/src/main/java/bisq/core/dao/presentation/DaoUtil.java new file mode 100644 index 0000000000..44a02536e4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/presentation/DaoUtil.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.dao.presentation; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.locale.Res; +import bisq.core.util.FormattingUtils; + +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.Locale; + +/** + * Util class for shared presentation code. + */ +public class DaoUtil { + + public static String getNextPhaseDuration(int height, DaoPhase.Phase phase, DaoFacade daoFacade) { + final int currentCycleDuration = daoFacade.getCurrentCycleDuration(); + long start = daoFacade.getFirstBlockOfPhaseForDisplay(height, phase) + currentCycleDuration; + long end = daoFacade.getLastBlockOfPhaseForDisplay(height, phase) + currentCycleDuration; + + long now = new Date().getTime(); + SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM", Locale.getDefault()); + SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault()); + String startDateTime = FormattingUtils.formatDateTime(new Date(now + (start - height) * 10 * 60 * 1000L), dateFormatter, timeFormatter); + String endDateTime = FormattingUtils.formatDateTime(new Date(now + (end - height) * 10 * 60 * 1000L), dateFormatter, timeFormatter); + + return Res.get("dao.cycle.phaseDurationWithoutBlocks", start, end, startDateTime, endDateTime); + } + + public static String getPhaseDuration(int height, DaoPhase.Phase phase, DaoFacade daoFacade) { + long start = daoFacade.getFirstBlockOfPhaseForDisplay(height, phase); + long end = daoFacade.getLastBlockOfPhaseForDisplay(height, phase); + long duration = daoFacade.getDurationForPhaseForDisplay(phase); + long now = new Date().getTime(); + SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM", Locale.getDefault()); + SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault()); + String startDateTime = FormattingUtils.formatDateTime(new Date(now + (start - height) * 10 * 60 * 1000L), dateFormatter, timeFormatter); + String endDateTime = FormattingUtils.formatDateTime(new Date(now + (end - height) * 10 * 60 * 1000L), dateFormatter, timeFormatter); + String durationTime = FormattingUtils.formatDurationAsWords(duration * 10 * 60 * 1000, false, false); + return Res.get("dao.cycle.phaseDuration", duration, durationTime, start, end, startDateTime, endDateTime); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateListener.java b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java new file mode 100644 index 0000000000..02b19da71a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.core.dao.state.model.blockchain.Block; + +public interface DaoStateListener { + default void onNewBlockHeight(int blockHeight) { + } + + default void onParseBlockChainComplete() { + } + + // Called before onParseTxsCompleteAfterBatchProcessing in case batch processing is complete + default void onParseBlockComplete(Block block) { + } + + default void onParseBlockCompleteAfterBatchProcessing(Block block) { + } + + // Called after the parsing of a block is complete and we do not allow any change in the daoState until the next + // block arrives. + default void onDaoStateChanged(Block block) { + } +} diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java new file mode 100644 index 0000000000..ebfeddf781 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -0,0 +1,1073 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.SpentInfo; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.model.blockchain.TxOutputType; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.ParamChange; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Provides access methods to DaoState data. + */ +@Slf4j +public class DaoStateService implements DaoSetupService { + private final DaoState daoState; + private final GenesisTxInfo genesisTxInfo; + private final BsqFormatter bsqFormatter; + private final List daoStateListeners = new CopyOnWriteArrayList<>(); + @Getter + private boolean parseBlockChainComplete; + private boolean allowDaoStateChange; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoStateService(DaoState daoState, GenesisTxInfo genesisTxInfo, BsqFormatter bsqFormatter) { + this.daoState = daoState; + this.genesisTxInfo = genesisTxInfo; + this.bsqFormatter = bsqFormatter; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + allowDaoStateChange = true; + assertDaoStateChange(); + daoState.setChainHeight(genesisTxInfo.getGenesisBlockHeight()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Snapshot + /////////////////////////////////////////////////////////////////////////////////////////// + + public void applySnapshot(DaoState snapshot) { + allowDaoStateChange = true; + assertDaoStateChange(); + + log.info("Apply snapshot with chain height {}", snapshot.getChainHeight()); + + daoState.setChainHeight(snapshot.getChainHeight()); + + daoState.setTxCache(snapshot.getTxCache()); + + daoState.getBlocks().clear(); + daoState.getBlocks().addAll(snapshot.getBlocks()); + + daoState.getCycles().clear(); + daoState.getCycles().addAll(snapshot.getCycles()); + + daoState.getUnspentTxOutputMap().clear(); + daoState.getUnspentTxOutputMap().putAll(snapshot.getUnspentTxOutputMap()); + + daoState.getSpentInfoMap().clear(); + daoState.getSpentInfoMap().putAll(snapshot.getSpentInfoMap()); + + daoState.getConfiscatedLockupTxList().clear(); + daoState.getConfiscatedLockupTxList().addAll(snapshot.getConfiscatedLockupTxList()); + + daoState.getIssuanceMap().clear(); + daoState.getIssuanceMap().putAll(snapshot.getIssuanceMap()); + + daoState.getParamChangeList().clear(); + daoState.getParamChangeList().addAll(snapshot.getParamChangeList()); + + daoState.getEvaluatedProposalList().clear(); + daoState.getEvaluatedProposalList().addAll(snapshot.getEvaluatedProposalList()); + + daoState.getDecryptedBallotsWithMeritsList().clear(); + daoState.getDecryptedBallotsWithMeritsList().addAll(snapshot.getDecryptedBallotsWithMeritsList()); + } + + public DaoState getClone() { + return DaoState.getClone(daoState); + } + + public byte[] getSerializedStateForHashChain() { + return daoState.getSerializedStateForHashChain(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ChainHeight + /////////////////////////////////////////////////////////////////////////////////////////// + + public int getChainHeight() { + return daoState.getChainHeight(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Cycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public LinkedList getCycles() { + return daoState.getCycles(); + } + + public void addCycle(Cycle cycle) { + assertDaoStateChange(); + getCycles().add(cycle); + } + + @Nullable + public Cycle getCurrentCycle() { + return !getCycles().isEmpty() ? getCycles().getLast() : null; + } + + public Optional getCycle(int height) { + return getCycles().stream() + .filter(cycle -> cycle.getHeightOfFirstBlock() <= height) + .filter(cycle -> cycle.getHeightOfLastBlock() >= height) + .findAny(); + } + + public Optional getStartHeightOfNextCycle(int blockHeight) { + return getCycle(blockHeight).map(cycle -> cycle.getHeightOfLastBlock() + 1); + } + + public Optional getStartHeightOfCurrentCycle(int blockHeight) { + return getCycle(blockHeight).map(cycle -> cycle.getHeightOfFirstBlock()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Block + /////////////////////////////////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Parser events + /////////////////////////////////////////////////////////////////////////////////////////// + + // First we get the blockHeight set + public void onNewBlockHeight(int blockHeight) { + allowDaoStateChange = true; + daoState.setChainHeight(blockHeight); + daoStateListeners.forEach(listener -> listener.onNewBlockHeight(blockHeight)); + } + + // Second we get the block added with empty txs + public void onNewBlockWithEmptyTxs(Block block) { + assertDaoStateChange(); + if (daoState.getBlocks().isEmpty() && block.getHeight() != getGenesisBlockHeight()) { + log.warn("We don't have any blocks yet and we received a block which is not the genesis block. " + + "We ignore that block as the first block need to be the genesis block. " + + "That might happen in edge cases at reorgs. Received block={}", block); + } else { + daoState.getBlocks().add(block); + + if (parseBlockChainComplete) + log.info("New Block added at blockHeight {}", block.getHeight()); + } + } + + // Third we add each successfully parsed BSQ tx to the last block + public void onNewTxForLastBlock(Block block, Tx tx) { + assertDaoStateChange(); + + getLastBlock().ifPresent(lastBlock -> { + if (block == lastBlock) { + // We need to ensure that the txs in all blocks are in sync with the txs in our txMap (cache). + block.addTx(tx); + daoState.addToTxCache(tx); + } else { + // Not clear if this case can happen but at onNewBlockWithEmptyTxs we handle such a potential edge + // case as well, so we need to reflect that here as well. + log.warn("Block for parsing does not match last block. That might happen in edge cases at reorgs. " + + "Received block={}", block); + } + }); + } + + // Fourth we get the onParseBlockComplete called after all rawTxs of blocks have been parsed + public void onParseBlockComplete(Block block) { + if (parseBlockChainComplete) + log.info("Parse block completed: Block height {}, {} BSQ transactions.", block.getHeight(), block.getTxs().size()); + + // Need to be called before onParseTxsCompleteAfterBatchProcessing as we use it in + // VoteResult and other listeners like balances usually listen on onParseTxsCompleteAfterBatchProcessing + // so we need to make sure that vote result calculation is completed before (e.g. for comp. request to + // update balance). + daoStateListeners.forEach(l -> l.onParseBlockComplete(block)); + + // We use 2 different handlers as we don't want to update domain listeners during batch processing of all + // blocks as that causes performance issues. In earlier versions when we updated at each block it took + // 50 sec. for 4000 blocks, after that change it was about 4 sec. + // Clients + if (parseBlockChainComplete) + daoStateListeners.forEach(l -> l.onParseBlockCompleteAfterBatchProcessing(block)); + + // Here listeners must not trigger any state change in the DAO as we trigger the validation service to + // generate a hash of the state. + allowDaoStateChange = false; + daoStateListeners.forEach(l -> l.onDaoStateChanged(block)); + } + + // Called after parsing of all pending blocks is completed + public void onParseBlockChainComplete() { + log.info("Parse blockchain completed"); + parseBlockChainComplete = true; + + getLastBlock().ifPresent(block -> { + daoStateListeners.forEach(l -> l.onParseBlockCompleteAfterBatchProcessing(block)); + }); + + daoStateListeners.forEach(DaoStateListener::onParseBlockChainComplete); + } + + + public LinkedList getBlocks() { + return daoState.getBlocks(); + } + + /** + * Whether specified block hash belongs to a block we already know about. + * + * @param blockHash The hash of a {@link Block}. + * @return True if the hash belongs to a {@link Block} we know about, otherwise + * {@code false}. + */ + public boolean isBlockHashKnown(String blockHash) { + return getBlocks().stream().anyMatch(block -> block.getHash().equals(blockHash)); + } + + public Optional getLastBlock() { + if (!getBlocks().isEmpty()) + return Optional.of(getBlocks().getLast()); + else + return Optional.empty(); + } + + public int getBlockHeightOfLastBlock() { + return getLastBlock().map(Block::getHeight).orElse(0); + } + + public Optional getBlockAtHeight(int height) { + return getBlocks().stream() + .filter(block -> block.getHeight() == height) + .findAny(); + } + + public boolean containsBlock(Block block) { + return getBlocks().contains(block); + } + + public boolean containsBlockHash(String blockHash) { + return getBlocks().stream().anyMatch(block -> block.getHash().equals(blockHash)); + } + + public long getBlockTime(int height) { + return getBlockAtHeight(height).map(Block::getTime).orElse(0L); + } + + public List getBlocksFromBlockHeight(int fromBlockHeight, int numMaxBlocks) { + // We limit requests to numMaxBlocks blocks, to avoid performance issues and too + // large network data in case a node requests too far back in history. + return getBlocks().stream() + .filter(block -> block.getHeight() >= fromBlockHeight) + .sorted(Comparator.comparing(Block::getHeight)) + .limit(numMaxBlocks) + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Genesis + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getGenesisTxId() { + return genesisTxInfo.getGenesisTxId(); + } + + public int getGenesisBlockHeight() { + return genesisTxInfo.getGenesisBlockHeight(); + } + + public Coin getGenesisTotalSupply() { + return Coin.valueOf(genesisTxInfo.getGenesisTotalSupply()); + } + + public Optional getGenesisTx() { + return getTx(getGenesisTxId()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Stream getUnorderedTxStream() { + return daoState.getTxCache().values().stream(); + } + + public int getNumTxs() { + return daoState.getTxCache().size(); + } + + public List getInvalidTxs() { + return getUnorderedTxStream().filter(tx -> tx.getTxType() == TxType.INVALID).collect(Collectors.toList()); + } + + public List getIrregularTxs() { + return getUnorderedTxStream().filter(tx -> tx.getTxType() == TxType.IRREGULAR).collect(Collectors.toList()); + } + + public Optional getTx(String txId) { + return Optional.ofNullable(daoState.getTxCache().get(txId)); + } + + public boolean containsTx(String txId) { + return getTx(txId).isPresent(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxType + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional getOptionalTxType(String txId) { + return getTx(txId).map(Tx::getTxType); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BurntFee (trade fee and fee burned at proof of burn) + /////////////////////////////////////////////////////////////////////////////////////////// + + public long getBurntFee(String txId) { + return getTx(txId).map(Tx::getBurntFee).orElse(0L); + } + + public boolean hasTxBurntFee(String txId) { + return getBurntFee(txId) > 0; + } + + public Set getTradeFeeTxs() { + return getUnorderedTxStream() + .filter(tx -> tx.getTxType() == TxType.PAY_TRADE_FEE) + .collect(Collectors.toSet()); + } + + public Set getProofOfBurnTxs() { + return getUnorderedTxStream() + .filter(tx -> tx.getTxType() == TxType.PROOF_OF_BURN) + .collect(Collectors.toSet()); + } + + // Any tx with burned BSQ + public Set getBurntFeeTxs() { + return getUnorderedTxStream() + .filter(tx -> tx.getBurntFee() > 0) + .collect(Collectors.toSet()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxInput + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional getConnectedTxOutput(TxInput txInput) { + return getTx(txInput.getConnectedTxOutputTxId()) + .map(tx -> tx.getTxOutputs().get(txInput.getConnectedTxOutputIndex())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxOutput + /////////////////////////////////////////////////////////////////////////////////////////// + + private Stream getUnorderedTxOutputStream() { + return getUnorderedTxStream() + .flatMap(tx -> tx.getTxOutputs().stream()); + } + + public boolean existsTxOutput(TxOutputKey key) { + return getUnorderedTxOutputStream().anyMatch(txOutput -> txOutput.getKey().equals(key)); + } + + public Optional getTxOutput(TxOutputKey txOutputKey) { + return getUnorderedTxOutputStream() + .filter(txOutput -> txOutput.getKey().equals(txOutputKey)) + .findAny(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UnspentTxOutput + /////////////////////////////////////////////////////////////////////////////////////////// + + public TreeMap getUnspentTxOutputMap() { + return daoState.getUnspentTxOutputMap(); + } + + public void addUnspentTxOutput(TxOutput txOutput) { + assertDaoStateChange(); + getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); + } + + public void removeUnspentTxOutput(TxOutput txOutput) { + assertDaoStateChange(); + getUnspentTxOutputMap().remove(txOutput.getKey()); + } + + public boolean isUnspent(TxOutputKey key) { + return getUnspentTxOutputMap().containsKey(key); + } + + public Set getUnspentTxOutputs() { + return new HashSet<>(getUnspentTxOutputMap().values()); + } + + public Optional getUnspentTxOutput(TxOutputKey key) { + return Optional.ofNullable(getUnspentTxOutputMap().getOrDefault(key, null)); + } + + public boolean isTxOutputSpendable(TxOutputKey key) { + if (!isUnspent(key)) + return false; + + Optional optionalTxOutput = getUnspentTxOutput(key); + // The above isUnspent call satisfies optionalTxOutput.isPresent() + checkArgument(optionalTxOutput.isPresent(), "optionalTxOutput must be present"); + TxOutput txOutput = optionalTxOutput.get(); + + switch (txOutput.getTxOutputType()) { + case UNDEFINED_OUTPUT: + return false; + case GENESIS_OUTPUT: + case BSQ_OUTPUT: + return true; + case BTC_OUTPUT: + return false; + case PROPOSAL_OP_RETURN_OUTPUT: + case COMP_REQ_OP_RETURN_OUTPUT: + case REIMBURSEMENT_OP_RETURN_OUTPUT: + case ISSUANCE_CANDIDATE_OUTPUT: + return true; + case BLIND_VOTE_LOCK_STAKE_OUTPUT: + return false; + case BLIND_VOTE_OP_RETURN_OUTPUT: + case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: + case VOTE_REVEAL_OP_RETURN_OUTPUT: + return true; + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + return false; + case LOCKUP_OUTPUT: + return false; + case LOCKUP_OP_RETURN_OUTPUT: + return true; + case UNLOCK_OUTPUT: + return isLockTimeOverForUnlockTxOutput(txOutput); + case INVALID_OUTPUT: + return false; + default: + return false; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxOutputType + /////////////////////////////////////////////////////////////////////////////////////////// + + private Set getTxOutputsByTxOutputType(TxOutputType txOutputType) { + return getUnorderedTxOutputStream() + .filter(txOutput -> txOutput.getTxOutputType() == txOutputType) + .collect(Collectors.toSet()); + } + + public boolean isBsqTxOutputType(TxOutput txOutput) { + final TxOutputType txOutputType = txOutput.getTxOutputType(); + switch (txOutputType) { + case UNDEFINED_OUTPUT: + return false; + case GENESIS_OUTPUT: + case BSQ_OUTPUT: + return true; + case BTC_OUTPUT: + return false; + case PROPOSAL_OP_RETURN_OUTPUT: + case COMP_REQ_OP_RETURN_OUTPUT: + case REIMBURSEMENT_OP_RETURN_OUTPUT: + return true; + case ISSUANCE_CANDIDATE_OUTPUT: + return isIssuanceTx(txOutput.getTxId()); + case BLIND_VOTE_LOCK_STAKE_OUTPUT: + case BLIND_VOTE_OP_RETURN_OUTPUT: + case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: + case VOTE_REVEAL_OP_RETURN_OUTPUT: + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + case LOCKUP_OUTPUT: + case LOCKUP_OP_RETURN_OUTPUT: + case UNLOCK_OUTPUT: + return true; + case INVALID_OUTPUT: + return false; + default: + return false; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxOutputType - Voting + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getUnspentBlindVoteStakeTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.BLIND_VOTE_LOCK_STAKE_OUTPUT).stream() + .filter(txOutput -> isUnspent(txOutput.getKey())) + .collect(Collectors.toSet()); + } + + public Set getVoteRevealOpReturnTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.VOTE_REVEAL_OP_RETURN_OUTPUT); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TxOutputType - Issuance + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getIssuanceCandidateTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Issuance + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addIssuance(Issuance issuance) { + assertDaoStateChange(); + daoState.getIssuanceMap().put(issuance.getTxId(), issuance); + } + + public Set getIssuanceSetForType(IssuanceType issuanceType) { + return daoState.getIssuanceMap().values().stream() + .filter(issuance -> issuance.getIssuanceType() == issuanceType) + .collect(Collectors.toSet()); + } + + public Optional getIssuance(String txId, IssuanceType issuanceType) { + return getIssuance(txId).filter(issuance -> issuance.getIssuanceType() == issuanceType); + } + + public Optional getIssuance(String txId) { + return Optional.ofNullable(daoState.getIssuanceMap().get(txId)); + } + + public boolean isIssuanceTx(String txId) { + return getIssuance(txId).isPresent(); + } + + public boolean isIssuanceTx(String txId, IssuanceType issuanceType) { + return getIssuance(txId, issuanceType).isPresent(); + } + + public int getIssuanceBlockHeight(String txId) { + return getIssuance(txId) + .map(Issuance::getChainHeight) + .orElse(0); + } + + public long getTotalIssuedAmount(IssuanceType issuanceType) { + return getIssuanceCandidateTxOutputs().stream() + .filter(txOutput -> isIssuanceTx(txOutput.getTxId(), issuanceType)) + .mapToLong(TxOutput::getValue) + .sum(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Not accepted issuance candidate outputs of past cycles + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isRejectedIssuanceOutput(TxOutputKey txOutputKey) { + Cycle currentCycle = getCurrentCycle(); + return currentCycle != null && + getIssuanceCandidateTxOutputs().stream() + .filter(txOutput -> txOutput.getKey().equals(txOutputKey)) + .filter(txOutput -> !currentCycle.isInCycle(txOutput.getBlockHeight())) + .anyMatch(txOutput -> !isIssuanceTx(txOutput.getTxId())); + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bond + /////////////////////////////////////////////////////////////////////////////////////////// + + // Terminology + // HashOfBondId - 20 bytes hash of the bond ID + // Lockup - txOutputs of LOCKUP type + // Unlocking - UNLOCK txOutputs that are not yet spendable due to lock time + // Unlocked - UNLOCK txOutputs that are spendable since the lock time has passed + // LockTime - 0 means that the funds are spendable at the same block of the UNLOCK tx. For the user that is not + // supported as we do not expose unconfirmed BSQ txs so lockTime of 1 is the smallest the user can actually use. + + // LockTime + public Optional getLockTime(String txId) { + return getTx(txId).map(Tx::getLockTime); + } + + public Optional getLockupHash(TxOutput txOutput) { + Optional lockupTx = Optional.empty(); + String txId = txOutput.getTxId(); + if (txOutput.getTxOutputType() == TxOutputType.LOCKUP_OUTPUT) { + lockupTx = getTx(txId); + } else if (isUnlockTxOutputAndLockTimeNotOver(txOutput)) { + if (getTx(txId).isPresent()) { + Tx unlockTx = getTx(txId).get(); + lockupTx = getTx(unlockTx.getTxInputs().get(0).getConnectedTxOutputTxId()); + } + } + if (lockupTx.isPresent()) { + byte[] opReturnData = lockupTx.get().getLastTxOutput().getOpReturnData(); + if (opReturnData != null) + return Optional.of(BondConsensus.getHashFromOpReturnData(opReturnData)); + } + return Optional.empty(); + } + + /* public Set getHashOfBondIdSet() { + return getTxOutputStream() + .filter(txOutput -> isUnspent(txOutput.getKey())) + .filter(txOutput -> txOutput.getTxOutputType() == TxOutputType.LOCKUP || + isUnlockTxOutputAndLockTimeNotOver(txOutput)) + .map(txOutput -> getHash(txOutput).orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + }*/ + + public boolean isUnlockTxOutputAndLockTimeNotOver(TxOutput txOutput) { + return txOutput.getTxOutputType() == TxOutputType.UNLOCK_OUTPUT && !isLockTimeOverForUnlockTxOutput(txOutput); + } + + // Lockup + public boolean isLockupOutput(TxOutputKey key) { + Optional opTxOutput = getUnspentTxOutput(key); + return opTxOutput.isPresent() && isLockupOutput(opTxOutput.get()); + } + + public boolean isLockupOutput(TxOutput txOutput) { + return txOutput.getTxOutputType() == TxOutputType.LOCKUP_OUTPUT; + } + + public Set getLockupTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.LOCKUP_OUTPUT); + } + + public Set getUnlockTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.UNLOCK_OUTPUT); + } + + public Set getUnspentLockUpTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.LOCKUP_OUTPUT).stream() + .filter(txOutput -> isUnspent(txOutput.getKey())) + .collect(Collectors.toSet()); + } + + public Optional getLockupTxOutput(String txId) { + return getTx(txId).flatMap(tx -> tx.getTxOutputs().stream() + .filter(this::isLockupOutput) + .findFirst()); + } + + public Optional getLockupOpReturnTxOutput(String txId) { + return getTx(txId).map(Tx::getLastTxOutput).filter(txOutput -> txOutput.getOpReturnData() != null); + } + + // Returns amount of all LOCKUP txOutputs (they might have been unlocking or unlocked in the meantime) + public long getTotalAmountOfLockupTxOutputs() { + return getLockupTxOutputs().stream() + .filter(txOutput -> !isConfiscatedLockupTxOutput(txOutput.getTxId())) + .mapToLong(TxOutput::getValue) + .sum(); + } + + // Returns the current locked up amount (excluding unlocking and unlocked) + public long getTotalLockupAmount() { + return getTotalAmountOfLockupTxOutputs() - getTotalAmountOfUnLockingTxOutputs() - getTotalAmountOfUnLockedTxOutputs(); + } + + + // Unlock + public boolean isUnspentUnlockOutput(TxOutputKey key) { + Optional opTxOutput = getUnspentTxOutput(key); + return opTxOutput.isPresent() && isUnlockOutput(opTxOutput.get()); + } + + public boolean isUnlockOutput(TxOutput txOutput) { + return txOutput.getTxOutputType() == TxOutputType.UNLOCK_OUTPUT; + } + + // Unlocking + // Return UNLOCK TxOutputs that are not yet spendable as lockTime is not over + public Stream getUnspentUnlockingTxOutputsStream() { + return getTxOutputsByTxOutputType(TxOutputType.UNLOCK_OUTPUT).stream() + .filter(txOutput -> isUnspent(txOutput.getKey())) + .filter(txOutput -> !isLockTimeOverForUnlockTxOutput(txOutput)); + } + + public long getTotalAmountOfUnLockingTxOutputs() { + return getUnspentUnlockingTxOutputsStream() + .filter(txOutput -> !isConfiscatedUnlockTxOutput(txOutput.getTxId())) + .mapToLong(TxOutput::getValue) + .sum(); + } + + public boolean isUnlockingAndUnspent(TxOutputKey key) { + Optional opTxOutput = getUnspentTxOutput(key); + return opTxOutput.isPresent() && isUnlockingAndUnspent(opTxOutput.get()); + } + + public boolean isUnlockingAndUnspent(String unlockTxId) { + Optional optionalTx = getTx(unlockTxId); + return optionalTx.isPresent() && isUnlockingAndUnspent(optionalTx.get().getTxOutputs().get(0)); + } + + public boolean isUnlockingAndUnspent(TxOutput unlockTxOutput) { + return unlockTxOutput.getTxOutputType() == TxOutputType.UNLOCK_OUTPUT && + isUnspent(unlockTxOutput.getKey()) && + !isLockTimeOverForUnlockTxOutput(unlockTxOutput); + } + + public Optional getLockupTxFromUnlockTxId(String unlockTxId) { + return getTx(unlockTxId).flatMap(tx -> getTx(tx.getTxInputs().get(0).getConnectedTxOutputTxId())); + } + + public Optional getUnlockTxFromLockupTxId(String lockupTxId) { + return getTx(lockupTxId).flatMap(tx -> getSpentInfo(tx.getTxOutputs().get(0))).flatMap(spentInfo -> getTx(spentInfo.getTxId())); + } + + // Unlocked + public Optional getUnlockBlockHeight(String txId) { + return getTx(txId).map(Tx::getUnlockBlockHeight); + } + + public boolean isLockTimeOverForUnlockTxOutput(TxOutput unlockTxOutput) { + checkArgument(isUnlockOutput(unlockTxOutput), "txOutput must be of type UNLOCK"); + return getUnlockBlockHeight(unlockTxOutput.getTxId()) + .map(unlockBlockHeight -> BondConsensus.isLockTimeOver(unlockBlockHeight, getChainHeight())) + .orElse(false); + } + + // We don't care here about the unspent state + public Stream getUnlockedTxOutputsStream() { + return getTxOutputsByTxOutputType(TxOutputType.UNLOCK_OUTPUT).stream() + .filter(txOutput -> !isConfiscatedUnlockTxOutput(txOutput.getTxId())) + .filter(this::isLockTimeOverForUnlockTxOutput); + } + + public long getTotalAmountOfUnLockedTxOutputs() { + return getUnlockedTxOutputsStream() + .mapToLong(TxOutput::getValue) + .sum(); + } + + public long getTotalAmountOfConfiscatedTxOutputs() { + return daoState.getConfiscatedLockupTxList() + .stream() + .flatMap(e -> getTx(e).stream()) + .mapToLong(tx -> tx.getLockupOutput().getValue()) + .sum(); + } + + public long getTotalAmountOfInvalidatedBsq() { + return getUnorderedTxStream().mapToLong(Tx::getInvalidatedBsq).sum(); + } + + // Contains burnt fee and invalidated bsq due invalid txs + public long getTotalAmountOfBurntBsq() { + return getUnorderedTxStream().mapToLong(Tx::getBurntBsq).sum(); + } + + // Confiscate bond + public void confiscateBond(String lockupTxId) { + Optional optionalTxOutput = getLockupTxOutput(lockupTxId); + if (optionalTxOutput.isPresent()) { + TxOutput lockupTxOutput = optionalTxOutput.get(); + if (isUnspent(lockupTxOutput.getKey())) { + log.warn("confiscateBond: lockupTxOutput {} is still unspent so we can confiscate it.", lockupTxOutput.getKey()); + doConfiscateBond(lockupTxId); + } else { + // We lookup for the unlock tx which need to be still in unlocking state + Optional optionalSpentInfo = getSpentInfo(lockupTxOutput); + checkArgument(optionalSpentInfo.isPresent(), "optionalSpentInfo must be present"); + String unlockTxId = optionalSpentInfo.get().getTxId(); + if (isUnlockingAndUnspent(unlockTxId)) { + // We found the unlock tx is still not spend + log.warn("confiscateBond: lockupTxOutput {} is still unspent so we can We confiscate it.", lockupTxOutput.getKey()); + doConfiscateBond(lockupTxId); + } else { + // We could be more radical here and confiscate the output if it is unspent but lock time is over, + // but it's probably better to stick to the rules that confiscation can only happen before lock time + // is over. + log.warn("We could not confiscate the bond because the unlock tx was already spent or lock time " + + "has exceeded. unlockTxId={}", unlockTxId); + } + } + } else { + log.warn("No lockupTxOutput found for lockupTxId {}", lockupTxId); + } + } + + private void doConfiscateBond(String lockupTxId) { + assertDaoStateChange(); + log.warn("TxId {} added to confiscatedLockupTxIdList.", lockupTxId); + daoState.getConfiscatedLockupTxList().add(lockupTxId); + } + + public boolean isConfiscatedOutput(TxOutputKey txOutputKey) { + if (isLockupOutput(txOutputKey)) + return isConfiscatedLockupTxOutput(txOutputKey.getTxId()); + else if (isUnspentUnlockOutput(txOutputKey)) + return isConfiscatedUnlockTxOutput(txOutputKey.getTxId()); + return false; + } + + public boolean isConfiscatedLockupTxOutput(String lockupTxId) { + return daoState.getConfiscatedLockupTxList().contains(lockupTxId); + } + + public boolean isConfiscatedUnlockTxOutput(String unlockTxId) { + return getLockupTxFromUnlockTxId(unlockTxId). + map(lockupTx -> isConfiscatedLockupTxOutput(lockupTx.getId())). + orElse(false); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Param + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setNewParam(int blockHeight, Param param, String paramValue) { + assertDaoStateChange(); + List paramChangeList = daoState.getParamChangeList(); + getStartHeightOfNextCycle(blockHeight) + .ifPresent(heightOfNewCycle -> { + ParamChange paramChange = new ParamChange(param.name(), paramValue, heightOfNewCycle); + paramChangeList.add(paramChange); + // Addition with older height should not be possible but to ensure correct sorting lets run a sort. + paramChangeList.sort(Comparator.comparingInt(ParamChange::getActivationHeight)); + }); + } + + public String getParamValue(Param param, int blockHeight) { + List paramChangeList = new ArrayList<>(daoState.getParamChangeList()); + if (!paramChangeList.isEmpty()) { + // List is sorted by height, we start from latest entries to find most recent entry. + for (int i = paramChangeList.size() - 1; i >= 0; i--) { + ParamChange paramChange = paramChangeList.get(i); + if (paramChange.getParamName().equals(param.name()) && + blockHeight >= paramChange.getActivationHeight()) { + return paramChange.getValue(); + } + } + } + + // If no value found we use default values + return param.getDefaultValue(); + } + + public List getParamChangeList(Param param) { + List values = new ArrayList<>(); + for (ParamChange paramChange : daoState.getParamChangeList()) { + if (paramChange.getParamName().equals(param.name())) { + values.add(getParamValueAsCoin(param, paramChange.getValue())); + } + } + return values; + } + + public Coin getParamValueAsCoin(Param param, String paramValue) { + return bsqFormatter.parseParamValueToCoin(param, paramValue); + } + + public double getParamValueAsPercentDouble(String paramValue) { + return ParsingUtils.parsePercentStringToDouble(paramValue); + } + + public int getParamValueAsBlock(String paramValue) { + return Integer.parseInt(paramValue); + } + + public Coin getParamValueAsCoin(Param param, int blockHeight) { + return getParamValueAsCoin(param, getParamValue(param, blockHeight)); + } + + public double getParamValueAsPercentDouble(Param param, int blockHeight) { + return getParamValueAsPercentDouble(getParamValue(param, blockHeight)); + } + + public int getParamValueAsBlock(Param param, int blockHeight) { + return getParamValueAsBlock(getParamValue(param, blockHeight)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // SpentInfo + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setSpentInfo(TxOutputKey txOutputKey, SpentInfo spentInfo) { + assertDaoStateChange(); + daoState.getSpentInfoMap().put(txOutputKey, spentInfo); + } + + public Optional getSpentInfo(TxOutput txOutput) { + return Optional.ofNullable(daoState.getSpentInfoMap().getOrDefault(txOutput.getKey(), null)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Vote result data + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getEvaluatedProposalList() { + return daoState.getEvaluatedProposalList(); + } + + public void addEvaluatedProposalSet(Set evaluatedProposals) { + assertDaoStateChange(); + + evaluatedProposals.stream() + .filter(e -> !daoState.getEvaluatedProposalList().contains(e)) + .forEach(daoState.getEvaluatedProposalList()::add); + + // We need deterministic order for the hash chain + daoState.getEvaluatedProposalList().sort(Comparator.comparing(EvaluatedProposal::getProposalTxId)); + } + + public List getDecryptedBallotsWithMeritsList() { + return daoState.getDecryptedBallotsWithMeritsList(); + } + + public void addDecryptedBallotsWithMeritsSet(Set decryptedBallotsWithMeritsSet) { + assertDaoStateChange(); + + decryptedBallotsWithMeritsSet.stream() + .filter(e -> !daoState.getDecryptedBallotsWithMeritsList().contains(e)) + .forEach(daoState.getDecryptedBallotsWithMeritsList()::add); + + // We need deterministic order for the hash chain + daoState.getDecryptedBallotsWithMeritsList().sort(Comparator.comparing(DecryptedBallotsWithMerits::getBlindVoteTxId)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Asset listing fee + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getAssetListingFeeOpReturnTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.ASSET_LISTING_FEE_OP_RETURN_OUTPUT); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Proof of burn + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getProofOfBurnOpReturnTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addDaoStateListener(DaoStateListener listener) { + daoStateListeners.add(listener); + } + + public void removeDaoStateListener(DaoStateListener listener) { + daoStateListeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public String daoStateToString() { + return daoState.toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void assertDaoStateChange() { + if (!allowDaoStateChange) + throw new RuntimeException("We got a call which would change the daoState outside of the allowed event phase"); + } +} + diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java new file mode 100644 index 0000000000..1752119b26 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java @@ -0,0 +1,196 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.storage.DaoStateStorageService; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; + +import java.util.LinkedList; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/** + * Manages periodical snapshots of the DaoState. + * At startup we apply a snapshot if available. + * At each trigger height we persist the latest snapshot candidate and set the current daoState as new candidate. + * The trigger height is determined by the SNAPSHOT_GRID. The latest persisted snapshot is min. the height of + * SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old. + */ +@Slf4j +public class DaoStateSnapshotService { + private static final int SNAPSHOT_GRID = 20; + + private final DaoStateService daoStateService; + private final GenesisTxInfo genesisTxInfo; + private final DaoStateStorageService daoStateStorageService; + private final DaoStateMonitoringService daoStateMonitoringService; + private final File storageDir; + + private DaoState daoStateSnapshotCandidate; + private LinkedList daoStateHashChainSnapshotCandidate = new LinkedList<>(); + private int chainHeightOfLastApplySnapshot; + @Setter + @Nullable + private Runnable daoRequiresRestartHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoStateSnapshotService(DaoStateService daoStateService, + GenesisTxInfo genesisTxInfo, + DaoStateStorageService daoStateStorageService, + DaoStateMonitoringService daoStateMonitoringService, + @Named(Config.STORAGE_DIR) File storageDir) { + this.daoStateService = daoStateService; + this.genesisTxInfo = genesisTxInfo; + this.daoStateStorageService = daoStateStorageService; + this.daoStateMonitoringService = daoStateMonitoringService; + this.storageDir = storageDir; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // We do not use DaoStateListener.onDaoStateChanged but let the DaoEventCoordinator call maybeCreateSnapshot to ensure the + // correct order of execution. + // We need to process during batch processing as well to write snapshots during that process. + public void maybeCreateSnapshot(Block block) { + int chainHeight = block.getHeight(); + + // Either we don't have a snapshot candidate yet, or if we have one the height at that snapshot candidate must be + // different to our current height. + boolean noSnapshotCandidateOrDifferentHeight = daoStateSnapshotCandidate == null || + daoStateSnapshotCandidate.getChainHeight() != chainHeight; + if (isSnapshotHeight(chainHeight) && + !daoStateService.getBlocks().isEmpty() && + isValidHeight(daoStateService.getBlocks().getLast().getHeight()) && + noSnapshotCandidateOrDifferentHeight) { + // At trigger event we store the latest snapshotCandidate to disc + long ts = System.currentTimeMillis(); + if (daoStateSnapshotCandidate != null) { + // Serialisation happens on the userThread so we do not need to clone the data. Write to disk happens + // in a thread but does not interfere with our objects as they got already serialized when passed to the + // write thread. We use requestPersistence so we do not write immediately but at next scheduled interval. + // This avoids frequent write at dao sync and better performance. + daoStateStorageService.requestPersistence(daoStateSnapshotCandidate, daoStateHashChainSnapshotCandidate); + log.info("Serializing snapshotCandidate for writing to Disc with height {} at height {} took {} ms", + daoStateSnapshotCandidate.getChainHeight(), chainHeight, System.currentTimeMillis() - ts); + } + + ts = System.currentTimeMillis(); + // Now we clone and keep it in memory for the next trigger event + daoStateSnapshotCandidate = daoStateService.getClone(); + daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain()); + + log.debug("Cloned new snapshotCandidate at height {} took {} ms", chainHeight, System.currentTimeMillis() - ts); + } + } + + public void applySnapshot(boolean fromReorg) { + DaoState persistedBsqState = daoStateStorageService.getPersistedBsqState(); + LinkedList persistedDaoStateHashChain = daoStateStorageService.getPersistedDaoStateHashChain(); + if (persistedBsqState != null) { + LinkedList blocks = persistedBsqState.getBlocks(); + int chainHeightOfPersisted = persistedBsqState.getChainHeight(); + if (!blocks.isEmpty()) { + int heightOfLastBlock = blocks.getLast().getHeight(); + log.debug("applySnapshot from persistedBsqState daoState with height of last block {}", heightOfLastBlock); + if (isValidHeight(heightOfLastBlock)) { + if (chainHeightOfLastApplySnapshot != chainHeightOfPersisted) { + chainHeightOfLastApplySnapshot = chainHeightOfPersisted; + daoStateService.applySnapshot(persistedBsqState); + daoStateMonitoringService.applySnapshot(persistedDaoStateHashChain); + } else { + // The reorg might have been caused by the previous parsing which might contains a range of + // blocks. + log.warn("We applied already a snapshot with chainHeight {}. " + + "We remove all dao store files and shutdown. After a restart resource files will " + + "be applied if available.", + chainHeightOfLastApplySnapshot); + resyncDaoStateFromResources(); + } + } + } else if (fromReorg) { + log.info("We got a reorg and we want to apply the snapshot but it is empty. " + + "That is expected in the first blocks until the first snapshot has been created. " + + "We remove all dao store files and shutdown. " + + "After a restart resource files will be applied if available."); + resyncDaoStateFromResources(); + } + } else { + log.info("Try to apply snapshot but no stored snapshot available. That is expected at first blocks."); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isValidHeight(int heightOfLastBlock) { + return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight(); + } + + private void resyncDaoStateFromResources() { + log.info("resyncDaoStateFromResources called"); + try { + daoStateStorageService.resyncDaoStateFromResources(storageDir); + + if (daoRequiresRestartHandler != null) { + daoRequiresRestartHandler.run(); + } + } catch (IOException e) { + log.error("Error at resyncDaoStateFromResources: {}", e.toString()); + } + } + + @VisibleForTesting + int getSnapshotHeight(int genesisHeight, int height, int grid) { + return Math.round(Math.max(genesisHeight + 3 * grid, height) / grid) * grid - grid; + } + + @VisibleForTesting + boolean isSnapshotHeight(int genesisHeight, int height, int grid) { + return height % grid == 0 && height >= getSnapshotHeight(genesisHeight, height, grid); + } + + private boolean isSnapshotHeight(int height) { + return isSnapshotHeight(genesisTxInfo.getGenesisBlockHeight(), height, SNAPSHOT_GRID); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java b/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java new file mode 100644 index 0000000000..94a056afdd --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java @@ -0,0 +1,170 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Named; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + + +/** + * Encapsulate the genesis txId and height. + * As we don't persist those data we don't want to have it in the DaoState directly and moved it to a separate class. + * Using a static final field in DaoState would not work well as we want to support that the data can be overwritten by + * program arguments for development testing and therefore it is set in the constructor via Guice. + */ +@SuppressWarnings("SpellCheckingInspection") +@Slf4j +public class GenesisTxInfo { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final String MAINNET_GENESIS_TX_ID = "4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5"; + private static final int MAINNET_GENESIS_BLOCK_HEIGHT = 571747; // 2019-04-15 + private static final Coin MAINNET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("3.65748"); + + private static final String TESTNET_GENESIS_TX_ID = "f35b62930b16a680ba6bc8ba8fecc4f1db65c5635b5a4b4b0445544649acf4f6"; + private static final int TESTNET_GENESIS_BLOCK_HEIGHT = 1564395; // 2019-06-21 + private static final Coin TESTNET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + + private static final String DAO_TESTNET_GENESIS_TX_ID = "cb316a186b9e88d1b8e1ce8dc79cc6a2080cc7bbc6df94f2be325d8253417af1"; + private static final int DAO_TESTNET_GENESIS_BLOCK_HEIGHT = 104; // 2019-02-19 + private static final Coin DAO_TESTNET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + + private static final String DAO_BETANET_GENESIS_TX_ID = "0bd66d8ff26476b55dfaf2a5db0c659a5d8635566488244df25606db63a08bd9"; + private static final int DAO_BETANET_GENESIS_BLOCK_HEIGHT = 567405; // 2019-03-16 + private static final Coin DAO_BETANET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("0.49998644"); // 499 986.44 BSQ / 0.49998644 BTC + + private static final String DAO_REGTEST_GENESIS_TX_ID = "d594ad0c5de53e261b5784e5eb2acec8b807c45b74450401f488d36b8acf2e14"; + private static final int DAO_REGTEST_GENESIS_BLOCK_HEIGHT = 104; // 2019-03-26 + private static final Coin DAO_REGTEST_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + + private static final String REGTEST_GENESIS_TX_ID = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf"; + private static final int REGTEST_GENESIS_BLOCK_HEIGHT = 111; + private static final Coin REGTEST_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + // We cannot use a static final as we want to set the txId also via program argument for development and then it + // is getting passed via Guice in the constructor. + @Getter + private final String genesisTxId; + @Getter + private final int genesisBlockHeight; + @Getter + private final long genesisTotalSupply; + + // mainnet + // this tx has a lot of outputs + // https://blockchain.info/de/tx/ee921650ab3f978881b8fe291e0c025e0da2b7dc684003d7a03d9649dfee2e15 + // BLOCK_HEIGHT 411779 + // 411812 has 693 recursions + // block 376078 has 2843 recursions and caused once a StackOverflowError, a second run worked. Took 1,2 sec. + + + // BTC MAIN NET + // new: --genesisBlockHeight=524717 --genesisTxId=81855816eca165f17f0668898faa8724a105196e90ffc4993f4cac980176674e + // private static final String DEFAULT_GENESIS_TX_ID = "e5c8313c4144d219b5f6b2dacf1d36f2d43a9039bb2fcd1bd57f8352a9c9809a"; + // private static final int DEFAULT_GENESIS_BLOCK_HEIGHT = 477865; // 2017-07-28 + + + // private static final String DEFAULT_GENESIS_TX_ID = "--"; + // private static final int DEFAULT_GENESIS_BLOCK_HEIGHT = 499000; // recursive test 137298, 499000 dec 2017 + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public GenesisTxInfo(@Named(Config.GENESIS_TX_ID) String genesisTxId, + @Named(Config.GENESIS_BLOCK_HEIGHT) int genesisBlockHeight, + @Named(Config.GENESIS_TOTAL_SUPPLY) long genesisTotalSupply) { + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + boolean isMainnet = baseCurrencyNetwork.isMainnet(); + boolean isTestnet = baseCurrencyNetwork.isTestnet(); + boolean isDaoTestNet = baseCurrencyNetwork.isDaoTestNet(); + boolean isDaoBetaNet = baseCurrencyNetwork.isDaoBetaNet(); + boolean isDaoRegTest = baseCurrencyNetwork.isDaoRegTest(); + boolean isRegtest = baseCurrencyNetwork.isRegtest(); + if (!genesisTxId.isEmpty()) { + this.genesisTxId = genesisTxId; + } else if (isMainnet) { + this.genesisTxId = MAINNET_GENESIS_TX_ID; + } else if (isTestnet) { + this.genesisTxId = TESTNET_GENESIS_TX_ID; + } else if (isDaoTestNet) { + this.genesisTxId = DAO_TESTNET_GENESIS_TX_ID; + } else if (isDaoBetaNet) { + this.genesisTxId = DAO_BETANET_GENESIS_TX_ID; + } else if (isDaoRegTest) { + this.genesisTxId = DAO_REGTEST_GENESIS_TX_ID; + } else if (isRegtest) { + this.genesisTxId = REGTEST_GENESIS_TX_ID; + } else { + this.genesisTxId = "genesisTxId is undefined"; + } + + if (genesisBlockHeight > -1) { + this.genesisBlockHeight = genesisBlockHeight; + } else if (isMainnet) { + this.genesisBlockHeight = MAINNET_GENESIS_BLOCK_HEIGHT; + } else if (isTestnet) { + this.genesisBlockHeight = TESTNET_GENESIS_BLOCK_HEIGHT; + } else if (isDaoTestNet) { + this.genesisBlockHeight = DAO_TESTNET_GENESIS_BLOCK_HEIGHT; + } else if (isDaoBetaNet) { + this.genesisBlockHeight = DAO_BETANET_GENESIS_BLOCK_HEIGHT; + } else if (isDaoRegTest) { + this.genesisBlockHeight = DAO_REGTEST_GENESIS_BLOCK_HEIGHT; + } else if (isRegtest) { + this.genesisBlockHeight = REGTEST_GENESIS_BLOCK_HEIGHT; + } else { + this.genesisBlockHeight = 0; + } + + if (genesisTotalSupply > -1) { + this.genesisTotalSupply = genesisTotalSupply; + } else if (isMainnet) { + this.genesisTotalSupply = MAINNET_GENESIS_TOTAL_SUPPLY.value; + } else if (isTestnet) { + this.genesisTotalSupply = TESTNET_GENESIS_TOTAL_SUPPLY.value; + } else if (isDaoTestNet) { + this.genesisTotalSupply = DAO_TESTNET_GENESIS_TOTAL_SUPPLY.value; + } else if (isDaoBetaNet) { + this.genesisTotalSupply = DAO_BETANET_GENESIS_TOTAL_SUPPLY.value; + } else if (isDaoRegTest) { + this.genesisTotalSupply = DAO_REGTEST_GENESIS_TOTAL_SUPPLY.value; + } else if (isRegtest) { + this.genesisTotalSupply = REGTEST_GENESIS_TOTAL_SUPPLY.value; + } else { + this.genesisTotalSupply = 0; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/DaoState.java b/core/src/main/java/bisq/core/dao/state/model/DaoState.java new file mode 100644 index 0000000000..bb65f7b30d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/DaoState.java @@ -0,0 +1,272 @@ +/* + * 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 bisq.core.dao.state.model; + +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.SpentInfo; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.ParamChange; + +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.JsonExclude; + +import com.google.protobuf.Message; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Root class for mutable state of the DAO. + * Holds both blockchain data as well as data derived from the governance process (voting). + *

    + * One BSQ block with empty txs adds 152 bytes which results in about 8 MB/year + * + * For supporting the hashChain we need to ensure deterministic sorting behaviour of all collections so we use a + * TreeMap which is sorted by the key. + */ +@Slf4j +public class DaoState implements PersistablePayload { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + public static DaoState getClone(DaoState daoState) { + return DaoState.fromProto(daoState.getBsqStateBuilder().build()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Fields + /////////////////////////////////////////////////////////////////////////////////////////// + + @Getter + private int chainHeight; // Is set initially to genesis height + @Getter + private final LinkedList blocks; + @Getter + private final LinkedList cycles; + + // These maps represent mutual data which can get changed at parsing a transaction + // We use TreeMaps instead of HashMaps because we need deterministic sorting of the maps for the hashChains + // used for the DAO monitor. + @Getter + private final TreeMap unspentTxOutputMap; + @Getter + private final TreeMap spentInfoMap; + + // These maps are related to state change triggered by voting + @Getter + private final List confiscatedLockupTxList; + @Getter + private final TreeMap issuanceMap; // key is txId + @Getter + private final List paramChangeList; + + // Vote result data + // All evaluated proposals which get added at the result phase + @Getter + private final List evaluatedProposalList; + // All voting data which get added at the result phase + @Getter + private final List decryptedBallotsWithMeritsList; + + // Transient data used only as an index - must be kept in sync with the block list + @JsonExclude + private transient final Map txCache; // key is txId + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoState() { + this(0, + new LinkedList<>(), + new LinkedList<>(), + new TreeMap<>(), + new TreeMap<>(), + new ArrayList<>(), + new TreeMap<>(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>() + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DaoState(int chainHeight, + LinkedList blocks, + LinkedList cycles, + TreeMap unspentTxOutputMap, + TreeMap spentInfoMap, + List confiscatedLockupTxList, + TreeMap issuanceMap, + List paramChangeList, + List evaluatedProposalList, + List decryptedBallotsWithMeritsList) { + this.chainHeight = chainHeight; + this.blocks = blocks; + this.cycles = cycles; + + this.unspentTxOutputMap = unspentTxOutputMap; + this.spentInfoMap = spentInfoMap; + + this.confiscatedLockupTxList = confiscatedLockupTxList; + this.issuanceMap = issuanceMap; + this.paramChangeList = paramChangeList; + this.evaluatedProposalList = evaluatedProposalList; + this.decryptedBallotsWithMeritsList = decryptedBallotsWithMeritsList; + + txCache = blocks.stream() + .flatMap(block -> block.getTxs().stream()) + .collect(Collectors.toMap(Tx::getId, Function.identity(), (x, y) -> x, HashMap::new)); + } + + @Override + public Message toProtoMessage() { + return getBsqStateBuilder().build(); + } + + public protobuf.DaoState.Builder getBsqStateBuilder() { + return getBsqStateBuilderExcludingBlocks().addAllBlocks(blocks.stream() + .map(Block::toProtoMessage) + .collect(Collectors.toList())); + } + + private protobuf.DaoState.Builder getBsqStateBuilderExcludingBlocks() { + protobuf.DaoState.Builder builder = protobuf.DaoState.newBuilder(); + builder.setChainHeight(chainHeight) + .addAllCycles(cycles.stream().map(Cycle::toProtoMessage).collect(Collectors.toList())) + .putAllUnspentTxOutputMap(unspentTxOutputMap.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toProtoMessage()))) + .putAllSpentInfoMap(spentInfoMap.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), entry -> entry.getValue().toProtoMessage()))) + .addAllConfiscatedLockupTxList(confiscatedLockupTxList) + .putAllIssuanceMap(issuanceMap.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().toProtoMessage()))) + .addAllParamChangeList(paramChangeList.stream().map(ParamChange::toProtoMessage).collect(Collectors.toList())) + .addAllEvaluatedProposalList(evaluatedProposalList.stream().map(EvaluatedProposal::toProtoMessage).collect(Collectors.toList())) + .addAllDecryptedBallotsWithMeritsList(decryptedBallotsWithMeritsList.stream().map(DecryptedBallotsWithMerits::toProtoMessage).collect(Collectors.toList())); + return builder; + } + + public static DaoState fromProto(protobuf.DaoState proto) { + LinkedList blocks = proto.getBlocksList().stream() + .map(Block::fromProto) + .collect(Collectors.toCollection(LinkedList::new)); + LinkedList cycles = proto.getCyclesList().stream() + .map(Cycle::fromProto).collect(Collectors.toCollection(LinkedList::new)); + TreeMap unspentTxOutputMap = new TreeMap<>(proto.getUnspentTxOutputMapMap().entrySet().stream() + .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue())))); + TreeMap spentInfoMap = new TreeMap<>(proto.getSpentInfoMapMap().entrySet().stream() + .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> SpentInfo.fromProto(e.getValue())))); + List confiscatedLockupTxList = new ArrayList<>(proto.getConfiscatedLockupTxListList()); + TreeMap issuanceMap = new TreeMap<>(proto.getIssuanceMapMap().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Issuance.fromProto(e.getValue())))); + List paramChangeList = proto.getParamChangeListList().stream() + .map(ParamChange::fromProto).collect(Collectors.toCollection(ArrayList::new)); + List evaluatedProposalList = proto.getEvaluatedProposalListList().stream() + .map(EvaluatedProposal::fromProto).collect(Collectors.toCollection(ArrayList::new)); + List decryptedBallotsWithMeritsList = proto.getDecryptedBallotsWithMeritsListList().stream() + .map(DecryptedBallotsWithMerits::fromProto).collect(Collectors.toCollection(ArrayList::new)); + return new DaoState(proto.getChainHeight(), + blocks, + cycles, + unspentTxOutputMap, + spentInfoMap, + confiscatedLockupTxList, + issuanceMap, + paramChangeList, + evaluatedProposalList, + decryptedBallotsWithMeritsList); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setChainHeight(int chainHeight) { + this.chainHeight = chainHeight; + } + + public byte[] getSerializedStateForHashChain() { + // We only add last block as for the hash chain we include the prev. hash in the new hash so the state of the + // earlier blocks is included in the hash. The past blocks cannot be changed anyway when a new block arrives. + // Reorgs are handled by rebuilding the hash chain from last snapshot. + // Using the full blocks list becomes quite heavy. 7000 blocks are + // about 1.4 MB and creating the hash takes 30 sec. By using just the last block we reduce the time to 7 sec. + return getBsqStateBuilderExcludingBlocks().addBlocks(getBlocks().getLast().toProtoMessage()).build().toByteArray(); + } + + public void addToTxCache(Tx tx) { + // We shouldn't get duplicate txIds, but use putIfAbsent instead of put for consistency with the map merge + // function used in the constructor to initialise txCache (and to exactly match the pre-caching behaviour). + txCache.putIfAbsent(tx.getId(), tx); + } + + public void setTxCache(Map txCache) { + this.txCache.clear(); + this.txCache.putAll(txCache); + } + + public Map getTxCache() { + return Collections.unmodifiableMap(txCache); + } + + @Override + public String toString() { + return "DaoState{" + + "\n chainHeight=" + chainHeight + + ",\n blocks=" + blocks + + ",\n cycles=" + cycles + + ",\n unspentTxOutputMap=" + unspentTxOutputMap + + ",\n spentInfoMap=" + spentInfoMap + + ",\n confiscatedLockupTxList=" + confiscatedLockupTxList + + ",\n issuanceMap=" + issuanceMap + + ",\n paramChangeList=" + paramChangeList + + ",\n evaluatedProposalList=" + evaluatedProposalList + + ",\n decryptedBallotsWithMeritsList=" + decryptedBallotsWithMeritsList + + ",\n txCache=" + txCache + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/ImmutableDaoStateModel.java b/core/src/main/java/bisq/core/dao/state/model/ImmutableDaoStateModel.java new file mode 100644 index 0000000000..010873ad6f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/ImmutableDaoStateModel.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.dao.state.model; + +/** + * Marker interface for objects which are stored in the daoState and therefore they need to be immutable. + */ +public interface ImmutableDaoStateModel { +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseBlock.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseBlock.java new file mode 100644 index 0000000000..d993cdfaba --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseBlock.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import java.util.Optional; + +import lombok.Data; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * The base class for RawBlock and Block containing the common immutable bitcoin + * blockchain specific data. + */ +@Immutable +@Data +public abstract class BaseBlock implements ImmutableDaoStateModel { + protected final int height; + protected final long time; // in ms + protected final String hash; + @Nullable // in case of first block in the blockchain + protected final String previousBlockHash; + + protected BaseBlock(int height, long time, String hash, @SuppressWarnings("NullableProblems") String previousBlockHash) { + this.height = height; + this.time = time; + this.hash = hash; + this.previousBlockHash = previousBlockHash; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected protobuf.BaseBlock.Builder getBaseBlockBuilder() { + protobuf.BaseBlock.Builder builder = protobuf.BaseBlock.newBuilder() + .setHeight(height) + .setTime(time) + .setHash(hash); + Optional.ofNullable(previousBlockHash).ifPresent(builder::setPreviousBlockHash); + return builder; + + } + + @Override + public String toString() { + return "BaseBlock{" + + "\n height=" + height + + ",\n time=" + time + + ",\n hash='" + hash + '\'' + + ",\n previousBlockHash='" + previousBlockHash + '\'' + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTx.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTx.java new file mode 100644 index 0000000000..19c9754808 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTx.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import com.google.common.collect.ImmutableList; + +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * The base class for the Tx classes with all common immutable data fields. + * + * TxOutputs are not added here as the sub classes use different data types. + * As not all subclasses implement PersistablePayload we leave it to the sub classes to implement the interface. + * A getBaseTxBuilder method though is available. + */ +@Immutable +@Slf4j +@Getter +@EqualsAndHashCode +public abstract class BaseTx implements ImmutableDaoStateModel { + protected final String txVersion; + protected final String id; + protected final int blockHeight; + protected final String blockHash; + protected final long time; + protected final ImmutableList txInputs; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected BaseTx(String txVersion, + String id, + int blockHeight, + String blockHash, + long time, + ImmutableList txInputs) { + this.txVersion = txVersion; + this.id = id; + this.blockHeight = blockHeight; + this.blockHash = blockHash; + this.time = time; + this.txInputs = txInputs; + } + + protected protobuf.BaseTx.Builder getBaseTxBuilder() { + return protobuf.BaseTx.newBuilder() + .setTxVersion(txVersion) + .setId(id) + .setBlockHeight(blockHeight) + .setBlockHash(blockHash) + .setTime(time) + .addAllTxInputs(txInputs.stream() + .map(TxInput::toProtoMessage) + .collect(Collectors.toList())); + } + + @Override + public String toString() { + return "BaseTx{" + + "\n txVersion='" + txVersion + '\'' + + ",\n id='" + id + '\'' + + ",\n blockHeight=" + blockHeight + + ",\n blockHash='" + blockHash + '\'' + + ",\n time=" + time + + ",\n txInputs=" + txInputs + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java new file mode 100644 index 0000000000..2e0cc6a2c3 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Optional; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Base class for TxOutput classes containing the immutable bitcoin specific blockchain data. + */ +@Slf4j +@Immutable +@Data +public abstract class BaseTxOutput implements ImmutableDaoStateModel { + protected final int index; + protected final long value; + protected final String txId; + + // Before v0.9.6 it was only set if dumpBlockchainData was set to true but we changed that with 0.9.6 + // so that it is always set. We still need to support it because of backward compatibility. + @Nullable + protected final PubKeyScript pubKeyScript; // Has about 50 bytes, total size of TxOutput is about 300 bytes. + @Nullable + protected final String address; + @Nullable + @JsonExclude + protected final byte[] opReturnData; + protected final int blockHeight; + + protected BaseTxOutput(int index, + long value, + String txId, + @Nullable PubKeyScript pubKeyScript, + @Nullable String address, + @Nullable byte[] opReturnData, + int blockHeight) { + this.index = index; + this.value = value; + this.txId = txId; + this.pubKeyScript = pubKeyScript; + this.address = address; + this.opReturnData = opReturnData; + this.blockHeight = blockHeight; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected protobuf.BaseTxOutput.Builder getRawTxOutputBuilder() { + final protobuf.BaseTxOutput.Builder builder = protobuf.BaseTxOutput.newBuilder() + .setIndex(index) + .setValue(value) + .setTxId(txId) + .setBlockHeight(blockHeight); + + Optional.ofNullable(pubKeyScript).ifPresent(e -> builder.setPubKeyScript(pubKeyScript.toProtoMessage())); + Optional.ofNullable(address).ifPresent(e -> builder.setAddress(address)); + Optional.ofNullable(opReturnData).ifPresent(e -> builder.setOpReturnData(ByteString.copyFrom(opReturnData))); + + return builder; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Util + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxOutputKey getKey() { + return new TxOutputKey(txId, index); + } + + @Override + public String toString() { + return "BaseTxOutput{" + + "\n index=" + index + + ",\n value=" + value + + ",\n txId='" + txId + '\'' + + ",\n pubKeyScript=" + pubKeyScript + + ",\n address='" + address + '\'' + + ",\n opReturnData=" + Utilities.bytesAsHexString(opReturnData) + + ",\n blockHeight=" + blockHeight + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/Block.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/Block.java new file mode 100644 index 0000000000..e438197e93 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/Block.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * The Block which gets persisted in the DaoState. During parsing transactions can be + * added to the txs list, therefore it is not an immutable list. + * + * It is difficult to make the block immutable by using the same pattern we use with Tx or TxOutput because we add the + * block at the beginning of the parsing to the daoState and add transactions during parsing. We need to have the state + * updated during parsing. If we would set then after the parsing the immutable block we might have inconsistent data. + * There might be a way to do it but it comes with high complexity and risks so for now we prefer to have that known + * issue with not being fully immutable at that level. + * + * An empty block (no BSQ txs) has 146 bytes in Protobuffer serialized form. + * + */ +@EqualsAndHashCode(callSuper = true) +public final class Block extends BaseBlock implements PersistablePayload, ImmutableDaoStateModel { + // We do not expose txs with a Lombok getter. We cannot make it immutable as we add transactions during parsing. + private final List txs; + + public Block(int height, long time, String hash, String previousBlockHash) { + this(height, time, hash, previousBlockHash, new ArrayList<>()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private Block(int height, + long time, + String hash, + String previousBlockHash, + List txs) { + super(height, + time, + hash, + previousBlockHash); + this.txs = txs; + } + + + @Override + public protobuf.BaseBlock toProtoMessage() { + protobuf.Block.Builder builder = protobuf.Block.newBuilder() + .addAllTxs(txs.stream() + .map(Tx::toProtoMessage) + .collect(Collectors.toList())); + return getBaseBlockBuilder().setBlock(builder).build(); + } + + public static Block fromProto(protobuf.BaseBlock proto) { + protobuf.Block blockProto = proto.getBlock(); + ImmutableList txs = blockProto.getTxsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(blockProto.getTxsList().stream() + .map(Tx::fromProto) + .collect(Collectors.toList())); + return new Block(proto.getHeight(), + proto.getTime(), + proto.getHash(), + proto.getPreviousBlockHash(), + txs); + } + + public void addTx(Tx tx) { + txs.add(tx); + } + + // We want to guarantee that no client can modify the list. We use unmodifiableList and not ImmutableList as + // we want that clients reflect any change to the source list. Also ImmutableList is more expensive as it + // creates a copy. + public List getTxs() { + return Collections.unmodifiableList(txs); + } + + @Override + public String toString() { + return "Block{" + + "\n txs=" + txs + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java new file mode 100644 index 0000000000..812288a684 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import java.util.Arrays; +import java.util.Optional; + +import lombok.Getter; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides byte constants for distinguishing the type of a DAO transaction used in the OP_RETURN data. + */ +@Immutable +public enum OpReturnType implements ImmutableDaoStateModel { + UNDEFINED((byte) 0x00), + PROPOSAL((byte) 0x10), + COMPENSATION_REQUEST((byte) 0x11), + REIMBURSEMENT_REQUEST((byte) 0x12), + BLIND_VOTE((byte) 0x13), + VOTE_REVEAL((byte) 0x14), + LOCKUP((byte) 0x15), + ASSET_LISTING_FEE((byte) 0x16), + PROOF_OF_BURN((byte) 0x17); + + @Getter + private byte type; + + OpReturnType(byte type) { + this.type = type; + } + + public static Optional getOpReturnType(byte type) { + return Arrays.stream(OpReturnType.values()) + .filter(opReturnType -> opReturnType.type == type) + .map(Optional::of) + .findAny() + .orElse(Optional.empty()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/PubKeyScript.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/PubKeyScript.java new file mode 100644 index 0000000000..adde8ae8d9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/PubKeyScript.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.node.full.rpc.dto.DtoPubKeyScript; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.common.collect.ImmutableList; + +import java.util.Objects; +import java.util.Optional; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@Value +@AllArgsConstructor +public class PubKeyScript implements PersistablePayload, ImmutableDaoStateModel { + private final int reqSigs; + private final ScriptType scriptType; + @Nullable + private final ImmutableList addresses; + private final String asm; + private final String hex; + + public PubKeyScript(DtoPubKeyScript scriptPubKey) { + this(scriptPubKey.getReqSigs() != null ? scriptPubKey.getReqSigs() : 0, + scriptPubKey.getType(), + scriptPubKey.getAddresses() != null ? ImmutableList.copyOf(scriptPubKey.getAddresses()) : null, + scriptPubKey.getAsm(), + scriptPubKey.getHex()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public protobuf.PubKeyScript toProtoMessage() { + final protobuf.PubKeyScript.Builder builder = protobuf.PubKeyScript.newBuilder() + .setReqSigs(reqSigs) + .setScriptType(scriptType.toProtoMessage()) + .setAsm(asm) + .setHex(hex); + Optional.ofNullable(addresses).ifPresent(builder::addAllAddresses); + return builder.build(); + } + + public static PubKeyScript fromProto(protobuf.PubKeyScript proto) { + return new PubKeyScript(proto.getReqSigs(), + ScriptType.fromProto(proto.getScriptType()), + proto.getAddressesList().isEmpty() ? null : ImmutableList.copyOf(proto.getAddressesList()), + proto.getAsm(), + proto.getHex()); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PubKeyScript)) return false; + if (!super.equals(o)) return false; + PubKeyScript that = (PubKeyScript) o; + return reqSigs == that.reqSigs && + scriptType.name().equals(that.scriptType.name()) && + Objects.equals(addresses, that.addresses) && + Objects.equals(asm, that.asm) && + Objects.equals(hex, that.hex); + } + + @Override + public int hashCode() { + + return Objects.hash(super.hashCode(), reqSigs, scriptType.name(), addresses, asm, hex); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/ScriptType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/ScriptType.java new file mode 100644 index 0000000000..6c560f1b88 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/ScriptType.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.ProtoUtil; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.AllArgsConstructor; +import lombok.ToString; + +import javax.annotation.concurrent.Immutable; + +@ToString +@Immutable +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public enum ScriptType implements ImmutableDaoStateModel { + UNDEFINED("undefined"), + // https://github.com/bitcoin/bitcoin/blob/master/src/script/standard.cpp + NONSTANDARD("nonstandard"), + PUB_KEY("pubkey"), + PUB_KEY_HASH("pubkeyhash"), + SCRIPT_HASH("scripthash"), + MULTISIG("multisig"), + NULL_DATA("nulldata"), + WITNESS_V0_KEYHASH("witness_v0_keyhash"), + WITNESS_V0_SCRIPTHASH("witness_v0_scripthash"), + WITNESS_V1_TAPROOT("witness_v1_taproot"), + WITNESS_UNKNOWN("witness_unknown"); + + private final String name; + + @JsonValue + private String getName() { + return name; + } + + @JsonCreator + public static ScriptType forName(String name) { + if (name != null) { + for (ScriptType scriptType : ScriptType.values()) { + if (name.equals(scriptType.getName())) { + return scriptType; + } + } + } + throw new IllegalArgumentException("Expected the argument to be a valid 'bitcoind' script type, " + + "but was invalid/unsupported instead. Received scriptType=" + name); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static ScriptType fromProto(protobuf.ScriptType scriptType) { + return ProtoUtil.enumFromProto(ScriptType.class, scriptType.name()); + } + + public protobuf.ScriptType toProtoMessage() { + return protobuf.ScriptType.valueOf(name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/SpentInfo.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/SpentInfo.java new file mode 100644 index 0000000000..fb01985ae2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/SpentInfo.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Value +public final class SpentInfo implements PersistablePayload, ImmutableDaoStateModel { + private final long blockHeight; + // Spending tx + private final String txId; + private final int inputIndex; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static SpentInfo fromProto(protobuf.SpentInfo proto) { + return new SpentInfo(proto.getBlockHeight(), + proto.getTxId(), + proto.getInputIndex()); + } + + public protobuf.SpentInfo toProtoMessage() { + return protobuf.SpentInfo.newBuilder() + .setBlockHeight(blockHeight) + .setTxId(txId) + .setInputIndex(inputIndex) + .build(); + } + + @Override + public String toString() { + return "SpentInfo{" + + "\n blockHeight=" + blockHeight + + ",\n txId='" + txId + '\'' + + ",\n inputIndex=" + inputIndex + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java new file mode 100644 index 0000000000..a90db56048 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java @@ -0,0 +1,198 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.node.parser.TempTx; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Immutable class for a Bsq transaction. + * Gets persisted. + */ +@Immutable +@Getter +public final class Tx extends BaseTx implements PersistablePayload, ImmutableDaoStateModel { + // Created after parsing of a tx is completed. We store only the immutable tx in the block. + public static Tx fromTempTx(TempTx tempTx) { + ImmutableList txOutputs = ImmutableList.copyOf(tempTx.getTempTxOutputs().stream() + .map(TxOutput::fromTempOutput) + .collect(Collectors.toList())); + + return new Tx(tempTx.getTxVersion(), + tempTx.getId(), + tempTx.getBlockHeight(), + tempTx.getBlockHash(), + tempTx.getTime(), + tempTx.getTxInputs(), + txOutputs, + tempTx.getTxType(), + tempTx.getBurntBsq()); + } + + private final ImmutableList txOutputs; + @Nullable + private final TxType txType; + // Can be burned fee or in case of an invalid tx the burned BSQ from all BSQ inputs + private final long burntBsq; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private Tx(String txVersion, + String id, + int blockHeight, + String blockHash, + long time, + ImmutableList txInputs, + ImmutableList txOutputs, + @Nullable TxType txType, + long burntBsq) { + super(txVersion, + id, + blockHeight, + blockHash, + time, + txInputs); + this.txOutputs = txOutputs; + this.txType = txType; + this.burntBsq = burntBsq; + + } + + @Override + public protobuf.BaseTx toProtoMessage() { + final protobuf.Tx.Builder builder = protobuf.Tx.newBuilder() + .addAllTxOutputs(txOutputs.stream() + .map(TxOutput::toProtoMessage) + .collect(Collectors.toList())) + .setBurntBsq(burntBsq); + Optional.ofNullable(txType).ifPresent(txType -> builder.setTxType(txType.toProtoMessage())); + return getBaseTxBuilder().setTx(builder).build(); + } + + public static Tx fromProto(protobuf.BaseTx protoBaseTx) { + ImmutableList txInputs = protoBaseTx.getTxInputsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(protoBaseTx.getTxInputsList().stream() + .map(TxInput::fromProto) + .collect(Collectors.toList())); + protobuf.Tx protoTx = protoBaseTx.getTx(); + ImmutableList outputs = protoTx.getTxOutputsList().isEmpty() ? + ImmutableList.copyOf(new ArrayList<>()) : + ImmutableList.copyOf(protoTx.getTxOutputsList().stream() + .map(TxOutput::fromProto) + .collect(Collectors.toList())); + return new Tx(protoBaseTx.getTxVersion(), + protoBaseTx.getId(), + protoBaseTx.getBlockHeight(), + protoBaseTx.getBlockHash(), + protoBaseTx.getTime(), + txInputs, + outputs, + TxType.fromProto(protoTx.getTxType()), + protoTx.getBurntBsq()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxOutput getLastTxOutput() { + return txOutputs.get(txOutputs.size() - 1); + } + + + public long getBurntBsq() { + return burntBsq; + } + + public long getBurntFee() { + return txType == TxType.INVALID ? 0 : burntBsq; + } + + public long getInvalidatedBsq() { + return txType == TxType.INVALID ? burntBsq : 0; + } + + public int getLockTime() { + return getLockupOutput().getLockTime(); + } + + public long getLockedAmount() { + return getLockupOutput().getValue(); + } + + // The lockTime is stored in the first output of the LOCKUP tx. + public TxOutput getLockupOutput() { + return txOutputs.get(0); + } + + // The unlockBlockHeight is stored in the first output of the UNLOCK tx. + public int getUnlockBlockHeight() { + return getLockupOutput().getUnlockBlockHeight(); + } + + @Override + public String toString() { + return "Tx{" + + "\n txOutputs=" + txOutputs + + ",\n txType=" + txType + + ",\n burntBsq=" + burntBsq + + "\n} " + super.toString(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tx)) return false; + if (!super.equals(o)) return false; + Tx tx = (Tx) o; + + String name = txType != null ? txType.name() : ""; + String name1 = tx.txType != null ? tx.txType.name() : ""; + boolean isTxTypeEquals = name.equals(name1); + + return burntBsq == tx.burntBsq && + Objects.equals(txOutputs, tx.txOutputs) && + isTxTypeEquals; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txOutputs, txType, burntBsq); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java new file mode 100644 index 0000000000..0ebcddf937 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java @@ -0,0 +1,91 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An input is really just a reference to the spending output. It gets identified by the + * txId and the index of that output. We use TxOutputKey to encapsulate that. + */ +@Immutable +@Value +@EqualsAndHashCode +@Slf4j +public final class TxInput implements PersistablePayload, ImmutableDaoStateModel { + private final String connectedTxOutputTxId; + private final int connectedTxOutputIndex; + @Nullable + private final String pubKey; // as hex + + public TxInput(String connectedTxOutputTxId, int connectedTxOutputIndex, @Nullable String pubKey) { + this.connectedTxOutputTxId = connectedTxOutputTxId; + this.connectedTxOutputIndex = connectedTxOutputIndex; + this.pubKey = pubKey; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public protobuf.TxInput toProtoMessage() { + final protobuf.TxInput.Builder builder = protobuf.TxInput.newBuilder() + .setConnectedTxOutputTxId(connectedTxOutputTxId) + .setConnectedTxOutputIndex(connectedTxOutputIndex); + + Optional.ofNullable(pubKey).ifPresent(builder::setPubKey); + + return builder.build(); + } + + public static TxInput fromProto(protobuf.TxInput proto) { + return new TxInput(proto.getConnectedTxOutputTxId(), + proto.getConnectedTxOutputIndex(), + proto.getPubKey().isEmpty() ? null : proto.getPubKey()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxOutputKey getConnectedTxOutputKey() { + return new TxOutputKey(connectedTxOutputTxId, connectedTxOutputIndex); + } + + @Override + public String toString() { + return "TxInput{" + + "\n connectedTxOutputTxId='" + connectedTxOutputTxId + '\'' + + ",\n connectedTxOutputIndex=" + connectedTxOutputIndex + + ",\n pubKey=" + pubKey + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutput.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutput.java new file mode 100644 index 0000000000..566216314e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutput.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.node.parser.TempTxOutput; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Objects; + +import lombok.Getter; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Contains immutable BSQ specific data (TxOutputType) and is used to get + * stored in the list of immutable Transactions (Tx) in a block. + * TempTxOutput get converted to immutable TxOutput after tx parsing is completed. + * Gets persisted. + */ +@Immutable +@Getter +public class TxOutput extends BaseTxOutput implements PersistablePayload, ImmutableDaoStateModel { + public static TxOutput fromTempOutput(TempTxOutput tempTxOutput) { + return new TxOutput(tempTxOutput.getIndex(), + tempTxOutput.getValue(), + tempTxOutput.getTxId(), + tempTxOutput.getPubKeyScript(), + tempTxOutput.getAddress(), + tempTxOutput.getOpReturnData(), + tempTxOutput.getBlockHeight(), + tempTxOutput.getTxOutputType(), + tempTxOutput.getLockTime(), + tempTxOutput.getUnlockBlockHeight()); + } + + private final TxOutputType txOutputType; + + // The lockTime is stored in the first output of the LOCKUP tx. + // If not set it is -1, 0 is a valid value. + private final int lockTime; + // The unlockBlockHeight is stored in the first output of the UNLOCK tx. + private final int unlockBlockHeight; + + private TxOutput(int index, + long value, + String txId, + @Nullable PubKeyScript pubKeyScript, + @Nullable String address, + @Nullable byte[] opReturnData, + int blockHeight, + TxOutputType txOutputType, + int lockTime, + int unlockBlockHeight) { + super(index, + value, + txId, + pubKeyScript, + address, + opReturnData, + blockHeight); + + this.txOutputType = txOutputType; + this.lockTime = lockTime; + this.unlockBlockHeight = unlockBlockHeight; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.BaseTxOutput toProtoMessage() { + protobuf.TxOutput.Builder builder = protobuf.TxOutput.newBuilder() + .setTxOutputType(txOutputType.toProtoMessage()) + .setLockTime(lockTime) + .setUnlockBlockHeight(unlockBlockHeight); + return getRawTxOutputBuilder().setTxOutput(builder).build(); + } + + public static TxOutput fromProto(protobuf.BaseTxOutput proto) { + protobuf.TxOutput protoTxOutput = proto.getTxOutput(); + return new TxOutput(proto.getIndex(), + proto.getValue(), + proto.getTxId(), + proto.hasPubKeyScript() ? PubKeyScript.fromProto(proto.getPubKeyScript()) : null, + proto.getAddress().isEmpty() ? null : proto.getAddress(), + proto.getOpReturnData().isEmpty() ? null : proto.getOpReturnData().toByteArray(), + proto.getBlockHeight(), + TxOutputType.fromProto(protoTxOutput.getTxOutputType()), + protoTxOutput.getLockTime(), + protoTxOutput.getUnlockBlockHeight()); + } + + + @Override + public String toString() { + return "TxOutput{" + + "\n txOutputType=" + txOutputType + + "\n lockTime=" + lockTime + + ",\n unlockBlockHeight=" + unlockBlockHeight + + "\n} " + super.toString(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TxOutput)) return false; + if (!super.equals(o)) return false; + TxOutput txOutput = (TxOutput) o; + return lockTime == txOutput.lockTime && + unlockBlockHeight == txOutput.unlockBlockHeight && + txOutputType.name().equals(txOutput.txOutputType.name()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txOutputType.name(), lockTime, unlockBlockHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java new file mode 100644 index 0000000000..282d69d278 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import lombok.Value; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.concurrent.Immutable; + +/** + * Convenience object for identifying a TxOutput. + * Used as key in maps in the daoState. + */ +@Immutable +@Value +public final class TxOutputKey implements ImmutableDaoStateModel, Comparable { + private final String txId; + private final int index; + + public TxOutputKey(String txId, int index) { + this.txId = txId; + this.index = index; + } + + @Override + public String toString() { + return txId + ":" + index; + } + + public static TxOutputKey getKeyFromString(String keyAsString) { + final String[] tokens = keyAsString.split(":"); + return new TxOutputKey(tokens[0], Integer.valueOf(tokens[1])); + } + + @Override + public int compareTo(@NotNull Object o) { + return toString().compareTo(o.toString()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java new file mode 100644 index 0000000000..51bfbd0197 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.ProtoUtil; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public enum TxOutputType implements ImmutableDaoStateModel { + UNDEFINED, // only fallback for backward compatibility in case we add a new value and old clients fall back to UNDEFINED + UNDEFINED_OUTPUT, + GENESIS_OUTPUT, + BSQ_OUTPUT, + BTC_OUTPUT, + PROPOSAL_OP_RETURN_OUTPUT, + COMP_REQ_OP_RETURN_OUTPUT, + REIMBURSEMENT_OP_RETURN_OUTPUT, + CONFISCATE_BOND_OP_RETURN_OUTPUT, + ISSUANCE_CANDIDATE_OUTPUT, + BLIND_VOTE_LOCK_STAKE_OUTPUT, + BLIND_VOTE_OP_RETURN_OUTPUT, + VOTE_REVEAL_UNLOCK_STAKE_OUTPUT, + VOTE_REVEAL_OP_RETURN_OUTPUT, + ASSET_LISTING_FEE_OP_RETURN_OUTPUT, + PROOF_OF_BURN_OP_RETURN_OUTPUT, + LOCKUP_OUTPUT, + LOCKUP_OP_RETURN_OUTPUT, + UNLOCK_OUTPUT, + INVALID_OUTPUT; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static TxOutputType fromProto(protobuf.TxOutputType txOutputType) { + return ProtoUtil.enumFromProto(TxOutputType.class, txOutputType.name()); + } + + public protobuf.TxOutputType toProtoMessage() { + return protobuf.TxOutputType.valueOf(name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java new file mode 100644 index 0000000000..ff1f782df9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.dao.state.model.blockchain; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.ProtoUtil; + +import lombok.Getter; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +public enum TxType implements ImmutableDaoStateModel { + UNDEFINED(false, false), // only fallback for backward compatibility in case we add a new value and old clients fall back to UNDEFINED + UNDEFINED_TX_TYPE(false, false), + UNVERIFIED(false, false), + INVALID(false, false), + GENESIS(false, false), + TRANSFER_BSQ(false, false), + PAY_TRADE_FEE(false, true), + PROPOSAL(true, true), + COMPENSATION_REQUEST(true, true), + REIMBURSEMENT_REQUEST(true, true), + BLIND_VOTE(true, true), + VOTE_REVEAL(true, false), + LOCKUP(true, false), + UNLOCK(true, false), + ASSET_LISTING_FEE(true, true), + PROOF_OF_BURN(true, true), + IRREGULAR(false, false); // the params are irrelevant here as we can have any tx that violated the rules set to irregular + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Getter + private final boolean hasOpReturn; + @Getter + private final boolean requiresFee; + + TxType(boolean hasOpReturn, boolean requiresFee) { + this.hasOpReturn = hasOpReturn; + this.requiresFee = requiresFee; + } + + @Nullable + public static TxType fromProto(protobuf.TxType txType) { + return ProtoUtil.enumFromProto(TxType.class, txType.name()); + } + + public protobuf.TxType toProtoMessage() { + return protobuf.TxType.valueOf(name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/package-info.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/package-info.java new file mode 100644 index 0000000000..ce91db4133 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +/** + * Holds all blockchain specific data which are used in the daoState. + */ + +package bisq.core.dao.state.model.blockchain; diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java b/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java new file mode 100644 index 0000000000..7a8ad92078 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Base class for all ballots like compensation request, generic request, remove asset ballots and + * change param ballots. + * It contains the Proposal and the Vote. If a Proposal is ignored for voting the vote object is null. + * + * One proposal has about 278 bytes + */ +@Immutable +@Slf4j +@Getter +@EqualsAndHashCode +public final class Ballot implements PersistablePayload, ConsensusCritical, ImmutableDaoStateModel { + protected final Proposal proposal; + + @Nullable + protected Vote vote; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Ballot(Proposal proposal) { + this(proposal, null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public Ballot(Proposal proposal, @Nullable Vote vote) { + this.proposal = proposal; + this.vote = vote; + } + + @Override + public protobuf.Ballot toProtoMessage() { + final protobuf.Ballot.Builder builder = protobuf.Ballot.newBuilder() + .setProposal(proposal.getProposalBuilder()); + Optional.ofNullable(vote).ifPresent(e -> builder.setVote((protobuf.Vote) e.toProtoMessage())); + return builder.build(); + } + + public static Ballot fromProto(protobuf.Ballot proto) { + return new Ballot(Proposal.fromProto(proto.getProposal()), + proto.hasVote() ? Vote.fromProto(proto.getVote()) : null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setVote(@Nullable Vote vote) { + this.vote = vote; + } + + public String getTxId() { + return proposal.getTxId(); + } + + public Optional getVoteAsOptional() { + return Optional.ofNullable(vote); + } + + @Override + public String toString() { + return "Ballot{" + + "\n proposal=" + proposal + + ",\n vote=" + vote + + "\n}"; + } + + public String info() { + return "Ballot{" + + "\n proposalTxId=" + proposal.getTxId() + + ",\n vote=" + vote + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/BallotList.java b/core/src/main/java/bisq/core/dao/state/model/governance/BallotList.java new file mode 100644 index 0000000000..25eaa415e6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/BallotList.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +import javax.annotation.concurrent.Immutable; + +/** + * PersistableEnvelope wrapper for list of ballots. + */ +@Immutable +@EqualsAndHashCode(callSuper = true) +public class BallotList extends PersistableList implements ConsensusCritical, ImmutableDaoStateModel { + + public BallotList(List list) { + super(list); + } + + public BallotList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PersistableEnvelope toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setBallotList(getBuilder()).build(); + } + + public protobuf.BallotList.Builder getBuilder() { + return protobuf.BallotList.newBuilder() + .addAllBallot(getList().stream() + .map(Ballot::toProtoMessage) + .collect(Collectors.toList())); + } + + public static BallotList fromProto(protobuf.BallotList proto) { + return new BallotList(new ArrayList<>(proto.getBallotList().stream() + .map(Ballot::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "BallotList: " + getList().stream() + .map(Ballot::info) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/BondedRoleType.java b/core/src/main/java/bisq/core/dao/state/model/governance/BondedRoleType.java new file mode 100644 index 0000000000..2f93bc7947 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/BondedRoleType.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.locale.Res; + +import bisq.common.config.Config; + +import lombok.Getter; + + +/** + * Data here must not be changed as it would break backward compatibility! In case we need to change we need to add a + * new entry and maintain the old one. Once all the role holders of an old deprecated role have revoked the + * role might get removed. + * + * Add entry to translation file "dao.bond.bondedRoleType...." + * + * Name of the BondedRoleType must not change as that is used for serialisation in Protobuffer. The data fields are not part of + * the PB serialisation so changes for those would not change the hash for the dao state hash chain. + * As the data is not used in consensus critical code yet changing fields can be tolerated. + * For mediators and arbitrators we will use automated verification of the bond so there might be issues when we change + * the values. So let's avoid changing anything here beside adding new entries. + * + */ +public enum BondedRoleType { + UNDEFINED(0, 0, "N/A", false), + // admins + GITHUB_ADMIN(50, 110, "https://bisq.network/roles/16", true), + FORUM_ADMIN(20, 110, "https://bisq.network/roles/19", true), + TWITTER_ADMIN(20, 110, "https://bisq.network/roles/21", true), + ROCKET_CHAT_ADMIN(20, 110, "https://bisq.network/roles/79", true),// Now Keybase Admin + YOUTUBE_ADMIN(10, 110, "https://bisq.network/roles/56", true), + + // maintainers + BISQ_MAINTAINER(50, 110, "https://bisq.network/roles/63", true), + BITCOINJ_MAINTAINER(20, 110, "https://bisq.network/roles/8", true), + NETLAYER_MAINTAINER(20, 110, "https://bisq.network/roles/81", true), + + // operators + WEBSITE_OPERATOR(50, 110, "https://bisq.network/roles/12", true), + FORUM_OPERATOR(50, 110, "https://bisq.network/roles/19", true), + SEED_NODE_OPERATOR(20, 110, "https://bisq.network/roles/15", true), + DATA_RELAY_NODE_OPERATOR(20, 110, "https://bisq.network/roles/14", true), + BTC_NODE_OPERATOR(5, 110, "https://bisq.network/roles/67", true), + MARKETS_OPERATOR(20, 110, "https://bisq.network/roles/9", true), + BSQ_EXPLORER_OPERATOR(20, 110, "https://bisq.network/roles/11", true), + MOBILE_NOTIFICATIONS_RELAY_OPERATOR(20, 110, "https://bisq.network/roles/82", true), + + // other + DOMAIN_NAME_HOLDER(50, 110, "https://bisq.network/roles/77", false), + DNS_ADMIN(20, 110, "https://bisq.network/roles/18", false), + MEDIATOR(10, 110, "https://bisq.network/roles/83", true), + ARBITRATOR(200, 110, "https://bisq.network/roles/13", true), + BTC_DONATION_ADDRESS_OWNER(50, 110, "https://bisq.network/roles/80", true); + + + // Will be multiplied with PARAM.BONDED_ROLE_FACTOR to get BSQ amount. + // As BSQ is volatile we need to adjust the bonds over time. + // To avoid changing the Enum we use the BONDED_ROLE_FACTOR param to react on BSQ price changes. + // Required bond = requiredBondUnit * PARAM.BONDED_ROLE_FACTOR.value + @Getter + private final long requiredBondUnit; + + // Unlock time in blocks + @Getter + private final int unlockTimeInBlocks; + @Getter + private final String link; + @Getter + private final boolean allowMultipleHolders; + + /** + * @param requiredBondUnit // requiredBondUnit for lockup tx (will be multiplied with PARAM.BONDED_ROLE_FACTOR for BSQ value) + * @param unlockTimeInDays // unlockTime in days + * @param link // Link to GitHub for role description + * @param allowMultipleHolders // If role can be held by multiple persons (e.g. seed nodes vs. domain name) + */ + BondedRoleType(long requiredBondUnit, int unlockTimeInDays, String link, boolean allowMultipleHolders) { + this.requiredBondUnit = requiredBondUnit; + this.unlockTimeInBlocks = Config.baseCurrencyNetwork().isMainnet() ? + unlockTimeInDays * 144 : // mainnet (144 blocks per day) + Config.baseCurrencyNetwork().isRegtest() ? + 5 : // regtest (arbitrarily low value for dev testing) + 144; // testnet (relatively short time for testing purposes) + this.link = link; + this.allowMultipleHolders = allowMultipleHolders; + } + + public String getDisplayString() { + return Res.get("dao.bond.bondedRoleType." + name()); + } + + @Override + public String toString() { + return "BondedRoleType{" + + "\n requiredBondUnit=" + requiredBondUnit + + ",\n unlockTime=" + unlockTimeInBlocks + + ",\n link='" + link + '\'' + + ",\n allowMultipleHolders=" + allowMultipleHolders + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java new file mode 100644 index 0000000000..d39fc72604 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java @@ -0,0 +1,167 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.util.CollectionUtils; + +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@Getter +public final class ChangeParamProposal extends Proposal implements ImmutableDaoStateModel { + private final Param param; + private final String paramValue; + + public ChangeParamProposal(String name, + String link, + Param param, + String paramValue, + Map extraDataMap) { + this(name, + link, + param, + paramValue, + Version.PROPOSAL, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ChangeParamProposal(String name, + String link, + Param param, + String paramValue, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + + this.param = param; + this.paramValue = paramValue; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.ChangeParamProposal.Builder builder = protobuf.ChangeParamProposal.newBuilder() + .setParam(param.name()) + .setParamValue(paramValue); + return super.getProposalBuilder().setChangeParamProposal(builder); + } + + public static ChangeParamProposal fromProto(protobuf.Proposal proto) { + final protobuf.ChangeParamProposal proposalProto = proto.getChangeParamProposal(); + return new ChangeParamProposal(proto.getName(), + proto.getLink(), + Param.fromProto(proposalProto), + proposalProto.getParamValue(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ProposalType getType() { + return ProposalType.CHANGE_PARAM; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_CHANGE_PARAM; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_CHANGE_PARAM; + } + + @Override + public TxType getTxType() { + return TxType.PROPOSAL; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new ChangeParamProposal(getName(), + getLink(), + getParam(), + getParamValue(), + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "ChangeParamProposal{" + + "\n param=" + param + + ",\n paramValue=" + paramValue + + "\n} " + super.toString(); + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ChangeParamProposal)) return false; + if (!super.equals(o)) return false; + ChangeParamProposal that = (ChangeParamProposal) o; + boolean paramTypeNameIsEquals = param.getParamType().name().equals(that.param.getParamType().name()); + boolean paramNameIsEquals = param.name().equals(that.param.name()); + return paramNameIsEquals && paramTypeNameIsEquals && + Objects.equals(paramValue, that.paramValue); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), param.getParamType().name(), param.name(), paramValue); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java new file mode 100644 index 0000000000..d23249b6c4 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java @@ -0,0 +1,166 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.IssuanceProposal; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.CollectionUtils; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class CompensationProposal extends Proposal implements IssuanceProposal, ImmutableDaoStateModel { + private final long requestedBsq; + private final String bsqAddress; + + public CompensationProposal(String name, + String link, + Coin requestedBsq, + String bsqAddress, + Map extraDataMap) { + this(name, + link, + bsqAddress, + requestedBsq.value, + Version.COMPENSATION_REQUEST, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CompensationProposal(String name, + String link, + String bsqAddress, + long requestedBsq, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + + this.requestedBsq = requestedBsq; + this.bsqAddress = bsqAddress; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.CompensationProposal.Builder builder = protobuf.CompensationProposal.newBuilder() + .setBsqAddress(bsqAddress) + .setRequestedBsq(requestedBsq); + return super.getProposalBuilder().setCompensationProposal(builder); + } + + public static CompensationProposal fromProto(protobuf.Proposal proto) { + final protobuf.CompensationProposal proposalProto = proto.getCompensationProposal(); + return new CompensationProposal(proto.getName(), + proto.getLink(), + proposalProto.getBsqAddress(), + proposalProto.getRequestedBsq(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Coin getRequestedBsq() { + return Coin.valueOf(requestedBsq); + } + + public LegacyAddress getAddress() throws AddressFormatException { + // Remove leading 'B' + String underlyingBtcAddress = bsqAddress.substring(1, bsqAddress.length()); + return LegacyAddress.fromBase58(Config.baseCurrencyNetworkParameters(), underlyingBtcAddress); + } + + + @Override + public ProposalType getType() { + return ProposalType.COMPENSATION_REQUEST; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_COMP_REQUEST; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_COMP_REQUEST; + } + + @Override + public TxType getTxType() { + return TxType.COMPENSATION_REQUEST; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new CompensationProposal(getName(), + getLink(), + getBsqAddress(), + getRequestedBsq().value, + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "CompensationProposal{" + + "\n requestedBsq=" + requestedBsq + + ",\n bsqAddress='" + bsqAddress + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java new file mode 100644 index 0000000000..e442454ab7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java @@ -0,0 +1,139 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.util.CollectionUtils; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class ConfiscateBondProposal extends Proposal implements ImmutableDaoStateModel { + private final String lockupTxId; + + public ConfiscateBondProposal(String name, + String link, + String lockupTxId, + Map extraDataMap) { + this(name, + link, + lockupTxId, + Version.PROPOSAL, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ConfiscateBondProposal(String name, + String link, + String lockupTxId, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + this.lockupTxId = lockupTxId; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.ConfiscateBondProposal.Builder builder = protobuf.ConfiscateBondProposal.newBuilder() + .setLockupTxId(lockupTxId); + return super.getProposalBuilder().setConfiscateBondProposal(builder); + } + + public static ConfiscateBondProposal fromProto(protobuf.Proposal proto) { + final protobuf.ConfiscateBondProposal proposalProto = proto.getConfiscateBondProposal(); + return new ConfiscateBondProposal(proto.getName(), + proto.getLink(), + proposalProto.getLockupTxId(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ProposalType getType() { + return ProposalType.CONFISCATE_BOND; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_CONFISCATION; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_CONFISCATION; + } + + @Override + public TxType getTxType() { + return TxType.PROPOSAL; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new ConfiscateBondProposal(getName(), + getLink(), + getLockupTxId(), + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "ConfiscateBondProposal{" + + "\n lockupTxId=" + lockupTxId + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Cycle.java b/core/src/main/java/bisq/core/dao/state/model/governance/Cycle.java new file mode 100644 index 0000000000..a1066757a0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Cycle.java @@ -0,0 +1,136 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.common.collect.ImmutableList; + +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +/** + * Cycle represents the monthly period for proposals and voting. + * It consists of a ordered list of phases represented by the phaseWrappers. + */ +@Immutable +@Value +public class Cycle implements PersistablePayload, ImmutableDaoStateModel { + // List is ordered according to the Phase enum. + private final ImmutableList daoPhaseList; + private final int heightOfFirstBlock; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Cycle(int heightOfFirstBlock, ImmutableList daoPhaseList) { + this.heightOfFirstBlock = heightOfFirstBlock; + this.daoPhaseList = daoPhaseList; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Cycle toProtoMessage() { + return protobuf.Cycle.newBuilder() + .setHeightOfFirstLock(heightOfFirstBlock) + .addAllDaoPhase(daoPhaseList.stream() + .map(DaoPhase::toProtoMessage) + .collect(Collectors.toList())) + .build(); + } + + public static Cycle fromProto(protobuf.Cycle proto) { + final ImmutableList daoPhaseList = ImmutableList.copyOf(proto.getDaoPhaseList().stream() + .map(DaoPhase::fromProto) + .collect(Collectors.toList())); + return new Cycle(proto.getHeightOfFirstLock(), daoPhaseList); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public int getHeightOfLastBlock() { + return heightOfFirstBlock + getDuration() - 1; + } + + public boolean isInPhase(int height, DaoPhase.Phase phase) { + return height >= getFirstBlockOfPhase(phase) && height <= getLastBlockOfPhase(phase); + } + + public boolean isInCycle(int height) { + return height >= getHeightOfFirstBlock() && height <= getHeightOfLastBlock(); + } + + public int getFirstBlockOfPhase(DaoPhase.Phase phase) { + return heightOfFirstBlock + daoPhaseList.stream() + .filter(item -> item.getPhase().ordinal() < phase.ordinal()) + .mapToInt(DaoPhase::getDuration).sum(); + } + + public int getLastBlockOfPhase(DaoPhase.Phase phase) { + return getFirstBlockOfPhase(phase) + getDuration(phase) - 1; + } + + public int getDurationOfPhase(DaoPhase.Phase phase) { + return daoPhaseList.stream() + .filter(item -> item.getPhase() == phase) + .mapToInt(DaoPhase::getDuration) + .sum(); + } + + public Optional getPhaseForHeight(int height) { + return daoPhaseList.stream() + .filter(item -> isInPhase(height, item.getPhase())) + .map(DaoPhase::getPhase) + .findAny(); + } + + private Optional getPhaseWrapper(DaoPhase.Phase phase) { + return daoPhaseList.stream().filter(item -> item.getPhase() == phase).findAny(); + } + + private int getDuration(DaoPhase.Phase phase) { + return getPhaseWrapper(phase).map(DaoPhase::getDuration).orElse(0); + } + + public int getDuration() { + return daoPhaseList.stream().mapToInt(DaoPhase::getDuration).sum(); + } + + @Override + public String toString() { + return "Cycle{" + + "\n daoPhaseList=" + daoPhaseList + + ",\n heightOfFirstBlock=" + heightOfFirstBlock + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/DaoPhase.java b/core/src/main/java/bisq/core/dao/state/model/governance/DaoPhase.java new file mode 100644 index 0000000000..ccd7091d1b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/DaoPhase.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Objects; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Encapsulated the phase enum with the duration. + * As the duration can change by voting we don't want to put the duration property in the enum but use that wrapper. + */ +@Immutable +@Value +@Slf4j +public class DaoPhase implements PersistablePayload, ImmutableDaoStateModel { + + /** + * Enum for phase of a cycle. + * + * We don't want to use an enum with the duration as field because the duration can change by voting and enums + * should be considered immutable. + */ + @Immutable + public enum Phase implements ImmutableDaoStateModel { + UNDEFINED, + PROPOSAL, + BREAK1, + BLIND_VOTE, + BREAK2, + VOTE_REVEAL, + BREAK3, + RESULT + } + + + private final Phase phase; + private final int duration; + + public DaoPhase(Phase phase, int duration) { + this.phase = phase; + this.duration = duration; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.DaoPhase toProtoMessage() { + return protobuf.DaoPhase.newBuilder() + .setPhaseOrdinal(phase.ordinal()) + .setDuration(duration) + .build(); + } + + public static DaoPhase fromProto(protobuf.DaoPhase proto) { + int ordinal = proto.getPhaseOrdinal(); + if (ordinal >= Phase.values().length) { + log.warn("We tried to access a ordinal outside of the DaoPhase.Phase enum bounds and set it to " + + "UNDEFINED. ordinal={}", ordinal); + return new DaoPhase(Phase.UNDEFINED, 0); + } + + return new DaoPhase(Phase.values()[ordinal], proto.getDuration()); + } + + + @Override + public String toString() { + return "DaoPhase{" + + "\n phase=" + phase + + ",\n duration=" + duration + + "\n}"; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DaoPhase)) return false; + if (!super.equals(o)) return false; + DaoPhase daoPhase = (DaoPhase) o; + return duration == daoPhase.duration && + phase.name().equals(daoPhase.phase.name()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), phase.name(), duration); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java b/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java new file mode 100644 index 0000000000..489ba012f7 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.merit.MeritConsensus; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Optional; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Holds all data from a decrypted vote item. + */ +@Immutable +@Slf4j +@Value +public class DecryptedBallotsWithMerits implements PersistablePayload, ImmutableDaoStateModel { + private final byte[] hashOfBlindVoteList; + private final String blindVoteTxId; + private final String voteRevealTxId; + private final long stake; + + // BallotList and meritList can be empty list in case we don't have a blind vote payload + private final BallotList ballotList; + private final MeritList meritList; + + public DecryptedBallotsWithMerits(byte[] hashOfBlindVoteList, String blindVoteTxId, String voteRevealTxId, long stake, + BallotList ballotList, MeritList meritList) { + this.hashOfBlindVoteList = hashOfBlindVoteList; + this.blindVoteTxId = blindVoteTxId; + this.voteRevealTxId = voteRevealTxId; + this.stake = stake; + this.ballotList = ballotList; + this.meritList = meritList; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.DecryptedBallotsWithMerits toProtoMessage() { + return getBuilder().build(); + } + + private protobuf.DecryptedBallotsWithMerits.Builder getBuilder() { + return protobuf.DecryptedBallotsWithMerits.newBuilder() + .setHashOfBlindVoteList(ByteString.copyFrom(hashOfBlindVoteList)) + .setBlindVoteTxId(blindVoteTxId) + .setVoteRevealTxId(voteRevealTxId) + .setStake(stake) + .setBallotList(ballotList.getBuilder()) + .setMeritList(meritList.getBuilder()); + } + + public static DecryptedBallotsWithMerits fromProto(protobuf.DecryptedBallotsWithMerits proto) { + return new DecryptedBallotsWithMerits(proto.getHashOfBlindVoteList().toByteArray(), + proto.getBlindVoteTxId(), + proto.getVoteRevealTxId(), + proto.getStake(), + BallotList.fromProto(proto.getBallotList()), + MeritList.fromProto(proto.getMeritList())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Optional getVote(String proposalTxId) { + return ballotList.stream() + .filter(ballot -> ballot.getTxId().equals(proposalTxId)) + .map(Ballot::getVote) + .findAny(); + } + + public long getMerit(DaoStateService daoStateService) { + return MeritConsensus.getMeritStake(blindVoteTxId, meritList, daoStateService); + } + + @Override + public String toString() { + return "DecryptedBallotsWithMerits{" + + "\n hashOfBlindVoteList=" + Utilities.bytesAsHexString(hashOfBlindVoteList) + + ",\n blindVoteTxId='" + blindVoteTxId + '\'' + + ",\n voteRevealTxId='" + voteRevealTxId + '\'' + + ",\n stake=" + stake + + ",\n ballotList=" + ballotList + + ",\n meritList=" + meritList + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/EvaluatedProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/EvaluatedProposal.java new file mode 100644 index 0000000000..c06bba0053 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/EvaluatedProposal.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Value +public class EvaluatedProposal implements PersistablePayload, ImmutableDaoStateModel { + private final boolean isAccepted; + private final ProposalVoteResult proposalVoteResult; + + public EvaluatedProposal(boolean isAccepted, ProposalVoteResult proposalVoteResult) { + this.isAccepted = isAccepted; + this.proposalVoteResult = proposalVoteResult; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.EvaluatedProposal toProtoMessage() { + protobuf.EvaluatedProposal.Builder builder = protobuf.EvaluatedProposal.newBuilder() + .setIsAccepted(isAccepted) + .setProposalVoteResult(proposalVoteResult.toProtoMessage()); + return builder.build(); + } + + public static EvaluatedProposal fromProto(protobuf.EvaluatedProposal proto) { + return new EvaluatedProposal(proto.getIsAccepted(), + ProposalVoteResult.fromProto(proto.getProposalVoteResult())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Proposal getProposal() { + return proposalVoteResult.getProposal(); + } + + public String getProposalTxId() { + return getProposal().getTxId(); + } + + @Override + public String toString() { + return "EvaluatedProposal{" + + "\n isAccepted=" + isAccepted + + ",\n proposalVoteResult=" + proposalVoteResult + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java new file mode 100644 index 0000000000..fa8c71b556 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java @@ -0,0 +1,129 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.util.CollectionUtils; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class GenericProposal extends Proposal implements ImmutableDaoStateModel { + + public GenericProposal(String name, + String link, + Map extraDataMap) { + this(name, + link, + Version.PROPOSAL, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GenericProposal(String name, + String link, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.GenericProposal.Builder builder = protobuf.GenericProposal.newBuilder(); + return super.getProposalBuilder().setGenericProposal(builder); + } + + public static GenericProposal fromProto(protobuf.Proposal proto) { + return new GenericProposal(proto.getName(), + proto.getLink(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ProposalType getType() { + return ProposalType.GENERIC; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_GENERIC; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_GENERIC; + } + + @Override + public TxType getTxType() { + return TxType.PROPOSAL; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new GenericProposal(getName(), + getLink(), + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "GenericProposal{" + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Issuance.java b/core/src/main/java/bisq/core/dao/state/model/governance/Issuance.java new file mode 100644 index 0000000000..45888ac722 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Issuance.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Objects; +import java.util.Optional; + +import lombok.Value; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Holds the issuance data (compensation request which was accepted in voting). + */ +@Immutable +@Value +public class Issuance implements PersistablePayload, NetworkPayload, ImmutableDaoStateModel { + private final String txId; // comp. request txId + private final int chainHeight; // of issuance (first block of result phase) + private final long amount; + + // sig key as hex of first input in issuance tx used for signing the merits + // Can be null (payToPubKey tx) but in our case it will never be null. Still keep it nullable to be safe. + @Nullable + private final String pubKey; + + private final IssuanceType issuanceType; + + public Issuance(String txId, int chainHeight, long amount, @Nullable String pubKey, IssuanceType issuanceType) { + this.txId = txId; + this.chainHeight = chainHeight; + this.amount = amount; + this.pubKey = pubKey; + this.issuanceType = issuanceType; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public protobuf.Issuance toProtoMessage() { + final protobuf.Issuance.Builder builder = protobuf.Issuance.newBuilder() + .setTxId(txId) + .setChainHeight(chainHeight) + .setAmount(amount) + .setIssuanceType(issuanceType.name()); + Optional.ofNullable(pubKey).ifPresent(e -> builder.setPubKey(pubKey)); + return builder.build(); + } + + public static Issuance fromProto(protobuf.Issuance proto) { + return new Issuance(proto.getTxId(), + proto.getChainHeight(), + proto.getAmount(), + proto.getPubKey().isEmpty() ? null : proto.getPubKey(), + proto.getIssuanceType().isEmpty() ? IssuanceType.UNDEFINED : ProtoUtil.enumFromProto(IssuanceType.class, proto.getIssuanceType())); + } + + @Override + public String toString() { + return "Issuance{" + + "\n txId='" + txId + '\'' + + ",\n chainHeight=" + chainHeight + + ",\n amount=" + amount + + ",\n pubKey='" + pubKey + '\'' + + ",\n issuanceType='" + issuanceType + '\'' + + "\n}"; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Issuance)) return false; + if (!super.equals(o)) return false; + Issuance issuance = (Issuance) o; + return chainHeight == issuance.chainHeight && + amount == issuance.amount && + Objects.equals(txId, issuance.txId) && + Objects.equals(pubKey, issuance.pubKey) && + issuanceType.name().equals(issuance.issuanceType.name()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txId, chainHeight, amount, pubKey, issuanceType.name()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/IssuanceType.java b/core/src/main/java/bisq/core/dao/state/model/governance/IssuanceType.java new file mode 100644 index 0000000000..1d0b14224a --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/IssuanceType.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public enum IssuanceType implements ImmutableDaoStateModel { + UNDEFINED, + COMPENSATION, + REIMBURSEMENT +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Merit.java b/core/src/main/java/bisq/core/dao/state/model/governance/Merit.java new file mode 100644 index 0000000000..7b449934b3 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Merit.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@EqualsAndHashCode +public class Merit implements PersistablePayload, NetworkPayload, ConsensusCritical, ImmutableDaoStateModel { + @Getter + private final Issuance issuance; + @Getter + private final byte[] signature; + + public Merit(Issuance issuance, byte[] signature) { + this.issuance = issuance; + this.signature = signature; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Merit toProtoMessage() { + final protobuf.Merit.Builder builder = protobuf.Merit.newBuilder() + .setIssuance(issuance.toProtoMessage()) + .setSignature(ByteString.copyFrom(signature)); + return builder.build(); + } + + public static Merit fromProto(protobuf.Merit proto) { + return new Merit(Issuance.fromProto(proto.getIssuance()), + proto.getSignature().toByteArray()); + } + + public String getIssuanceTxId() { + return issuance.getTxId(); + } + + @Override + public String toString() { + return "Merit{" + + "\n issuance=" + issuance + + ",\n signature=" + Utilities.bytesAsHexString(signature) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/MeritList.java b/core/src/main/java/bisq/core/dao/state/model/governance/MeritList.java new file mode 100644 index 0000000000..334ac98872 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/MeritList.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.Proto; + +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.Value; + +@Value +public class MeritList implements Proto, ConsensusCritical, ImmutableDaoStateModel { + private final List list; + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.MeritList toProtoMessage() { + return getBuilder().build(); + } + + public protobuf.MeritList.Builder getBuilder() { + return protobuf.MeritList.newBuilder() + .addAllMerit(getList().stream() + .map(Merit::toProtoMessage) + .collect(Collectors.toList())); + } + + public static MeritList fromProto(protobuf.MeritList proto) { + return new MeritList(new ArrayList<>(proto.getMeritList().stream() + .map(Merit::fromProto) + .collect(Collectors.toList()))); + } + + public static MeritList getMeritListFromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return MeritList.fromProto(protobuf.MeritList.parseFrom(bytes)); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ParamChange.java b/core/src/main/java/bisq/core/dao/state/model/governance/ParamChange.java new file mode 100644 index 0000000000..622d3272f1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ParamChange.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +/** + * Holds the data for a parameter change. Gets persisted with the DaoState. + */ +@Immutable +@Value +public class ParamChange implements PersistablePayload, ImmutableDaoStateModel { + // We use the enum name instead of the enum to be more flexible with changes at updates + private final String paramName; + private final String value; + private final int activationHeight; + + public ParamChange(String paramName, String value, int activationHeight) { + this.paramName = paramName; + this.value = value; + this.activationHeight = activationHeight; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + public protobuf.ParamChange toProtoMessage() { + return protobuf.ParamChange.newBuilder() + .setParamName(paramName) + .setParamValue(value) + .setActivationHeight(activationHeight) + .build(); + } + + public static ParamChange fromProto(protobuf.ParamChange proto) { + return new ParamChange(proto.getParamName(), + proto.getParamValue(), + proto.getActivationHeight()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java new file mode 100644 index 0000000000..585ffd9bc8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java @@ -0,0 +1,153 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.ExtraDataMapValidator; + +import java.util.Date; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Base class for proposals. + */ +@Immutable +@Slf4j +@Getter +@EqualsAndHashCode +public abstract class Proposal implements PersistablePayload, NetworkPayload, ConsensusCritical, ImmutableDaoStateModel { + protected final String name; + protected final String link; + protected final byte version; + protected final long creationDate; + @Nullable + protected final String txId; + + // This hash map allows addition of data in future versions without breaking consensus + @Nullable + protected final Map extraDataMap; + + protected Proposal(String name, + String link, + byte version, + long creationDate, + @Nullable String txId, + @Nullable Map extraDataMap) { + this.name = name; + this.link = link; + this.version = version; + this.creationDate = creationDate; + this.txId = txId; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.Proposal.Builder builder = protobuf.Proposal.newBuilder() + .setName(name) + .setLink(link) + .setVersion(version) + .setCreationDate(creationDate); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + Optional.ofNullable(txId).ifPresent(builder::setTxId); + return builder; + } + + @Override + public protobuf.Proposal toProtoMessage() { + return getProposalBuilder().build(); + } + + public static Proposal fromProto(protobuf.Proposal proto) { + switch (proto.getMessageCase()) { + case COMPENSATION_PROPOSAL: + return CompensationProposal.fromProto(proto); + case REIMBURSEMENT_PROPOSAL: + return ReimbursementProposal.fromProto(proto); + case CHANGE_PARAM_PROPOSAL: + return ChangeParamProposal.fromProto(proto); + case ROLE_PROPOSAL: + return RoleProposal.fromProto(proto); + case CONFISCATE_BOND_PROPOSAL: + return ConfiscateBondProposal.fromProto(proto); + case GENERIC_PROPOSAL: + return GenericProposal.fromProto(proto); + case REMOVE_ASSET_PROPOSAL: + return RemoveAssetProposal.fromProto(proto); + case MESSAGE_NOT_SET: + default: + throw new ProtobufferRuntimeException("Unknown message case: " + proto); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public Date getCreationDateAsDate() { + return new Date(getCreationDate()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + public abstract Proposal cloneProposalAndAddTxId(String txId); + + public abstract ProposalType getType(); + + public abstract TxType getTxType(); + + public abstract Param getQuorumParam(); + + public abstract Param getThresholdParam(); + + @Override + public String toString() { + return "Proposal{" + + "\n txId='" + txId + '\'' + + ",\n name='" + name + '\'' + + ",\n link='" + link + '\'' + + ",\n txId='" + txId + '\'' + + ",\n version=" + version + + ",\n creationDate=" + new Date(creationDate) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ProposalVoteResult.java b/core/src/main/java/bisq/core/dao/state/model/governance/ProposalVoteResult.java new file mode 100644 index 0000000000..765c97e9d6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ProposalVoteResult.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.persistable.PersistablePayload; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkArgument; + +@Immutable +@Value +@Slf4j +public class ProposalVoteResult implements PersistablePayload, ImmutableDaoStateModel { + private final Proposal proposal; + private final long stakeOfAcceptedVotes; + private final long stakeOfRejectedVotes; + private final int numAcceptedVotes; + private final int numRejectedVotes; + private final int numIgnoredVotes; + + public ProposalVoteResult(Proposal proposal, long stakeOfAcceptedVotes, long stakeOfRejectedVotes, + int numAcceptedVotes, int numRejectedVotes, int numIgnoredVotes) { + this.proposal = proposal; + this.stakeOfAcceptedVotes = stakeOfAcceptedVotes; + this.stakeOfRejectedVotes = stakeOfRejectedVotes; + this.numAcceptedVotes = numAcceptedVotes; + this.numRejectedVotes = numRejectedVotes; + this.numIgnoredVotes = numIgnoredVotes; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.ProposalVoteResult toProtoMessage() { + protobuf.ProposalVoteResult.Builder builder = protobuf.ProposalVoteResult.newBuilder() + .setProposal(proposal.toProtoMessage()) + .setStakeOfAcceptedVotes(stakeOfAcceptedVotes) + .setStakeOfRejectedVotes(stakeOfRejectedVotes) + .setNumAcceptedVotes(numAcceptedVotes) + .setNumRejectedVotes(numRejectedVotes) + .setNumIgnoredVotes(numIgnoredVotes); + return builder.build(); + } + + public static ProposalVoteResult fromProto(protobuf.ProposalVoteResult proto) { + return new ProposalVoteResult(Proposal.fromProto(proto.getProposal()), + proto.getStakeOfAcceptedVotes(), + proto.getStakeOfRejectedVotes(), + proto.getNumAcceptedVotes(), + proto.getNumRejectedVotes(), + proto.getNumIgnoredVotes()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public int getNumActiveVotes() { + return numAcceptedVotes + numRejectedVotes; + } + + public long getQuorum() { + // Quorum is sum of all votes independent if accepted or rejected. + log.debug("Quorum: proposalTxId: {}, totalStake: {}, stakeOfAcceptedVotes: {}, stakeOfRejectedVotes: {}", + proposal.getTxId(), getTotalStake(), stakeOfAcceptedVotes, stakeOfRejectedVotes); + return getTotalStake(); + } + + public long getThreshold() { + checkArgument(stakeOfAcceptedVotes >= 0, "stakeOfAcceptedVotes must not be negative"); + checkArgument(stakeOfRejectedVotes >= 0, "stakeOfRejectedVotes must not be negative"); + if (stakeOfAcceptedVotes == 0) { + return 0; + } + return stakeOfAcceptedVotes * 10_000 / getTotalStake(); + } + + @Override + public String toString() { + return "ProposalVoteResult{" + + "\n proposal=" + proposal + + ",\n stakeOfAcceptedVotes=" + stakeOfAcceptedVotes + + ",\n stakeOfRejectedVotes=" + stakeOfRejectedVotes + + ",\n numAcceptedVotes=" + numAcceptedVotes + + ",\n numRejectedVotes=" + numRejectedVotes + + ",\n numIgnoredVotes=" + numIgnoredVotes + + "\n}"; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private long getTotalStake() { + return stakeOfAcceptedVotes + stakeOfRejectedVotes; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java new file mode 100644 index 0000000000..fb83ab23d5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java @@ -0,0 +1,166 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.IssuanceProposal; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.CollectionUtils; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class ReimbursementProposal extends Proposal implements IssuanceProposal, ImmutableDaoStateModel { + private final long requestedBsq; + private final String bsqAddress; + + public ReimbursementProposal(String name, + String link, + Coin requestedBsq, + String bsqAddress, + Map extraDataMap) { + this(name, + link, + bsqAddress, + requestedBsq.value, + Version.REIMBURSEMENT_REQUEST, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ReimbursementProposal(String name, + String link, + String bsqAddress, + long requestedBsq, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + + this.requestedBsq = requestedBsq; + this.bsqAddress = bsqAddress; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.ReimbursementProposal.Builder builder = protobuf.ReimbursementProposal.newBuilder() + .setBsqAddress(bsqAddress) + .setRequestedBsq(requestedBsq); + return super.getProposalBuilder().setReimbursementProposal(builder); + } + + public static ReimbursementProposal fromProto(protobuf.Proposal proto) { + final protobuf.ReimbursementProposal proposalProto = proto.getReimbursementProposal(); + return new ReimbursementProposal(proto.getName(), + proto.getLink(), + proposalProto.getBsqAddress(), + proposalProto.getRequestedBsq(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Coin getRequestedBsq() { + return Coin.valueOf(requestedBsq); + } + + public LegacyAddress getAddress() throws AddressFormatException { + // Remove leading 'B' + String underlyingBtcAddress = bsqAddress.substring(1, bsqAddress.length()); + return LegacyAddress.fromBase58(Config.baseCurrencyNetworkParameters(), underlyingBtcAddress); + } + + + @Override + public ProposalType getType() { + return ProposalType.REIMBURSEMENT_REQUEST; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_REIMBURSEMENT; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_REIMBURSEMENT; + } + + @Override + public TxType getTxType() { + return TxType.REIMBURSEMENT_REQUEST; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new ReimbursementProposal(getName(), + getLink(), + getBsqAddress(), + getRequestedBsq().value, + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "ReimbursementProposal{" + + "\n requestedBsq=" + requestedBsq + + ",\n bsqAddress='" + bsqAddress + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java new file mode 100644 index 0000000000..db9cbf7522 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.util.CollectionUtils; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class RemoveAssetProposal extends Proposal implements ImmutableDaoStateModel { + private final String tickerSymbol; + + public RemoveAssetProposal(String name, + String link, + String tickerSymbol, + Map extraDataMap) { + this(name, + link, + tickerSymbol, + Version.PROPOSAL, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RemoveAssetProposal(String name, + String link, + String tickerSymbol, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + + this.tickerSymbol = tickerSymbol; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.RemoveAssetProposal.Builder builder = protobuf.RemoveAssetProposal.newBuilder() + .setTickerSymbol(tickerSymbol); + return super.getProposalBuilder().setRemoveAssetProposal(builder); + } + + public static RemoveAssetProposal fromProto(protobuf.Proposal proto) { + final protobuf.RemoveAssetProposal proposalProto = proto.getRemoveAssetProposal(); + return new RemoveAssetProposal(proto.getName(), + proto.getLink(), + proposalProto.getTickerSymbol(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ProposalType getType() { + return ProposalType.REMOVE_ASSET; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_REMOVE_ASSET; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_REMOVE_ASSET; + } + + @Override + public TxType getTxType() { + return TxType.PROPOSAL; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new RemoveAssetProposal(getName(), + getLink(), + getTickerSymbol(), + getVersion(), + getCreationDate(), + txId, + extraDataMap); + } + + @Override + public String toString() { + return "GenericProposal{" + + "\n tickerSymbol=" + tickerSymbol + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Role.java b/core/src/main/java/bisq/core/dao/state/model/governance/Role.java new file mode 100644 index 0000000000..4850b31541 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Role.java @@ -0,0 +1,149 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.bond.BondedAsset; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.locale.Res; + +import bisq.common.crypto.Hash; +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Objects; +import java.util.UUID; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Immutable data for a role. Is stored in the DaoState as part of the evaluated proposals. + */ +@Immutable +@Slf4j +@Value +public final class Role implements PersistablePayload, NetworkPayload, BondedAsset, ImmutableDaoStateModel { + private final String uid; + private final String name; + private final String link; + private final BondedRoleType bondedRoleType; + + // Only used as cache + transient private final byte[] hash; + + /** + * @param name Full name or nickname + * @param link GitHub account or forum account of user + * @param bondedRoleType BondedRoleType + */ + public Role(String name, + String link, + BondedRoleType bondedRoleType) { + this(UUID.randomUUID().toString(), + name, + link, + bondedRoleType + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private Role(String uid, + String name, + String link, + BondedRoleType bondedRoleType) { + this.uid = uid; + this.name = name; + this.link = link; + this.bondedRoleType = bondedRoleType; + + hash = Hash.getSha256Ripemd160hash(toProtoMessage().toByteArray()); + } + + @Override + public protobuf.Role toProtoMessage() { + protobuf.Role.Builder builder = protobuf.Role.newBuilder() + .setUid(uid) + .setName(name) + .setLink(link) + .setBondedRoleType(bondedRoleType.name()); + return builder.build(); + } + + public static Role fromProto(protobuf.Role proto) { + return new Role(proto.getUid(), + proto.getName(), + proto.getLink(), + ProtoUtil.enumFromProto(BondedRoleType.class, proto.getBondedRoleType())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BondedAsset implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public String getDisplayString() { + return Res.get("dao.bond.bondedRoleType." + bondedRoleType.name()) + ": " + name; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // We use only the immutable data + // bondedRoleType must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Role that = (Role) o; + return Objects.equals(uid, that.uid) && + Objects.equals(name, that.name) && + Objects.equals(link, that.link) && + bondedRoleType.name().equals(that.bondedRoleType.name()); + } + + @Override + public int hashCode() { + return Objects.hash(uid, name, link, bondedRoleType.name()); + } + + @Override + public String toString() { + return "Role{" + + "\n uid='" + uid + '\'' + + ",\n name='" + name + '\'' + + ",\n link='" + link + '\'' + + ",\n bondedRoleType=" + bondedRoleType + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java new file mode 100644 index 0000000000..a24a0b9542 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java @@ -0,0 +1,153 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.Version; +import bisq.common.util.CollectionUtils; + +import java.util.Date; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class RoleProposal extends Proposal implements ImmutableDaoStateModel { + private final Role role; + private final long requiredBondUnit; + private final int unlockTime; // in blocks + + public RoleProposal(Role role, Map extraDataMap) { + this(role.getName(), + role.getLink(), + role, + role.getBondedRoleType().getRequiredBondUnit(), + role.getBondedRoleType().getUnlockTimeInBlocks(), + Version.PROPOSAL, + new Date().getTime(), + null, + extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RoleProposal(String name, + String link, + Role role, + long requiredBondUnit, + int unlockTime, + byte version, + long creationDate, + String txId, + Map extraDataMap) { + super(name, + link, + version, + creationDate, + txId, + extraDataMap); + + this.role = role; + this.requiredBondUnit = requiredBondUnit; + this.unlockTime = unlockTime; + } + + @Override + public protobuf.Proposal.Builder getProposalBuilder() { + final protobuf.RoleProposal.Builder builder = protobuf.RoleProposal.newBuilder() + .setRole(role.toProtoMessage()) + .setRequiredBondUnit(requiredBondUnit) + .setUnlockTime(unlockTime); + return super.getProposalBuilder().setRoleProposal(builder); + } + + public static RoleProposal fromProto(protobuf.Proposal proto) { + final protobuf.RoleProposal proposalProto = proto.getRoleProposal(); + return new RoleProposal(proto.getName(), + proto.getLink(), + Role.fromProto(proposalProto.getRole()), + proposalProto.getRequiredBondUnit(), + proposalProto.getUnlockTime(), + (byte) proto.getVersion(), + proto.getCreationDate(), + proto.getTxId(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ProposalType getType() { + return ProposalType.BONDED_ROLE; + } + + @Override + public Param getQuorumParam() { + return Param.QUORUM_ROLE; + } + + @Override + public Param getThresholdParam() { + return Param.THRESHOLD_ROLE; + } + + @Override + public TxType getTxType() { + return TxType.PROPOSAL; + } + + @Override + public Proposal cloneProposalAndAddTxId(String txId) { + return new RoleProposal(name, + link, + role, + requiredBondUnit, + unlockTime, + version, + creationDate, + txId, + extraDataMap); + } + + @Override + public String toString() { + return "RoleProposal{" + + "\n role=" + role + + "\n requiredBondUnit=" + requiredBondUnit + + "\n unlockTime=" + unlockTime + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Vote.java b/core/src/main/java/bisq/core/dao/state/model/governance/Vote.java new file mode 100644 index 0000000000..cb29cfbadd --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Vote.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.core.dao.state.model.governance; + +import bisq.core.dao.governance.ConsensusCritical; +import bisq.core.dao.state.model.ImmutableDaoStateModel; + +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.Message; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@Value +public class Vote implements PersistablePayload, NetworkPayload, ConsensusCritical, ImmutableDaoStateModel { + private boolean accepted; + + public Vote(boolean accepted) { + this.accepted = accepted; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + return protobuf.Vote.newBuilder() + .setAccepted(accepted) + .build(); + } + + public static Vote fromProto(protobuf.Vote proto) { + return new Vote(proto.getAccepted()); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/package-info.java b/core/src/main/java/bisq/core/dao/state/model/governance/package-info.java new file mode 100644 index 0000000000..171cf8dc03 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +/** + * Contains all governance specific data used in the daoState. + */ + +package bisq.core.dao.state.model.governance; diff --git a/core/src/main/java/bisq/core/dao/state/model/package-info.java b/core/src/main/java/bisq/core/dao/state/model/package-info.java new file mode 100644 index 0000000000..7d344b2a1c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/package-info.java @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +/** + * This package contains all immutable model classes used in the daoState. DaoState is persisted and it can be seen as + * the immutable ledger of the Bisq DAO. + * We want to make very clear which data are contained here that's we we break up grouping by domains and moved all + * model classes here. + */ + +package bisq.core.dao.state.model; diff --git a/core/src/main/java/bisq/core/dao/state/storage/DaoStateStorageService.java b/core/src/main/java/bisq/core/dao/state/storage/DaoStateStorageService.java new file mode 100644 index 0000000000..ece0166da8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/storage/DaoStateStorageService.java @@ -0,0 +1,135 @@ +/* + * 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 bisq.core.dao.state.storage; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.state.model.DaoState; + +import bisq.network.p2p.storage.persistence.ResourceDataStoreService; +import bisq.network.p2p.storage.persistence.StoreService; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; +import java.io.IOException; + +import java.util.LinkedList; + +import lombok.extern.slf4j.Slf4j; + +/** + * Manages persistence of the daoState. + */ +@Slf4j +public class DaoStateStorageService extends StoreService { + private static final String FILE_NAME = "DaoStateStore"; + + private final DaoState daoState; + private final DaoStateMonitoringService daoStateMonitoringService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoStateStorageService(ResourceDataStoreService resourceDataStoreService, + DaoState daoState, + DaoStateMonitoringService daoStateMonitoringService, + @Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + this.daoState = daoState; + this.daoStateMonitoringService = daoStateMonitoringService; + + resourceDataStoreService.addService(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + public void requestPersistence(DaoState daoState, LinkedList daoStateHashChain) { + store.setDaoState(daoState); + store.setDaoStateHashChain(daoStateHashChain); + persistenceManager.requestPersistence(); + } + + public DaoState getPersistedBsqState() { + return store.getDaoState(); + } + + public LinkedList getPersistedDaoStateHashChain() { + return store.getDaoStateHashChain(); + } + + public void resyncDaoStateFromGenesis(Runnable resultHandler) { + store.setDaoState(new DaoState()); + store.setDaoStateHashChain(new LinkedList<>()); + persistenceManager.persistNow(resultHandler); + } + + public void resyncDaoStateFromResources(File storageDir) throws IOException { + // We delete all DAO consensus payload data and remove the daoState so it will rebuild from latest + // resource files. + long currentTime = System.currentTimeMillis(); + String backupDirName = "out_of_sync_dao_data"; + String newFileName = "BlindVoteStore_" + currentTime; + FileUtil.removeAndBackupFile(storageDir, new File(storageDir, "BlindVoteStore"), newFileName, backupDirName); + + newFileName = "ProposalStore_" + currentTime; + FileUtil.removeAndBackupFile(storageDir, new File(storageDir, "ProposalStore"), newFileName, backupDirName); + + // We also need to remove ballot list as it contains the proposals as well. It will be recreated at resync + newFileName = "BallotList_" + currentTime; + FileUtil.removeAndBackupFile(storageDir, new File(storageDir, "BallotList"), newFileName, backupDirName); + + newFileName = "UnconfirmedBsqChangeOutputList_" + currentTime; + FileUtil.removeAndBackupFile(storageDir, new File(storageDir, "UnconfirmedBsqChangeOutputList"), newFileName, backupDirName); + + newFileName = "DaoStateStore_" + currentTime; + FileUtil.removeAndBackupFile(storageDir, new File(storageDir, "DaoStateStore"), newFileName, backupDirName); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected DaoStateStore createStore() { + return new DaoStateStore(DaoState.getClone(daoState), new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain())); + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/storage/DaoStateStore.java b/core/src/main/java/bisq/core/dao/state/storage/DaoStateStore.java new file mode 100644 index 0000000000..fe99e39724 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/storage/DaoStateStore.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.dao.state.storage; + +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.state.model.DaoState; + +import bisq.common.proto.persistable.PersistableEnvelope; + +import com.google.protobuf.Message; + +import java.util.LinkedList; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + +@Slf4j +public class DaoStateStore implements PersistableEnvelope { + // DaoState is always a clone and must not be used for read access beside initial read from disc when we apply + // the snapshot! + @Getter + @Setter + private DaoState daoState; + @Getter + @Setter + private LinkedList daoStateHashChain; + + DaoStateStore(DaoState daoState, LinkedList daoStateHashChain) { + this.daoState = daoState; + this.daoStateHashChain = daoStateHashChain; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public Message toProtoMessage() { + checkNotNull(daoState, "daoState must not be null when toProtoMessage is invoked"); + protobuf.DaoStateStore.Builder builder = protobuf.DaoStateStore.newBuilder() + .setDaoState(daoState.getBsqStateBuilder()) + .addAllDaoStateHash(daoStateHashChain.stream() + .map(DaoStateHash::toProtoMessage) + .collect(Collectors.toList())); + return protobuf.PersistableEnvelope.newBuilder() + .setDaoStateStore(builder) + .build(); + } + + public static DaoStateStore fromProto(protobuf.DaoStateStore proto) { + LinkedList daoStateHashList = proto.getDaoStateHashList().isEmpty() ? + new LinkedList<>() : + new LinkedList<>(proto.getDaoStateHashList().stream() + .map(DaoStateHash::fromProto) + .collect(Collectors.toList())); + return new DaoStateStore(DaoState.fromProto(proto.getDaoState()), daoStateHashList); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputList.java b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputList.java new file mode 100644 index 0000000000..2deae25697 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputList.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.dao.state.unconfirmed; + +import bisq.common.proto.persistable.PersistableList; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public class UnconfirmedBsqChangeOutputList extends PersistableList { + + UnconfirmedBsqChangeOutputList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private UnconfirmedBsqChangeOutputList(List list) { + super(list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setUnconfirmedBsqChangeOutputList(protobuf.UnconfirmedBsqChangeOutputList.newBuilder() + .addAllUnconfirmedTxOutput(getList().stream().map(UnconfirmedTxOutput::toProtoMessage).collect(Collectors.toList()))) + .build(); + } + + public static UnconfirmedBsqChangeOutputList fromProto(protobuf.UnconfirmedBsqChangeOutputList proto) { + return new UnconfirmedBsqChangeOutputList(new ArrayList<>(proto.getUnconfirmedTxOutputList().stream() + .map(UnconfirmedTxOutput::fromProto) + .collect(Collectors.toList()))); + } + + public boolean containsTxOutput(UnconfirmedTxOutput txOutput) { + return getList().stream().anyMatch(output -> output.getKey().equals(txOutput.getKey())); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java new file mode 100644 index 0000000000..4436c2bcb8 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java @@ -0,0 +1,209 @@ +/* + * 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 bisq.core.dao.state.unconfirmed; + +import bisq.core.dao.state.model.blockchain.TxType; + +import bisq.common.app.DevEnv; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.wallet.Wallet; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UnconfirmedBsqChangeOutputListService implements PersistedDataHost { + private final UnconfirmedBsqChangeOutputList unconfirmedBsqChangeOutputList = new UnconfirmedBsqChangeOutputList(); + private final PersistenceManager persistenceManager; + + @Inject + public UnconfirmedBsqChangeOutputListService(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(unconfirmedBsqChangeOutputList, PersistenceManager.Source.PRIVATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + if (DevEnv.isDaoActivated()) { + persistenceManager.readPersisted(persisted -> { + unconfirmedBsqChangeOutputList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Once a tx gets committed to our BSQ wallet we store the change output for allowing it to be spent in follow-up + * transactions. + */ + public void onCommitTx(Transaction tx, TxType txType, Wallet wallet) { + // We remove all potential connected outputs from our inputs as they would have been spent. + removeConnectedOutputsOfInputsOfTx(tx); + + int changeOutputIndex; + switch (txType) { + case UNDEFINED_TX_TYPE: + case UNVERIFIED: + case INVALID: + case GENESIS: + return; + case TRANSFER_BSQ: + changeOutputIndex = 1; // output 0 is receiver's address + break; + case PAY_TRADE_FEE: + changeOutputIndex = 0; + break; + case PROPOSAL: + changeOutputIndex = 0; + break; + case COMPENSATION_REQUEST: + case REIMBURSEMENT_REQUEST: + changeOutputIndex = 0; + break; + case BLIND_VOTE: + changeOutputIndex = 1; // output 0 is stake + break; + case VOTE_REVEAL: + changeOutputIndex = 0; + break; + case LOCKUP: + changeOutputIndex = 1; // output 0 is lockup amount + break; + case UNLOCK: + // We don't allow to spend the unlocking funds as there is the lock time which need to pass, + // otherwise the funds get burned! + return; + case ASSET_LISTING_FEE: + changeOutputIndex = 0; + break; + case PROOF_OF_BURN: + changeOutputIndex = 0; + break; + case IRREGULAR: + return; + default: + return; + } + + // It can be that we don't have a BSQ and a BTC change output. + // If no BSQ change but a BTC change the index points to the BTC output and then + // we detect that it is not part of our wallet. + // If there is a BSQ change but no BTC change it has no effect as we ignore BTC outputs anyway. + // If both change outputs do not exist then we might point to an index outside + // of the list and we return at our scope check. + + // If no BTC output (unlikely but + // possible) the index points to the BTC output and then we detect that it is not part of our wallet. + // + List outputs = tx.getOutputs(); + if (changeOutputIndex > outputs.size() - 1) + return; + + TransactionOutput change = outputs.get(changeOutputIndex); + if (!change.isMine(wallet)) + return; + + UnconfirmedTxOutput txOutput = UnconfirmedTxOutput.fromTransactionOutput(change); + if (unconfirmedBsqChangeOutputList.containsTxOutput(txOutput)) + return; + + unconfirmedBsqChangeOutputList.add(txOutput); + requestPersistence(); + } + + public void onReorganize() { + reset(); + } + + public void onSpvResync() { + reset(); + } + + public void onTransactionConfidenceChanged(Transaction tx) { + if (tx != null && + tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + removeConnectedOutputsOfInputsOfTx(tx); + + tx.getOutputs().forEach(transactionOutput -> { + UnconfirmedTxOutput txOutput = UnconfirmedTxOutput.fromTransactionOutput(transactionOutput); + if (unconfirmedBsqChangeOutputList.containsTxOutput(txOutput)) { + unconfirmedBsqChangeOutputList.remove(txOutput); + } + }); + } + } + + public boolean hasTransactionOutput(TransactionOutput output) { + return unconfirmedBsqChangeOutputList.containsTxOutput(UnconfirmedTxOutput.fromTransactionOutput(output)); + } + + public Coin getBalance() { + return Coin.valueOf(unconfirmedBsqChangeOutputList.stream().mapToLong(UnconfirmedTxOutput::getValue).sum()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void removeConnectedOutputsOfInputsOfTx(Transaction tx) { + tx.getInputs().stream() + .map(TransactionInput::getConnectedOutput) + .filter(Objects::nonNull) + .map(UnconfirmedTxOutput::fromTransactionOutput) + .filter(unconfirmedBsqChangeOutputList::containsTxOutput) + .forEach(txOutput -> { + unconfirmedBsqChangeOutputList.remove(txOutput); + requestPersistence(); + }); + } + + private void reset() { + unconfirmedBsqChangeOutputList.clear(); + requestPersistence(); + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedTxOutput.java b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedTxOutput.java new file mode 100644 index 0000000000..48f7550dc9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedTxOutput.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.dao.state.unconfirmed; + +import bisq.core.dao.state.model.ImmutableDaoStateModel; +import bisq.core.dao.state.model.blockchain.TxOutputKey; + +import bisq.common.proto.persistable.PersistablePayload; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * Used for tracking unconfirmed change outputs to allow them to be spent in follow up + * transactions in txType permits it. We can assume that the user is not intending to + * double spend own transactions as well that he does not try to spend an invalid BSQ + * output to a BSQ address. + * We do not allow spending unconfirmed BSQ outputs received from elsewhere. + */ +@Slf4j +@Immutable +@Data +public final class UnconfirmedTxOutput implements PersistablePayload, ImmutableDaoStateModel { + + public static UnconfirmedTxOutput fromTransactionOutput(TransactionOutput transactionOutput) { + Transaction parentTransaction = transactionOutput.getParentTransaction(); + if (parentTransaction != null) { + return new UnconfirmedTxOutput(transactionOutput.getIndex(), + transactionOutput.getValue().value, + parentTransaction.getTxId().toString()); + } else { + log.warn("parentTransaction of transactionOutput is null. " + + "This must not happen. " + + "We could throw an exception as well " + + "here but we prefer to be for now more tolerant and just " + + "assign the value 0 if that would be the case. transactionOutput={}", + transactionOutput); + return new UnconfirmedTxOutput(transactionOutput.getIndex(), + 0, + "null"); + } + } + + protected final int index; + protected final long value; + protected final String txId; + + private UnconfirmedTxOutput(int index, + long value, + String txId) { + this.index = index; + this.value = value; + this.txId = txId; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public protobuf.UnconfirmedTxOutput toProtoMessage() { + return protobuf.UnconfirmedTxOutput.newBuilder() + .setIndex(index) + .setValue(value) + .setTxId(txId).build(); + } + + public static UnconfirmedTxOutput fromProto(protobuf.UnconfirmedTxOutput proto) { + return new UnconfirmedTxOutput(proto.getIndex(), + proto.getValue(), + proto.getTxId()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxOutputKey getKey() { + return new TxOutputKey(txId, index); + } + + @Override + public String toString() { + return "UnconfirmedTxOutput{" + + "\n index=" + index + + ",\n value=" + value + + ",\n txId='" + txId + '\'' + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/exceptions/TradePriceOutOfToleranceException.java b/core/src/main/java/bisq/core/exceptions/TradePriceOutOfToleranceException.java new file mode 100644 index 0000000000..302267b97b --- /dev/null +++ b/core/src/main/java/bisq/core/exceptions/TradePriceOutOfToleranceException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.exceptions; + +public class TradePriceOutOfToleranceException extends Exception { + public TradePriceOutOfToleranceException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java new file mode 100644 index 0000000000..be43c3f746 --- /dev/null +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -0,0 +1,414 @@ +/* + * 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 bisq.core.filter; + +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.crypto.Sig; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import com.google.common.annotations.VisibleForTesting; + +import java.security.PublicKey; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Value +public final class Filter implements ProtectedStoragePayload, ExpirablePayload { + public static final long TTL = TimeUnit.DAYS.toMillis(180); + + private final List bannedOfferIds; + private final List nodeAddressesBannedFromTrading; + private final List bannedAutoConfExplorers; + private final List bannedPaymentAccounts; + private final List bannedCurrencies; + private final List bannedPaymentMethods; + private final List arbitrators; + private final List seedNodes; + private final List priceRelayNodes; + private final boolean preventPublicBtcNetwork; + private final List btcNodes; + // SignatureAsBase64 is not set initially as we use the serialized data for signing. We set it after signature is + // created by cloning the object with a non-null sig. + @Nullable + private final String signatureAsBase64; + // The pub EC key from the dev who has signed and published the filter (different to ownerPubKeyBytes) + private final String signerPubKeyAsHex; + + // The pub key used for the data protection in the p2p storage + private final byte[] ownerPubKeyBytes; + private final boolean disableDao; + private final String disableDaoBelowVersion; + private final String disableTradeBelowVersion; + private final List mediators; + private final List refundAgents; + + private final List bannedAccountWitnessSignerPubKeys; + + private final List btcFeeReceiverAddresses; + + private final long creationDate; + + private final List bannedPrivilegedDevPubKeys; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + private Map extraDataMap; + + private transient PublicKey ownerPubKey; + + // added at v1.3.8 + private final boolean disableAutoConf; + + // added at v1.5.5 + private final Set nodeAddressesBannedFromNetwork; + private final boolean disableApi; + + // added at v1.6.0 + private final boolean disableMempoolValidation; + + // After we have created the signature from the filter data we clone it and apply the signature + static Filter cloneWithSig(Filter filter, String signatureAsBase64) { + return new Filter(filter.getBannedOfferIds(), + filter.getNodeAddressesBannedFromTrading(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedAccountWitnessSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + signatureAsBase64, + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys(), + filter.isDisableAutoConf(), + filter.getBannedAutoConfExplorers(), + filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableMempoolValidation(), + filter.isDisableApi()); + } + + // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again + static Filter cloneWithoutSig(Filter filter) { + return new Filter(filter.getBannedOfferIds(), + filter.getNodeAddressesBannedFromTrading(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedAccountWitnessSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + null, + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys(), + filter.isDisableAutoConf(), + filter.getBannedAutoConfExplorers(), + filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableMempoolValidation(), + filter.isDisableApi()); + } + + public Filter(List bannedOfferIds, + List nodeAddressesBannedFromTrading, + List bannedPaymentAccounts, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, + boolean preventPublicBtcNetwork, + List btcNodes, + boolean disableDao, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedAccountWitnessSignerPubKeys, + List btcFeeReceiverAddresses, + PublicKey ownerPubKey, + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys, + boolean disableAutoConf, + List bannedAutoConfExplorers, + Set nodeAddressesBannedFromNetwork, + boolean disableMempoolValidation, + boolean disableApi) { + this(bannedOfferIds, + nodeAddressesBannedFromTrading, + bannedPaymentAccounts, + bannedCurrencies, + bannedPaymentMethods, + arbitrators, + seedNodes, + priceRelayNodes, + preventPublicBtcNetwork, + btcNodes, + disableDao, + disableDaoBelowVersion, + disableTradeBelowVersion, + mediators, + refundAgents, + bannedAccountWitnessSignerPubKeys, + btcFeeReceiverAddresses, + Sig.getPublicKeyBytes(ownerPubKey), + System.currentTimeMillis(), + null, + null, + signerPubKeyAsHex, + bannedPrivilegedDevPubKeys, + disableAutoConf, + bannedAutoConfExplorers, + nodeAddressesBannedFromNetwork, + disableMempoolValidation, + disableApi); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + public Filter(List bannedOfferIds, + List nodeAddressesBannedFromTrading, + List bannedPaymentAccounts, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, + boolean preventPublicBtcNetwork, + List btcNodes, + boolean disableDao, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedAccountWitnessSignerPubKeys, + List btcFeeReceiverAddresses, + byte[] ownerPubKeyBytes, + long creationDate, + @Nullable Map extraDataMap, + @Nullable String signatureAsBase64, + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys, + boolean disableAutoConf, + List bannedAutoConfExplorers, + Set nodeAddressesBannedFromNetwork, + boolean disableMempoolValidation, + boolean disableApi) { + this.bannedOfferIds = bannedOfferIds; + this.nodeAddressesBannedFromTrading = nodeAddressesBannedFromTrading; + this.bannedPaymentAccounts = bannedPaymentAccounts; + this.bannedCurrencies = bannedCurrencies; + this.bannedPaymentMethods = bannedPaymentMethods; + this.arbitrators = arbitrators; + this.seedNodes = seedNodes; + this.priceRelayNodes = priceRelayNodes; + this.preventPublicBtcNetwork = preventPublicBtcNetwork; + this.btcNodes = btcNodes; + this.disableDao = disableDao; + this.disableDaoBelowVersion = disableDaoBelowVersion; + this.disableTradeBelowVersion = disableTradeBelowVersion; + this.mediators = mediators; + this.refundAgents = refundAgents; + this.bannedAccountWitnessSignerPubKeys = bannedAccountWitnessSignerPubKeys; + this.btcFeeReceiverAddresses = btcFeeReceiverAddresses; + this.ownerPubKeyBytes = ownerPubKeyBytes; + this.creationDate = creationDate; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + this.signatureAsBase64 = signatureAsBase64; + this.signerPubKeyAsHex = signerPubKeyAsHex; + this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; + this.disableAutoConf = disableAutoConf; + this.bannedAutoConfExplorers = bannedAutoConfExplorers; + this.nodeAddressesBannedFromNetwork = nodeAddressesBannedFromNetwork; + this.disableMempoolValidation = disableMempoolValidation; + this.disableApi = disableApi; + + // ownerPubKeyBytes can be null when called from tests + if (ownerPubKeyBytes != null) { + ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + } else { + ownerPubKey = null; + } + } + + @Override + public protobuf.StoragePayload toProtoMessage() { + List paymentAccountFilterList = bannedPaymentAccounts.stream() + .map(PaymentAccountFilter::toProtoMessage) + .collect(Collectors.toList()); + + protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds) + .addAllNodeAddressesBannedFromTrading(nodeAddressesBannedFromTrading) + .addAllBannedPaymentAccounts(paymentAccountFilterList) + .addAllBannedCurrencies(bannedCurrencies) + .addAllBannedPaymentMethods(bannedPaymentMethods) + .addAllArbitrators(arbitrators) + .addAllSeedNodes(seedNodes) + .addAllPriceRelayNodes(priceRelayNodes) + .setPreventPublicBtcNetwork(preventPublicBtcNetwork) + .addAllBtcNodes(btcNodes) + .setDisableDao(disableDao) + .setDisableDaoBelowVersion(disableDaoBelowVersion) + .setDisableTradeBelowVersion(disableTradeBelowVersion) + .addAllMediators(mediators) + .addAllRefundAgents(refundAgents) + .addAllBannedSignerPubKeys(bannedAccountWitnessSignerPubKeys) + .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) + .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .setSignerPubKeyAsHex(signerPubKeyAsHex) + .setCreationDate(creationDate) + .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys) + .setDisableAutoConf(disableAutoConf) + .addAllBannedAutoConfExplorers(bannedAutoConfExplorers) + .addAllNodeAddressesBannedFromNetwork(nodeAddressesBannedFromNetwork) + .setDisableMempoolValidation(disableMempoolValidation) + .setDisableApi(disableApi); + + Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + + return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); + } + + public static Filter fromProto(protobuf.Filter proto) { + List bannedPaymentAccountsList = proto.getBannedPaymentAccountsList().stream() + .map(PaymentAccountFilter::fromProto) + .collect(Collectors.toList()); + + + return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()), + ProtoUtil.protocolStringListToList(proto.getNodeAddressesBannedFromTradingList()), + bannedPaymentAccountsList, + ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()), + ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()), + ProtoUtil.protocolStringListToList(proto.getArbitratorsList()), + ProtoUtil.protocolStringListToList(proto.getSeedNodesList()), + ProtoUtil.protocolStringListToList(proto.getPriceRelayNodesList()), + proto.getPreventPublicBtcNetwork(), + ProtoUtil.protocolStringListToList(proto.getBtcNodesList()), + proto.getDisableDao(), + proto.getDisableDaoBelowVersion(), + proto.getDisableTradeBelowVersion(), + ProtoUtil.protocolStringListToList(proto.getMediatorsList()), + ProtoUtil.protocolStringListToList(proto.getRefundAgentsList()), + ProtoUtil.protocolStringListToList(proto.getBannedSignerPubKeysList()), + ProtoUtil.protocolStringListToList(proto.getBtcFeeReceiverAddressesList()), + proto.getOwnerPubKeyBytes().toByteArray(), + proto.getCreationDate(), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), + proto.getSignatureAsBase64(), + proto.getSignerPubKeyAsHex(), + ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()), + proto.getDisableAutoConf(), + ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()), + ProtoUtil.protocolStringListToSet(proto.getNodeAddressesBannedFromNetworkList()), + proto.getDisableMempoolValidation(), + proto.getDisableApi() + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + @Override + public String toString() { + return "Filter{" + + "\n bannedOfferIds=" + bannedOfferIds + + ",\n nodeAddressesBannedFromTrading=" + nodeAddressesBannedFromTrading + + ",\n bannedAutoConfExplorers=" + bannedAutoConfExplorers + + ",\n bannedPaymentAccounts=" + bannedPaymentAccounts + + ",\n bannedCurrencies=" + bannedCurrencies + + ",\n bannedPaymentMethods=" + bannedPaymentMethods + + ",\n arbitrators=" + arbitrators + + ",\n seedNodes=" + seedNodes + + ",\n priceRelayNodes=" + priceRelayNodes + + ",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork + + ",\n btcNodes=" + btcNodes + + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' + + ",\n signerPubKeyAsHex='" + signerPubKeyAsHex + '\'' + + ",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) + + ",\n disableDao=" + disableDao + + ",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' + + ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + + ",\n mediators=" + mediators + + ",\n refundAgents=" + refundAgents + + ",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys + + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + + ",\n creationDate=" + creationDate + + ",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys + + ",\n extraDataMap=" + extraDataMap + + ",\n ownerPubKey=" + ownerPubKey + + ",\n disableAutoConf=" + disableAutoConf + + ",\n nodeAddressesBannedFromNetwork=" + nodeAddressesBannedFromNetwork + + ",\n disableMempoolValidation=" + disableMempoolValidation + + ",\n disableApi=" + disableApi + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java new file mode 100644 index 0000000000..163ddbfee3 --- /dev/null +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -0,0 +1,661 @@ +/* + * 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 bisq.core.filter; + +import bisq.core.btc.nodes.BtcNodes; +import bisq.core.locale.Res; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.ProvidersRepository; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.P2PServiceListener; +import bisq.network.p2p.network.NetworkFilter; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.app.DevEnv; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.config.ConfigFileEditor; +import bisq.common.crypto.KeyRing; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Sha256Hash; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import org.bouncycastle.util.encoders.Base64; + +import java.security.PublicKey; + +import java.nio.charset.StandardCharsets; + +import java.math.BigInteger; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import java.lang.reflect.Method; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.Utils.HEX; + +/** + * We only support one active filter, if we receive multiple we use the one with the more recent creationDate. + */ +@Slf4j +public class FilterManager { + private static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; + private static final String BANNED_SEED_NODES = "bannedSeedNodes"; + private static final String BANNED_BTC_NODES = "bannedBtcNodes"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onFilterAdded(Filter filter); + } + + private final P2PService p2PService; + private final KeyRing keyRing; + private final User user; + private final Preferences preferences; + private final ConfigFileEditor configFileEditor; + private final ProvidersRepository providersRepository; + private final boolean ignoreDevMsg; + private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + private final List publicKeys; + private ECKey filterSigningKey; + private final Set invalidFilters = new HashSet<>(); + private Consumer filterWarningHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public FilterManager(P2PService p2PService, + KeyRing keyRing, + User user, + Preferences preferences, + Config config, + ProvidersRepository providersRepository, + NetworkFilter networkFilter, + @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + this.p2PService = p2PService; + this.keyRing = keyRing; + this.user = user; + this.preferences = preferences; + this.configFileEditor = new ConfigFileEditor(config.configFile); + this.providersRepository = providersRepository; + this.ignoreDevMsg = ignoreDevMsg; + + publicKeys = useDevPrivilegeKeys ? + Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : + List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", + "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", + "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); + + networkFilter.setBannedNodeFunction(this::isNodeAddressBannedFromNetwork); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + if (ignoreDevMsg) { + return; + } + + p2PService.getP2PDataStorage().getMap().values().stream() + .map(ProtectedStorageEntry::getProtectedStoragePayload) + .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) + .map(protectedStoragePayload -> (Filter) protectedStoragePayload) + .forEach(this::onFilterAddedFromNetwork); + + // On mainNet we expect to have received a filter object, if not show a popup to the user to inform the + // Bisq devs. + if (Config.baseCurrencyNetwork().isMainnet() && getFilter() == null) { + filterWarningHandler.accept(Res.get("popup.warning.noFilter")); + } + + p2PService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + onFilterAddedFromNetwork(filter); + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + onFilterRemovedFromNetwork(filter); + }); + } + }); + + p2PService.addP2PServiceListener(new P2PServiceListener() { + @Override + public void onDataReceived() { + } + + @Override + public void onNoSeedNodeAvailable() { + } + + @Override + public void onNoPeersAvailable() { + } + + @Override + public void onUpdatedDataReceived() { + // We should have received all data at that point and if the filters were not set we + // clean up the persisted banned nodes in the options file as it might be that we missed the filter + // remove message if we have not been online. + if (filterProperty.get() == null) { + clearBannedNodes(); + } + } + + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } + }); + } + + public void setFilterWarningHandler(Consumer filterWarningHandler) { + this.filterWarningHandler = filterWarningHandler; + + addListener(filter -> { + if (filter != null && filterWarningHandler != null) { + if (filter.getSeedNodes() != null && !filter.getSeedNodes().isEmpty()) { + log.info("One of the seed nodes got banned. {}", filter.getSeedNodes()); + // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. + // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed"))); + } + + if (filter.getPriceRelayNodes() != null && !filter.getPriceRelayNodes().isEmpty()) { + log.info("One of the price relay nodes got banned. {}", filter.getPriceRelayNodes()); + // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. + // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay"))); + } + + if (requireUpdateToNewVersionForTrading()) { + filterWarningHandler.accept(Res.get("popup.warning.mandatoryUpdate.trading")); + } + + if (requireUpdateToNewVersionForDAO()) { + filterWarningHandler.accept(Res.get("popup.warning.mandatoryUpdate.dao")); + } + if (filter.isDisableDao()) { + filterWarningHandler.accept(Res.get("popup.warning.disable.dao")); + } + } + }); + } + + public boolean isPrivilegedDevPubKeyBanned(String pubKeyAsHex) { + Filter filter = getFilter(); + if (filter == null) { + return false; + } + + return filter.getBannedPrivilegedDevPubKeys().contains(pubKeyAsHex); + } + + public boolean canAddDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { + return false; + } + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("Key in invalid"); + return false; + } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } + return true; + } + + public String getSignerPubKeyAsHex(String privKeyString) { + ECKey ecKey = toECKey(privKeyString); + return getPubKeyAsHex(ecKey); + } + + public void addDevFilter(Filter filterWithoutSig, String privKeyString) { + setFilterSigningKey(privKeyString); + String signatureAsBase64 = getSignature(filterWithoutSig); + Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); + user.setDevelopersFilter(filterWithSig); + + p2PService.addProtectedStorageEntry(filterWithSig); + + // Cleanup potential old filters created in the past with same priv key + invalidFilters.forEach(filter -> { + removeInvalidFilters(filter, privKeyString); + }); + } + + public void addToInvalidFilters(Filter filter) { + invalidFilters.add(filter); + } + + public void removeInvalidFilters(Filter filter, String privKeyString) { + log.info("Remove invalid filter {}", filter); + setFilterSigningKey(privKeyString); + String signatureAsBase64 = getSignature(Filter.cloneWithoutSig(filter)); + Filter filterWithSig = Filter.cloneWithSig(filter, signatureAsBase64); + boolean result = p2PService.removeData(filterWithSig); + if (!result) { + log.warn("Could not remove filter {}", filter); + } + } + + public boolean canRemoveDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { + return false; + } + + Filter developersFilter = getDevFilter(); + if (developersFilter == null) { + log.warn("There is no persisted dev filter to be removed."); + return false; + } + + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("Key in invalid."); + return false; + } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (!developersFilter.getSignerPubKeyAsHex().equals(pubKeyAsHex)) { + log.warn("pubKeyAsHex derived from private key does not match filterSignerPubKey. " + + "filterSignerPubKey={}, pubKeyAsHex derived from private key={}", + developersFilter.getSignerPubKeyAsHex(), pubKeyAsHex); + return false; + } + + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } + + return true; + } + + public void removeDevFilter(String privKeyString) { + setFilterSigningKey(privKeyString); + Filter filterWithSig = user.getDevelopersFilter(); + if (filterWithSig == null) { + // Should not happen as UI button is deactivated in that case + return; + } + + if (p2PService.removeData(filterWithSig)) { + user.setDevelopersFilter(null); + } else { + log.warn("Removing dev filter from network failed"); + } + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public ObjectProperty filterProperty() { + return filterProperty; + } + + @Nullable + public Filter getFilter() { + return filterProperty.get(); + } + + @Nullable + public Filter getDevFilter() { + return user.getDevelopersFilter(); + } + + public PublicKey getOwnerPubKey() { + return keyRing.getSignatureKeyPair().getPublic(); + } + + public boolean isCurrencyBanned(String currencyCode) { + return getFilter() != null && + getFilter().getBannedCurrencies() != null && + getFilter().getBannedCurrencies().stream() + .anyMatch(e -> e.equals(currencyCode)); + } + + public boolean isPaymentMethodBanned(PaymentMethod paymentMethod) { + return getFilter() != null && + getFilter().getBannedPaymentMethods() != null && + getFilter().getBannedPaymentMethods().stream() + .anyMatch(e -> e.equals(paymentMethod.getId())); + } + + public boolean isOfferIdBanned(String offerId) { + return getFilter() != null && + getFilter().getBannedOfferIds().stream() + .anyMatch(e -> e.equals(offerId)); + } + + public boolean isNodeAddressBanned(NodeAddress nodeAddress) { + return getFilter() != null && + getFilter().getNodeAddressesBannedFromTrading().stream() + .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); + } + + public boolean isNodeAddressBannedFromNetwork(NodeAddress nodeAddress) { + return getFilter() != null && + getFilter().getNodeAddressesBannedFromNetwork().stream() + .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); + } + + public boolean isAutoConfExplorerBanned(String address) { + return getFilter() != null && + getFilter().getBannedAutoConfExplorers().stream() + .anyMatch(e -> e.equals(address)); + } + + public boolean requireUpdateToNewVersionForTrading() { + if (getFilter() == null) { + return false; + } + + boolean requireUpdateToNewVersion = false; + String getDisableTradeBelowVersion = getFilter().getDisableTradeBelowVersion(); + if (getDisableTradeBelowVersion != null && !getDisableTradeBelowVersion.isEmpty()) { + requireUpdateToNewVersion = Version.isNewVersion(getDisableTradeBelowVersion); + } + + return requireUpdateToNewVersion; + } + + public boolean requireUpdateToNewVersionForDAO() { + if (getFilter() == null) { + return false; + } + + boolean requireUpdateToNewVersion = false; + String disableDaoBelowVersion = getFilter().getDisableDaoBelowVersion(); + if (disableDaoBelowVersion != null && !disableDaoBelowVersion.isEmpty()) { + requireUpdateToNewVersion = Version.isNewVersion(disableDaoBelowVersion); + } + + return requireUpdateToNewVersion; + } + + public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload) { + return getFilter() != null && + getFilter().getBannedPaymentAccounts().stream() + .filter(paymentAccountFilter -> paymentAccountFilter.getPaymentMethodId().equals( + paymentAccountPayload.getPaymentMethodId())) + .anyMatch(paymentAccountFilter -> { + try { + Method method = paymentAccountPayload.getClass().getMethod(paymentAccountFilter.getGetMethodName()); + // We invoke getter methods (no args), e.g. getHolderName + String valueFromInvoke = (String) method.invoke(paymentAccountPayload); + return valueFromInvoke.equalsIgnoreCase(paymentAccountFilter.getValue()); + } catch (Throwable e) { + log.error(e.getMessage()); + return false; + } + }); + } + + public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) { + return getFilter() != null && + getFilter().getBannedAccountWitnessSignerPubKeys() != null && + getFilter().getBannedAccountWitnessSignerPubKeys().stream() + .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onFilterAddedFromNetwork(Filter newFilter) { + Filter currentFilter = getFilter(); + + if (!isFilterPublicKeyInList(newFilter)) { + if (newFilter.getSignerPubKeyAsHex() != null && !newFilter.getSignerPubKeyAsHex().isEmpty()) { + log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex()); + } else { + log.info("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex not set (expected case for pre v1.3.9 filter)"); + } + return; + } + if (!isSignatureValid(newFilter)) { + log.warn("verifySignature failed. Filter={}", newFilter); + return; + } + + if (currentFilter != null) { + if (currentFilter.getCreationDate() > newFilter.getCreationDate()) { + log.warn("We received a new filter from the network but the creation date is older than the " + + "filter we have already. We ignore the new filter."); + + addToInvalidFilters(newFilter); + return; + } else { + log.warn("We received a new filter from the network and the creation date is newer than the " + + "filter we have already. We ignore the old filter."); + addToInvalidFilters(currentFilter); + } + + if (isPrivilegedDevPubKeyBanned(newFilter.getSignerPubKeyAsHex())) { + log.warn("Pub key of filter is banned. currentFilter={}, newFilter={}", currentFilter, newFilter); + return; + } + } + + // Our new filter is newer so we apply it. + // We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key + // for deploying filters and this is only in place to avoid unintended situations of multiple filters + // from multiple devs or if same dev publishes new filter from different app without the persisted devFilter. + filterProperty.set(newFilter); + + // Seed nodes are requested at startup before we get the filter so we only apply the banned + // nodes at the next startup and don't update the list in the P2P network domain. + // We persist it to the property file which is read before any other initialisation. + saveBannedNodes(BANNED_SEED_NODES, newFilter.getSeedNodes()); + saveBannedNodes(BANNED_BTC_NODES, newFilter.getBtcNodes()); + + // Banned price relay nodes we can apply at runtime + List priceRelayNodes = newFilter.getPriceRelayNodes(); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); + + //TODO should be moved to client with listening on onFilterAdded + providersRepository.applyBannedNodes(priceRelayNodes); + + //TODO should be moved to client with listening on onFilterAdded + if (newFilter.isPreventPublicBtcNetwork() && + preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) { + preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); + } + + listeners.forEach(e -> e.onFilterAdded(newFilter)); + } + + private void onFilterRemovedFromNetwork(Filter filter) { + if (!isFilterPublicKeyInList(filter)) { + log.warn("isFilterPublicKeyInList failed. Filter={}", filter); + return; + } + if (!isSignatureValid(filter)) { + log.warn("verifySignature failed. Filter={}", filter); + return; + } + + // We don't check for banned filter as we want to remove a banned filter anyway. + + if (!filterProperty.get().equals(filter)) { + return; + } + + clearBannedNodes(); + + if (filter.equals(user.getDevelopersFilter())) { + user.setDevelopersFilter(null); + } + filterProperty.set(null); + } + + // Clears options files from banned nodes + private void clearBannedNodes() { + saveBannedNodes(BANNED_BTC_NODES, null); + saveBannedNodes(BANNED_SEED_NODES, null); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); + + if (providersRepository.getBannedNodes() != null) { + providersRepository.applyBannedNodes(null); + } + } + + private void saveBannedNodes(String optionName, List bannedNodes) { + if (bannedNodes != null) + configFileEditor.setOption(optionName, String.join(",", bannedNodes)); + else + configFileEditor.clearOption(optionName); + } + + private boolean isValidDevPrivilegeKey(String privKeyString) { + try { + ECKey filterSigningKey = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(filterSigningKey); + return isPublicKeyInList(pubKeyAsHex); + } catch (Throwable t) { + return false; + } + } + + private void setFilterSigningKey(String privKeyString) { + this.filterSigningKey = toECKey(privKeyString); + } + + private String getSignature(Filter filterWithoutSig) { + Sha256Hash hash = getSha256Hash(filterWithoutSig); + ECKey.ECDSASignature ecdsaSignature = filterSigningKey.sign(hash); + byte[] encodeToDER = ecdsaSignature.encodeToDER(); + return new String(Base64.encode(encodeToDER), StandardCharsets.UTF_8); + } + + private boolean isFilterPublicKeyInList(Filter filter) { + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + if (!isPublicKeyInList(signerPubKeyAsHex)) { + log.info("Invalid filter (expected case for pre v1.3.9 filter as we still keep that in the network " + + "but the new version does not recognize it as valid filter): " + + "signerPubKeyAsHex from filter is not part of our pub key list. " + + "signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}", + signerPubKeyAsHex, publicKeys, new Date(filter.getCreationDate())); + return false; + } + return true; + } + + private boolean isPublicKeyInList(String pubKeyAsHex) { + boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); + if (!isPublicKeyInList) { + log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); + } + return isPublicKeyInList; + } + + private boolean isSignatureValid(Filter filter) { + try { + Filter filterForSigVerification = Filter.cloneWithoutSig(filter); + Sha256Hash hash = getSha256Hash(filterForSigVerification); + + checkNotNull(filter.getSignatureAsBase64(), "filter.getSignatureAsBase64() must not be null"); + byte[] sigData = Base64.decode(filter.getSignatureAsBase64()); + ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(sigData); + + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + byte[] decode = HEX.decode(signerPubKeyAsHex); + ECKey ecPubKey = ECKey.fromPublicOnly(decode); + return ecPubKey.verify(hash, ecdsaSignature); + } catch (Throwable e) { + log.warn("verifySignature failed. filter={}", filter); + return false; + } + } + + private ECKey toECKey(String privKeyString) { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + } + + private Sha256Hash getSha256Hash(Filter filter) { + byte[] filterData = filter.toProtoMessage().toByteArray(); + return Sha256Hash.of(filterData); + } + + private String getPubKeyAsHex(ECKey ecKey) { + return HEX.encode(ecKey.getPubKey()); + } +} diff --git a/core/src/main/java/bisq/core/filter/FilterModule.java b/core/src/main/java/bisq/core/filter/FilterModule.java new file mode 100644 index 0000000000..d676ef65a8 --- /dev/null +++ b/core/src/main/java/bisq/core/filter/FilterModule.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.core.filter; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +import static bisq.common.config.Config.IGNORE_DEV_MSG; +import static com.google.inject.name.Names.named; + +public class FilterModule extends AppModule { + + public FilterModule(Config config) { + super(config); + } + + @Override + protected final void configure() { + bind(FilterManager.class).in(Singleton.class); + bindConstant().annotatedWith(named(IGNORE_DEV_MSG)).to(config.ignoreDevMsg); + } +} diff --git a/core/src/main/java/bisq/core/filter/PaymentAccountFilter.java b/core/src/main/java/bisq/core/filter/PaymentAccountFilter.java new file mode 100644 index 0000000000..0e8dca0f7f --- /dev/null +++ b/core/src/main/java/bisq/core/filter/PaymentAccountFilter.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.filter; + +import bisq.common.proto.network.NetworkPayload; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Value +@Slf4j +public class PaymentAccountFilter implements NetworkPayload { + private final String paymentMethodId; + private final String getMethodName; + private final String value; + + public PaymentAccountFilter(String paymentMethodId, String getMethodName, String value) { + this.paymentMethodId = paymentMethodId; + this.getMethodName = getMethodName; + this.value = value; + } + + @Override + public protobuf.PaymentAccountFilter toProtoMessage() { + return protobuf.PaymentAccountFilter.newBuilder() + .setPaymentMethodId(paymentMethodId) + .setGetMethodName(getMethodName) + .setValue(value) + .build(); + } + + public static PaymentAccountFilter fromProto(protobuf.PaymentAccountFilter proto) { + return new PaymentAccountFilter(proto.getPaymentMethodId(), + proto.getGetMethodName(), + proto.getValue()); + } +} diff --git a/core/src/main/java/bisq/core/locale/BankUtil.java b/core/src/main/java/bisq/core/locale/BankUtil.java new file mode 100644 index 0000000000..eb401b7328 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/BankUtil.java @@ -0,0 +1,298 @@ +/* + * 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 bisq.core.locale; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BankUtil { + + private static final Logger log = LoggerFactory.getLogger(BankUtil.class); + + // BankName + @SuppressWarnings("SameReturnValue") + public static boolean isBankNameRequired(@SuppressWarnings("unused") String countryCode) { + // Currently we always return true but let's keep that method to be more flexible in case we want to not show + // it at some new payment method. + return true; + /* + switch (countryCode) { + // We show always the bank name as it is needed in specific banks. + // Though that handling should be optimized in futures. + case "GB": + case "US": + case "NZ": + case "AU": + case "CA": + case "SE": + case "HK": + return false; + case "MX": + case "BR": + case "AR": + return true; + default: + return true; + }*/ + } + + public static String getBankNameLabel(String countryCode) { + switch (countryCode) { + case "BR": + return Res.get("payment.bank.name"); + default: + return isBankNameRequired(countryCode) ? Res.get("payment.bank.name") : Res.get("payment.bank.nameOptional"); + } + } + + // BankId + public static boolean isBankIdRequired(String countryCode) { + switch (countryCode) { + case "GB": + case "US": + case "BR": + case "NZ": + case "AU": + case "SE": + case "CL": + case "NO": + case "AR": + return false; + case "CA": + case "MX": + case "HK": + return true; + default: + return true; + } + } + + public static String getBankIdLabel(String countryCode) { + switch (countryCode) { + case "CA": + return "Institution Number";// do not translate as it is used in English only + case "MX": + case "HK": + return Res.get("payment.bankCode"); + default: + return isBankIdRequired(countryCode) ? Res.get("payment.bankId") : Res.get("payment.bankIdOptional"); + } + + } + + // BranchId + public static boolean isBranchIdRequired(String countryCode) { + switch (countryCode) { + case "GB": + case "US": + case "BR": + case "AU": + case "CA": + return true; + case "NZ": + case "MX": + case "HK": + case "SE": + case "NO": + return false; + default: + return true; + } + } + + public static String getBranchIdLabel(String countryCode) { + switch (countryCode) { + case "GB": + return "UK sort code"; // do not translate as it is used in English only + case "US": + return "Routing Number"; // do not translate as it is used in English only + case "BR": + return "Código da Agência"; // do not translate as it is used in Portuguese only + case "AU": + return "BSB code"; // do not translate as it is used in English only + case "CA": + return "Transit Number"; // do not translate as it is used in English only + default: + return isBranchIdRequired(countryCode) ? Res.get("payment.branchNr") : Res.get("payment.branchNrOptional"); + } + } + + + // AccountNr + @SuppressWarnings("SameReturnValue") + public static boolean isAccountNrRequired(String countryCode) { + switch (countryCode) { + default: + return true; + } + } + + public static String getAccountNrLabel(String countryCode) { + switch (countryCode) { + case "GB": + case "US": + case "BR": + case "NZ": + case "AU": + case "CA": + case "HK": + return Res.get("payment.accountNr"); + case "NO": + return "Kontonummer"; // do not translate as it is used in Norwegian and Swedish only + case "SE": + return "Kontonummer"; // do not translate as it is used in Norwegian and Swedish only + case "MX": + return "CLABE"; // do not translate as it is used in Spanish only + case "CL": + return "Cuenta"; // do not translate as it is used in Spanish only + case "AR": + return "Número de cuenta"; // do not translate as it is used in Spanish only + default: + return Res.get("payment.accountNrLabel"); + } + } + + // AccountType + public static boolean isAccountTypeRequired(String countryCode) { + switch (countryCode) { + case "US": + case "BR": + case "CA": + return true; + default: + return false; + } + } + + public static String getAccountTypeLabel(String countryCode) { + switch (countryCode) { + case "US": + case "BR": + case "CA": + return Res.get("payment.accountType"); + default: + return ""; + } + } + + public static List getAccountTypeValues(String countryCode) { + switch (countryCode) { + case "US": + case "BR": + case "CA": + return Arrays.asList(Res.get("payment.checking"), Res.get("payment.savings")); + default: + return new ArrayList<>(); + } + } + + + // HolderId + public static boolean isHolderIdRequired(String countryCode) { + switch (countryCode) { + case "BR": + case "CL": + case "AR": + return true; + default: + return false; + } + } + + public static String getHolderIdLabel(String countryCode) { + switch (countryCode) { + case "BR": + return "Cadastro de Pessoas Físicas (CPF)"; // do not translate as it is used in Portuguese only + case "CL": + return "Rol Único Tributario (RUT)"; // do not translate as it is used in Spanish only + case "AR": + return "CUIL/CUIT"; // do not translate as it is used in Spanish only + default: + return Res.get("payment.personalId"); + } + } + + public static String getHolderIdLabelShort(String countryCode) { + switch (countryCode) { + case "BR": + return "CPF"; // do not translate as it is used in portuguese only + case "CL": + return "RUT"; // do not translate as it is used in spanish only + case "AR": + return "CUIT"; + default: + return "ID"; + } + } + + // Validation + public static boolean useValidation(String countryCode) { + switch (countryCode) { + case "GB": + case "US": + case "BR": + case "AU": + case "CA": + case "NZ": + case "MX": + case "HK": + case "SE": + case "NO": + case "AR": + return true; + default: + return false; + } + } + + public static boolean isStateRequired(String countryCode) { + switch (countryCode) { + case "US": + case "CA": + case "AU": + case "MY": + case "MX": + case "CN": + return true; + default: + return false; + } + } + + public static boolean isNationalAccountIdRequired(String countryCode) { + switch (countryCode) { + case "AR": + return true; + default: + return false; + } + } + + public static String getNationalAccountIdLabel(String countryCode) { + switch (countryCode) { + case "AR": + return Res.get("payment.national.account.id.AR"); + default: + return ""; + } + } +} diff --git a/core/src/main/java/bisq/core/locale/Country.java b/core/src/main/java/bisq/core/locale/Country.java new file mode 100644 index 0000000000..50e3f6b293 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/Country.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.locale; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.Message; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@EqualsAndHashCode +@ToString +public final class Country implements PersistablePayload { + public final String code; + public final String name; + public final Region region; + + public Country(String code, String name, Region region) { + this.code = code; + this.name = name; + this.region = region; + } + + @Override + public Message toProtoMessage() { + return protobuf.Country.newBuilder().setCode(code).setName(name) + .setRegion(protobuf.Region.newBuilder().setCode(region.code).setName(region.name)).build(); + } + + public static Country fromProto(protobuf.Country proto) { + return new Country(proto.getCode(), + proto.getName(), + Region.fromProto(proto.getRegion())); + } +} diff --git a/core/src/main/java/bisq/core/locale/CountryUtil.java b/core/src/main/java/bisq/core/locale/CountryUtil.java new file mode 100644 index 0000000000..d788881355 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/CountryUtil.java @@ -0,0 +1,507 @@ +/* + * 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 bisq.core.locale; + +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CountryUtil { + public static List getAllSepaEuroCountries() { + List list = new ArrayList<>(); + String[] codes = {"AT", "BE", "CY", "DE", "EE", "FI", "FR", "GR", "IE", + "IT", "LV", "LT", "LU", "MC", "MT", "NL", "PT", "SK", "SI", "ES", "AD", "SM", "VA"}; + populateCountryListByCodes(list, codes); + list.sort((a, b) -> a.name.compareTo(b.name)); + + return list; + } + + public static List getAllRevolutCountries() { + List list = new ArrayList<>(); + String[] codes = {"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", + "DE", "GR", "HU", "IS", "IE", "IT", "LV", "LI", "LT", "LU", "MT", "NL", + "NO", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", + "AU", "CA", "SG", "CH", "US"}; + populateCountryListByCodes(list, codes); + list.sort((a, b) -> a.name.compareTo(b.name)); + + return list; + } + + public static List getAllAmazonGiftCardCountries() { + List list = new ArrayList<>(); + String[] codes = {"AU", "CA", "FR", "DE", "IT", "NL", "ES", "GB", "IN", "JP", + "SA", "SE", "SG", "TR", "US"}; + populateCountryListByCodes(list, codes); + list.sort((a, b) -> a.name.compareTo(b.name)); + + return list; + } + + public static List getAllSepaInstantEuroCountries() { + return getAllSepaEuroCountries(); + } + + private static void populateCountryListByCodes(List list, String[] codes) { + for (String code : codes) { + Locale locale = new Locale(LanguageUtil.getDefaultLanguage(), code, ""); + final String countryCode = locale.getCountry(); + String regionCode = getRegionCode(countryCode); + final Region region = new Region(regionCode, getRegionName(regionCode)); + Country country = new Country(countryCode, locale.getDisplayCountry(), region); + if (countryCode.equals("XK")) + country = new Country(countryCode, getNameByCode(countryCode), region); + list.add(country); + } + } + + public static boolean containsAllSepaEuroCountries(List countryCodesToCompare) { + countryCodesToCompare.sort(String::compareTo); + List countryCodesBase = getAllSepaEuroCountries().stream().map(c -> c.code).collect(Collectors.toList()); + return countryCodesToCompare.toString().equals(countryCodesBase.toString()); + } + + public static boolean containsAllSepaInstantEuroCountries(List countryCodesToCompare) { + return containsAllSepaEuroCountries(countryCodesToCompare); + } + + public static List getAllSepaNonEuroCountries() { + List list = new ArrayList<>(); + String[] codes = {"BG", "HR", "CZ", "DK", "GB", "HU", "PL", "RO", + "SE", "IS", "NO", "LI", "CH", "JE"}; + populateCountryListByCodes(list, codes); + list.sort((a, b) -> a.name.compareTo(b.name)); + return list; + } + + public static List getAllSepaInstantNonEuroCountries() { + return getAllSepaNonEuroCountries(); + } + + public static List getAllSepaCountries() { + List list = new ArrayList<>(); + list.addAll(getAllSepaEuroCountries()); + list.addAll(getAllSepaNonEuroCountries()); + return list; + } + + public static List getAllSepaInstantCountries() { + // TODO find reliable source for list + // //Austria, Estonia, Germany, Italy, Latvia, Lithuania, the Netherlands and Spain. + + /* List list = new ArrayList<>(); + String[] codes = {"AT", "DE", "EE", + "IT", "LV", "LT", "NL", "ES"}; + populateCountryListByCodes(list, codes); + list.sort((a, b) -> a.name.compareTo(b.name));*/ + return getAllSepaCountries(); + } + + public static Country getDefaultCountry() { + String regionCode = getRegionCode(getLocale().getCountry()); + final Region region = new Region(regionCode, getRegionName(regionCode)); + return new Country(getLocale().getCountry(), getLocale().getDisplayCountry(), region); + } + + public static Optional findCountryByCode(String countryCode) { + return getAllCountries().stream().filter(e -> e.code.equals(countryCode)).findAny(); + } + + public static String getNameByCode(String countryCode) { + if (countryCode.equals("XK")) + return "Republic of Kosovo"; + else + return new Locale(LanguageUtil.getDefaultLanguage(), countryCode).getDisplayCountry(); + } + + public static String getNameAndCode(String countryCode) { + if (countryCode.isEmpty()) + return ""; + return getNameByCode(countryCode) + " (" + countryCode + ")"; + } + + public static String getCodesString(List countryCodes) { + return countryCodes.stream().collect(Collectors.joining(", ")); + } + + public static String getNamesByCodesString(List countryCodes) { + return getNamesByCodes(countryCodes).stream().collect(Collectors.joining(",\n")); + } + + public static List getAllRegions() { + final List allRegions = new ArrayList<>(); + + String regionCode = "AM"; + Region region = new Region(regionCode, getRegionName(regionCode)); + allRegions.add(region); + + regionCode = "AF"; + region = new Region(regionCode, getRegionName(regionCode)); + allRegions.add(region); + + regionCode = "EU"; + region = new Region(regionCode, getRegionName(regionCode)); + allRegions.add(region); + + regionCode = "AS"; + region = new Region(regionCode, getRegionName(regionCode)); + allRegions.add(region); + + regionCode = "OC"; + region = new Region(regionCode, getRegionName(regionCode)); + allRegions.add(region); + + return allRegions; + } + + public static List getAllCountriesForRegion(Region selectedRegion) { + return Lists.newArrayList(Collections2.filter(getAllCountries(), country -> + selectedRegion != null && country != null && selectedRegion.equals(country.region))); + } + + public static List getAllCountries() { + final Set allCountries = new HashSet<>(); + for (final Locale locale : getAllCountryLocales()) { + String regionCode = getRegionCode(locale.getCountry()); + final Region region = new Region(regionCode, getRegionName(regionCode)); + Country country = new Country(locale.getCountry(), locale.getDisplayCountry(), region); + if (locale.getCountry().equals("XK")) + country = new Country(locale.getCountry(), "Republic of Kosovo", region); + allCountries.add(country); + } + + allCountries.add(new Country("GE", "Georgia", new Region("AS", getRegionName("AS")))); + allCountries.add(new Country("BW", "Botswana", new Region("AF", getRegionName("AF")))); + allCountries.add(new Country("IR", "Iran", new Region("AS", getRegionName("AS")))); + + final List allCountriesList = new ArrayList<>(allCountries); + allCountriesList.sort((locale1, locale2) -> locale1.name.compareTo(locale2.name)); + return allCountriesList; + } + + private static List getAllCountryLocales() { + List allLocales = LocaleUtil.getAllLocales(); + + // Filter duplicate locale entries + Set allLocalesAsSet = allLocales.stream().filter(locale -> !locale.getCountry().isEmpty()) + .collect(Collectors.toSet()); + + List allCountryLocales = new ArrayList<>(); + allCountryLocales.addAll(allLocalesAsSet); + allCountryLocales.sort((locale1, locale2) -> locale1.getDisplayCountry().compareTo(locale2.getDisplayCountry())); + return allCountryLocales; + } + + private static List getNamesByCodes(List countryCodes) { + return countryCodes.stream().map(CountryUtil::getNameByCode).collect(Collectors.toList()); + } + + private static String getRegionName(final String regionCode) { + return regionCodeToNameMap.get(regionCode); + } + + private static final Map regionCodeToNameMap = new HashMap<>(); + // Key is: ISO 3166 code, value is region code as defined in regionCodeToNameMap + private static final Map regionByCountryCodeMap = new HashMap<>(); + + static { + regionCodeToNameMap.put("AM", "Americas"); + regionCodeToNameMap.put("AF", "Africa"); + regionCodeToNameMap.put("EU", "Europe"); + regionCodeToNameMap.put("AS", "Asia"); + regionCodeToNameMap.put("OC", "Oceania"); + + // Data extracted from https://restcountries.eu/rest/v2/all?fields=name;region;subregion;alpha2Code;languages + regionByCountryCodeMap.put("AF", "AS"); // name=Afghanistan / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("AX", "EU"); // name=Åland Islands / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("AL", "EU"); // name=Albania / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("DZ", "AF"); // name=Algeria / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("AS", "OC"); // name=American Samoa / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("AD", "EU"); // name=Andorra / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("AO", "AF"); // name=Angola / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("AI", "AM"); // name=Anguilla / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("AG", "AM"); // name=Antigua and Barbuda / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("AR", "AM"); // name=Argentina / region=Americas / subregion=South America + regionByCountryCodeMap.put("AM", "AS"); // name=Armenia / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("AW", "AM"); // name=Aruba / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("AU", "OC"); // name=Australia / region=Oceania / subregion=Australia and New Zealand + regionByCountryCodeMap.put("AT", "EU"); // name=Austria / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("AZ", "AS"); // name=Azerbaijan / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("BS", "AM"); // name=Bahamas / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("BH", "AS"); // name=Bahrain / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("BD", "AS"); // name=Bangladesh / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("BB", "AM"); // name=Barbados / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("BY", "EU"); // name=Belarus / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("BE", "EU"); // name=Belgium / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("BZ", "AM"); // name=Belize / region=Americas / subregion=Central America + regionByCountryCodeMap.put("BJ", "AF"); // name=Benin / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("BM", "AM"); // name=Bermuda / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("BT", "AS"); // name=Bhutan / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("BO", "AM"); // name=Bolivia (Plurinational State of) / region=Americas / subregion=South America + regionByCountryCodeMap.put("BQ", "AM"); // name=Bonaire, Sint Eustatius and Saba / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("BA", "EU"); // name=Bosnia and Herzegovina / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("BW", "AF"); // name=Botswana / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("BR", "AM"); // name=Brazil / region=Americas / subregion=South America + regionByCountryCodeMap.put("IO", "AF"); // name=British Indian Ocean Territory / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("UM", "AM"); // name=United States Minor Outlying Islands / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("VG", "AM"); // name=Virgin Islands (British) / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("VI", "AM"); // name=Virgin Islands (U.S.) / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("BN", "AS"); // name=Brunei Darussalam / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("BG", "EU"); // name=Bulgaria / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("BF", "AF"); // name=Burkina Faso / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("BI", "AF"); // name=Burundi / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("KH", "AS"); // name=Cambodia / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("CM", "AF"); // name=Cameroon / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("CA", "AM"); // name=Canada / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("CV", "AF"); // name=Cabo Verde / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("KY", "AM"); // name=Cayman Islands / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("CF", "AF"); // name=Central African Republic / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("TD", "AF"); // name=Chad / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("CL", "AM"); // name=Chile / region=Americas / subregion=South America + regionByCountryCodeMap.put("CN", "AS"); // name=China / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("CX", "OC"); // name=Christmas Island / region=Oceania / subregion=Australia and New Zealand + regionByCountryCodeMap.put("CC", "OC"); // name=Cocos (Keeling) Islands / region=Oceania / subregion=Australia and New Zealand + regionByCountryCodeMap.put("CO", "AM"); // name=Colombia / region=Americas / subregion=South America + regionByCountryCodeMap.put("KM", "AF"); // name=Comoros / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("CG", "AF"); // name=Congo / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("CD", "AF"); // name=Congo (Democratic Republic of the) / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("CK", "OC"); // name=Cook Islands / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("CR", "AM"); // name=Costa Rica / region=Americas / subregion=Central America + regionByCountryCodeMap.put("HR", "EU"); // name=Croatia / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("CU", "AM"); // name=Cuba / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("CW", "AM"); // name=Curaçao / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("CY", "EU"); // name=Cyprus / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("CZ", "EU"); // name=Czech Republic / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("DK", "EU"); // name=Denmark / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("DJ", "AF"); // name=Djibouti / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("DM", "AM"); // name=Dominica / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("DO", "AM"); // name=Dominican Republic / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("EC", "AM"); // name=Ecuador / region=Americas / subregion=South America + regionByCountryCodeMap.put("EG", "AF"); // name=Egypt / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("SV", "AM"); // name=El Salvador / region=Americas / subregion=Central America + regionByCountryCodeMap.put("GQ", "AF"); // name=Equatorial Guinea / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("ER", "AF"); // name=Eritrea / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("EE", "EU"); // name=Estonia / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("ET", "AF"); // name=Ethiopia / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("FK", "AM"); // name=Falkland Islands (Malvinas) / region=Americas / subregion=South America + regionByCountryCodeMap.put("FO", "EU"); // name=Faroe Islands / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("FJ", "OC"); // name=Fiji / region=Oceania / subregion=Melanesia + regionByCountryCodeMap.put("FI", "EU"); // name=Finland / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("FR", "EU"); // name=France / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("GF", "AM"); // name=French Guiana / region=Americas / subregion=South America + regionByCountryCodeMap.put("PF", "OC"); // name=French Polynesia / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("TF", "AF"); // name=French Southern Territories / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("GA", "AF"); // name=Gabon / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("GM", "AF"); // name=Gambia / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("GE", "AS"); // name=Georgia / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("DE", "EU"); // name=Germany / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("GH", "AF"); // name=Ghana / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("GI", "EU"); // name=Gibraltar / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("GR", "EU"); // name=Greece / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("GL", "AM"); // name=Greenland / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("GD", "AM"); // name=Grenada / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("GP", "AM"); // name=Guadeloupe / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("GU", "OC"); // name=Guam / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("GT", "AM"); // name=Guatemala / region=Americas / subregion=Central America + regionByCountryCodeMap.put("GG", "EU"); // name=Guernsey / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("GN", "AF"); // name=Guinea / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("GW", "AF"); // name=Guinea-Bissau / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("GY", "AM"); // name=Guyana / region=Americas / subregion=South America + regionByCountryCodeMap.put("HT", "AM"); // name=Haiti / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("VA", "EU"); // name=Holy See / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("HN", "AM"); // name=Honduras / region=Americas / subregion=Central America + regionByCountryCodeMap.put("HK", "AS"); // name=Hong Kong / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("HU", "EU"); // name=Hungary / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("IS", "EU"); // name=Iceland / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("IN", "AS"); // name=India / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("ID", "AS"); // name=Indonesia / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("CI", "AF"); // name=Côte d'Ivoire / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("IR", "AS"); // name=Iran (Islamic Republic of) / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("IQ", "AS"); // name=Iraq / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("IE", "EU"); // name=Ireland / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("IM", "EU"); // name=Isle of Man / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("IL", "AS"); // name=Israel / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("IT", "EU"); // name=Italy / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("JM", "AM"); // name=Jamaica / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("JP", "AS"); // name=Japan / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("JE", "EU"); // name=Jersey / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("JO", "AS"); // name=Jordan / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("KZ", "AS"); // name=Kazakhstan / region=Asia / subregion=Central Asia + regionByCountryCodeMap.put("KE", "AF"); // name=Kenya / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("KI", "OC"); // name=Kiribati / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("KW", "AS"); // name=Kuwait / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("KG", "AS"); // name=Kyrgyzstan / region=Asia / subregion=Central Asia + regionByCountryCodeMap.put("LA", "AS"); // name=Lao People's Democratic Republic / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("LV", "EU"); // name=Latvia / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("LB", "AS"); // name=Lebanon / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("LS", "AF"); // name=Lesotho / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("LR", "AF"); // name=Liberia / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("LY", "AF"); // name=Libya / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("LI", "EU"); // name=Liechtenstein / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("LT", "EU"); // name=Lithuania / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("LU", "EU"); // name=Luxembourg / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("MO", "AS"); // name=Macao / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("MK", "EU"); // name=Macedonia (the former Yugoslav Republic of) / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("MG", "AF"); // name=Madagascar / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("MW", "AF"); // name=Malawi / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("MY", "AS"); // name=Malaysia / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("MV", "AS"); // name=Maldives / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("ML", "AF"); // name=Mali / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("MT", "EU"); // name=Malta / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("MH", "OC"); // name=Marshall Islands / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("MQ", "AM"); // name=Martinique / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("MR", "AF"); // name=Mauritania / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("MU", "AF"); // name=Mauritius / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("YT", "AF"); // name=Mayotte / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("MX", "AM"); // name=Mexico / region=Americas / subregion=Central America + regionByCountryCodeMap.put("FM", "OC"); // name=Micronesia (Federated States of) / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("MD", "EU"); // name=Moldova (Republic of) / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("MC", "EU"); // name=Monaco / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("MN", "AS"); // name=Mongolia / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("ME", "EU"); // name=Montenegro / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("MS", "AM"); // name=Montserrat / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("MA", "AF"); // name=Morocco / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("MZ", "AF"); // name=Mozambique / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("MM", "AS"); // name=Myanmar / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("NA", "AF"); // name=Namibia / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("NR", "OC"); // name=Nauru / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("NP", "AS"); // name=Nepal / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("NL", "EU"); // name=Netherlands / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("NC", "OC"); // name=New Caledonia / region=Oceania / subregion=Melanesia + regionByCountryCodeMap.put("NZ", "OC"); // name=New Zealand / region=Oceania / subregion=Australia and New Zealand + regionByCountryCodeMap.put("NI", "AM"); // name=Nicaragua / region=Americas / subregion=Central America + regionByCountryCodeMap.put("NE", "AF"); // name=Niger / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("NG", "AF"); // name=Nigeria / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("NU", "OC"); // name=Niue / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("NF", "OC"); // name=Norfolk Island / region=Oceania / subregion=Australia and New Zealand + regionByCountryCodeMap.put("KP", "AS"); // name=Korea (Democratic People's Republic of) / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("MP", "OC"); // name=Northern Mariana Islands / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("NO", "EU"); // name=Norway / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("OM", "AS"); // name=Oman / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("PK", "AS"); // name=Pakistan / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("PW", "OC"); // name=Palau / region=Oceania / subregion=Micronesia + regionByCountryCodeMap.put("PS", "AS"); // name=Palestine, State of / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("PA", "AM"); // name=Panama / region=Americas / subregion=Central America + regionByCountryCodeMap.put("PG", "OC"); // name=Papua New Guinea / region=Oceania / subregion=Melanesia + regionByCountryCodeMap.put("PY", "AM"); // name=Paraguay / region=Americas / subregion=South America + regionByCountryCodeMap.put("PE", "AM"); // name=Peru / region=Americas / subregion=South America + regionByCountryCodeMap.put("PH", "AS"); // name=Philippines / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("PN", "OC"); // name=Pitcairn / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("PL", "EU"); // name=Poland / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("PT", "EU"); // name=Portugal / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("PR", "AM"); // name=Puerto Rico / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("QA", "AS"); // name=Qatar / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("XK", "EU"); // name=Republic of Kosovo / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("RE", "AF"); // name=Réunion / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("RO", "EU"); // name=Romania / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("RU", "EU"); // name=Russian Federation / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("RW", "AF"); // name=Rwanda / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("BL", "AM"); // name=Saint Barthélemy / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("SH", "AF"); // name=Saint Helena, Ascension and Tristan da Cunha / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("KN", "AM"); // name=Saint Kitts and Nevis / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("LC", "AM"); // name=Saint Lucia / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("MF", "AM"); // name=Saint Martin (French part) / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("PM", "AM"); // name=Saint Pierre and Miquelon / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("VC", "AM"); // name=Saint Vincent and the Grenadines / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("WS", "OC"); // name=Samoa / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("SM", "EU"); // name=San Marino / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("ST", "AF"); // name=Sao Tome and Principe / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("SA", "AS"); // name=Saudi Arabia / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("SN", "AF"); // name=Senegal / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("RS", "EU"); // name=Serbia / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("SC", "AF"); // name=Seychelles / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("SL", "AF"); // name=Sierra Leone / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("SG", "AS"); // name=Singapore / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("SX", "AM"); // name=Sint Maarten (Dutch part) / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("SK", "EU"); // name=Slovakia / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("SI", "EU"); // name=Slovenia / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("SB", "OC"); // name=Solomon Islands / region=Oceania / subregion=Melanesia + regionByCountryCodeMap.put("SO", "AF"); // name=Somalia / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("ZA", "AF"); // name=South Africa / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("GS", "AM"); // name=South Georgia and the South Sandwich Islands / region=Americas / subregion=South America + regionByCountryCodeMap.put("KR", "AS"); // name=Korea (Republic of) / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("SS", "AF"); // name=South Sudan / region=Africa / subregion=Middle Africa + regionByCountryCodeMap.put("ES", "EU"); // name=Spain / region=Europe / subregion=Southern Europe + regionByCountryCodeMap.put("LK", "AS"); // name=Sri Lanka / region=Asia / subregion=Southern Asia + regionByCountryCodeMap.put("SD", "AF"); // name=Sudan / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("SR", "AM"); // name=Suriname / region=Americas / subregion=South America + regionByCountryCodeMap.put("SJ", "EU"); // name=Svalbard and Jan Mayen / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("SZ", "AF"); // name=Swaziland / region=Africa / subregion=Southern Africa + regionByCountryCodeMap.put("SE", "EU"); // name=Sweden / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("CH", "EU"); // name=Switzerland / region=Europe / subregion=Western Europe + regionByCountryCodeMap.put("SY", "AS"); // name=Syrian Arab Republic / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("TW", "AS"); // name=Taiwan / region=Asia / subregion=Eastern Asia + regionByCountryCodeMap.put("TJ", "AS"); // name=Tajikistan / region=Asia / subregion=Central Asia + regionByCountryCodeMap.put("TZ", "AF"); // name=Tanzania, United Republic of / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("TH", "AS"); // name=Thailand / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("TL", "AS"); // name=Timor-Leste / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("TG", "AF"); // name=Togo / region=Africa / subregion=Western Africa + regionByCountryCodeMap.put("TK", "OC"); // name=Tokelau / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("TO", "OC"); // name=Tonga / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("TT", "AM"); // name=Trinidad and Tobago / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("TN", "AF"); // name=Tunisia / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("TR", "AS"); // name=Turkey / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("TM", "AS"); // name=Turkmenistan / region=Asia / subregion=Central Asia + regionByCountryCodeMap.put("TC", "AM"); // name=Turks and Caicos Islands / region=Americas / subregion=Caribbean + regionByCountryCodeMap.put("TV", "OC"); // name=Tuvalu / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("UG", "AF"); // name=Uganda / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("UA", "EU"); // name=Ukraine / region=Europe / subregion=Eastern Europe + regionByCountryCodeMap.put("AE", "AS"); // name=United Arab Emirates / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("GB", "EU"); // name=United Kingdom of Great Britain and Northern Ireland / region=Europe / subregion=Northern Europe + regionByCountryCodeMap.put("US", "AM"); // name=United States of America / region=Americas / subregion=Northern America + regionByCountryCodeMap.put("UY", "AM"); // name=Uruguay / region=Americas / subregion=South America + regionByCountryCodeMap.put("UZ", "AS"); // name=Uzbekistan / region=Asia / subregion=Central Asia + regionByCountryCodeMap.put("VU", "OC"); // name=Vanuatu / region=Oceania / subregion=Melanesia + regionByCountryCodeMap.put("VE", "AM"); // name=Venezuela (Bolivarian Republic of) / region=Americas / subregion=South America + regionByCountryCodeMap.put("VN", "AS"); // name=Viet Nam / region=Asia / subregion=South-Eastern Asia + regionByCountryCodeMap.put("WF", "OC"); // name=Wallis and Futuna / region=Oceania / subregion=Polynesia + regionByCountryCodeMap.put("EH", "AF"); // name=Western Sahara / region=Africa / subregion=Northern Africa + regionByCountryCodeMap.put("YE", "AS"); // name=Yemen / region=Asia / subregion=Western Asia + regionByCountryCodeMap.put("ZM", "AF"); // name=Zambia / region=Africa / subregion=Eastern Africa + regionByCountryCodeMap.put("ZW", "AF"); // name=Zimbabwe / region=Africa / subregion=Eastern Africa + } + + public static String getRegionCode(String countryCode) { + if (regionByCountryCodeMap.containsKey(countryCode)) + return regionByCountryCodeMap.get(countryCode); + else + return "Undefined"; + } + + public static String getDefaultCountryCode() { + // might be set later in pref or config, so not use Preferences.getDefaultLocale() anywhere in the code + return getLocale().getCountry(); + } + + private static Locale getLocale() { + return GlobalSettings.getLocale(); + } +} diff --git a/core/src/main/java/bisq/core/locale/CryptoCurrency.java b/core/src/main/java/bisq/core/locale/CryptoCurrency.java new file mode 100644 index 0000000000..14681b4cd7 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/CryptoCurrency.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.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 = "✦ "; + + @Getter + private boolean isAsset = false; + + public CryptoCurrency(String currencyCode, + String name) { + this(currencyCode, name, false); + } + + public CryptoCurrency(String currencyCode, + String name, + boolean isAsset) { + super(currencyCode, name); + this.isAsset = isAsset; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + return getTradeCurrencyBuilder() + .setCryptoCurrency(protobuf.CryptoCurrency.newBuilder() + .setIsAsset(isAsset)) + .build(); + } + + public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) { + return new CryptoCurrency(proto.getCode(), + proto.getName(), + proto.getCryptoCurrency().getIsAsset()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getDisplayPrefix() { + return PREFIX; + } + +} diff --git a/core/src/main/java/bisq/core/locale/CurrencyTuple.java b/core/src/main/java/bisq/core/locale/CurrencyTuple.java new file mode 100644 index 0000000000..d644538bff --- /dev/null +++ b/core/src/main/java/bisq/core/locale/CurrencyTuple.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.core.locale; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode +public class CurrencyTuple { + public final String code; + public final String name; + public final int precision; // precision 4 is 1/10000 -> 0.0001 is smallest unit + + public CurrencyTuple(String code, String name) { + // We use Fiat class and the precision is 4 + // In future we might add custom precision per currency + this(code, name, 4); + } + + public CurrencyTuple(String code, String name, int precision) { + this.code = code; + this.name = name; + this.precision = precision; + } +} diff --git a/core/src/main/java/bisq/core/locale/CurrencyUtil.java b/core/src/main/java/bisq/core/locale/CurrencyUtil.java new file mode 100644 index 0000000000..360a0b8c34 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/CurrencyUtil.java @@ -0,0 +1,681 @@ +/* + * 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 bisq.core.locale; + +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.filter.FilterManager; + +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; +import bisq.asset.Coin; +import bisq.asset.Token; +import bisq.asset.coins.BSQ; + +import bisq.common.app.DevEnv; +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import com.google.common.base.Suppliers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Currency; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +@Slf4j +public class CurrencyUtil { + public static void setup() { + setBaseCurrencyCode(Config.baseCurrencyNetwork().getCurrencyCode()); + } + + private static final AssetRegistry assetRegistry = new AssetRegistry(); + + private static String baseCurrencyCode = "BTC"; + + // Calls to isFiatCurrency 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% + // for isFiatCurrency calls (1777 ms vs 3467 ms). + // See: https://github.com/bisq-network/bisq/pull/4955#issuecomment-745302802 + private static final Map isFiatCurrencyMap = new ConcurrentHashMap<>(); + private static final Map isCryptoCurrencyMap = new ConcurrentHashMap<>(); + + private static Supplier> fiatCurrencyMapSupplier = Suppliers.memoize( + CurrencyUtil::createFiatCurrencyMap)::get; + private static Supplier> cryptoCurrencyMapSupplier = Suppliers.memoize( + CurrencyUtil::createCryptoCurrencyMap)::get; + + public static void setBaseCurrencyCode(String baseCurrencyCode) { + CurrencyUtil.baseCurrencyCode = baseCurrencyCode; + } + + public static Collection getAllSortedFiatCurrencies() { + return fiatCurrencyMapSupplier.get().values(); + } + + private static Map createFiatCurrencyMap() { + return CountryUtil.getAllCountries().stream() + .map(country -> getCurrencyByCountryCode(country.code)) + .sorted(TradeCurrency::compareTo) + .distinct() + .collect(Collectors.toMap(TradeCurrency::getCode, Function.identity(), (x, y) -> x, LinkedHashMap::new)); + } + + public static List getMainFiatCurrencies() { + TradeCurrency defaultTradeCurrency = getDefaultTradeCurrency(); + List list = new ArrayList<>(); + // Top traded currencies + list.add(new FiatCurrency("USD")); + list.add(new FiatCurrency("EUR")); + list.add(new FiatCurrency("GBP")); + list.add(new FiatCurrency("CAD")); + list.add(new FiatCurrency("AUD")); + list.add(new FiatCurrency("RUB")); + list.add(new FiatCurrency("INR")); + list.add(new FiatCurrency("NGN")); + + list.sort(TradeCurrency::compareTo); + + FiatCurrency defaultFiatCurrency = + defaultTradeCurrency instanceof FiatCurrency ? (FiatCurrency) defaultTradeCurrency : null; + if (defaultFiatCurrency != null && list.contains(defaultFiatCurrency)) { + list.remove(defaultTradeCurrency); + list.add(0, defaultFiatCurrency); + } + return list; + } + + public static Collection getAllSortedCryptoCurrencies() { + return cryptoCurrencyMapSupplier.get().values(); + } + + private static Map createCryptoCurrencyMap() { + return getSortedAssetStream() + .map(CurrencyUtil::assetToCryptoCurrency) + .collect(Collectors.toMap(TradeCurrency::getCode, Function.identity(), (x, y) -> x, LinkedHashMap::new)); + } + + public static Stream getSortedAssetStream() { + return assetRegistry.stream() + .filter(CurrencyUtil::assetIsNotBaseCurrency) + .filter(asset -> isNotBsqOrBsqTradingActivated(asset, Config.baseCurrencyNetwork(), DevEnv.isDaoTradingActivated())) + .filter(asset -> assetMatchesNetworkIfMainnet(asset, Config.baseCurrencyNetwork())) + .sorted(Comparator.comparing(Asset::getName)); + } + + public static List getMainCryptoCurrencies() { + final List result = new ArrayList<>(); + result.add(new CryptoCurrency("XRC", "Bitcoin Rhodium")); + + if (DevEnv.isDaoTradingActivated()) + result.add(new CryptoCurrency("BSQ", "BSQ")); + + result.add(new CryptoCurrency("BEAM", "Beam")); + result.add(new CryptoCurrency("DASH", "Dash")); + result.add(new CryptoCurrency("DCR", "Decred")); + result.add(new CryptoCurrency("ETH", "Ether")); + result.add(new CryptoCurrency("GRIN", "Grin")); + result.add(new CryptoCurrency("L-BTC", "Liquid Bitcoin")); + result.add(new CryptoCurrency("LTC", "Litecoin")); + result.add(new CryptoCurrency("XMR", "Monero")); + result.add(new CryptoCurrency("NMC", "Namecoin")); + result.add(new CryptoCurrency("SF", "Siafund")); + result.add(new CryptoCurrency("ZEC", "Zcash")); + result.sort(TradeCurrency::compareTo); + + return result; + } + + public static List getRemovedCryptoCurrencies() { + final List currencies = new ArrayList<>(); + currencies.add(new CryptoCurrency("BCH", "Bitcoin Cash")); + currencies.add(new CryptoCurrency("BCHC", "Bitcoin Clashic")); + currencies.add(new CryptoCurrency("ACH", "AchieveCoin")); + currencies.add(new CryptoCurrency("SC", "Siacoin")); + currencies.add(new CryptoCurrency("PPI", "PiedPiper Coin")); + currencies.add(new CryptoCurrency("PEPECASH", "Pepe Cash")); + currencies.add(new CryptoCurrency("GRC", "Gridcoin")); + currencies.add(new CryptoCurrency("LTZ", "LitecoinZ")); + currencies.add(new CryptoCurrency("ZOC", "01coin")); + currencies.add(new CryptoCurrency("BURST", "Burstcoin")); + currencies.add(new CryptoCurrency("STEEM", "Steem")); + currencies.add(new CryptoCurrency("DAC", "DACash")); + currencies.add(new CryptoCurrency("RDD", "ReddCoin")); + return currencies; + } + + public static List getAllAdvancedCashCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("USD"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("RUB"), + new FiatCurrency("UAH"), + new FiatCurrency("KZT"), + new FiatCurrency("BRL") + )); + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + public static List getAllMoneyGramCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("AED"), + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("BND"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("FJD"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("HUF"), + new FiatCurrency("IDR"), + new FiatCurrency("ILS"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("KRW"), + new FiatCurrency("KWD"), + new FiatCurrency("LKR"), + new FiatCurrency("MAD"), + new FiatCurrency("MGA"), + new FiatCurrency("MXN"), + new FiatCurrency("MYR"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("OMR"), + new FiatCurrency("PEN"), + new FiatCurrency("PGK"), + new FiatCurrency("PHP"), + new FiatCurrency("PKR"), + new FiatCurrency("PLN"), + new FiatCurrency("SAR"), + new FiatCurrency("SBD"), + new FiatCurrency("SCR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TOP"), + new FiatCurrency("TRY"), + new FiatCurrency("TWD"), + new FiatCurrency("USD"), + new FiatCurrency("VND"), + new FiatCurrency("VUV"), + new FiatCurrency("WST"), + new FiatCurrency("XOF"), + new FiatCurrency("XPF"), + new FiatCurrency("ZAR") + )); + + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + // https://support.uphold.com/hc/en-us/articles/202473803-Supported-currencies + public static List getAllUpholdCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("USD"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("CNY"), + new FiatCurrency("JPY"), + new FiatCurrency("CHF"), + new FiatCurrency("INR"), + new FiatCurrency("MXN"), + new FiatCurrency("AUD"), + new FiatCurrency("CAD"), + new FiatCurrency("HKD"), + new FiatCurrency("NZD"), + new FiatCurrency("SGD"), + new FiatCurrency("KES"), + new FiatCurrency("ILS"), + new FiatCurrency("DKK"), + new FiatCurrency("NOK"), + new FiatCurrency("SEK"), + new FiatCurrency("PLN"), + new FiatCurrency("ARS"), + new FiatCurrency("BRL"), + new FiatCurrency("AED"), + new FiatCurrency("PHP") + )); + + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + // https://github.com/bisq-network/proposals/issues/243 + public static List getAllTransferwiseCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("XOF"), + new FiatCurrency("BGN"), + new FiatCurrency("CAD"), + new FiatCurrency("CLP"), + new FiatCurrency("HRK"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EGP"), + new FiatCurrency("EUR"), + new FiatCurrency("GEL"), + new FiatCurrency("HKD"), + new FiatCurrency("HUF"), + new FiatCurrency("IDR"), + new FiatCurrency("ILS"), + new FiatCurrency("JPY"), + new FiatCurrency("KES"), + new FiatCurrency("MYR"), + new FiatCurrency("MXN"), + new FiatCurrency("MAD"), + new FiatCurrency("NPR"), + new FiatCurrency("NZD"), + new FiatCurrency("NOK"), + new FiatCurrency("PKR"), + new FiatCurrency("PEN"), + new FiatCurrency("PHP"), + new FiatCurrency("PLN"), + new FiatCurrency("RON"), + new FiatCurrency("RUB"), + new FiatCurrency("SGD"), + new FiatCurrency("ZAR"), + new FiatCurrency("KRW"), + new FiatCurrency("SEK"), + new FiatCurrency("CHF"), + new FiatCurrency("THB"), + new FiatCurrency("TRY"), + new FiatCurrency("UGX"), + new FiatCurrency("AED"), + new FiatCurrency("GBP"), + new FiatCurrency("VND"), + new FiatCurrency("ZMW") + )); + + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + public static List getAllAmazonGiftCardCurrencies() { + List currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("AUD"), + new FiatCurrency("CAD"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("SAR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("TRY"), + new FiatCurrency("USD") + )); + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + // https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange + public static List getAllRevolutCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("AED"), + new FiatCurrency("AUD"), + new FiatCurrency("BGN"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("HRK"), + new FiatCurrency("HUF"), + new FiatCurrency("ILS"), + new FiatCurrency("ISK"), + new FiatCurrency("JPY"), + new FiatCurrency("MAD"), + new FiatCurrency("MXN"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PLN"), + new FiatCurrency("QAR"), + new FiatCurrency("RON"), + new FiatCurrency("RSD"), + new FiatCurrency("RUB"), + new FiatCurrency("SAR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TRY"), + new FiatCurrency("USD"), + new FiatCurrency("ZAR") + )); + + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + public static List getMatureMarketCurrencies() { + ArrayList currencies = new ArrayList<>(Arrays.asList( + new FiatCurrency("EUR"), + new FiatCurrency("USD"), + new FiatCurrency("GBP"), + new FiatCurrency("CAD"), + new FiatCurrency("AUD"), + new FiatCurrency("BRL") + )); + currencies.sort(Comparator.comparing(TradeCurrency::getCode)); + return currencies; + } + + public static boolean isFiatCurrency(String currencyCode) { + if (currencyCode != null && isFiatCurrencyMap.containsKey(currencyCode)) { + return isFiatCurrencyMap.get(currencyCode); + } + + try { + boolean isFiatCurrency = currencyCode != null + && !currencyCode.isEmpty() + && !isCryptoCurrency(currencyCode) + && Currency.getInstance(currencyCode) != null; + + if (currencyCode != null) { + isFiatCurrencyMap.put(currencyCode, isFiatCurrency); + } + + return isFiatCurrency; + } catch (Throwable t) { + if (currencyCode != null) { + isFiatCurrencyMap.put(currencyCode, false); + } + return false; + } + } + + public static Optional getFiatCurrency(String currencyCode) { + return Optional.ofNullable(fiatCurrencyMapSupplier.get().get(currencyCode)); + } + + /** + * We return true if it is BTC or any of our currencies available in the assetRegistry. + * For removed assets it would fail as they are not found but we don't want to conclude that they are fiat then. + * As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Fiat if not found + * we return true as well in case we have no fiat currency for the code. + * + * As we use a boolean result for isCryptoCurrency and isFiatCurrency we do not treat missing currencies correctly. + * To throw an exception might be an option but that will require quite a lot of code change, so we don't do that + * for the moment, but could be considered for the future. Another maybe better option is to introduce an enum which + * contains 3 entries (CryptoCurrency, Fiat, Undefined). + */ + public static boolean isCryptoCurrency(String currencyCode) { + if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode)) { + return isCryptoCurrencyMap.get(currencyCode); + } + + boolean isCryptoCurrency; + if (currencyCode == null) { + // Some tests call that method with null values. Should be fixed in the tests but to not break them return false. + isCryptoCurrency = false; + } else if (currencyCode.equals("BTC")) { + // BTC is not part of our assetRegistry so treat it extra here. Other old base currencies (LTC, DOGE, DASH) + // are not supported anymore so we can ignore that case. + isCryptoCurrency = true; + } else if (getCryptoCurrency(currencyCode).isPresent()) { + // If we find the code in our assetRegistry we return true. + // It might be that an asset was removed from the assetsRegistry, we deal with such cases below by checking if + // it is a fiat currency + isCryptoCurrency = true; + } else if (!getFiatCurrency(currencyCode).isPresent()) { + // In case the code is from a removed asset we cross check if there exist a fiat currency with that code, + // if we don't find a fiat currency we treat it as a crypto currency. + isCryptoCurrency = true; + } else { + // If we would have found a fiat currency we return false + isCryptoCurrency = false; + } + + if (currencyCode != null) { + isCryptoCurrencyMap.put(currencyCode, isCryptoCurrency); + } + + return isCryptoCurrency; + } + + public static Optional getCryptoCurrency(String currencyCode) { + return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode)); + } + + public static Optional getTradeCurrency(String currencyCode) { + Optional fiatCurrencyOptional = getFiatCurrency(currencyCode); + if (fiatCurrencyOptional.isPresent() && isFiatCurrency(currencyCode)) + return Optional.of(fiatCurrencyOptional.get()); + + Optional cryptoCurrencyOptional = getCryptoCurrency(currencyCode); + if (cryptoCurrencyOptional.isPresent() && isCryptoCurrency(currencyCode)) + return Optional.of(cryptoCurrencyOptional.get()); + + return Optional.empty(); + } + + public static Optional> getTradeCurrencies(List currencyCodes) { + List tradeCurrencies = new ArrayList<>(); + currencyCodes.stream().forEachOrdered(c -> + tradeCurrencies.add(getTradeCurrency(c).orElseThrow(() -> + new IllegalArgumentException(format("%s is not a valid trade currency code", c))))); + return tradeCurrencies.isEmpty() + ? Optional.empty() + : Optional.of(tradeCurrencies); + } + + public static Optional> getTradeCurrenciesInList(List currencyCodes, + List validCurrencies) { + Optional> tradeCurrencies = getTradeCurrencies(currencyCodes); + Consumer> validateCandidateCurrencies = (list) -> { + for (TradeCurrency tradeCurrency : list) { + if (!validCurrencies.contains(tradeCurrency)) { + throw new IllegalArgumentException( + format("%s is not a member of valid currencies list", + tradeCurrency.getCode())); + } + } + }; + tradeCurrencies.ifPresent(validateCandidateCurrencies); + return tradeCurrencies; + } + + public static FiatCurrency getCurrencyByCountryCode(String countryCode) { + if (countryCode.equals("XK")) + return new FiatCurrency("EUR"); + + Currency currency = Currency.getInstance(new Locale(LanguageUtil.getDefaultLanguage(), countryCode)); + return new FiatCurrency(currency.getCurrencyCode()); + } + + + public static String getNameByCode(String currencyCode) { + if (isCryptoCurrency(currencyCode)) { + // We might not find the name in case we have a call for a removed asset. + // If BTC is the code (used in tests) we also want return Bitcoin as name. + final Optional removedCryptoCurrency = getRemovedCryptoCurrencies().stream() + .filter(cryptoCurrency -> cryptoCurrency.getCode().equals(currencyCode)) + .findAny(); + + String btcOrRemovedAsset = "BTC".equals(currencyCode) ? "Bitcoin" : + removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); + return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(btcOrRemovedAsset); + } + try { + return Currency.getInstance(currencyCode).getDisplayName(); + } catch (Throwable t) { + log.debug("No currency name available {}", t.getMessage()); + return currencyCode; + } + } + + public static Optional findCryptoCurrencyByName(String currencyName) { + return getAllSortedCryptoCurrencies().stream() + .filter(e -> e.getName().equals(currencyName)) + .findAny(); + } + + public static String getNameAndCode(String currencyCode) { + return getNameByCode(currencyCode) + " (" + currencyCode + ")"; + } + + public static TradeCurrency getDefaultTradeCurrency() { + return GlobalSettings.getDefaultTradeCurrency(); + } + + private static boolean assetIsNotBaseCurrency(Asset asset) { + return !assetMatchesCurrencyCode(asset, baseCurrencyCode); + } + + // TODO We handle assets of other types (Token, ERC20) as matching the network which is not correct. + // We should add support for network property in those tokens as well. + public static boolean assetMatchesNetwork(Asset asset, BaseCurrencyNetwork baseCurrencyNetwork) { + return !(asset instanceof Coin) || + ((Coin) asset).getNetwork().name().equals(baseCurrencyNetwork.getNetwork()); + } + + // We only check for coins not other types of assets (TODO network check should be supported for all assets) + public static boolean assetMatchesNetworkIfMainnet(Asset asset, BaseCurrencyNetwork baseCurrencyNetwork) { + return !(asset instanceof Coin) || + coinMatchesNetworkIfMainnet((Coin) asset, baseCurrencyNetwork); + } + + // We want all coins available also in testnet or regtest for testing purpose + public static boolean coinMatchesNetworkIfMainnet(Coin coin, BaseCurrencyNetwork baseCurrencyNetwork) { + boolean matchesNetwork = assetMatchesNetwork(coin, baseCurrencyNetwork); + return !baseCurrencyNetwork.isMainnet() || matchesNetwork; + } + + private static CryptoCurrency assetToCryptoCurrency(Asset asset) { + return new CryptoCurrency(asset.getTickerSymbol(), asset.getName(), asset instanceof Token); + } + + private static boolean isNotBsqOrBsqTradingActivated(Asset asset, + BaseCurrencyNetwork baseCurrencyNetwork, + boolean daoTradingActivated) { + return !(asset instanceof BSQ) || + daoTradingActivated && assetMatchesNetwork(asset, baseCurrencyNetwork); + } + + public static boolean assetMatchesCurrencyCode(Asset asset, String currencyCode) { + return currencyCode.equals(asset.getTickerSymbol()); + } + + public static Optional findAsset(AssetRegistry assetRegistry, String currencyCode, + BaseCurrencyNetwork baseCurrencyNetwork, boolean daoTradingActivated) { + List assets = assetRegistry.stream() + .filter(asset -> assetMatchesCurrencyCode(asset, currencyCode)).collect(Collectors.toList()); + + // If we don't have the ticker symbol we throw an exception + if (!assets.stream().findFirst().isPresent()) + return Optional.empty(); + + if (currencyCode.equals("BSQ") && baseCurrencyNetwork.isMainnet() && !daoTradingActivated) + return Optional.empty(); + + // We check for exact match with network, e.g. BTC$TESTNET + Optional optionalAssetMatchesNetwork = assets.stream() + .filter(asset -> assetMatchesNetwork(asset, baseCurrencyNetwork)) + .findFirst(); + if (optionalAssetMatchesNetwork.isPresent()) + return optionalAssetMatchesNetwork; + + // In testnet or regtest we want to show all coins as well. Most coins have only Mainnet defined so we deliver + // that if no exact match was found in previous step + if (!baseCurrencyNetwork.isMainnet()) { + Optional optionalAsset = assets.stream().findFirst(); + checkArgument(optionalAsset.isPresent(), "optionalAsset must be present as we checked for " + + "not matching ticker symbols already above"); + return optionalAsset; + } + + // If we are in mainnet we need have a mainnet asset defined. + throw new IllegalArgumentException("We are on mainnet and we could not find an asset with network type mainnet"); + } + + public static Optional findAsset(String tickerSymbol) { + return assetRegistry.stream() + .filter(asset -> asset.getTickerSymbol().equals(tickerSymbol)) + .findAny(); + } + + public static Optional findAsset(String tickerSymbol, BaseCurrencyNetwork baseCurrencyNetwork) { + return assetRegistry.stream() + .filter(asset -> asset.getTickerSymbol().equals(tickerSymbol)) + .filter(asset -> assetMatchesNetwork(asset, baseCurrencyNetwork)) + .findAny(); + } + + // Excludes all assets which got removed by DAO voting + public static List getActiveSortedCryptoCurrencies(AssetService assetService, + FilterManager filterManager) { + return getAllSortedCryptoCurrencies().stream() + .filter(e -> e.getCode().equals("BSQ") || assetService.isActive(e.getCode())) + .filter(e -> !filterManager.isCurrencyBanned(e.getCode())) + .collect(Collectors.toList()); + } + + public static String getCurrencyPair(String currencyCode) { + if (isFiatCurrency(currencyCode)) + return Res.getBaseCurrencyCode() + "/" + currencyCode; + else + return currencyCode + "/" + Res.getBaseCurrencyCode(); + } + + public static String getCounterCurrency(String currencyCode) { + if (isFiatCurrency(currencyCode)) + return currencyCode; + else + return Res.getBaseCurrencyCode(); + } + + public static String getPriceWithCurrencyCode(String currencyCode) { + return getPriceWithCurrencyCode(currencyCode, "shared.priceInCurForCur"); + } + + public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) { + if (isCryptoCurrency(currencyCode)) + return Res.get(translationKey, Res.getBaseCurrencyCode(), currencyCode); + else + return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode()); + } + + public static String getOfferVolumeCode(String currencyCode) { + return Res.get("shared.offerVolumeCode", currencyCode); + } +} diff --git a/core/src/main/java/bisq/core/locale/FiatCurrency.java b/core/src/main/java/bisq/core/locale/FiatCurrency.java new file mode 100644 index 0000000000..d51a444f75 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/FiatCurrency.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.locale; + + +import com.google.protobuf.Message; + +import java.util.Currency; +import java.util.Locale; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +public final class FiatCurrency extends TradeCurrency { + // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 + private final static String PREFIX = "★ "; + + private final Currency currency; + + public FiatCurrency(String currencyCode) { + this(Currency.getInstance(currencyCode), getLocale()); + } + + @SuppressWarnings("WeakerAccess") + public FiatCurrency(Currency currency) { + this(currency, getLocale()); + } + + @SuppressWarnings("WeakerAccess") + public FiatCurrency(Currency currency, Locale locale) { + super(currency.getCurrencyCode(), currency.getDisplayName(locale)); + this.currency = currency; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + protobuf.Currency.Builder currencyBuilder = protobuf.Currency.newBuilder().setCurrencyCode(currency.getCurrencyCode()); + protobuf.FiatCurrency.Builder fiatCurrencyBuilder = protobuf.FiatCurrency.newBuilder().setCurrency(currencyBuilder); + return getTradeCurrencyBuilder() + .setFiatCurrency(fiatCurrencyBuilder) + .build(); + } + + public static FiatCurrency fromProto(protobuf.TradeCurrency proto) { + return new FiatCurrency(proto.getCode()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + private static Locale getLocale() { + return GlobalSettings.getLocale(); + } + + @Override + public String getDisplayPrefix() { + return PREFIX; + } + +} diff --git a/core/src/main/java/bisq/core/locale/GlobalSettings.java b/core/src/main/java/bisq/core/locale/GlobalSettings.java new file mode 100644 index 0000000000..46856fd7a9 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/GlobalSettings.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.locale; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.Locale; + +public class GlobalSettings { + private static boolean useAnimations = true; + private static Locale locale; + private static final ObjectProperty localeProperty = new SimpleObjectProperty<>(locale); + private static TradeCurrency defaultTradeCurrency; + private static String btcDenomination; + + static { + locale = Locale.getDefault(); + + // On some systems there is no country defined, in that case we use en_US + if (locale.getCountry() == null || locale.getCountry().isEmpty()) + locale = Locale.US; + } + + public static void setLocale(Locale locale) { + GlobalSettings.locale = locale; + localeProperty.set(locale); + } + + public static void setUseAnimations(boolean useAnimations) { + GlobalSettings.useAnimations = useAnimations; + } + + public static void setDefaultTradeCurrency(TradeCurrency fiatCurrency) { + GlobalSettings.defaultTradeCurrency = fiatCurrency; + } + + + public static void setBtcDenomination(String btcDenomination) { + GlobalSettings.btcDenomination = btcDenomination; + } + + public static TradeCurrency getDefaultTradeCurrency() { + return defaultTradeCurrency; + } + + public static String getBtcDenomination() { + return btcDenomination; + } + + public static ReadOnlyObjectProperty localeProperty() { + return localeProperty; + } + + public static boolean getUseAnimations() { + return useAnimations; + } + + public static Locale getLocale() { + return locale; + } +} diff --git a/core/src/main/java/bisq/core/locale/LanguageUtil.java b/core/src/main/java/bisq/core/locale/LanguageUtil.java new file mode 100644 index 0000000000..018360b30d --- /dev/null +++ b/core/src/main/java/bisq/core/locale/LanguageUtil.java @@ -0,0 +1,139 @@ +/* + * 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 bisq.core.locale; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class LanguageUtil { + private static final List userLanguageCodes = Arrays.asList( + "en", // English + "de", // German + "es", // Spanish + "pt", // Portuguese + "pt-BR", // Portuguese (Brazil) + "zh-Hans", // Chinese [Han Simplified] + "zh-Hant", // Chinese [Han Traditional] + "ru", // Russian + "fr", // French + "vi", // Vietnamese + "th", // Thai + "ja", // Japanese + "fa", // Persian + "it", // Italian + "cs" // Czech + /* + // not translated yet + "el", // Greek + "sr-Latn-RS", // Serbian [Latin] (Serbia) + "hu", // Hungarian + "ro", // Romanian + "tr" // Turkish + "iw", // Hebrew + "hi", // Hindi + "ko", // Korean + "pl", // Polish + "sv", // Swedish + "no", // Norwegian + "nl", // Dutch + "be", // Belarusian + "fi", // Finnish + "bg", // Bulgarian + "lt", // Lithuanian + "lv", // Latvian + "hr", // Croatian + "uk", // Ukrainian + "sk", // Slovak + "sl", // Slovenian + "ga", // Irish + "sq", // Albanian + "ca", // Catalan + "mk", // Macedonian + "kk", // Kazakh + "km", // Khmer + "sw", // Swahili + "in", // Indonesian + "ms", // Malay + "is", // Icelandic + "et", // Estonian + "ar", // Arabic + "vi", // Vietnamese + "th", // Thai + "da", // Danish + "mt" // Maltese + */ + ); + + private static final List rtlLanguagesCodes = Arrays.asList( + "fa", // Persian + "ar", // Arabic + "iw" // Hebrew + ); + + public static List getAllLanguageCodes() { + List allLocales = LocaleUtil.getAllLocales(); + + // Filter duplicate locale entries + Set allLocalesAsSet = allLocales.stream().filter(locale -> !locale.getLanguage().isEmpty() && + !locale.getDisplayLanguage().isEmpty()) + .map(Locale::getLanguage) + .collect(Collectors.toSet()); + + List allLanguageCodes = new ArrayList<>(); + allLanguageCodes.addAll(allLocalesAsSet); + allLanguageCodes.sort((o1, o2) -> getDisplayName(o1).compareTo(getDisplayName(o2))); + return allLanguageCodes; + } + + public static String getDefaultLanguage() { + // might be set later in pref or config, so not use defaultLocale anywhere in the code + return getLocale().getLanguage(); + } + + public static String getDefaultLanguageLocaleAsCode() { + return new Locale(LanguageUtil.getDefaultLanguage()).getLanguage(); + } + + public static String getEnglishLanguageLocaleCode() { + return new Locale(Locale.ENGLISH.getLanguage()).getLanguage(); + } + + public static String getDisplayName(String code) { + Locale locale = Locale.forLanguageTag(code); + return locale.getDisplayName(locale); + } + + public static boolean isDefaultLanguageRTL() { + return rtlLanguagesCodes.contains(LanguageUtil.getDefaultLanguageLocaleAsCode()); + } + + public static List getUserLanguageCodes() { + return userLanguageCodes; + } + + private static Locale getLocale() { + return GlobalSettings.getLocale(); + } +} diff --git a/core/src/main/java/bisq/core/locale/LocaleUtil.java b/core/src/main/java/bisq/core/locale/LocaleUtil.java new file mode 100644 index 0000000000..4b4d61ca3d --- /dev/null +++ b/core/src/main/java/bisq/core/locale/LocaleUtil.java @@ -0,0 +1,280 @@ +/* + * 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 bisq.core.locale; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class LocaleUtil { + public static List getAllLocales() { + + // Data from https://restcountries.eu/rest/v2/all?fields=name;region;subregion;alpha2Code;languages + List allLocales = new ArrayList<>(); + + allLocales.add(new Locale("ps", "AF")); // Afghanistan / lang=Pashto + allLocales.add(new Locale("sv", "AX")); // Åland Islands / lang=Swedish + allLocales.add(new Locale("sq", "AL")); // Albania / lang=Albanian + allLocales.add(new Locale("ar", "DZ")); // Algeria / lang=Arabic + allLocales.add(new Locale("en", "AS")); // American Samoa / lang=English + allLocales.add(new Locale("ca", "AD")); // Andorra / lang=Catalan + allLocales.add(new Locale("pt", "AO")); // Angola / lang=Portuguese + allLocales.add(new Locale("en", "AI")); // Anguilla / lang=English + allLocales.add(new Locale("en", "AG")); // Antigua and Barbuda / lang=English + allLocales.add(new Locale("es", "AR")); // Argentina / lang=Spanish + allLocales.add(new Locale("hy", "AM")); // Armenia / lang=Armenian + allLocales.add(new Locale("nl", "AW")); // Aruba / lang=Dutch + allLocales.add(new Locale("en", "AU")); // Australia / lang=English + allLocales.add(new Locale("de", "AT")); // Austria / lang=German + allLocales.add(new Locale("az", "AZ")); // Azerbaijan / lang=Azerbaijani + allLocales.add(new Locale("en", "BS")); // Bahamas / lang=English + allLocales.add(new Locale("ar", "BH")); // Bahrain / lang=Arabic + allLocales.add(new Locale("bn", "BD")); // Bangladesh / lang=Bengali + allLocales.add(new Locale("en", "BB")); // Barbados / lang=English + allLocales.add(new Locale("be", "BY")); // Belarus / lang=Belarusian + allLocales.add(new Locale("nl", "BE")); // Belgium / lang=Dutch + allLocales.add(new Locale("en", "BZ")); // Belize / lang=English + allLocales.add(new Locale("fr", "BJ")); // Benin / lang=French + allLocales.add(new Locale("en", "BM")); // Bermuda / lang=English + allLocales.add(new Locale("dz", "BT")); // Bhutan / lang=Dzongkha + allLocales.add(new Locale("es", "BO")); // Bolivia (Plurinational State of) / lang=Spanish + allLocales.add(new Locale("nl", "BQ")); // Bonaire, Sint Eustatius and Saba / lang=Dutch + allLocales.add(new Locale("bs", "BA")); // Bosnia and Herzegovina / lang=Bosnian + allLocales.add(new Locale("en", "BW")); // Botswana / lang=English + allLocales.add(new Locale("pt", "BR")); // Brazil / lang=Portuguese + allLocales.add(new Locale("en", "IO")); // British Indian Ocean Territory / lang=English + allLocales.add(new Locale("en", "UM")); // United States Minor Outlying Islands / lang=English + allLocales.add(new Locale("en", "VG")); // Virgin Islands (British) / lang=English + allLocales.add(new Locale("en", "VI")); // Virgin Islands (U.S.) / lang=English + allLocales.add(new Locale("ms", "BN")); // Brunei Darussalam / lang=Malay + allLocales.add(new Locale("bg", "BG")); // Bulgaria / lang=Bulgarian + allLocales.add(new Locale("fr", "BF")); // Burkina Faso / lang=French + allLocales.add(new Locale("fr", "BI")); // Burundi / lang=French + allLocales.add(new Locale("km", "KH")); // Cambodia / lang=Khmer + allLocales.add(new Locale("en", "CM")); // Cameroon / lang=English + allLocales.add(new Locale("en", "CA")); // Canada / lang=English + allLocales.add(new Locale("pt", "CV")); // Cabo Verde / lang=Portuguese + allLocales.add(new Locale("en", "KY")); // Cayman Islands / lang=English + allLocales.add(new Locale("fr", "CF")); // Central African Republic / lang=French + allLocales.add(new Locale("fr", "TD")); // Chad / lang=French + allLocales.add(new Locale("es", "CL")); // Chile / lang=Spanish + allLocales.add(new Locale("zh", "CN")); // China / lang=Chinese + allLocales.add(new Locale("en", "CX")); // Christmas Island / lang=English + allLocales.add(new Locale("en", "CC")); // Cocos (Keeling) Islands / lang=English + allLocales.add(new Locale("es", "CO")); // Colombia / lang=Spanish + allLocales.add(new Locale("ar", "KM")); // Comoros / lang=Arabic + allLocales.add(new Locale("fr", "CG")); // Congo / lang=French + allLocales.add(new Locale("fr", "CD")); // Congo (Democratic Republic of the) / lang=French + allLocales.add(new Locale("en", "CK")); // Cook Islands / lang=English + allLocales.add(new Locale("es", "CR")); // Costa Rica / lang=Spanish + allLocales.add(new Locale("hr", "HR")); // Croatia / lang=Croatian + allLocales.add(new Locale("es", "CU")); // Cuba / lang=Spanish + allLocales.add(new Locale("nl", "CW")); // Curaçao / lang=Dutch + allLocales.add(new Locale("el", "CY")); // Cyprus / lang=Greek (modern) + allLocales.add(new Locale("cs", "CZ")); // Czech Republic / lang=Czech + allLocales.add(new Locale("da", "DK")); // Denmark / lang=Danish + allLocales.add(new Locale("fr", "DJ")); // Djibouti / lang=French + allLocales.add(new Locale("en", "DM")); // Dominica / lang=English + allLocales.add(new Locale("es", "DO")); // Dominican Republic / lang=Spanish + allLocales.add(new Locale("es", "EC")); // Ecuador / lang=Spanish + allLocales.add(new Locale("ar", "EG")); // Egypt / lang=Arabic + allLocales.add(new Locale("es", "SV")); // El Salvador / lang=Spanish + allLocales.add(new Locale("es", "GQ")); // Equatorial Guinea / lang=Spanish + allLocales.add(new Locale("ti", "ER")); // Eritrea / lang=Tigrinya + allLocales.add(new Locale("et", "EE")); // Estonia / lang=Estonian + allLocales.add(new Locale("am", "ET")); // Ethiopia / lang=Amharic + allLocales.add(new Locale("en", "FK")); // Falkland Islands (Malvinas) / lang=English + allLocales.add(new Locale("fo", "FO")); // Faroe Islands / lang=Faroese + allLocales.add(new Locale("en", "FJ")); // Fiji / lang=English + allLocales.add(new Locale("fi", "FI")); // Finland / lang=Finnish + allLocales.add(new Locale("fr", "FR")); // France / lang=French + allLocales.add(new Locale("fr", "GF")); // French Guiana / lang=French + allLocales.add(new Locale("fr", "PF")); // French Polynesia / lang=French + allLocales.add(new Locale("fr", "TF")); // French Southern Territories / lang=French + allLocales.add(new Locale("fr", "GA")); // Gabon / lang=French + allLocales.add(new Locale("en", "GM")); // Gambia / lang=English + allLocales.add(new Locale("ka", "GE")); // Georgia / lang=Georgian + allLocales.add(new Locale("de", "DE")); // Germany / lang=German + allLocales.add(new Locale("en", "GH")); // Ghana / lang=English + allLocales.add(new Locale("en", "GI")); // Gibraltar / lang=English + allLocales.add(new Locale("el", "GR")); // Greece / lang=Greek (modern) + allLocales.add(new Locale("kl", "GL")); // Greenland / lang=Kalaallisut + allLocales.add(new Locale("en", "GD")); // Grenada / lang=English + allLocales.add(new Locale("fr", "GP")); // Guadeloupe / lang=French + allLocales.add(new Locale("en", "GU")); // Guam / lang=English + allLocales.add(new Locale("es", "GT")); // Guatemala / lang=Spanish + allLocales.add(new Locale("en", "GG")); // Guernsey / lang=English + allLocales.add(new Locale("fr", "GN")); // Guinea / lang=French + allLocales.add(new Locale("pt", "GW")); // Guinea-Bissau / lang=Portuguese + allLocales.add(new Locale("en", "GY")); // Guyana / lang=English + allLocales.add(new Locale("fr", "HT")); // Haiti / lang=French + allLocales.add(new Locale("la", "VA")); // Holy See / lang=Latin + allLocales.add(new Locale("es", "HN")); // Honduras / lang=Spanish + allLocales.add(new Locale("en", "HK")); // Hong Kong / lang=English + allLocales.add(new Locale("hu", "HU")); // Hungary / lang=Hungarian + allLocales.add(new Locale("is", "IS")); // Iceland / lang=Icelandic + allLocales.add(new Locale("hi", "IN")); // India / lang=Hindi + allLocales.add(new Locale("id", "ID")); // Indonesia / lang=Indonesian + allLocales.add(new Locale("fr", "CI")); // Côte d'Ivoire / lang=French + allLocales.add(new Locale("fa", "IR")); // Iran (Islamic Republic of) / lang=Persian (Farsi) + allLocales.add(new Locale("ar", "IQ")); // Iraq / lang=Arabic + allLocales.add(new Locale("ga", "IE")); // Ireland / lang=Irish + allLocales.add(new Locale("en", "IM")); // Isle of Man / lang=English + allLocales.add(new Locale("he", "IL")); // Israel / lang=Hebrew (modern) + allLocales.add(new Locale("it", "IT")); // Italy / lang=Italian + allLocales.add(new Locale("en", "JM")); // Jamaica / lang=English + allLocales.add(new Locale("ja", "JP")); // Japan / lang=Japanese + allLocales.add(new Locale("en", "JE")); // Jersey / lang=English + allLocales.add(new Locale("ar", "JO")); // Jordan / lang=Arabic + allLocales.add(new Locale("kk", "KZ")); // Kazakhstan / lang=Kazakh + allLocales.add(new Locale("en", "KE")); // Kenya / lang=English + allLocales.add(new Locale("en", "KI")); // Kiribati / lang=English + allLocales.add(new Locale("ar", "KW")); // Kuwait / lang=Arabic + allLocales.add(new Locale("ky", "KG")); // Kyrgyzstan / lang=Kyrgyz + allLocales.add(new Locale("lo", "LA")); // Lao People's Democratic Republic / lang=Lao + allLocales.add(new Locale("lv", "LV")); // Latvia / lang=Latvian + allLocales.add(new Locale("ar", "LB")); // Lebanon / lang=Arabic + allLocales.add(new Locale("en", "LS")); // Lesotho / lang=English + allLocales.add(new Locale("en", "LR")); // Liberia / lang=English + allLocales.add(new Locale("ar", "LY")); // Libya / lang=Arabic + allLocales.add(new Locale("de", "LI")); // Liechtenstein / lang=German + allLocales.add(new Locale("lt", "LT")); // Lithuania / lang=Lithuanian + allLocales.add(new Locale("fr", "LU")); // Luxembourg / lang=French + allLocales.add(new Locale("zh", "MO")); // Macao / lang=Chinese + allLocales.add(new Locale("mk", "MK")); // Macedonia (the former Yugoslav Republic of) / lang=Macedonian + allLocales.add(new Locale("fr", "MG")); // Madagascar / lang=French + allLocales.add(new Locale("en", "MW")); // Malawi / lang=English + allLocales.add(new Locale("en", "MY")); // Malaysia / lang=Malaysian + allLocales.add(new Locale("dv", "MV")); // Maldives / lang=Divehi + allLocales.add(new Locale("fr", "ML")); // Mali / lang=French + allLocales.add(new Locale("mt", "MT")); // Malta / lang=Maltese + allLocales.add(new Locale("en", "MH")); // Marshall Islands / lang=English + allLocales.add(new Locale("fr", "MQ")); // Martinique / lang=French + allLocales.add(new Locale("ar", "MR")); // Mauritania / lang=Arabic + allLocales.add(new Locale("en", "MU")); // Mauritius / lang=English + allLocales.add(new Locale("fr", "YT")); // Mayotte / lang=French + allLocales.add(new Locale("es", "MX")); // Mexico / lang=Spanish + allLocales.add(new Locale("en", "FM")); // Micronesia (Federated States of) / lang=English + allLocales.add(new Locale("ro", "MD")); // Moldova (Republic of) / lang=Romanian + allLocales.add(new Locale("fr", "MC")); // Monaco / lang=French + allLocales.add(new Locale("mn", "MN")); // Mongolia / lang=Mongolian + allLocales.add(new Locale("sr", "ME")); // Montenegro / lang=Serbian + allLocales.add(new Locale("en", "MS")); // Montserrat / lang=English + allLocales.add(new Locale("ar", "MA")); // Morocco / lang=Arabic + allLocales.add(new Locale("pt", "MZ")); // Mozambique / lang=Portuguese + allLocales.add(new Locale("my", "MM")); // Myanmar / lang=Burmese + allLocales.add(new Locale("en", "NA")); // Namibia / lang=English + allLocales.add(new Locale("en", "NR")); // Nauru / lang=English + allLocales.add(new Locale("ne", "NP")); // Nepal / lang=Nepali + allLocales.add(new Locale("nl", "NL")); // Netherlands / lang=Dutch + allLocales.add(new Locale("fr", "NC")); // New Caledonia / lang=French + allLocales.add(new Locale("en", "NZ")); // New Zealand / lang=English + allLocales.add(new Locale("es", "NI")); // Nicaragua / lang=Spanish + allLocales.add(new Locale("fr", "NE")); // Niger / lang=French + allLocales.add(new Locale("en", "NG")); // Nigeria / lang=English + allLocales.add(new Locale("en", "NU")); // Niue / lang=English + allLocales.add(new Locale("en", "NF")); // Norfolk Island / lang=English + allLocales.add(new Locale("ko", "KP")); // Korea (Democratic People's Republic of) / lang=Korean + allLocales.add(new Locale("en", "MP")); // Northern Mariana Islands / lang=English + allLocales.add(new Locale("no", "NO")); // Norway / lang=Norwegian + allLocales.add(new Locale("ar", "OM")); // Oman / lang=Arabic + allLocales.add(new Locale("en", "PK")); // Pakistan / lang=English + allLocales.add(new Locale("en", "PW")); // Palau / lang=English + allLocales.add(new Locale("ar", "PS")); // Palestine, State of / lang=Arabic + allLocales.add(new Locale("es", "PA")); // Panama / lang=Spanish + allLocales.add(new Locale("en", "PG")); // Papua New Guinea / lang=English + allLocales.add(new Locale("es", "PY")); // Paraguay / lang=Spanish + allLocales.add(new Locale("es", "PE")); // Peru / lang=Spanish + allLocales.add(new Locale("en", "PH")); // Philippines / lang=English + allLocales.add(new Locale("en", "PN")); // Pitcairn / lang=English + allLocales.add(new Locale("pl", "PL")); // Poland / lang=Polish + allLocales.add(new Locale("pt", "PT")); // Portugal / lang=Portuguese + allLocales.add(new Locale("es", "PR")); // Puerto Rico / lang=Spanish + allLocales.add(new Locale("ar", "QA")); // Qatar / lang=Arabic + allLocales.add(new Locale("sq", "XK")); // Republic of Kosovo / lang=Albanian + allLocales.add(new Locale("fr", "RE")); // Réunion / lang=French + allLocales.add(new Locale("ro", "RO")); // Romania / lang=Romanian + allLocales.add(new Locale("ru", "RU")); // Russian Federation / lang=Russian + allLocales.add(new Locale("rw", "RW")); // Rwanda / lang=Kinyarwanda + allLocales.add(new Locale("fr", "BL")); // Saint Barthélemy / lang=French + allLocales.add(new Locale("en", "SH")); // Saint Helena, Ascension and Tristan da Cunha / lang=English + allLocales.add(new Locale("en", "KN")); // Saint Kitts and Nevis / lang=English + allLocales.add(new Locale("en", "LC")); // Saint Lucia / lang=English + allLocales.add(new Locale("en", "MF")); // Saint Martin (French part) / lang=English + allLocales.add(new Locale("fr", "PM")); // Saint Pierre and Miquelon / lang=French + allLocales.add(new Locale("en", "VC")); // Saint Vincent and the Grenadines / lang=English + allLocales.add(new Locale("sm", "WS")); // Samoa / lang=Samoan + allLocales.add(new Locale("it", "SM")); // San Marino / lang=Italian + allLocales.add(new Locale("pt", "ST")); // Sao Tome and Principe / lang=Portuguese + allLocales.add(new Locale("ar", "SA")); // Saudi Arabia / lang=Arabic + allLocales.add(new Locale("fr", "SN")); // Senegal / lang=French + allLocales.add(new Locale("sr", "RS")); // Serbia / lang=Serbian + allLocales.add(new Locale("fr", "SC")); // Seychelles / lang=French + allLocales.add(new Locale("en", "SL")); // Sierra Leone / lang=English + allLocales.add(new Locale("en", "SG")); // Singapore / lang=English + allLocales.add(new Locale("nl", "SX")); // Sint Maarten (Dutch part) / lang=Dutch + allLocales.add(new Locale("sk", "SK")); // Slovakia / lang=Slovak + allLocales.add(new Locale("sl", "SI")); // Slovenia / lang=Slovene + allLocales.add(new Locale("en", "SB")); // Solomon Islands / lang=English + allLocales.add(new Locale("so", "SO")); // Somalia / lang=Somali + allLocales.add(new Locale("af", "ZA")); // South Africa / lang=Afrikaans + allLocales.add(new Locale("en", "GS")); // South Georgia and the South Sandwich Islands / lang=English + allLocales.add(new Locale("ko", "KR")); // Korea (Republic of) / lang=Korean + allLocales.add(new Locale("en", "SS")); // South Sudan / lang=English + allLocales.add(new Locale("es", "ES")); // Spain / lang=Spanish + allLocales.add(new Locale("si", "LK")); // Sri Lanka / lang=Sinhalese + allLocales.add(new Locale("ar", "SD")); // Sudan / lang=Arabic + allLocales.add(new Locale("nl", "SR")); // Suriname / lang=Dutch + allLocales.add(new Locale("no", "SJ")); // Svalbard and Jan Mayen / lang=Norwegian + allLocales.add(new Locale("en", "SZ")); // Swaziland / lang=English + allLocales.add(new Locale("sv", "SE")); // Sweden / lang=Swedish + allLocales.add(new Locale("de", "CH")); // Switzerland / lang=German + allLocales.add(new Locale("ar", "SY")); // Syrian Arab Republic / lang=Arabic + allLocales.add(new Locale("zh", "TW")); // Taiwan / lang=Chinese + allLocales.add(new Locale("tg", "TJ")); // Tajikistan / lang=Tajik + allLocales.add(new Locale("sw", "TZ")); // Tanzania, United Republic of / lang=Swahili + allLocales.add(new Locale("th", "TH")); // Thailand / lang=Thai + allLocales.add(new Locale("pt", "TL")); // Timor-Leste / lang=Portuguese + allLocales.add(new Locale("fr", "TG")); // Togo / lang=French + allLocales.add(new Locale("en", "TK")); // Tokelau / lang=English + allLocales.add(new Locale("en", "TO")); // Tonga / lang=English + allLocales.add(new Locale("en", "TT")); // Trinidad and Tobago / lang=English + allLocales.add(new Locale("ar", "TN")); // Tunisia / lang=Arabic + allLocales.add(new Locale("tr", "TR")); // Turkey / lang=Turkish + allLocales.add(new Locale("tk", "TM")); // Turkmenistan / lang=Turkmen + allLocales.add(new Locale("en", "TC")); // Turks and Caicos Islands / lang=English + allLocales.add(new Locale("en", "TV")); // Tuvalu / lang=English + allLocales.add(new Locale("en", "UG")); // Uganda / lang=English + allLocales.add(new Locale("uk", "UA")); // Ukraine / lang=Ukrainian + allLocales.add(new Locale("ar", "AE")); // United Arab Emirates / lang=Arabic + allLocales.add(new Locale("en", "GB")); // United Kingdom of Great Britain and Northern Ireland / lang=English + allLocales.add(new Locale("en", "US")); // United States of America / lang=English + allLocales.add(new Locale("es", "UY")); // Uruguay / lang=Spanish + allLocales.add(new Locale("uz", "UZ")); // Uzbekistan / lang=Uzbek + allLocales.add(new Locale("bi", "VU")); // Vanuatu / lang=Bislama + allLocales.add(new Locale("es", "VE")); // Venezuela (Bolivarian Republic of) / lang=Spanish + allLocales.add(new Locale("vi", "VN")); // Viet Nam / lang=Vietnamese + allLocales.add(new Locale("fr", "WF")); // Wallis and Futuna / lang=French + allLocales.add(new Locale("es", "EH")); // Western Sahara / lang=Spanish + allLocales.add(new Locale("ar", "YE")); // Yemen / lang=Arabic + allLocales.add(new Locale("en", "ZM")); // Zambia / lang=English + allLocales.add(new Locale("en", "ZW")); // Zimbabwe / lang=English + + return allLocales; + } +} diff --git a/core/src/main/java/bisq/core/locale/Region.java b/core/src/main/java/bisq/core/locale/Region.java new file mode 100644 index 0000000000..3ffd3b8aad --- /dev/null +++ b/core/src/main/java/bisq/core/locale/Region.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.locale; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.Message; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@EqualsAndHashCode +@ToString +public final class Region implements PersistablePayload { + public final String code; + public final String name; + + public Region(String code, String name) { + this.code = code; + this.name = name; + } + + @Override + public Message toProtoMessage() { + return protobuf.Region.newBuilder().setCode(code).setName(name).build(); + } + + public static Region fromProto(protobuf.Region proto) { + return new Region(proto.getCode(), proto.getName()); + } +} diff --git a/core/src/main/java/bisq/core/locale/Res.java b/core/src/main/java/bisq/core/locale/Res.java new file mode 100644 index 0000000000..fcf9141c91 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/Res.java @@ -0,0 +1,162 @@ +/* + * 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 bisq.core.locale; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import org.apache.commons.lang3.StringUtils; + +import java.text.MessageFormat; + +import java.net.URL; +import java.net.URLConnection; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class Res { + public static void setup() { + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + setBaseCurrencyCode(baseCurrencyNetwork.getCurrencyCode()); + setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); + } + + @SuppressWarnings("CanBeFinal") + private static ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n.displayStrings", GlobalSettings.getLocale(), new UTF8Control()); + + static { + GlobalSettings.localeProperty().addListener((observable, oldValue, newValue) -> { + if ("en".equalsIgnoreCase(newValue.getLanguage())) + newValue = Locale.ROOT; + resourceBundle = ResourceBundle.getBundle("i18n.displayStrings", newValue, new UTF8Control()); + }); + } + + public static String getWithCol(String key) { + return get(key) + ":"; + } + + public static String getWithColAndCap(String key) { + return StringUtils.capitalize(get(key)) + ":"; + } + + public static ResourceBundle getResourceBundle() { + return resourceBundle; + } + + private static String baseCurrencyCode; + private static String baseCurrencyName; + private static String baseCurrencyNameLowerCase; + + public static void setBaseCurrencyCode(String baseCurrencyCode) { + Res.baseCurrencyCode = baseCurrencyCode; + } + + public static void setBaseCurrencyName(String baseCurrencyName) { + Res.baseCurrencyName = baseCurrencyName; + baseCurrencyNameLowerCase = baseCurrencyName.toLowerCase(); + } + + public static String getBaseCurrencyCode() { + return baseCurrencyCode; + } + + public static String getBaseCurrencyName() { + return baseCurrencyName; + } + + // Capitalize first character + public static String getWithCap(String key) { + return StringUtils.capitalize(get(key)); + } + + public static String getWithCol(String key, Object... arguments) { + return get(key, arguments) + ":"; + } + + public static String get(String key, Object... arguments) { + return MessageFormat.format(Res.get(key), arguments); + } + + public static String get(String key) { + try { + return resourceBundle.getString(key) + .replace("BTC", baseCurrencyCode) + .replace("Bitcoin", baseCurrencyName) + .replace("bitcoin", baseCurrencyNameLowerCase); + } catch (MissingResourceException e) { + log.warn("Missing resource for key: {}", key); + e.printStackTrace(); + if (DevEnv.isDevMode()) + UserThread.runAfter(() -> { + // We delay a bit to not throw while UI is not ready + throw new RuntimeException("Missing resource for key: " + key); + }, 1); + + return key; + } + } +} + +// Adds UTF8 support for property files +class UTF8Control extends ResourceBundle.Control { + + public ResourceBundle newBundle(String baseName, @NotNull Locale locale, @NotNull String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, IOException { + // Below is a copy of the default implementation. + final String bundleName = toBundleName(baseName, locale); + final String resourceName = toResourceName(bundleName, "properties"); + ResourceBundle bundle = null; + InputStream stream = null; + if (reload) { + final URL url = loader.getResource(resourceName); + if (url != null) { + final URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + stream = connection.getInputStream(); + } + } + } else { + stream = loader.getResourceAsStream(resourceName); + } + if (stream != null) { + try { + // Only this line is changed to make it read properties files as UTF-8. + bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8")); + } finally { + stream.close(); + } + } + return bundle; + } +} diff --git a/core/src/main/java/bisq/core/locale/TradeCurrency.java b/core/src/main/java/bisq/core/locale/TradeCurrency.java new file mode 100644 index 0000000000..42741dadb1 --- /dev/null +++ b/core/src/main/java/bisq/core/locale/TradeCurrency.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.core.locale; + +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.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) { + this.code = code; + this.name = name; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static TradeCurrency fromProto(protobuf.TradeCurrency proto) { + switch (proto.getMessageCase()) { + case FIAT_CURRENCY: + return FiatCurrency.fromProto(proto); + case CRYPTO_CURRENCY: + return CryptoCurrency.fromProto(proto); + default: + throw new ProtobufferRuntimeException("Unknown message case: " + proto.getMessageCase()); + } + } + + public protobuf.TradeCurrency.Builder getTradeCurrencyBuilder() { + return protobuf.TradeCurrency.newBuilder() + .setCode(code) + .setName(name); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getDisplayPrefix() { + return ""; + } + + public String getNameAndCode() { + return name + " (" + code + ")"; + } + + public String getCodeAndName() { + return code + " (" + name + ")"; + } + + @Override + public int compareTo(@NotNull TradeCurrency other) { + return this.name.compareTo(other.name); + } + +} diff --git a/core/src/main/java/bisq/core/monetary/Altcoin.java b/core/src/main/java/bisq/core/monetary/Altcoin.java new file mode 100644 index 0000000000..87c4e75426 --- /dev/null +++ b/core/src/main/java/bisq/core/monetary/Altcoin.java @@ -0,0 +1,219 @@ +/* + * 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 bisq.core.monetary; + +import bisq.core.util.ParsingUtils; + +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.MonetaryFormat; + +import com.google.common.math.LongMath; + +import java.math.BigDecimal; + +import org.jetbrains.annotations.NotNull; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Cloned from Fiat class and altered SMALLEST_UNIT_EXPONENT as Fiat is final. + *

    + * Represents a monetary fiat value. It was decided to not fold this into {@link org.bitcoinj.core.Coin} because of type + * safety. Volume values always come with an attached currency code. + *

    + * This class is immutable. + */ +public final class Altcoin implements Monetary, Comparable { + /** + * The absolute value of exponent of the value of a "smallest unit" in scientific notation. We picked 4 rather than + * 2, because in financial applications it's common to use sub-cent precision. + */ + public static final int SMALLEST_UNIT_EXPONENT = 8; + private static final MonetaryFormat FRIENDLY_FORMAT = new MonetaryFormat().shift(0).minDecimals(2).repeatOptionalDecimals(2, 1).postfixCode(); + private static final MonetaryFormat PLAIN_FORMAT = new MonetaryFormat().shift(0).minDecimals(0).repeatOptionalDecimals(1, 8).noCode(); + + /** + * The number of smallest units of this monetary value. + */ + public final long value; + public final String currencyCode; + + private Altcoin(final String currencyCode, final long value) { + this.value = value; + this.currencyCode = currencyCode; + } + + public static Altcoin valueOf(final String currencyCode, final long value) { + return new Altcoin(currencyCode, value); + } + + @Override + public int smallestUnitExponent() { + return SMALLEST_UNIT_EXPONENT; + } + + /** + * Returns the number of "smallest units" of this monetary value. + */ + @Override + public long getValue() { + return value; + } + + public String getCurrencyCode() { + return currencyCode; + } + + /** + * Parses an amount expressed in the way humans are used to. + *

    + *

    + * This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, for example "0", "1", "0.10", + * "1.23E3", "1234.5E-5". + * + * @throws IllegalArgumentException if you try to specify fractional satoshis, or a value out of range. + */ + public static Altcoin parseAltcoin(final String currencyCode, String input) { + String cleaned = ParsingUtils.convertCharsForNumber(input); + try { + long val = new BigDecimal(cleaned).movePointRight(SMALLEST_UNIT_EXPONENT) + .toBigIntegerExact().longValue(); + return Altcoin.valueOf(currencyCode, val); + } catch (ArithmeticException e) { + throw new IllegalArgumentException(e); + } + } + + public Altcoin add(final Altcoin value) { + checkArgument(value.currencyCode.equals(currencyCode)); + return new Altcoin(currencyCode, LongMath.checkedAdd(this.value, value.value)); + } + + public Altcoin subtract(final Altcoin value) { + checkArgument(value.currencyCode.equals(currencyCode)); + return new Altcoin(currencyCode, LongMath.checkedSubtract(this.value, value.value)); + } + + public Altcoin multiply(final long factor) { + return new Altcoin(currencyCode, LongMath.checkedMultiply(this.value, factor)); + } + + public Altcoin divide(final long divisor) { + return new Altcoin(currencyCode, this.value / divisor); + } + + public Altcoin[] divideAndRemainder(final long divisor) { + return new Altcoin[]{new Altcoin(currencyCode, this.value / divisor), new Altcoin(currencyCode, this.value % divisor)}; + } + + public long divide(final Altcoin divisor) { + checkArgument(divisor.currencyCode.equals(currencyCode)); + return this.value / divisor.value; + } + + /** + * Returns true if and only if this instance represents a monetary value greater than zero, otherwise false. + */ + public boolean isPositive() { + return signum() == 1; + } + + /** + * Returns true if and only if this instance represents a monetary value less than zero, otherwise false. + */ + public boolean isNegative() { + return signum() == -1; + } + + /** + * Returns true if and only if this instance represents zero monetary value, otherwise false. + */ + public boolean isZero() { + return signum() == 0; + } + + /** + * Returns true if the monetary value represented by this instance is greater than that of the given other Coin, + * otherwise false. + */ + public boolean isGreaterThan(Altcoin other) { + return compareTo(other) > 0; + } + + /** + * Returns true if the monetary value represented by this instance is less than that of the given other Coin, + * otherwise false. + */ + public boolean isLessThan(Altcoin other) { + return compareTo(other) < 0; + } + + @Override + public int signum() { + if (this.value == 0) + return 0; + return this.value < 0 ? -1 : 1; + } + + public Altcoin negate() { + return new Altcoin(currencyCode, -this.value); + } + + public String toFriendlyString() { + return FRIENDLY_FORMAT.code(0, currencyCode).format(this).toString(); + } + + /** + *

    + * Returns the value as a plain string denominated in BTC. The result is unformatted with no trailing zeroes. For + * instance, a value of 150000 satoshis gives an output string of "0.0015" BTC + *

    + */ + public String toPlainString() { + return PLAIN_FORMAT.format(this).toString(); + } + + @Override + public String toString() { + return toPlainString(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) + return true; + if (o == null || o.getClass() != getClass()) + return false; + final Altcoin other = (Altcoin) o; + return this.value == other.value && this.currencyCode.equals(other.currencyCode); + } + + @Override + public int hashCode() { + return (int) this.value + 37 * this.currencyCode.hashCode(); + } + + @Override + public int compareTo(@NotNull final Altcoin other) { + if (!this.currencyCode.equals(other.currencyCode)) + return this.currencyCode.compareTo(other.currencyCode); + if (this.value != other.value) + return this.value > other.value ? 1 : -1; + return 0; + } +} diff --git a/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java b/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java new file mode 100644 index 0000000000..4064c2c288 --- /dev/null +++ b/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.monetary; + +import org.bitcoinj.core.Coin; + +import java.math.BigInteger; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +// Cloned from ExchangeRate. Use Altcoin instead of Fiat. +@Slf4j +public class AltcoinExchangeRate { + /** + * An exchange rate is expressed as a ratio of a {@link Coin} and a {@link Altcoin} amount. + */ + + public final Coin coin; + public final Altcoin altcoin; + + /** + * Construct exchange rate. This amount of coin is worth that amount of altcoin. + */ + @SuppressWarnings("SameParameterValue") + public AltcoinExchangeRate(Coin coin, Altcoin altcoin) { + checkArgument(coin.isPositive()); + checkArgument(altcoin.isPositive()); + checkArgument(altcoin.currencyCode != null, "currency code required"); + this.coin = coin; + this.altcoin = altcoin; + } + + /** + * Construct exchange rate. One coin is worth this amount of altcoin. + */ + public AltcoinExchangeRate(Altcoin altcoin) { + this(Coin.COIN, altcoin); + } + + /** + * Convert a coin amount to an altcoin amount using this exchange rate. + * + * @throws ArithmeticException if the converted altcoin amount is too high or too low. + */ + public Altcoin coinToAltcoin(Coin convertCoin) { + BigInteger converted = BigInteger.valueOf(coin.value) + .multiply(BigInteger.valueOf(convertCoin.value)) + .divide(BigInteger.valueOf(altcoin.value)); + if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) + throw new ArithmeticException("Overflow"); + return Altcoin.valueOf(altcoin.currencyCode, converted.longValue()); + } + + /** + * Convert a altcoin amount to a coin amount using this exchange rate. + * + * @throws ArithmeticException if the converted coin amount is too high or too low. + */ + public Coin altcoinToCoin(Altcoin convertAltcoin) { + checkArgument(convertAltcoin.currencyCode.equals(altcoin.currencyCode), "Currency mismatch: %s vs %s", + convertAltcoin.currencyCode, altcoin.currencyCode); + // Use BigInteger because it's much easier to maintain full precision without overflowing. + BigInteger converted = BigInteger.valueOf(altcoin.value) + .multiply(BigInteger.valueOf(convertAltcoin.value)) + .divide(BigInteger.valueOf(coin.value)); + if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) + throw new ArithmeticException("Overflow"); + try { + return Coin.valueOf(converted.longValue()); + } catch (IllegalArgumentException x) { + throw new ArithmeticException("Overflow: " + x.getMessage()); + } + } +} diff --git a/core/src/main/java/bisq/core/monetary/MonetaryWrapper.java b/core/src/main/java/bisq/core/monetary/MonetaryWrapper.java new file mode 100644 index 0000000000..ec7980a384 --- /dev/null +++ b/core/src/main/java/bisq/core/monetary/MonetaryWrapper.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.core.monetary; + +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.MonetaryFormat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class MonetaryWrapper { + private static final Logger log = LoggerFactory.getLogger(MonetaryWrapper.class); + + /// Instance of Fiat or Altcoin + protected final Monetary monetary; + protected final MonetaryFormat fiatFormat = MonetaryFormat.FIAT.repeatOptionalDecimals(0, 0); + protected final MonetaryFormat altCoinFormat = MonetaryFormat.FIAT.repeatOptionalDecimals(0, 0); + + public MonetaryWrapper(Monetary monetary) { + this.monetary = monetary; + } + + public Monetary getMonetary() { + return monetary; + } + + public boolean isZero() { + return monetary.getValue() == 0; + } + + public int smallestUnitExponent() { + return monetary.smallestUnitExponent(); + } + + public long getValue() { + return monetary.getValue(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) + return true; + if (o == null || o.getClass() != getClass()) + return false; + final Monetary otherMonetary = ((MonetaryWrapper) o).getMonetary(); + return monetary.getValue() == otherMonetary.getValue(); + } + + @Override + public int hashCode() { + return (int) monetary.getValue(); + } +} diff --git a/core/src/main/java/bisq/core/monetary/Price.java b/core/src/main/java/bisq/core/monetary/Price.java new file mode 100644 index 0000000000..e07a896efe --- /dev/null +++ b/core/src/main/java/bisq/core/monetary/Price.java @@ -0,0 +1,145 @@ +/* + * 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 bisq.core.monetary; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.util.ParsingUtils; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +/** + * Bitcoin price value with variable precision. + *

    + *
    + * We wrap an object implementing the {@link Monetary} interface from bitcoinj. We respect the + * number of decimal digits of precision specified in the {@code smallestUnitExponent()}, defined in + * those classes, like {@link Fiat} or {@link Altcoin}. + */ +public class Price extends MonetaryWrapper implements Comparable { + private static final Logger log = LoggerFactory.getLogger(Price.class); + + /** + * Create a new {@code Price} from specified {@code Monetary}. + * + * @param monetary + */ + public Price(Monetary monetary) { + super(monetary); + } + + /** + * Parse the Bitcoin {@code Price} given a {@code currencyCode} and {@code inputValue}. + * + * @param currencyCode The currency code to parse, e.g "USD" or "LTC". + * @param input The input value to parse as a String, e.g "2.54" or "-0.0001". + * @return The parsed Price. + */ + public static Price parse(String currencyCode, String input) { + String cleaned = ParsingUtils.convertCharsForNumber(input); + if (CurrencyUtil.isFiatCurrency(currencyCode)) + return new Price(Fiat.parseFiat(currencyCode, cleaned)); + else + return new Price(Altcoin.parseAltcoin(currencyCode, cleaned)); + } + + /** + * Parse the Bitcoin {@code Price} given a {@code currencyCode} and {@code inputValue}. + * + * @param currencyCode The currency code to parse, e.g "USD" or "LTC". + * @param value The value to parse. + * @return The parsed Price. + */ + public static Price valueOf(String currencyCode, long value) { + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + return new Price(Fiat.valueOf(currencyCode, value)); + } else { + return new Price(Altcoin.valueOf(currencyCode, value)); + } + } + + public Volume getVolumeByAmount(Coin amount) { + if (monetary instanceof Fiat) + return new Volume(new ExchangeRate((Fiat) monetary).coinToFiat(amount)); + else if (monetary instanceof Altcoin) + return new Volume(new AltcoinExchangeRate((Altcoin) monetary).coinToAltcoin(amount)); + else + throw new IllegalStateException("Monetary must be either of type Fiat or Altcoin"); + } + + public Coin getAmountByVolume(Volume volume) { + Monetary monetary = volume.getMonetary(); + if (monetary instanceof Fiat && this.monetary instanceof Fiat) + return new ExchangeRate((Fiat) this.monetary).fiatToCoin((Fiat) monetary); + else if (monetary instanceof Altcoin && this.monetary instanceof Altcoin) + return new AltcoinExchangeRate((Altcoin) this.monetary).altcoinToCoin((Altcoin) monetary); + else + return Coin.ZERO; + } + + public String getCurrencyCode() { + return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode(); + } + + public long getValue() { + return monetary.getValue(); + } + + @Override + public int compareTo(@NotNull Price other) { + if (!this.getCurrencyCode().equals(other.getCurrencyCode())) + return this.getCurrencyCode().compareTo(other.getCurrencyCode()); + if (this.getValue() != other.getValue()) + return this.getValue() > other.getValue() ? 1 : -1; + return 0; + } + + public boolean isPositive() { + return monetary instanceof Altcoin ? ((Altcoin) monetary).isPositive() : ((Fiat) monetary).isPositive(); + } + + public Price subtract(Price other) { + if (monetary instanceof Altcoin) { + return new Price(((Altcoin) monetary).subtract((Altcoin) other.monetary)); + } else { + return new Price(((Fiat) monetary).subtract((Fiat) other.monetary)); + } + } + + public String toFriendlyString() { + return monetary instanceof Altcoin ? + ((Altcoin) monetary).toFriendlyString() + "/BTC" : + ((Fiat) monetary).toFriendlyString().replace(((Fiat) monetary).currencyCode, "") + "BTC/" + ((Fiat) monetary).currencyCode; + } + + public String toPlainString() { + return monetary instanceof Altcoin ? ((Altcoin) monetary).toPlainString() : ((Fiat) monetary).toPlainString(); + } + + @Override + public String toString() { + return toPlainString(); + } +} diff --git a/core/src/main/java/bisq/core/monetary/Volume.java b/core/src/main/java/bisq/core/monetary/Volume.java new file mode 100644 index 0000000000..bd2bc752e0 --- /dev/null +++ b/core/src/main/java/bisq/core/monetary/Volume.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.core.monetary; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.util.ParsingUtils; + +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.Fiat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +public class Volume extends MonetaryWrapper implements Comparable { + private static final Logger log = LoggerFactory.getLogger(Volume.class); + + public Volume(Monetary monetary) { + super(monetary); + } + + public static Volume parse(String input, String currencyCode) { + String cleaned = ParsingUtils.convertCharsForNumber(input); + if (CurrencyUtil.isFiatCurrency(currencyCode)) + return new Volume(Fiat.parseFiat(currencyCode, cleaned)); + else + return new Volume(Altcoin.parseAltcoin(currencyCode, cleaned)); + } + + @Override + public int compareTo(@NotNull Volume other) { + if (!this.getCurrencyCode().equals(other.getCurrencyCode())) + return this.getCurrencyCode().compareTo(other.getCurrencyCode()); + if (this.getValue() != other.getValue()) + return this.getValue() > other.getValue() ? 1 : -1; + return 0; + } + + public String getCurrencyCode() { + return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode(); + } + + public String toPlainString() { + return monetary instanceof Altcoin ? ((Altcoin) monetary).toPlainString() : ((Fiat) monetary).toPlainString(); + } + + @Override + public String toString() { + return toPlainString(); + } +} diff --git a/core/src/main/java/bisq/core/network/CoreNetworkFilter.java b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java new file mode 100644 index 0000000000..b261d42153 --- /dev/null +++ b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.network; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkFilter; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CoreNetworkFilter implements NetworkFilter { + private final Set bannedPeersFromOptions = new HashSet<>(); + private Function bannedNodeFunction; + + /** + * @param banList List of banned peers from program argument + */ + @Inject + public CoreNetworkFilter(@Named(Config.BAN_LIST) List banList) { + banList.stream().map(NodeAddress::new).forEach(bannedPeersFromOptions::add); + } + + @Override + public void setBannedNodeFunction(Function bannedNodeFunction) { + this.bannedNodeFunction = bannedNodeFunction; + } + + @Override + public boolean isPeerBanned(NodeAddress nodeAddress) { + return bannedPeersFromOptions.contains(nodeAddress) || + bannedNodeFunction != null && bannedNodeFunction.apply(nodeAddress); + } +} diff --git a/core/src/main/java/bisq/core/network/MessageState.java b/core/src/main/java/bisq/core/network/MessageState.java new file mode 100644 index 0000000000..25a36729ff --- /dev/null +++ b/core/src/main/java/bisq/core/network/MessageState.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.core.network; + +public enum MessageState { + UNDEFINED, + SENT, + ARRIVED, + STORED_IN_MAILBOX, + ACKNOWLEDGED, + FAILED +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java new file mode 100644 index 0000000000..6bd646bc29 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java @@ -0,0 +1,182 @@ +/* + * 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 bisq.core.network.p2p.inventory; + +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.state.DaoStateService; +import bisq.core.filter.Filter; +import bisq.core.filter.FilterManager; +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.core.network.p2p.inventory.model.InventoryItem; +import bisq.core.network.p2p.inventory.model.RequestInfo; + +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.Statistic; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Profiler; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.base.Enums; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +import java.lang.management.ManagementFactory; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GetInventoryRequestHandler implements MessageListener { + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final P2PDataStorage p2PDataStorage; + private final DaoStateService daoStateService; + private final DaoStateMonitoringService daoStateMonitoringService; + private final ProposalStateMonitoringService proposalStateMonitoringService; + private final BlindVoteStateMonitoringService blindVoteStateMonitoringService; + private final FilterManager filterManager; + private final int maxConnections; + + @Inject + public GetInventoryRequestHandler(NetworkNode networkNode, + PeerManager peerManager, + P2PDataStorage p2PDataStorage, + DaoStateService daoStateService, + DaoStateMonitoringService daoStateMonitoringService, + ProposalStateMonitoringService proposalStateMonitoringService, + BlindVoteStateMonitoringService blindVoteStateMonitoringService, + FilterManager filterManager, + @Named(Config.MAX_CONNECTIONS) int maxConnections) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.p2PDataStorage = p2PDataStorage; + this.daoStateService = daoStateService; + this.daoStateMonitoringService = daoStateMonitoringService; + this.proposalStateMonitoringService = proposalStateMonitoringService; + this.blindVoteStateMonitoringService = blindVoteStateMonitoringService; + this.filterManager = filterManager; + this.maxConnections = maxConnections; + + this.networkNode.addMessageListener(this); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetInventoryRequest) { + // Data + GetInventoryRequest getInventoryRequest = (GetInventoryRequest) networkEnvelope; + Map dataObjects = new HashMap<>(); + p2PDataStorage.getMapForDataResponse(getInventoryRequest.getVersion()).values().stream() + .map(e -> e.getClass().getSimpleName()) + .forEach(className -> addClassNameToMap(dataObjects, className)); + p2PDataStorage.getMap().values().stream() + .map(ProtectedStorageEntry::getProtectedStoragePayload) + .map(e -> e.getClass().getSimpleName()) + .forEach(className -> addClassNameToMap(dataObjects, className)); + Map inventory = new HashMap<>(); + dataObjects.forEach((key, value) -> inventory.put(key, String.valueOf(value))); + + // DAO + int numBsqBlocks = daoStateService.getBlocks().size(); + inventory.put(InventoryItem.numBsqBlocks, String.valueOf(numBsqBlocks)); + + int daoStateChainHeight = daoStateService.getChainHeight(); + inventory.put(InventoryItem.daoStateChainHeight, String.valueOf(daoStateChainHeight)); + + LinkedList daoStateBlockChain = daoStateMonitoringService.getDaoStateBlockChain(); + if (!daoStateBlockChain.isEmpty()) { + String daoStateHash = Utilities.bytesAsHexString(daoStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.daoStateHash, daoStateHash); + } + + LinkedList proposalStateBlockChain = proposalStateMonitoringService.getProposalStateBlockChain(); + if (!proposalStateBlockChain.isEmpty()) { + String proposalHash = Utilities.bytesAsHexString(proposalStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.proposalHash, proposalHash); + } + + LinkedList blindVoteStateBlockChain = blindVoteStateMonitoringService.getBlindVoteStateBlockChain(); + if (!blindVoteStateBlockChain.isEmpty()) { + String blindVoteHash = Utilities.bytesAsHexString(blindVoteStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.blindVoteHash, blindVoteHash); + } + + // network + inventory.put(InventoryItem.maxConnections, String.valueOf(maxConnections)); + inventory.put(InventoryItem.numConnections, String.valueOf(networkNode.getAllConnections().size())); + inventory.put(InventoryItem.peakNumConnections, String.valueOf(peerManager.getPeakNumConnections())); + inventory.put(InventoryItem.numAllConnectionsLostEvents, String.valueOf(peerManager.getNumAllConnectionsLostEvents())); + peerManager.maybeResetNumAllConnectionsLostEvents(); + inventory.put(InventoryItem.sentBytes, String.valueOf(Statistic.totalSentBytesProperty().get())); + inventory.put(InventoryItem.sentBytesPerSec, String.valueOf(Statistic.totalSentBytesPerSecProperty().get())); + inventory.put(InventoryItem.receivedBytes, String.valueOf(Statistic.totalReceivedBytesProperty().get())); + inventory.put(InventoryItem.receivedBytesPerSec, String.valueOf(Statistic.totalReceivedBytesPerSecProperty().get())); + inventory.put(InventoryItem.receivedMessagesPerSec, String.valueOf(Statistic.numTotalReceivedMessagesPerSecProperty().get())); + inventory.put(InventoryItem.sentMessagesPerSec, String.valueOf(Statistic.numTotalSentMessagesPerSecProperty().get())); + + // node + inventory.put(InventoryItem.version, Version.VERSION); + inventory.put(InventoryItem.commitHash, RequestInfo.COMMIT_HASH); + inventory.put(InventoryItem.usedMemory, String.valueOf(Profiler.getUsedMemoryInBytes())); + inventory.put(InventoryItem.jvmStartTime, String.valueOf(ManagementFactory.getRuntimeMXBean().getStartTime())); + + Filter filter = filterManager.getFilter(); + if (filter != null) { + inventory.put(InventoryItem.filteredSeeds, Joiner.on("," + System.getProperty("line.separator")).join(filter.getSeedNodes())); + } + + log.info("Send inventory {} to {}", inventory, connection.getPeersNodeAddressOptional()); + GetInventoryResponse getInventoryResponse = new GetInventoryResponse(inventory); + networkNode.sendMessage(connection, getInventoryResponse); + } + } + + public void shutDown() { + networkNode.removeMessageListener(this); + } + + private void addClassNameToMap(Map dataObjects, String className) { + Optional optionalEnum = Enums.getIfPresent(InventoryItem.class, className); + if (optionalEnum.isPresent()) { + InventoryItem key = optionalEnum.get(); + dataObjects.putIfAbsent(key, 0); + int prev = dataObjects.get(key); + dataObjects.put(key, prev + 1); + } + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java new file mode 100644 index 0000000000..816bb786fe --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.network.p2p.inventory; + +import bisq.core.network.p2p.inventory.model.InventoryItem; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.handlers.ErrorMessageHandler; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GetInventoryRequestManager { + private final NetworkNode networkNode; + private final Map requesterMap = new HashMap<>(); + + @Inject + public GetInventoryRequestManager(NetworkNode networkNode) { + this.networkNode = networkNode; + } + + public void request(NodeAddress nodeAddress, + Consumer> resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (requesterMap.containsKey(nodeAddress)) { + log.warn("There was still a pending request for {}. We shut it down and make a new request", + nodeAddress.getFullAddress()); + requesterMap.get(nodeAddress).shutDown(); + } + + GetInventoryRequester getInventoryRequester = new GetInventoryRequester(networkNode, + nodeAddress, + resultMap -> { + requesterMap.remove(nodeAddress); + resultHandler.accept(resultMap); + }, + errorMessage -> { + requesterMap.remove(nodeAddress); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + requesterMap.put(nodeAddress, getInventoryRequester); + getInventoryRequester.request(); + } + + public void shutDown() { + requesterMap.values().forEach(GetInventoryRequester::shutDown); + requesterMap.clear(); + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java new file mode 100644 index 0000000000..02ea7596b3 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.core.network.p2p.inventory; + +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.core.network.p2p.inventory.model.InventoryItem; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.ConnectionListener; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.Map; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GetInventoryRequester implements MessageListener, ConnectionListener { + private final static int TIMEOUT_SEC = 180; + + private final NetworkNode networkNode; + private final NodeAddress nodeAddress; + private final Consumer> resultHandler; + private final ErrorMessageHandler errorMessageHandler; + private Timer timer; + + public GetInventoryRequester(NetworkNode networkNode, + NodeAddress nodeAddress, + Consumer> resultHandler, + ErrorMessageHandler errorMessageHandler) { + this.networkNode = networkNode; + this.nodeAddress = nodeAddress; + this.resultHandler = resultHandler; + this.errorMessageHandler = errorMessageHandler; + } + + public void request() { + networkNode.addMessageListener(this); + networkNode.addConnectionListener(this); + + timer = UserThread.runAfter(this::onTimeOut, TIMEOUT_SEC); + + GetInventoryRequest getInventoryRequest = new GetInventoryRequest(Version.VERSION); + networkNode.sendMessage(nodeAddress, getInventoryRequest); + } + + private void onTimeOut() { + errorMessageHandler.handleErrorMessage("Request timeout"); + shutDown(); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetInventoryResponse) { + connection.getPeersNodeAddressOptional().ifPresent(peer -> { + if (peer.equals(nodeAddress)) { + GetInventoryResponse getInventoryResponse = (GetInventoryResponse) networkEnvelope; + resultHandler.accept(getInventoryResponse.getInventory()); + shutDown(); + + // We shut down our connection after work as our node is not helpful for the network. + UserThread.runAfter(() -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER), 1); + } + }); + } + } + + public void shutDown() { + if (timer != null) { + timer.stop(); + timer = null; + } + networkNode.removeMessageListener(this); + networkNode.removeConnectionListener(this); + } + + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, + Connection connection) { + connection.getPeersNodeAddressOptional().ifPresent(address -> { + if (address.equals(nodeAddress)) { + if (!closeConnectionReason.isIntended) { + errorMessageHandler.handleErrorMessage("Connected closed because of " + closeConnectionReason.name()); + } + shutDown(); + } + }); + } + + @Override + public void onError(Throwable throwable) { + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java new file mode 100644 index 0000000000..7b46d04fec --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.core.network.p2p.inventory.messages; + + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode(callSuper = false) +@ToString +public class GetInventoryRequest extends NetworkEnvelope { + private final String version; + + public GetInventoryRequest(String version) { + this(version, Version.getP2PMessageVersion()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetInventoryRequest(String version, int messageVersion) { + super(messageVersion); + + this.version = version; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetInventoryRequest(protobuf.GetInventoryRequest.newBuilder() + .setVersion(version)) + .build(); + } + + public static GetInventoryRequest fromProto(protobuf.GetInventoryRequest proto, int messageVersion) { + return new GetInventoryRequest(proto.getVersion(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java new file mode 100644 index 0000000000..2dd79bfc1d --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.network.p2p.inventory.messages; + +import bisq.core.network.p2p.inventory.model.InventoryItem; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import com.google.common.base.Enums; +import com.google.common.base.Optional; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode(callSuper = false) +@ToString +public class GetInventoryResponse extends NetworkEnvelope { + private final Map inventory; + + public GetInventoryResponse(Map inventory) { + this(inventory, Version.getP2PMessageVersion()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetInventoryResponse(Map inventory, int messageVersion) { + super(messageVersion); + + this.inventory = inventory; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + // For protobuf we use a map with a string key + Map map = new HashMap<>(); + inventory.forEach((key, value) -> map.put(key.getKey(), value)); + return getNetworkEnvelopeBuilder() + .setGetInventoryResponse(protobuf.GetInventoryResponse.newBuilder() + .putAllInventory(map)) + .build(); + } + + public static GetInventoryResponse fromProto(protobuf.GetInventoryResponse proto, int messageVersion) { + // For protobuf we use a map with a string key + Map map = proto.getInventoryMap(); + Map inventory = new HashMap<>(); + map.forEach((key, value) -> { + Optional optional = Enums.getIfPresent(InventoryItem.class, key); + if (optional.isPresent()) { + inventory.put(optional.get(), value); + } + }); + return new GetInventoryResponse(inventory, messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java new file mode 100644 index 0000000000..3f50d91c1c --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class Average { + public static Map of(Set requestInfoSet) { + Map averageValuesPerItem = new HashMap<>(); + Arrays.asList(InventoryItem.values()).forEach(inventoryItem -> { + if (inventoryItem.isNumberValue()) { + averageValuesPerItem.put(inventoryItem, getAverage(requestInfoSet, inventoryItem)); + } + }); + return averageValuesPerItem; + } + + public static double getAverage(Set requestInfoSet, InventoryItem inventoryItem) { + return requestInfoSet.stream() + .map(RequestInfo::getDataMap) + .filter(map -> map.containsKey(inventoryItem)) + .map(map -> map.get(inventoryItem).getValue()) + .filter(Objects::nonNull) + .mapToDouble(Double::parseDouble) + .average() + .orElse(0d); + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java new file mode 100644 index 0000000000..68531c4221 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +import bisq.common.util.Tuple2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public class DeviationByIntegerDiff implements DeviationType { + private final int warnTrigger; + private final int alertTrigger; + + public DeviationByIntegerDiff(int warnTrigger, int alertTrigger) { + this.warnTrigger = warnTrigger; + this.alertTrigger = alertTrigger; + } + + public DeviationSeverity getDeviationSeverity(Collection> collection, + @Nullable String value, + InventoryItem inventoryItem) { + DeviationSeverity deviationSeverity = DeviationSeverity.OK; + if (value == null) { + return deviationSeverity; + } + + Map sameItemsByValue = new HashMap<>(); + collection.stream() + .filter(list -> !list.isEmpty()) + .map(list -> list.get(list.size() - 1)) // We use last item only + .map(RequestInfo::getDataMap) + .map(e -> e.get(inventoryItem).getValue()) + .filter(Objects::nonNull) + .forEach(e -> { + sameItemsByValue.putIfAbsent(e, 0); + int prev = sameItemsByValue.get(e); + sameItemsByValue.put(e, prev + 1); + }); + if (sameItemsByValue.size() > 1) { + List> sameItems = new ArrayList<>(); + sameItemsByValue.forEach((k, v) -> sameItems.add(new Tuple2<>(k, v))); + sameItems.sort(Comparator.comparing(o -> o.second)); + Collections.reverse(sameItems); + String majority = sameItems.get(0).first; + if (!majority.equals(value)) { + int majorityAsInt = Integer.parseInt(majority); + int valueAsInt = Integer.parseInt(value); + int diff = Math.abs(majorityAsInt - valueAsInt); + if (diff >= alertTrigger) { + deviationSeverity = DeviationSeverity.ALERT; + } else if (diff >= warnTrigger) { + deviationSeverity = DeviationSeverity.WARN; + } else { + deviationSeverity = DeviationSeverity.OK; + } + } + } + return deviationSeverity; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java new file mode 100644 index 0000000000..1887aa0409 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +public class DeviationByPercentage implements DeviationType { + private final double lowerAlertTrigger; + private final double upperAlertTrigger; + private final double lowerWarnTrigger; + private final double upperWarnTrigger; + + // In case want to see the % deviation but not trigger any warnings or alerts + public DeviationByPercentage() { + this(0, Double.MAX_VALUE, 0, Double.MAX_VALUE); + } + + public DeviationByPercentage(double lowerAlertTrigger, + double upperAlertTrigger, + double lowerWarnTrigger, + double upperWarnTrigger) { + this.lowerAlertTrigger = lowerAlertTrigger; + this.upperAlertTrigger = upperAlertTrigger; + this.lowerWarnTrigger = lowerWarnTrigger; + this.upperWarnTrigger = upperWarnTrigger; + } + + public DeviationSeverity getDeviationSeverity(double deviation) { + if (deviation <= lowerAlertTrigger || deviation >= upperAlertTrigger) { + return DeviationSeverity.ALERT; + } + + if (deviation <= lowerWarnTrigger || deviation >= upperWarnTrigger) { + return DeviationSeverity.WARN; + } + + return DeviationSeverity.OK; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java new file mode 100644 index 0000000000..6ca4afc76f --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +import bisq.common.util.Tuple2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public class DeviationOfHashes implements DeviationType { + public DeviationSeverity getDeviationSeverity(Collection> collection, + @Nullable String value, + InventoryItem inventoryItem, + String currentBlockHeight) { + DeviationSeverity deviationSeverity = DeviationSeverity.OK; + if (value == null) { + return deviationSeverity; + } + + Map sameHashesPerHashListByHash = new HashMap<>(); + collection.stream() + .filter(list -> !list.isEmpty()) + .map(list -> list.get(list.size() - 1)) // We use last item only + .map(RequestInfo::getDataMap) + .filter(map -> currentBlockHeight.equals(map.get(InventoryItem.daoStateChainHeight).getValue())) + .map(map -> map.get(inventoryItem).getValue()) + .filter(Objects::nonNull) + .forEach(v -> { + sameHashesPerHashListByHash.putIfAbsent(v, 0); + int prev = sameHashesPerHashListByHash.get(v); + sameHashesPerHashListByHash.put(v, prev + 1); + }); + if (sameHashesPerHashListByHash.size() > 1) { + List> sameHashesPerHashList = new ArrayList<>(); + sameHashesPerHashListByHash.forEach((k, v) -> sameHashesPerHashList.add(new Tuple2<>(k, v))); + sameHashesPerHashList.sort(Comparator.comparing(o -> o.second)); + Collections.reverse(sameHashesPerHashList); + + // It could be that first and any following list entry has same number of hashes, but we ignore that as + // it is reason enough to alert the operators in case not all hashes are the same. + if (sameHashesPerHashList.get(0).first.equals(value)) { + // We are in the majority group. + // We also set a warning to make sure the operators act quickly and to check if there are + // more severe issues. + deviationSeverity = DeviationSeverity.WARN; + } else { + deviationSeverity = DeviationSeverity.ALERT; + } + } + return deviationSeverity; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java new file mode 100644 index 0000000000..866d89eac8 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +public enum DeviationSeverity { + IGNORED, + OK, + WARN, + ALERT +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java new file mode 100644 index 0000000000..565292eee7 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +public interface DeviationType { +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java new file mode 100644 index 0000000000..4f91a2023b --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java @@ -0,0 +1,191 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +import bisq.common.util.Tuple2; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum InventoryItem { + // Percentage deviation + OfferPayload("OfferPayload", + true, + new DeviationByPercentage(0.5, 1.5, 0.75, 1.25), 10), + MailboxStoragePayload("MailboxStoragePayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), + TradeStatistics3("TradeStatistics3", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), + AccountAgeWitness("AccountAgeWitness", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), + SignedWitness("SignedWitness", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), + + // Should be same value + Alert("Alert", + true, + new DeviationByIntegerDiff(1, 1), 2), + Filter("Filter", + true, + new DeviationByIntegerDiff(1, 1), 2), + Mediator("Mediator", + true, + new DeviationByIntegerDiff(1, 1), 2), + RefundAgent("RefundAgent", + true, + new DeviationByIntegerDiff(1, 1), 2), + + // Should be very close values + TempProposalPayload("TempProposalPayload", + true, + new DeviationByIntegerDiff(3, 5), 2), + ProposalPayload("ProposalPayload", + true, + new DeviationByIntegerDiff(1, 2), 2), + BlindVotePayload("BlindVotePayload", + true, + new DeviationByIntegerDiff(1, 2), 2), + + // Should be very close values + daoStateChainHeight("daoStateChainHeight", + true, + new DeviationByIntegerDiff(2, 4), 3), + numBsqBlocks("numBsqBlocks", + true, + new DeviationByIntegerDiff(2, 4), 3), + + // Has to be same values at same block + daoStateHash("daoStateHash", + false, + new DeviationOfHashes(), 1), + proposalHash("proposalHash", + false, + new DeviationOfHashes(), 1), + blindVoteHash("blindVoteHash", + false, + new DeviationOfHashes(), 1), + + // Percentage deviation + maxConnections("maxConnections", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5), 2), + numConnections("numConnections", + true, + new DeviationByPercentage(0, 3, 0, 2.5), 2), + peakNumConnections("peakNumConnections", + true, + new DeviationByPercentage(0, 3, 0, 2.5), 2), + numAllConnectionsLostEvents("numAllConnectionsLostEvents", + true, + new DeviationByIntegerDiff(1, 2), 1), + sentBytesPerSec("sentBytesPerSec", + true, + new DeviationByPercentage(), 5), + receivedBytesPerSec("receivedBytesPerSec", + true, + new DeviationByPercentage(), 5), + receivedMessagesPerSec("receivedMessagesPerSec", + true, + new DeviationByPercentage(), 5), + sentMessagesPerSec("sentMessagesPerSec", + true, + new DeviationByPercentage(), 5), + + // No deviation check + sentBytes("sentBytes", true), + receivedBytes("receivedBytes", true), + + // No deviation check + version("version", false), + commitHash("commitHash", false), + usedMemory("usedMemory", true), + jvmStartTime("jvmStartTime", true), + filteredSeeds("filteredSeeds", false); + + @Getter + private final String key; + @Getter + private final boolean isNumberValue; + @Getter + @Nullable + private DeviationType deviationType; + + // The number of past requests we check to see if there have been repeated alerts or warnings. The higher the + // number the more repeated alert need to have happened to cause a notification alert. + // Smallest number is 1, as that takes only the last request data and does not look further back. + @Getter + private int deviationTolerance = 1; + + InventoryItem(String key, boolean isNumberValue) { + this.key = key; + this.isNumberValue = isNumberValue; + } + + InventoryItem(String key, boolean isNumberValue, @NotNull DeviationType deviationType, int deviationTolerance) { + this(key, isNumberValue); + this.deviationType = deviationType; + this.deviationTolerance = deviationTolerance; + } + + @Nullable + public Tuple2 getDeviationAndAverage(Map averageValues, + @Nullable String value) { + if (averageValues.containsKey(this) && value != null) { + double averageValue = averageValues.get(this); + return new Tuple2<>(getDeviation(value, averageValue), averageValue); + } + return null; + } + + @Nullable + public Double getDeviation(@Nullable String value, double average) { + if (deviationType != null && value != null && average != 0 && isNumberValue) { + return Double.parseDouble(value) / average; + } + return null; + } + + public DeviationSeverity getDeviationSeverity(Double deviation, + Collection> collection, + @Nullable String value, + String currentBlockHeight) { + if (deviationType == null || deviation == null || value == null) { + return DeviationSeverity.OK; + } + + if (deviationType instanceof DeviationByPercentage) { + return ((DeviationByPercentage) deviationType).getDeviationSeverity(deviation); + } else if (deviationType instanceof DeviationByIntegerDiff) { + return ((DeviationByIntegerDiff) deviationType).getDeviationSeverity(collection, value, this); + } else if (deviationType instanceof DeviationOfHashes) { + return ((DeviationOfHashes) deviationType).getDeviationSeverity(collection, value, this, currentBlockHeight); + } else { + return DeviationSeverity.OK; + } + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java new file mode 100644 index 0000000000..c55a9e04ee --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.network.p2p.inventory.model; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; +import lombok.Value; + +import org.jetbrains.annotations.Nullable; + +@Getter +public class RequestInfo { + // Carries latest commit hash of feature changes (not latest commit as that is then the commit for editing that field) + public static final String COMMIT_HASH = "c07d47a8"; + + private final long requestStartTime; + @Setter + private long responseTime; + @Nullable + @Setter + private String errorMessage; + + private final Map dataMap = new HashMap<>(); + + public RequestInfo(long requestStartTime) { + this.requestStartTime = requestStartTime; + } + + public String getDisplayValue(InventoryItem inventoryItem) { + String value = getValue(inventoryItem); + return value != null ? value : "n/a"; + } + + @Nullable + public String getValue(InventoryItem inventoryItem) { + return dataMap.containsKey(inventoryItem) ? + dataMap.get(inventoryItem).getValue() : + null; + } + + public boolean hasError() { + return errorMessage != null && !errorMessage.isEmpty(); + } + + @Value + public static class Data { + private final String value; + @Nullable + private final Double average; + private final Double deviation; + private final DeviationSeverity deviationSeverity; + private final boolean persistentWarning; + private final boolean persistentAlert; + + public Data(String value, + @Nullable Double average, + Double deviation, + DeviationSeverity deviationSeverity, + boolean persistentWarning, + boolean persistentAlert) { + this.value = value; + this.average = average; + this.deviation = deviation; + this.deviationSeverity = deviationSeverity; + this.persistentWarning = persistentWarning; + this.persistentAlert = persistentAlert; + } + + @Override + public String toString() { + return "InventoryData{" + + "\n value='" + value + '\'' + + ",\n average=" + average + + ",\n deviation=" + deviation + + ",\n deviationSeverity=" + deviationSeverity + + ",\n persistentWarning=" + persistentWarning + + ",\n persistentAlert=" + persistentAlert + + "\n}"; + } + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java new file mode 100644 index 0000000000..4c52456fb6 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java @@ -0,0 +1,127 @@ +/* + * 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 bisq.core.network.p2p.seed; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.seed.SeedNodeRepository; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +// If a new BaseCurrencyNetwork type gets added we need to add the resource file for it as well! +@Slf4j +@Singleton +public class DefaultSeedNodeRepository implements SeedNodeRepository { + //TODO add support for localhost addresses + private static final Pattern pattern = Pattern.compile("^([a-z0-9]+\\.onion:\\d+)"); + private static final String ENDING = ".seednodes"; + private final Collection cache = new HashSet<>(); + private final Config config; + + @Inject + public DefaultSeedNodeRepository(Config config) { + this.config = config; + } + + private void reload() { + try { + // see if there are any seed nodes configured manually + if (!config.seedNodes.isEmpty()) { + cache.clear(); + config.seedNodes.forEach(s -> cache.add(new NodeAddress(s))); + + return; + } + + cache.clear(); + List result = getSeedNodeAddressesFromPropertyFile(config.baseCurrencyNetwork.name().toLowerCase()); + cache.addAll(result); + + // filter + cache.removeAll( + config.bannedSeedNodes.stream() + .filter(n -> !n.isEmpty()) + .map(NodeAddress::new) + .collect(Collectors.toSet())); + + log.info("Seed nodes: {}", cache); + } catch (Throwable t) { + log.error("exception in DefaultSeedNodeRepository", t); + t.printStackTrace(); + throw t; + } + } + + public static Optional readSeedNodePropertyFile(String fileName) { + InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream( + fileName + ENDING); + if (fileInputStream == null) { + return Optional.empty(); + } + return Optional.of(new BufferedReader(new InputStreamReader(fileInputStream))); + } + + public static List getSeedNodeAddressesFromPropertyFile(String fileName) { + List list = new ArrayList<>(); + readSeedNodePropertyFile(fileName).ifPresent(seedNodeFile -> { + seedNodeFile.lines().forEach(line -> { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) + list.add(new NodeAddress(matcher.group(1))); + + // Maybe better include in regex... + if (line.startsWith("localhost")) { + String[] strings = line.split(" \\(@"); + String node = strings[0]; + list.add(new NodeAddress(node)); + } + }); + }); + return list; + } + + public Collection getSeedNodeAddresses() { + if (cache.isEmpty()) + reload(); + + return cache; + } + + public boolean isSeedNode(NodeAddress nodeAddress) { + if (cache.isEmpty()) + reload(); + return cache.contains(nodeAddress); + } +} diff --git a/core/src/main/java/bisq/core/notifications/MobileMessage.java b/core/src/main/java/bisq/core/notifications/MobileMessage.java new file mode 100644 index 0000000000..1d12239031 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileMessage.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.notifications; + +import bisq.common.util.JsonExclude; + +import java.util.Date; + +import lombok.Value; + +@Value +public class MobileMessage { + private long sentDate; + private String txId; + private String title; + private String message; + @JsonExclude + transient private MobileMessageType mobileMessageType; + private String type; + private String actionRequired; + private int version; + + public MobileMessage(String title, String message, MobileMessageType mobileMessageType) { + this(title, message, "", mobileMessageType); + } + + public MobileMessage(String title, String message, String txId, MobileMessageType mobileMessageType) { + this.title = title; + this.message = message; + this.txId = txId; + this.mobileMessageType = mobileMessageType; + + this.type = mobileMessageType.name(); + actionRequired = ""; + sentDate = new Date().getTime(); + version = 1; + } +} diff --git a/core/src/main/java/bisq/core/notifications/MobileMessageEncryption.java b/core/src/main/java/bisq/core/notifications/MobileMessageEncryption.java new file mode 100644 index 0000000000..37ddfe88d4 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileMessageEncryption.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.notifications; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.base.Charsets; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import java.security.NoSuchAlgorithmException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class MobileMessageEncryption { + private SecretKeySpec keySpec; + private Cipher cipher; + + @Inject + public MobileMessageEncryption() { + } + + public void setKey(String key) { + keySpec = new SecretKeySpec(key.getBytes(Charsets.UTF_8), "AES"); + try { + cipher = Cipher.getInstance("AES/CBC/NOPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + e.printStackTrace(); + } + } + + public String encrypt(String valueToEncrypt, String iv) throws Exception { + while (valueToEncrypt.length() % 16 != 0) { + valueToEncrypt = valueToEncrypt + " "; + } + + if (iv.length() != 16) { + throw new Exception("iv not 16 characters"); + } + IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(Charsets.UTF_8)); + byte[] encryptedBytes = doEncrypt(valueToEncrypt, ivSpec); + return Base64.encodeBase64String(encryptedBytes); + } + + private byte[] doEncrypt(String text, IvParameterSpec ivSpec) throws Exception { + if (text == null || text.length() == 0) { + throw new Exception("Empty string"); + } + + byte[] encrypted; + try { + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + encrypted = cipher.doFinal(text.getBytes(Charsets.UTF_8)); + } catch (Exception e) { + throw new Exception("[encrypt] " + e.getMessage()); + } + return encrypted; + } +} diff --git a/core/src/main/java/bisq/core/notifications/MobileMessageType.java b/core/src/main/java/bisq/core/notifications/MobileMessageType.java new file mode 100644 index 0000000000..5562fac805 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileMessageType.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.notifications; + +public enum MobileMessageType { + SETUP_CONFIRMATION, + OFFER, + TRADE, + DISPUTE, + PRICE, + MARKET, + ERASE +} diff --git a/core/src/main/java/bisq/core/notifications/MobileModel.java b/core/src/main/java/bisq/core/notifications/MobileModel.java new file mode 100644 index 0000000000..acc11f2483 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileModel.java @@ -0,0 +1,166 @@ +/* + * 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 bisq.core.notifications; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.Arrays; + +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Data +@Slf4j +@Singleton +public class MobileModel { + public static final String PHONE_SEPARATOR_ESCAPED = "\\|"; // see https://stackoverflow.com/questions/5675704/java-string-split-not-returning-the-right-values + public static final String PHONE_SEPARATOR_WRITING = "|"; + + public enum OS { + UNDEFINED(""), + IOS("iOS"), + IOS_DEV("iOSDev"), + ANDROID("android"); + + @Getter + private String magicString; + + OS(String magicString) { + this.magicString = magicString; + } + } + + @Nullable + private OS os; + @Nullable + private String descriptor; + @Nullable + private String key; + @Nullable + private String token; + private boolean isContentAvailable = true; + + @Inject + public MobileModel() { + } + + public void reset() { + os = null; + key = null; + token = null; + } + + public void applyKeyAndToken(String keyAndToken) { + log.info("applyKeyAndToken: keyAndToken={}", keyAndToken.substring(0, 20) + "...(truncated in log for privacy reasons)"); + String[] tokens = keyAndToken.split(PHONE_SEPARATOR_ESCAPED); + String magic = tokens[0]; + descriptor = tokens[1]; + key = tokens[2]; + token = tokens[3]; + if (magic.equals(OS.IOS.getMagicString())) + os = OS.IOS; + else if (magic.equals(OS.IOS_DEV.getMagicString())) + os = OS.IOS_DEV; + else if (magic.equals(OS.ANDROID.getMagicString())) + os = OS.ANDROID; + + isContentAvailable = parseDescriptor(descriptor); + } + + @VisibleForTesting + boolean parseDescriptor(String descriptor) { + // phone descriptors + /* + iPod Touch 5 + iPod Touch 6 + iPhone 4 + iPhone 4s + iPhone 5 + iPhone 5c + iPhone 5s + iPhone 6 + iPhone 6 Plus + iPhone 6s + iPhone 6s Plus + iPhone 7 + iPhone 7 Plus + iPhone SE + iPhone 8 + iPhone 8 Plus + iPhone X + iPhone XS + iPhone XS Max + iPhone XR + iPhone 11 + iPhone 11 Pro + iPhone 11 Pro Max + iPad 2 + iPad 3 + iPad 4 + iPad Air + iPad Air 2 + iPad 5 + iPad 6 + iPad Mini + iPad Mini 2 + iPad Mini 3 + iPad Mini 4 + iPad Pro 9.7 Inch + iPad Pro 12.9 Inch + iPad Pro 12.9 Inch 2. Generation + iPad Pro 10.5 Inch + */ + // iPhone 6 does not support isContentAvailable, iPhone 6s and 7 does. + // We don't know about other versions, but let's assume all above iPhone 6 are ok. + if (descriptor != null) { + String[] descriptorTokens = descriptor.split(" "); + if (descriptorTokens.length >= 1) { + String model = descriptorTokens[0]; + if (model.equals("iPhone")) { + String versionString = descriptorTokens[1]; + String[] validVersions = {"X", "XS", "XR"}; + if (Arrays.asList(validVersions).contains(versionString)) { + return true; + } + String versionSuffix = ""; + if (versionString.matches("\\d[^\\d]")) { + versionSuffix = versionString.substring(1); + versionString = versionString.substring(0, 1); + } else if (versionString.matches("\\d{2}[^\\d]")) { + versionSuffix = versionString.substring(2); + versionString = versionString.substring(0, 2); + } + try { + int version = Integer.parseInt(versionString); + return version > 6 || (version == 6 && versionSuffix.equalsIgnoreCase("s")); + } catch (Throwable ignore) { + } + } else { + return (model.equals("iPad")) && descriptorTokens[1].equals("Pro"); + } + } + } + return false; + } +} diff --git a/core/src/main/java/bisq/core/notifications/MobileNotificationService.java b/core/src/main/java/bisq/core/notifications/MobileNotificationService.java new file mode 100644 index 0000000000..76ba9313df --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileNotificationService.java @@ -0,0 +1,324 @@ +/* + * 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 bisq.core.notifications; + +import bisq.core.user.Preferences; + +import bisq.network.http.HttpClient; + +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.Utilities; + +import com.google.gson.Gson; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import javax.inject.Named; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import org.apache.commons.codec.binary.Hex; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import java.util.UUID; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public class MobileNotificationService { + // Used in Relay app to response of a success state. We won't want a code dependency just for that string so we keep it + // duplicated in relay and here. Must not be changed. + private static final String SUCCESS = "success"; + private static final String DEV_URL_LOCALHOST = "http://localhost:8080/"; + private static final String DEV_URL = "http://165.227.40.124:8080/"; + private static final String URL = "http://jtboonrvwmq7frkj.onion/"; + private static final String BISQ_MESSAGE_IOS_MAGIC = "BisqMessageiOS"; + private static final String BISQ_MESSAGE_ANDROID_MAGIC = "BisqMessageAndroid"; + + private final Preferences preferences; + private final MobileMessageEncryption mobileMessageEncryption; + private final MobileNotificationValidator mobileNotificationValidator; + private final HttpClient httpClient; + + private final ListeningExecutorService executorService = Utilities.getListeningExecutorService( + "MobileNotificationService", 10, 15, 10 * 60); + @Getter + private final MobileModel mobileModel; + + @Getter + private boolean setupConfirmationSent; + @Getter + private BooleanProperty useSoundProperty = new SimpleBooleanProperty(); + @Getter + private BooleanProperty useTradeNotificationsProperty = new SimpleBooleanProperty(); + @Getter + private BooleanProperty useMarketNotificationsProperty = new SimpleBooleanProperty(); + @Getter + private BooleanProperty usePriceNotificationsProperty = new SimpleBooleanProperty(); + + @Inject + public MobileNotificationService(Preferences preferences, + MobileMessageEncryption mobileMessageEncryption, + MobileNotificationValidator mobileNotificationValidator, + MobileModel mobileModel, + HttpClient httpClient, + @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalHost) { + this.preferences = preferences; + this.mobileMessageEncryption = mobileMessageEncryption; + this.mobileNotificationValidator = mobileNotificationValidator; + this.httpClient = httpClient; + this.mobileModel = mobileModel; + + // httpClient.setBaseUrl(useLocalHost ? DEV_URL_LOCALHOST : URL); + + httpClient.setBaseUrl(useLocalHost ? DEV_URL : URL); + httpClient.setIgnoreSocks5Proxy(false); + } + + public void onAllServicesInitialized() { + String keyAndToken = preferences.getPhoneKeyAndToken(); + if (mobileNotificationValidator.isValid(keyAndToken)) { + setupConfirmationSent = true; + mobileModel.applyKeyAndToken(keyAndToken); + mobileMessageEncryption.setKey(mobileModel.getKey()); + } + useTradeNotificationsProperty.set(preferences.isUseTradeNotifications()); + useMarketNotificationsProperty.set(preferences.isUseMarketNotifications()); + usePriceNotificationsProperty.set(preferences.isUsePriceNotifications()); + useSoundProperty.set(preferences.isUseSoundForMobileNotifications()); + } + + public boolean sendMessage(MobileMessage message) throws Exception { + return sendMessage(message, useSoundProperty.get()); + } + + public boolean applyKeyAndToken(String keyAndToken) { + if (mobileNotificationValidator.isValid(keyAndToken)) { + mobileModel.applyKeyAndToken(keyAndToken); + mobileMessageEncryption.setKey(mobileModel.getKey()); + preferences.setPhoneKeyAndToken(keyAndToken); + if (!setupConfirmationSent) { + try { + sendConfirmationMessage(); + setupConfirmationSent = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } else { + return false; + } + } + + /** + * + * @param message The message to send + * @param useSound If a sound should be used on the mobile device. + * @return Returns true if the message was sent. It does not reflect if the sending was successful. + * The result and error handlers carry that information. + * @throws Exception + */ + public boolean sendMessage(MobileMessage message, + boolean useSound) throws Exception { + return sendMessage(message, useSound, + result -> log.debug("sendMessage result=" + result), + throwable -> log.error("sendMessage failed. throwable=" + throwable.toString())); + } + + /** + * + * @param message The message to send + * @param useSound If a sound should be used on the mobile device. + * @param resultHandler The result of the send operation (sent on a custom thread) + * @param errorHandler Carries the throwable if an error occurred at sending (sent on a custom thread) + * @return Returns true if the message was sent. It does not reflect if the sending was successful. + * The result and error handlers carry that information. + * @throws Exception + */ + private boolean sendMessage(MobileMessage message, + boolean useSound, + Consumer resultHandler, + Consumer errorHandler) throws Exception { + if (mobileModel.getKey() == null) + return false; + + boolean doSend; + switch (message.getMobileMessageType()) { + case SETUP_CONFIRMATION: + doSend = true; + break; + case OFFER: + case TRADE: + case DISPUTE: + doSend = useTradeNotificationsProperty.get(); + break; + case PRICE: + doSend = usePriceNotificationsProperty.get(); + break; + case MARKET: + doSend = useMarketNotificationsProperty.get(); + break; + case ERASE: + doSend = true; + break; + default: + doSend = false; + } + + if (!doSend) + return false; + + log.info("Send message: '{}'", message.getMessage()); + + + log.info("sendMessage message={}", message); + Gson gson = new Gson(); + String json = gson.toJson(message); + log.info("json " + json); + + StringBuilder padded = new StringBuilder(json); + while (padded.length() % 16 != 0) { + padded.append(" "); + } + json = padded.toString(); + + // generate 16 random characters for iv + String uuid = UUID.randomUUID().toString(); + uuid = uuid.replace("-", ""); + String iv = uuid.substring(0, 16); + + String cipher = mobileMessageEncryption.encrypt(json, iv); + log.info("key = " + mobileModel.getKey()); + log.info("iv = " + iv); + log.info("encryptedJson = " + cipher); + + doSendMessage(iv, cipher, useSound, resultHandler, errorHandler); + return true; + } + + public void sendEraseMessage() throws Exception { + MobileMessage message = new MobileMessage("", + "", + MobileMessageType.ERASE); + sendMessage(message, false); + } + + public void reset() { + mobileModel.reset(); + preferences.setPhoneKeyAndToken(null); + setupConfirmationSent = false; + } + + + private void sendConfirmationMessage() throws Exception { + log.info("sendConfirmationMessage"); + MobileMessage message = new MobileMessage("", + "", + MobileMessageType.SETUP_CONFIRMATION); + sendMessage(message, true); + } + + private void doSendMessage(String iv, + String cipher, + boolean useSound, + Consumer resultHandler, + Consumer errorHandler) throws Exception { + if (httpClient.hasPendingRequest()) { + log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient); + return; + } + + String msg; + if (mobileModel.getOs() == null) + throw new RuntimeException("No mobileModel OS set"); + + switch (mobileModel.getOs()) { + case IOS: + msg = BISQ_MESSAGE_IOS_MAGIC; + break; + case IOS_DEV: + msg = BISQ_MESSAGE_IOS_MAGIC; + break; + case ANDROID: + msg = BISQ_MESSAGE_ANDROID_MAGIC; + break; + case UNDEFINED: + default: + throw new RuntimeException("No mobileModel OS set"); + } + msg += MobileModel.PHONE_SEPARATOR_WRITING + iv + MobileModel.PHONE_SEPARATOR_WRITING + cipher; + boolean isAndroid = mobileModel.getOs() == MobileModel.OS.ANDROID; + boolean isProduction = mobileModel.getOs() == MobileModel.OS.IOS; + + checkNotNull(mobileModel.getToken(), "mobileModel.getToken() must not be null"); + String tokenAsHex = Hex.encodeHexString(mobileModel.getToken().getBytes("UTF-8")); + String msgAsHex = Hex.encodeHexString(msg.getBytes("UTF-8")); + String param = "relay?" + + "isAndroid=" + isAndroid + + "&isProduction=" + isProduction + + "&isContentAvailable=" + mobileModel.isContentAvailable() + + "&snd=" + useSound + + "&token=" + tokenAsHex + "&" + + "msg=" + msgAsHex; + + log.info("Send: token={}", mobileModel.getToken()); + log.info("Send: msg={}", msg); + log.info("Send: isAndroid={}\nuseSound={}\ntokenAsHex={}\nmsgAsHex={}", + isAndroid, useSound, tokenAsHex, msgAsHex); + + String threadName = "sendMobileNotification-" + msgAsHex.substring(0, 5) + "..."; + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName(threadName); + String result = httpClient.get(param, "User-Agent", + "bisq/" + Version.VERSION + ", uid:" + httpClient.getUid()); + log.info("sendMobileNotification result: " + result); + checkArgument(result.equals(SUCCESS), "Result was not 'success'. result=" + result); + return result; + }); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(String result) { + UserThread.execute(() -> resultHandler.accept(result)); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> errorHandler.accept(throwable)); + } + }, MoreExecutors.directExecutor()); + } +} diff --git a/core/src/main/java/bisq/core/notifications/MobileNotificationValidator.java b/core/src/main/java/bisq/core/notifications/MobileNotificationValidator.java new file mode 100644 index 0000000000..5cf6f2aeb5 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/MobileNotificationValidator.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.core.notifications; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class MobileNotificationValidator { + @Inject + public MobileNotificationValidator() { + } + + public boolean isValid(String keyAndToken) { + if (keyAndToken == null) + return false; + + String[] tokens = keyAndToken.split(MobileModel.PHONE_SEPARATOR_ESCAPED); + if (tokens.length != 4) { + log.error("invalid pairing ID format: not 4 sections separated by " + MobileModel.PHONE_SEPARATOR_WRITING); + return false; + } + String magic = tokens[0]; + String key = tokens[2]; + String phoneId = tokens[3]; + + if (key.length() != 32) { + log.error("invalid pairing ID format: key not 32 bytes"); + return false; + } + + if (magic.equals(MobileModel.OS.IOS.getMagicString()) || + magic.equals(MobileModel.OS.IOS_DEV.getMagicString())) { + if (phoneId.length() != 64) { + log.error("invalid Bisq MobileModel ID format: iOS token not 64 bytes"); + return false; + } + } else if (magic.equals(MobileModel.OS.ANDROID.getMagicString())) { + if (phoneId.length() < 32) { + log.error("invalid Bisq MobileModel ID format: Android token too short (<32 bytes)"); + return false; + } + } else { + log.error("invalid Bisq MobileModel ID format"); + return false; + } + + return true; + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java new file mode 100644 index 0000000000..6f201a0aa3 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -0,0 +1,134 @@ +/* + * 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 bisq.core.notifications.alerts; + +import bisq.core.locale.Res; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileMessageType; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.messages.ChatMessage; + +import bisq.network.p2p.P2PService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class DisputeMsgEvents { + private final RefundManager refundManager; + private final MediationManager mediationManager; + private final P2PService p2PService; + private final MobileNotificationService mobileNotificationService; + + @Inject + public DisputeMsgEvents(RefundManager refundManager, + MediationManager mediationManager, + P2PService p2PService, + MobileNotificationService mobileNotificationService) { + this.refundManager = refundManager; + this.mediationManager = mediationManager; + this.p2PService = p2PService; + this.mobileNotificationService = mobileNotificationService; + } + + public void onAllServicesInitialized() { + refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(this::setDisputeListener); + } + }); + refundManager.getDisputesAsObservableList().forEach(this::setDisputeListener); + + mediationManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(this::setDisputeListener); + } + }); + mediationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); + + // We do not need a handling for unread messages as mailbox messages arrive later and will trigger the + // event listeners. But the existing messages are not causing a notification. + } + + public static MobileMessage getTestMsg() { + String shortId = UUID.randomUUID().toString().substring(0, 8); + return new MobileMessage(Res.get("account.notifications.dispute.message.title"), + Res.get("account.notifications.dispute.message.msg", shortId), + shortId, + MobileMessageType.DISPUTE); + } + + private void setDisputeListener(Dispute dispute) { + log.debug("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); + dispute.getChatMessages().addListener((ListChangeListener) c -> { + log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(chatMessage -> onChatMessage(chatMessage, dispute)); + } + }); + } + + private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { + if (chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { + return; + } + + // We only send msg in case we are not the sender + String shortId = chatMessage.getShortId(); + MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"), + Res.get("account.notifications.dispute.message.msg", shortId), + shortId, + MobileMessageType.DISPUTE); + try { + mobileNotificationService.sendMessage(message); + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); + } + + // We check at every new message if it might be a message sent after the dispute had been closed. If that is the + // case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute + // message arrived. + ObservableList chatMessages = dispute.getChatMessages(); + // If last message is not a result message we re-open as we might have received a new message from the + // trader/mediator/arbitrator who has reopened the case + if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) { + dispute.reOpen(); + if (dispute.getSupportType() == SupportType.MEDIATION) { + mediationManager.requestPersistence(); + } else if (dispute.getSupportType() == SupportType.REFUND) { + refundManager.requestPersistence(); + } + } + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java b/core/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java new file mode 100644 index 0000000000..4b208250dd --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.notifications.alerts; + +import bisq.core.locale.Res; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileMessageType; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ListChangeListener; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class MyOfferTakenEvents { + private final OpenOfferManager openOfferManager; + private final MobileNotificationService mobileNotificationService; + + @Inject + public MyOfferTakenEvents(OpenOfferManager openOfferManager, MobileNotificationService mobileNotificationService) { + this.openOfferManager = openOfferManager; + this.mobileNotificationService = mobileNotificationService; + } + + public void onAllServicesInitialized() { + openOfferManager.getObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasRemoved()) + c.getRemoved().forEach(this::onOpenOfferRemoved); + }); + openOfferManager.getObservableList().forEach(this::onOpenOfferRemoved); + } + + private void onOpenOfferRemoved(OpenOffer openOffer) { + OpenOffer.State state = openOffer.getState(); + if (state == OpenOffer.State.RESERVED) { + log.info("We got a offer removed. id={}, state={}", openOffer.getId(), state); + String shortId = openOffer.getShortId(); + MobileMessage message = new MobileMessage(Res.get("account.notifications.offer.message.title"), + Res.get("account.notifications.offer.message.msg", shortId), + shortId, + MobileMessageType.OFFER); + try { + mobileNotificationService.sendMessage(message); + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + } + + public static MobileMessage getTestMsg() { + String shortId = UUID.randomUUID().toString().substring(0, 8); + return new MobileMessage(Res.get("account.notifications.offer.message.title"), + Res.get("account.notifications.offer.message.msg", shortId), + shortId, + MobileMessageType.OFFER); + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java new file mode 100644 index 0000000000..7bfe955693 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java @@ -0,0 +1,128 @@ +/* + * 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 bisq.core.notifications.alerts; + +import bisq.core.locale.Res; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileMessageType; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; + +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ListChangeListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class TradeEvents { + private final PubKeyRing pubKeyRing; + private final TradeManager tradeManager; + private final MobileNotificationService mobileNotificationService; + + @Inject + public TradeEvents(TradeManager tradeManager, KeyRing keyRing, MobileNotificationService mobileNotificationService) { + this.tradeManager = tradeManager; + this.mobileNotificationService = mobileNotificationService; + this.pubKeyRing = keyRing.getPubKeyRing(); + } + + public void onAllServicesInitialized() { + tradeManager.getObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(this::setTradePhaseListener); + } + }); + tradeManager.getObservableList().forEach(this::setTradePhaseListener); + } + + private void setTradePhaseListener(Trade trade) { + log.info("We got a new trade. id={}", trade.getId()); + if (!trade.isPayoutPublished()) { + trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { + String msg = null; + String shortId = trade.getShortId(); + switch (newValue) { + case INIT: + case TAKER_FEE_PUBLISHED: + case DEPOSIT_PUBLISHED: + break; + case DEPOSIT_CONFIRMED: + if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing())) + msg = Res.get("account.notifications.trade.message.msg.conf", shortId); + break; + case FIAT_SENT: + // We only notify the seller + if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getSellerPubKeyRing())) + msg = Res.get("account.notifications.trade.message.msg.started", shortId); + break; + case FIAT_RECEIVED: + break; + case PAYOUT_PUBLISHED: + // We only notify the buyer + if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing())) + msg = Res.get("account.notifications.trade.message.msg.completed", shortId); + break; + case WITHDRAWN: + break; + } + if (msg != null) { + MobileMessage message = new MobileMessage(Res.get("account.notifications.trade.message.title"), + msg, + shortId, + MobileMessageType.TRADE); + try { + mobileNotificationService.sendMessage(message); + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + }); + } + } + + public static List getTestMessages() { + String shortId = UUID.randomUUID().toString().substring(0, 8); + List list = new ArrayList<>(); + list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), + Res.get("account.notifications.trade.message.msg.conf", shortId), + shortId, + MobileMessageType.TRADE)); + list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), + Res.get("account.notifications.trade.message.msg.started", shortId), + shortId, + MobileMessageType.TRADE)); + list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), + Res.get("account.notifications.trade.message.msg.completed", shortId), + shortId, + MobileMessageType.TRADE)); + return list; + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java new file mode 100644 index 0000000000..dc34de9eff --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.notifications.alerts.market; + +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +public class MarketAlertFilter implements PersistablePayload { + private PaymentAccount paymentAccount; + private int triggerValue; + private boolean isBuyOffer; + private List alertIds; + + + public MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer) { + this(paymentAccount, triggerValue, isBuyOffer, new ArrayList<>()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * + * @param paymentAccount // The payment account used for the filter + * @param triggerValue // Percentage distance from market price (100 for 1.00%) + * @param isBuyOffer // It the offer is a buy offer + * @param alertIds // List of offerIds for which we have sent already an alert + */ + private MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer, List alertIds) { + this.paymentAccount = paymentAccount; + this.triggerValue = triggerValue; + this.isBuyOffer = isBuyOffer; + this.alertIds = alertIds; + } + + @Override + public protobuf.MarketAlertFilter toProtoMessage() { + return protobuf.MarketAlertFilter.newBuilder() + .setPaymentAccount(paymentAccount.toProtoMessage()) + .setTriggerValue(triggerValue) + .setIsBuyOffer(isBuyOffer) + .addAllAlertIds(alertIds) + .build(); + } + + public static MarketAlertFilter fromProto(protobuf.MarketAlertFilter proto, CoreProtoResolver coreProtoResolver) { + List list = proto.getAlertIdsList().isEmpty() ? + new ArrayList<>() : new ArrayList<>(proto.getAlertIdsList()); + return new MarketAlertFilter(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver), + proto.getTriggerValue(), + proto.getIsBuyOffer(), + list); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addAlertId(String alertId) { + if (notContainsAlertId(alertId)) + alertIds.add(alertId); + } + + public boolean notContainsAlertId(String alertId) { + return !alertIds.contains(alertId); + } + + @Override + public String toString() { + return "MarketAlertFilter{" + + "\n paymentAccount=" + paymentAccount + + ",\n triggerValue=" + triggerValue + + ",\n isBuyOffer=" + isBuyOffer + + ",\n alertIds=" + alertIds + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java new file mode 100644 index 0000000000..bd170c0879 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java @@ -0,0 +1,216 @@ +/* + * 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 bisq.core.notifications.alerts.market; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileMessageType; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferPayload; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; + +import bisq.common.crypto.KeyRing; +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.List; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class MarketAlerts { + private final OfferBookService offerBookService; + private final MobileNotificationService mobileNotificationService; + private final User user; + private final PriceFeedService priceFeedService; + private final KeyRing keyRing; + + @Inject + private MarketAlerts(OfferBookService offerBookService, MobileNotificationService mobileNotificationService, + User user, PriceFeedService priceFeedService, KeyRing keyRing) { + this.offerBookService = offerBookService; + this.mobileNotificationService = mobileNotificationService; + this.user = user; + this.priceFeedService = priceFeedService; + this.keyRing = keyRing; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { + @Override + public void onAdded(Offer offer) { + onOfferAdded(offer); + } + + @Override + public void onRemoved(Offer offer) { + } + }); + applyFilterOnAllOffers(); + } + + public void addMarketAlertFilter(MarketAlertFilter filter) { + user.addMarketAlertFilter(filter); + applyFilterOnAllOffers(); + } + + public void removeMarketAlertFilter(MarketAlertFilter filter) { + user.removeMarketAlertFilter(filter); + } + + public List getMarketAlertFilters() { + return user.getMarketAlertFilters(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyFilterOnAllOffers() { + offerBookService.getOffers().forEach(this::onOfferAdded); + } + + // We combine the offer ID and the price (either as % price or as fixed price) to get also updates for edited offers + // % price get multiplied by 10000 to have 0.12% be converted to 12. For fixed price we have precision of 8 for + // altcoins and precision of 4 for fiat. + private String getAlertId(Offer offer) { + double price = offer.isUseMarketBasedPrice() ? offer.getMarketPriceMargin() * 10000 : offer.getOfferPayload().getPrice(); + String priceString = String.valueOf((long) price); + return offer.getId() + "|" + priceString; + } + + private void onOfferAdded(Offer offer) { + String currencyCode = offer.getCurrencyCode(); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + Price offerPrice = offer.getPrice(); + if (marketPrice != null && offerPrice != null) { + boolean isSellOffer = offer.getDirection() == OfferPayload.Direction.SELL; + String shortOfferId = offer.getShortId(); + boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(currencyCode); + String alertId = getAlertId(offer); + user.getMarketAlertFilters().stream() + .filter(marketAlertFilter -> !offer.isMyOffer(keyRing)) + .filter(marketAlertFilter -> offer.getPaymentMethod().equals(marketAlertFilter.getPaymentAccount().getPaymentMethod())) + .filter(marketAlertFilter -> marketAlertFilter.notContainsAlertId(alertId)) + .forEach(marketAlertFilter -> { + int triggerValue = marketAlertFilter.getTriggerValue(); + boolean isTriggerForBuyOffer = marketAlertFilter.isBuyOffer(); + double marketPriceAsDouble1 = marketPrice.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double marketPriceAsDouble = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble1, precision); + double offerPriceValue = offerPrice.getValue(); + double ratio = offerPriceValue / marketPriceAsDouble; + ratio = 1 - ratio; + if (isFiatCurrency && isSellOffer) + ratio *= -1; + else if (!isFiatCurrency && !isSellOffer) + ratio *= -1; + + ratio = ratio * 10000; + boolean triggered = ratio <= triggerValue; + if (!triggered) + return; + + boolean isTriggerForBuyOfferAndTriggered = !isSellOffer && isTriggerForBuyOffer; + boolean isTriggerForSellOfferAndTriggered = isSellOffer && !isTriggerForBuyOffer; + if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) { + String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy"); + String marketDir; + if (isFiatCurrency) { + if (isSellOffer) { + marketDir = ratio > 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); + } else { + marketDir = ratio < 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); + } + } else { + if (isSellOffer) { + marketDir = ratio < 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); + } else { + marketDir = ratio > 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); + } + } + + ratio = Math.abs(ratio); + String msg = Res.get("account.notifications.marketAlert.message.msg", + direction, + CurrencyUtil.getCurrencyPair(currencyCode), + FormattingUtils.formatPrice(offerPrice), + FormattingUtils.formatToPercentWithSymbol(ratio / 10000d), + marketDir, + Res.get(offer.getPaymentMethod().getId()), + shortOfferId); + MobileMessage message = new MobileMessage(Res.get("account.notifications.marketAlert.message.title"), + msg, + shortOfferId, + MobileMessageType.MARKET); + try { + boolean wasSent = mobileNotificationService.sendMessage(message); + if (wasSent) { + // In case we have disabled alerts wasSent is false and we do not + // persist the offer + marketAlertFilter.addAlertId(alertId); + user.requestPersistence(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + + public static MobileMessage getTestMsg() { + String shortId = UUID.randomUUID().toString().substring(0, 8); + return new MobileMessage(Res.get("account.notifications.marketAlert.message.title"), + "A new 'sell BTC/USD' offer with price 6019.2744 (5.36% below market price) and payment method " + + "'Perfect Money' was published to the Bisq offerbook.\n" + + "Offer ID: wygiaw.", + shortId, + MobileMessageType.MARKET); + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java b/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java new file mode 100644 index 0000000000..9f7b258d90 --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.core.notifications.alerts.price; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileMessageType; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class PriceAlert { + private final PriceFeedService priceFeedService; + private final MobileNotificationService mobileNotificationService; + private final User user; + + @Inject + public PriceAlert(PriceFeedService priceFeedService, MobileNotificationService mobileNotificationService, User user) { + this.priceFeedService = priceFeedService; + this.user = user; + this.mobileNotificationService = mobileNotificationService; + } + + public void onAllServicesInitialized() { + priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> update()); + } + + private void update() { + if (user.getPriceAlertFilter() != null) { + PriceAlertFilter filter = user.getPriceAlertFilter(); + String currencyCode = filter.getCurrencyCode(); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null) { + int exp = CurrencyUtil.isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + double priceAsDouble = marketPrice.getPrice(); + long priceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(priceAsDouble, exp)); + String currencyName = CurrencyUtil.getNameByCode(currencyCode); + if (priceAsLong > filter.getHigh() || priceAsLong < filter.getLow()) { + String msg = Res.get("account.notifications.priceAlert.message.msg", + currencyName, + FormattingUtils.formatMarketPrice(priceAsDouble, currencyCode), + CurrencyUtil.getCurrencyPair(currencyCode)); + MobileMessage message = new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName), + msg, + MobileMessageType.PRICE); + log.error(msg); + try { + mobileNotificationService.sendMessage(message); + + // If an alert got triggered we remove the filter. + user.removePriceAlertFilter(); + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + } + } + } + + public static MobileMessage getTestMsg() { + String currencyCode = "USD"; + String currencyName = CurrencyUtil.getNameByCode(currencyCode); + String msg = Res.get("account.notifications.priceAlert.message.msg", + currencyName, + "6023.34", + "BTC/USD"); + return new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName), + msg, + MobileMessageType.PRICE); + } +} diff --git a/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java b/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java new file mode 100644 index 0000000000..f14896f3ee --- /dev/null +++ b/core/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.notifications.alerts.price; + +import bisq.common.proto.persistable.PersistablePayload; + +import lombok.Value; + +@Value +public class PriceAlertFilter implements PersistablePayload { + String currencyCode; + long high; + long low; + + public PriceAlertFilter(String currencyCode, long high, long low) { + this.currencyCode = currencyCode; + this.high = high; + this.low = low; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PriceAlertFilter toProtoMessage() { + return protobuf.PriceAlertFilter.newBuilder() + .setCurrencyCode(currencyCode) + .setHigh(high) + .setLow(low).build(); + } + + public static PriceAlertFilter fromProto(protobuf.PriceAlertFilter proto) { + return new PriceAlertFilter(proto.getCurrencyCode(), proto.getHigh(), proto.getLow()); + } +} diff --git a/core/src/main/java/bisq/core/offer/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/AvailabilityResult.java new file mode 100644 index 0000000000..e4ad982163 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/AvailabilityResult.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.offer; + +public enum AvailabilityResult { + UNKNOWN_FAILURE("cannot take offer for unknown reason"), + AVAILABLE("offer available"), + OFFER_TAKEN("offer taken"), + PRICE_OUT_OF_TOLERANCE("cannot take offer because taker's price is outside tolerance"), + MARKET_PRICE_NOT_AVAILABLE("cannot take offer because market price for calculating trade price is unavailable"), + @SuppressWarnings("unused") NO_ARBITRATORS("cannot take offer because no arbitrators are available"), + NO_MEDIATORS("cannot take offer because no mediators are available"), + USER_IGNORED("cannot take offer because user is ignored"), + @SuppressWarnings("unused") MISSING_MANDATORY_CAPABILITY("description not available"), + @SuppressWarnings("unused") NO_REFUND_AGENTS("cannot take offer because no refund agents are available"), + UNCONF_TX_LIMIT_HIT("cannot take offer because you have too many unconfirmed transactions at this moment"), + MAKER_DENIED_API_USER("cannot take offer because maker is api user"), + PRICE_CHECK_FAILED("cannot take offer because trade price check failed"); + + private final String description; + + AvailabilityResult(String description) { + this.description = description; + } + + public String description() { + return description; + } + + public static AvailabilityResult fromProto(protobuf.AvailabilityResult proto) { + return AvailabilityResult.valueOf(proto.name()); + } +} diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java new file mode 100644 index 0000000000..733eee1a7f --- /dev/null +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -0,0 +1,300 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.btc.TxFeeEstimationService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.coin.CoinUtil; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.app.Version; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class CreateOfferService { + private final OfferUtil offerUtil; + private final TxFeeEstimationService txFeeEstimationService; + private final PriceFeedService priceFeedService; + private final P2PService p2PService; + private final PubKeyRing pubKeyRing; + private final User user; + private final BtcWalletService btcWalletService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public CreateOfferService(OfferUtil offerUtil, + TxFeeEstimationService txFeeEstimationService, + PriceFeedService priceFeedService, + P2PService p2PService, + PubKeyRing pubKeyRing, + User user, + BtcWalletService btcWalletService) { + this.offerUtil = offerUtil; + this.txFeeEstimationService = txFeeEstimationService; + this.priceFeedService = priceFeedService; + this.p2PService = p2PService; + this.pubKeyRing = pubKeyRing; + this.user = user; + this.btcWalletService = btcWalletService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getRandomOfferId() { + return Utilities.getRandomPrefix(5, 8) + "-" + + UUID.randomUUID().toString() + "-" + + Version.VERSION.replace(".", ""); + } + + public Offer createAndGetOffer(String offerId, + OfferPayload.Direction direction, + String currencyCode, + Coin amount, + Coin minAmount, + Price price, + Coin txFee, + boolean useMarketBasedPrice, + double marketPriceMargin, + double buyerSecurityDepositAsDouble, + PaymentAccount paymentAccount) { + + log.info("create and get offer with offerId={}, \n" + + "currencyCode={}, \n" + + "direction={}, \n" + + "price={}, \n" + + "useMarketBasedPrice={}, \n" + + "marketPriceMargin={}, \n" + + "amount={}, \n" + + "minAmount={}, \n" + + "buyerSecurityDeposit={}", + offerId, + currencyCode, + direction, + price.getValue(), + useMarketBasedPrice, + marketPriceMargin, + amount.value, + minAmount.value, + buyerSecurityDepositAsDouble); + + long creationTime = new Date().getTime(); + NodeAddress makerAddress = p2PService.getAddress(); + boolean useMarketBasedPriceValue = useMarketBasedPrice && + isMarketPriceAvailable(currencyCode) && + !paymentAccount.isHalCashAccount(); + + long priceAsLong = price != null && !useMarketBasedPriceValue ? price.getValue() : 0L; + double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; + long amountAsLong = amount != null ? amount.getValue() : 0L; + long minAmountAsLong = minAmount != null ? minAmount.getValue() : 0L; + boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); + String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode(); + String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode; + List acceptedArbitratorAddresses = user.getAcceptedArbitratorAddresses(); + ArrayList arbitratorNodeAddresses = acceptedArbitratorAddresses != null ? + Lists.newArrayList(acceptedArbitratorAddresses) : + new ArrayList<>(); + List acceptedMediatorAddresses = user.getAcceptedMediatorAddresses(); + ArrayList mediatorNodeAddresses = acceptedMediatorAddresses != null ? + Lists.newArrayList(acceptedMediatorAddresses) : + new ArrayList<>(); + String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount); + List acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount); + String bankId = PaymentAccountUtil.getBankId(paymentAccount); + List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); + double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble); + Coin txFeeFromFeeService = getEstimatedFeeAndTxVsize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first; + Coin txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService; + Coin makerFeeAsCoin = offerUtil.getMakerFee(amount); + boolean isCurrencyForMakerFeeBtc = offerUtil.isCurrencyForMakerFeeBtc(amount); + Coin buyerSecurityDepositAsCoin = getBuyerSecurityDeposit(amount, buyerSecurityDepositAsDouble); + Coin sellerSecurityDepositAsCoin = getSellerSecurityDeposit(amount, sellerSecurityDeposit); + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); + long maxTradePeriod = paymentAccount.getMaxTradePeriod(); + + // reserved for future use cases + // Use null values if not set + boolean isPrivateOffer = false; + boolean useAutoClose = false; + boolean useReOpenAfterAutoClose = false; + long lowerClosePrice = 0; + long upperClosePrice = 0; + String hashOfChallenge = null; + Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, + currencyCode, + direction); + + offerUtil.validateOfferData( + buyerSecurityDepositAsDouble, + paymentAccount, + currencyCode, + makerFeeAsCoin); + + OfferPayload offerPayload = new OfferPayload(offerId, + creationTime, + makerAddress, + pubKeyRing, + OfferPayload.Direction.valueOf(direction.name()), + priceAsLong, + marketPriceMarginParam, + useMarketBasedPriceValue, + amountAsLong, + minAmountAsLong, + baseCurrencyCode, + counterCurrencyCode, + arbitratorNodeAddresses, + mediatorNodeAddresses, + paymentAccount.getPaymentMethod().getId(), + paymentAccount.getId(), + null, + countryCode, + acceptedCountryCodes, + bankId, + acceptedBanks, + Version.VERSION, + btcWalletService.getLastBlockSeenHeight(), + txFeeToUse.value, + makerFeeAsCoin.value, + isCurrencyForMakerFeeBtc, + buyerSecurityDepositAsCoin.value, + sellerSecurityDepositAsCoin.value, + maxTradeLimit, + maxTradePeriod, + useAutoClose, + useReOpenAfterAutoClose, + upperClosePrice, + lowerClosePrice, + isPrivateOffer, + hashOfChallenge, + extraDataMap, + Version.TRADE_PROTOCOL_VERSION); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + return offer; + } + + public Tuple2 getEstimatedFeeAndTxVsize(Coin amount, + OfferPayload.Direction direction, + double buyerSecurityDeposit, + double sellerSecurityDeposit) { + Coin reservedFundsForOffer = getReservedFundsForOffer(direction, + amount, + buyerSecurityDeposit, + sellerSecurityDeposit); + return txFeeEstimationService.getEstimatedFeeAndTxVsizeForMaker(reservedFundsForOffer, + offerUtil.getMakerFee(amount)); + } + + public Coin getReservedFundsForOffer(OfferPayload.Direction direction, + Coin amount, + double buyerSecurityDeposit, + double sellerSecurityDeposit) { + + Coin reservedFundsForOffer = getSecurityDeposit(direction, + amount, + buyerSecurityDeposit, + sellerSecurityDeposit); + if (!offerUtil.isBuyOffer(direction)) + reservedFundsForOffer = reservedFundsForOffer.add(amount); + + return reservedFundsForOffer; + } + + public Coin getSecurityDeposit(OfferPayload.Direction direction, + Coin 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(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isMarketPriceAvailable(String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + return marketPrice != null && marketPrice.isExternallyProvidedPrice(); + } + + private Coin getBuyerSecurityDeposit(Coin amount, double buyerSecurityDeposit) { + Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(buyerSecurityDeposit, amount); + return getBoundedBuyerSecurityDeposit(percentOfAmountAsCoin); + } + + private Coin getSellerSecurityDeposit(Coin amount, double sellerSecurityDeposit) { + Coin amountAsCoin = (amount == null) ? Coin.ZERO : amount; + Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(sellerSecurityDeposit, amountAsCoin); + return getBoundedSellerSecurityDeposit(percentOfAmountAsCoin); + } + + private Coin getBoundedBuyerSecurityDeposit(Coin 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 + // MinBuyerSecurityDepositAsCoin from Restrictions. + return Coin.valueOf(Math.max(Restrictions.getMinBuyerSecurityDepositAsCoin().value, value.value)); + } + + private Coin getBoundedSellerSecurityDeposit(Coin 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 + // MinSellerSecurityDepositAsCoin from Restrictions. + return Coin.valueOf(Math.max(Restrictions.getMinSellerSecurityDepositAsCoin().value, value.value)); + } +} diff --git a/core/src/main/java/bisq/core/offer/MarketPriceNotAvailableException.java b/core/src/main/java/bisq/core/offer/MarketPriceNotAvailableException.java new file mode 100644 index 0000000000..35ee04a794 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/MarketPriceNotAvailableException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.offer; + +public class MarketPriceNotAvailableException extends Exception { + public MarketPriceNotAvailableException(@SuppressWarnings("SameParameterValue") String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java new file mode 100644 index 0000000000..05305dadf0 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -0,0 +1,566 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.exceptions.TradePriceOutOfToleranceException; +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.availability.OfferAvailabilityModel; +import bisq.core.offer.availability.OfferAvailabilityProtocol; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.util.VolumeUtil; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.JsonExclude; +import bisq.common.util.MathUtils; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.security.PublicKey; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class Offer implements NetworkPayload, PersistablePayload { + + // We allow max. 1 % difference between own offerPayload price calculation and takers calculation. + // Market price might be different at maker's and takers side so we need a bit of tolerance. + // The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations + // from one provider. + private final static double PRICE_TOLERANCE = 0.01; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Enums + /////////////////////////////////////////////////////////////////////////////////////////// + + public enum State { + UNKNOWN, + OFFER_FEE_PAID, + AVAILABLE, + NOT_AVAILABLE, + REMOVED, + MAKER_OFFLINE + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + @Getter + private final OfferPayload offerPayload; + @JsonExclude + @Getter + final transient private ObjectProperty stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN); + @JsonExclude + @Nullable + transient private OfferAvailabilityProtocol availabilityProtocol; + @JsonExclude + @Getter + final transient private StringProperty errorMessageProperty = new SimpleStringProperty(); + @JsonExclude + @Nullable + @Setter + transient private PriceFeedService priceFeedService; + + // Used only as cache + @Nullable + @JsonExclude + transient private String currencyCode; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Offer(OfferPayload offerPayload) { + this.offerPayload = offerPayload; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Offer toProtoMessage() { + return protobuf.Offer.newBuilder().setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()).build(); + } + + public static Offer fromProto(protobuf.Offer proto) { + return new Offer(OfferPayload.fromProto(proto.getOfferPayload())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Availability + /////////////////////////////////////////////////////////////////////////////////////////// + + public void checkOfferAvailability(OfferAvailabilityModel model, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + availabilityProtocol = new OfferAvailabilityProtocol(model, + () -> { + cancelAvailabilityRequest(); + resultHandler.handleResult(); + }, + (errorMessage) -> { + if (availabilityProtocol != null) + availabilityProtocol.cancel(); + log.error(errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + availabilityProtocol.sendOfferAvailabilityRequest(); + } + + public void cancelAvailabilityRequest() { + if (availabilityProtocol != null) + availabilityProtocol.cancel(); + } + + @Nullable + public Price getPrice() { + String currencyCode = getCurrencyCode(); + if (offerPayload.isUseMarketBasedPrice()) { + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double factor; + double marketPriceMargin = offerPayload.getMarketPriceMargin(); + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + factor = getDirection() == OfferPayload.Direction.SELL ? + 1 - marketPriceMargin : 1 + marketPriceMargin; + } else { + factor = getDirection() == OfferPayload.Direction.BUY ? + 1 - marketPriceMargin : 1 + marketPriceMargin; + } + double marketPriceAsDouble = marketPrice.getPrice(); + double targetPriceAsDouble = marketPriceAsDouble * factor; + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); + final long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e.toString() + "\n" + + "That case should never happen."); + return null; + } + } else { + log.trace("We don't have a market price. " + + "That case could only happen if you don't have a price feed."); + return null; + } + } else { + return Price.valueOf(currencyCode, offerPayload.getPrice()); + } + } + + public void checkTradePriceTolerance(long takersTradePrice) throws TradePriceOutOfToleranceException, + MarketPriceNotAvailableException, IllegalArgumentException { + Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice); + Price offerPrice = getPrice(); + if (offerPrice == null) + throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); + + checkArgument(takersTradePrice > 0, "takersTradePrice must be positive"); + + double relation = (double) takersTradePrice / (double) offerPrice.getValue(); + // We allow max. 2 % difference between own offerPayload price calculation and takers calculation. + // Market price might be different at maker's and takers side so we need a bit of tolerance. + // The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations + // from one provider. + + double deviation = Math.abs(1 - relation); + log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", + getShortId(), getCurrencyCode(), takersTradePrice, offerPrice.getValue(), + deviation * 100 + "%"); + if (deviation > PRICE_TOLERANCE) { + String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + + "takersPrice=" + tradePrice.getValue() + "\n" + + "makersPrice=" + offerPrice.getValue(); + log.warn(msg); + throw new TradePriceOutOfToleranceException(msg); + } + } + + @Nullable + public Volume getVolumeByAmount(Coin amount) { + Price price = getPrice(); + if (price != null && amount != null) { + Volume volumeByAmount = price.getVolumeByAmount(amount); + if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode())) + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); + + return volumeByAmount; + } else { + return null; + } + } + + public void resetState() { + setState(Offer.State.UNKNOWN); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setState(Offer.State state) { + stateProperty().set(state); + } + + public ObjectProperty stateProperty() { + return stateProperty; + } + + public void setOfferFeePaymentTxId(String offerFeePaymentTxID) { + offerPayload.setOfferFeePaymentTxId(offerFeePaymentTxID); + } + + public void setErrorMessage(String errorMessage) { + this.errorMessageProperty.set(errorMessage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + // converted payload properties + public Coin getTxFee() { + return Coin.valueOf(offerPayload.getTxFee()); + } + + public Coin getMakerFee() { + return Coin.valueOf(offerPayload.getMakerFee()); + } + + public boolean isCurrencyForMakerFeeBtc() { + return offerPayload.isCurrencyForMakerFeeBtc(); + } + + public Coin getBuyerSecurityDeposit() { + return Coin.valueOf(offerPayload.getBuyerSecurityDeposit()); + } + + public Coin getSellerSecurityDeposit() { + return Coin.valueOf(offerPayload.getSellerSecurityDeposit()); + } + + public Coin getMaxTradeLimit() { + return Coin.valueOf(offerPayload.getMaxTradeLimit()); + } + + public Coin getAmount() { + return Coin.valueOf(offerPayload.getAmount()); + } + + public Coin getMinAmount() { + return Coin.valueOf(offerPayload.getMinAmount()); + } + + public boolean isRange() { + return offerPayload.getAmount() != offerPayload.getMinAmount(); + } + + public Date getDate() { + return new Date(offerPayload.getDate()); + } + + public PaymentMethod getPaymentMethod() { + return PaymentMethod.getPaymentMethodById(offerPayload.getPaymentMethodId()); + } + + // utils + public String getShortId() { + return Utilities.getShortId(offerPayload.getId()); + } + + @Nullable + public Volume getVolume() { + return getVolumeByAmount(getAmount()); + } + + @Nullable + public Volume getMinVolume() { + return getVolumeByAmount(getMinAmount()); + } + + public boolean isBuyOffer() { + return getDirection() == OfferPayload.Direction.BUY; + } + + public OfferPayload.Direction getMirroredDirection() { + return getDirection() == OfferPayload.Direction.BUY ? OfferPayload.Direction.SELL : OfferPayload.Direction.BUY; + } + + public boolean isMyOffer(KeyRing keyRing) { + return getPubKeyRing().equals(keyRing.getPubKeyRing()); + } + + + public Optional getAccountAgeWitnessHashAsHex() { + Map extraDataMap = getExtraDataMap(); + if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.ACCOUNT_AGE_WITNESS_HASH)) + return Optional.of(extraDataMap.get(OfferPayload.ACCOUNT_AGE_WITNESS_HASH)); + else + return Optional.empty(); + } + + public String getF2FCity() { + if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_CITY)) + return getExtraDataMap().get(OfferPayload.F2F_CITY); + else + return ""; + } + + public String getExtraInfo() { + if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_BY_MAIL_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.CASH_BY_MAIL_EXTRA_INFO); + else + return ""; + } + + public String getPaymentMethodNameWithCountryCode() { + String method = this.getPaymentMethod().getShortName(); + String methodCountryCode = this.getCountryCode(); + if (methodCountryCode != null) + method = method + " (" + methodCountryCode + ")"; + return method; + } + + // domain properties + public Offer.State getState() { + return stateProperty.get(); + } + + public ReadOnlyStringProperty errorMessageProperty() { + return errorMessageProperty; + } + + public String getErrorMessage() { + return errorMessageProperty.get(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegate Getter (boilerplate code generated via IntelliJ generate delegate feature) + /////////////////////////////////////////////////////////////////////////////////////////// + + public OfferPayload.Direction getDirection() { + return offerPayload.getDirection(); + } + + public String getId() { + return offerPayload.getId(); + } + + @Nullable + public List getAcceptedBankIds() { + return offerPayload.getAcceptedBankIds(); + } + + @Nullable + public String getBankId() { + return offerPayload.getBankId(); + } + + @Nullable + public List getAcceptedCountryCodes() { + return offerPayload.getAcceptedCountryCodes(); + } + + @Nullable + public String getCountryCode() { + return offerPayload.getCountryCode(); + } + + public String getCurrencyCode() { + if (currencyCode != null) { + return currencyCode; + } + + currencyCode = offerPayload.getBaseCurrencyCode().equals("BTC") ? + offerPayload.getCounterCurrencyCode() : + offerPayload.getBaseCurrencyCode(); + return currencyCode; + } + + public long getProtocolVersion() { + return offerPayload.getProtocolVersion(); + } + + public boolean isUseMarketBasedPrice() { + return offerPayload.isUseMarketBasedPrice(); + } + + public double getMarketPriceMargin() { + return offerPayload.getMarketPriceMargin(); + } + + public NodeAddress getMakerNodeAddress() { + return offerPayload.getOwnerNodeAddress(); + } + + public PubKeyRing getPubKeyRing() { + return offerPayload.getPubKeyRing(); + } + + public String getMakerPaymentAccountId() { + return offerPayload.getMakerPaymentAccountId(); + } + + public String getOfferFeePaymentTxId() { + return offerPayload.getOfferFeePaymentTxId(); + } + + public String getVersionNr() { + return offerPayload.getVersionNr(); + } + + public long getMaxTradePeriod() { + return offerPayload.getMaxTradePeriod(); + } + + public NodeAddress getOwnerNodeAddress() { + return offerPayload.getOwnerNodeAddress(); + } + + // Yet unused + public PublicKey getOwnerPubKey() { + return offerPayload.getOwnerPubKey(); + } + + @Nullable + public Map getExtraDataMap() { + return offerPayload.getExtraDataMap(); + } + + public boolean isUseAutoClose() { + return offerPayload.isUseAutoClose(); + } + + public long getBlockHeightAtOfferCreation() { + return offerPayload.getBlockHeightAtOfferCreation(); + } + + @Nullable + public String getHashOfChallenge() { + return offerPayload.getHashOfChallenge(); + } + + public boolean isPrivateOffer() { + return offerPayload.isPrivateOffer(); + } + + public long getUpperClosePrice() { + return offerPayload.getUpperClosePrice(); + } + + public long getLowerClosePrice() { + return offerPayload.getLowerClosePrice(); + } + + public boolean isUseReOpenAfterAutoClose() { + return offerPayload.isUseReOpenAfterAutoClose(); + } + + public boolean isXmrAutoConf() { + if (!isXmr()) { + return false; + } + if (getExtraDataMap() == null || !getExtraDataMap().containsKey(OfferPayload.XMR_AUTO_CONF)) { + return false; + } + + return getExtraDataMap().get(OfferPayload.XMR_AUTO_CONF).equals(OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE); + } + + public boolean isXmr() { + return getCurrencyCode().equals("XMR"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Offer offer = (Offer) o; + + if (offerPayload != null ? !offerPayload.equals(offer.offerPayload) : offer.offerPayload != null) return false; + //noinspection SimplifiableIfStatement + if (getState() != offer.getState()) return false; + return !(getErrorMessage() != null ? !getErrorMessage().equals(offer.getErrorMessage()) : offer.getErrorMessage() != null); + + } + + @Override + public int hashCode() { + int result = offerPayload != null ? offerPayload.hashCode() : 0; + result = 31 * result + (getState() != null ? getState().hashCode() : 0); + result = 31 * result + (getErrorMessage() != null ? getErrorMessage().hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Offer{" + + "getErrorMessage()='" + getErrorMessage() + '\'' + + ", state=" + getState() + + ", offerPayload=" + offerPayload + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java new file mode 100644 index 0000000000..64b3900463 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -0,0 +1,249 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.filter.FilterManager; +import bisq.core.locale.Res; +import bisq.core.provider.price.PriceFeedService; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.file.JsonFileManager; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +/** + * 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). + */ +public class OfferBookService { + private static final Logger log = LoggerFactory.getLogger(OfferBookService.class); + + public interface OfferBookChangedListener { + void onAdded(Offer offer); + + void onRemoved(Offer offer); + } + + private final P2PService p2PService; + private final PriceFeedService priceFeedService; + private final List offerBookChangedListeners = new LinkedList<>(); + private final FilterManager filterManager; + private final JsonFileManager jsonFileManager; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public OfferBookService(P2PService p2PService, + PriceFeedService priceFeedService, + FilterManager filterManager, + @Named(Config.STORAGE_DIR) File storageDir, + @Named(Config.DUMP_STATISTICS) boolean dumpStatistics) { + this.p2PService = p2PService; + this.priceFeedService = priceFeedService; + this.filterManager = filterManager; + jsonFileManager = new JsonFileManager(storageDir); + + p2PService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> { + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + listener.onAdded(offer); + } + })); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> { + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + listener.onRemoved(offer); + } + })); + } + }); + + if (dumpStatistics) { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + addOfferBookChangedListener(new OfferBookChangedListener() { + @Override + public void onAdded(Offer offer) { + doDumpStatistics(); + } + + @Override + public void onRemoved(Offer offer) { + doDumpStatistics(); + } + }); + UserThread.runAfter(OfferBookService.this::doDumpStatistics, 1); + } + }); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (filterManager.requireUpdateToNewVersionForTrading()) { + errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); + return; + } + + boolean result = p2PService.addProtectedStorageEntry(offer.getOfferPayload()); + if (result) { + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Add offer failed"); + } + } + + public void refreshTTL(OfferPayload offerPayload, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (filterManager.requireUpdateToNewVersionForTrading()) { + errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); + return; + } + + boolean result = p2PService.refreshTTL(offerPayload); + if (result) { + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Refresh TTL failed."); + } + } + + public void activateOffer(Offer offer, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { + addOffer(offer, resultHandler, errorMessageHandler); + } + + public void deactivateOffer(OfferPayload offerPayload, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { + removeOffer(offerPayload, resultHandler, errorMessageHandler); + } + + public void removeOffer(OfferPayload offerPayload, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { + if (p2PService.removeData(offerPayload)) { + if (resultHandler != null) + resultHandler.handleResult(); + } else { + if (errorMessageHandler != null) + errorMessageHandler.handleErrorMessage("Remove offer failed"); + } + } + + 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); + return offer; + }) + .collect(Collectors.toList()); + } + + public void removeOfferAtShutDown(OfferPayload offerPayload) { + removeOffer(offerPayload, null, null); + } + + public boolean isBootstrapped() { + return p2PService.isBootstrapped(); + } + + public void addOfferBookChangedListener(OfferBookChangedListener offerBookChangedListener) { + offerBookChangedListeners.add(offerBookChangedListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void doDumpStatistics() { + // We filter the case that it is a MarketBasedPrice but the price is not available + // That should only be possible if the price feed provider is not available + final List offerForJsonList = getOffers().stream() + .filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCurrencyCode()) != null) + .map(offer -> { + try { + return new OfferForJson(offer.getDirection(), + offer.getCurrencyCode(), + offer.getMinAmount(), + offer.getAmount(), + offer.getPrice(), + offer.getDate(), + offer.getId(), + offer.isUseMarketBasedPrice(), + offer.getMarketPriceMargin(), + offer.getPaymentMethod() + ); + } catch (Throwable t) { + // In case an offer was corrupted with null values we ignore it + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(offerForJsonList), "offers_statistics"); + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferFilter.java b/core/src/main/java/bisq/core/offer/OfferFilter.java new file mode 100644 index 0000000000..bfd37f2af1 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferFilter.java @@ -0,0 +1,209 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.SetChangeListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class OfferFilter { + private final User user; + private final Preferences preferences; + private final FilterManager filterManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); + private final Map myInsufficientTradeLimitCache = new HashMap<>(); + + @Inject + public OfferFilter(User user, + Preferences preferences, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService) { + this.user = user; + this.preferences = preferences; + this.filterManager = filterManager; + this.accountAgeWitnessService = accountAgeWitnessService; + + if (user != null) { + // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> + myInsufficientTradeLimitCache.clear()); + } + } + + public enum Result { + VALID(true), + API_DISABLED, + HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER, + HAS_NOT_SAME_PROTOCOL_VERSION, + IS_IGNORED, + IS_OFFER_BANNED, + IS_CURRENCY_BANNED, + IS_PAYMENT_METHOD_BANNED, + IS_NODE_ADDRESS_BANNED, + REQUIRE_UPDATE_TO_NEW_VERSION, + IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT, + IS_MY_INSUFFICIENT_TRADE_LIMIT; + + @Getter + private final boolean isValid; + + Result(boolean isValid) { + this.isValid = isValid; + } + + Result() { + this(false); + } + } + + public Result canTakeOffer(Offer offer, boolean isTakerApiUser) { + if (isTakerApiUser && filterManager.getFilter() != null && filterManager.getFilter().isDisableApi()) { + return Result.API_DISABLED; + } + if (!isAnyPaymentAccountValidForOffer(offer)) { + return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; + } + if (!hasSameProtocolVersion(offer)) { + return Result.HAS_NOT_SAME_PROTOCOL_VERSION; + } + if (isIgnored(offer)) { + return Result.IS_IGNORED; + } + if (isOfferBanned(offer)) { + return Result.IS_OFFER_BANNED; + } + if (isCurrencyBanned(offer)) { + return Result.IS_CURRENCY_BANNED; + } + if (isPaymentMethodBanned(offer)) { + return Result.IS_PAYMENT_METHOD_BANNED; + } + if (isNodeAddressBanned(offer)) { + return Result.IS_NODE_ADDRESS_BANNED; + } + if (requireUpdateToNewVersion()) { + return Result.REQUIRE_UPDATE_TO_NEW_VERSION; + } + if (isInsufficientCounterpartyTradeLimit(offer)) { + return Result.IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT; + } + if (isMyInsufficientTradeLimit(offer)) { + return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; + } + + return Result.VALID; + } + + public boolean isAnyPaymentAccountValidForOffer(Offer offer) { + return user.getPaymentAccounts() != null && + PaymentAccountUtil.isAnyPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); + } + + public boolean hasSameProtocolVersion(Offer offer) { + return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + } + + public boolean isIgnored(Offer offer) { + return preferences.getIgnoreTradersList().stream() + .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + } + + public boolean isOfferBanned(Offer offer) { + return filterManager.isOfferIdBanned(offer.getId()); + } + + public boolean isCurrencyBanned(Offer offer) { + return filterManager.isCurrencyBanned(offer.getCurrencyCode()); + } + + public boolean isPaymentMethodBanned(Offer offer) { + return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); + } + + public boolean isNodeAddressBanned(Offer offer) { + return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); + } + + public boolean requireUpdateToNewVersion() { + return filterManager.requireUpdateToNewVersionForTrading(); + } + + // This call is a bit expensive so we cache results + public boolean isInsufficientCounterpartyTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { + return insufficientCounterpartyTradeLimitCache.get(offerId); + } + + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), + errorMessage -> { + }); + insufficientCounterpartyTradeLimitCache.put(offerId, result); + return result; + } + + // This call is a bit expensive so we cache results + public boolean isMyInsufficientTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (myInsufficientTradeLimitCache.containsKey(offerId)) { + return myInsufficientTradeLimitCache.get(offerId); + } + + Optional accountOptional = PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, + user.getPaymentAccounts(), + accountAgeWitnessService); + long myTradeLimit = accountOptional + .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection())) + .orElse(0L); + long offerMinAmount = offer.getMinAmount().value; + log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", + accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", + Coin.valueOf(myTradeLimit).toFriendlyString(), + Coin.valueOf(offerMinAmount).toFriendlyString()); + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + accountOptional.isPresent() && + myTradeLimit < offerMinAmount; + myInsufficientTradeLimitCache.put(offerId, result); + return result; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferForJson.java b/core/src/main/java/bisq/core/offer/OfferForJson.java new file mode 100644 index 0000000000..afac82ae4e --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferForJson.java @@ -0,0 +1,168 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.payment.payload.PaymentMethod; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.MonetaryFormat; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +public class OfferForJson { + private static final Logger log = LoggerFactory.getLogger(OfferForJson.class); + + public final OfferPayload.Direction direction; + public final String currencyCode; + public final long minAmount; + public final long amount; + public final long price; + public final long date; + public final boolean useMarketBasedPrice; + public final double marketPriceMargin; + public final String paymentMethod; + public final String id; + + // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) + public String currencyPair; + public OfferPayload.Direction primaryMarketDirection; + + public String priceDisplayString; + public String primaryMarketAmountDisplayString; + public String primaryMarketMinAmountDisplayString; + public String primaryMarketVolumeDisplayString; + public String primaryMarketMinVolumeDisplayString; + + public long primaryMarketPrice; + public long primaryMarketAmount; + public long primaryMarketMinAmount; + public long primaryMarketVolume; + public long primaryMarketMinVolume; + + @JsonIgnore + transient private final MonetaryFormat fiatFormat = new MonetaryFormat().shift(0).minDecimals(4).repeatOptionalDecimals(0, 0); + @JsonIgnore + transient private final MonetaryFormat altcoinFormat = new MonetaryFormat().shift(0).minDecimals(8).repeatOptionalDecimals(0, 0); + @JsonIgnore + transient private final MonetaryFormat coinFormat = MonetaryFormat.BTC; + + + public OfferForJson(OfferPayload.Direction direction, + String currencyCode, + Coin minAmount, + Coin amount, + @Nullable Price price, + Date date, + String id, + boolean useMarketBasedPrice, + double marketPriceMargin, + PaymentMethod paymentMethod) { + + this.direction = direction; + this.currencyCode = currencyCode; + this.minAmount = minAmount.value; + this.amount = amount.value; + this.price = price.getValue(); + this.date = date.getTime(); + this.id = id; + this.useMarketBasedPrice = useMarketBasedPrice; + this.marketPriceMargin = marketPriceMargin; + this.paymentMethod = paymentMethod.getId(); + + setDisplayStrings(); + } + + private void setDisplayStrings() { + try { + final Price price = getPrice(); + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + primaryMarketDirection = direction == OfferPayload.Direction.BUY ? OfferPayload.Direction.SELL : OfferPayload.Direction.BUY; + currencyPair = currencyCode + "/" + Res.getBaseCurrencyCode(); + + // int precision = 8; + //decimalFormat.setMaximumFractionDigits(precision); + + // amount and volume is inverted for json + priceDisplayString = altcoinFormat.noCode().format(price.getMonetary()).toString(); + primaryMarketMinAmountDisplayString = altcoinFormat.noCode().format(getMinVolume().getMonetary()).toString(); + primaryMarketAmountDisplayString = altcoinFormat.noCode().format(getVolume().getMonetary()).toString(); + primaryMarketMinVolumeDisplayString = coinFormat.noCode().format(getMinAmountAsCoin()).toString(); + primaryMarketVolumeDisplayString = coinFormat.noCode().format(getAmountAsCoin()).toString(); + + primaryMarketPrice = price.getValue(); + primaryMarketMinAmount = getMinVolume().getValue(); + primaryMarketAmount = getVolume().getValue(); + primaryMarketMinVolume = getMinAmountAsCoin().getValue(); + primaryMarketVolume = getAmountAsCoin().getValue(); + } else { + primaryMarketDirection = direction; + currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode; + + priceDisplayString = fiatFormat.noCode().format(price.getMonetary()).toString(); + primaryMarketMinAmountDisplayString = coinFormat.noCode().format(getMinAmountAsCoin()).toString(); + primaryMarketAmountDisplayString = coinFormat.noCode().format(getAmountAsCoin()).toString(); + primaryMarketMinVolumeDisplayString = fiatFormat.noCode().format(getMinVolume().getMonetary()).toString(); + primaryMarketVolumeDisplayString = fiatFormat.noCode().format(getVolume().getMonetary()).toString(); + + // we use precision 4 for fiat based price but on the markets api we use precision 8 so we scale up by 10000 + primaryMarketPrice = (long) MathUtils.scaleUpByPowerOf10(price.getValue(), 4); + primaryMarketMinVolume = (long) MathUtils.scaleUpByPowerOf10(getMinVolume().getValue(), 4); + primaryMarketVolume = (long) MathUtils.scaleUpByPowerOf10(getVolume().getValue(), 4); + + primaryMarketMinAmount = getMinAmountAsCoin().getValue(); + primaryMarketAmount = getAmountAsCoin().getValue(); + } + + } catch (Throwable t) { + log.error("Error at setDisplayStrings: " + t.getMessage()); + } + } + + private Price getPrice() { + return Price.valueOf(currencyCode, price); + } + + private Coin getAmountAsCoin() { + return Coin.valueOf(amount); + } + + private Coin getMinAmountAsCoin() { + return Coin.valueOf(minAmount); + } + + private Volume getVolume() { + return getPrice().getVolumeByAmount(getAmountAsCoin()); + } + + private Volume getMinVolume() { + return getPrice().getVolumeByAmount(getMinAmountAsCoin()); + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferModule.java b/core/src/main/java/bisq/core/offer/OfferModule.java new file mode 100644 index 0000000000..9fccd22437 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferModule.java @@ -0,0 +1,39 @@ +/* + * 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 bisq.core.offer; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class OfferModule extends AppModule { + + public OfferModule(Config config) { + super(config); + } + + @Override + protected final void configure() { + bind(OpenOfferManager.class).in(Singleton.class); + bind(OfferBookService.class).in(Singleton.class); + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java new file mode 100644 index 0000000000..92f6ece603 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -0,0 +1,436 @@ +/* + * 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 bisq.core.offer; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; +import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.JsonExclude; + +import java.security.PublicKey; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +// OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a +// backward compatible way. Maybe a candidate when segwit activation is done as hardfork? + +@EqualsAndHashCode +@Getter +@Slf4j +public final class OfferPayload implements ProtectedStoragePayload, ExpirablePayload, RequiresOwnerIsOnlinePayload { + public static final long TTL = TimeUnit.MINUTES.toMillis(9); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Enum + /////////////////////////////////////////////////////////////////////////////////////////// + + public enum Direction { + BUY, + SELL; + + public static OfferPayload.Direction fromProto(protobuf.OfferPayload.Direction direction) { + return ProtoUtil.enumFromProto(OfferPayload.Direction.class, direction.name()); + } + + public static protobuf.OfferPayload.Direction toProtoMessage(Direction direction) { + return protobuf.OfferPayload.Direction.valueOf(direction.name()); + } + } + + // Keys for extra map + // Only set for fiat offers + public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; + public static final String REFERRAL_ID = "referralId"; + // Only used in payment method F2F + public static final String F2F_CITY = "f2fCity"; + public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; + public static final String CASH_BY_MAIL_EXTRA_INFO = "cashByMailExtraInfo"; + + // Comma separated list of ordinal of a bisq.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 + // of the offer supports both capabilities we add "11, 12" to capabilities. + public static final String CAPABILITIES = "capabilities"; + // If maker is seller and has xmrAutoConf enabled it is set to "1" otherwise it is not set + public static final String XMR_AUTO_CONF = "xmrAutoConf"; + public static final String XMR_AUTO_CONF_ENABLED_VALUE = "1"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final String id; + private final long date; + private final NodeAddress ownerNodeAddress; + @JsonExclude + private final PubKeyRing pubKeyRing; + private final Direction direction; + // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 + private final long price; + // Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0. + // E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price. + // Positive values is always the usual case where you want a better price as the market. + // E.g. Buy offer with market price 400.- leads to a 360.- price. + // Sell offer with market price 400.- leads to a 440.- price. + private final double marketPriceMargin; + // We use 2 type of prices: fixed price or price based on distance from market price + private final boolean useMarketBasedPrice; + private final long amount; + private final long minAmount; + + // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency + // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + private final String baseCurrencyCode; + private final String counterCurrencyCode; + + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) + private final List arbitratorNodeAddresses; + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) + private final List mediatorNodeAddresses; + private final String paymentMethodId; + private final String makerPaymentAccountId; + // Mutable property. Has to be set before offer is save in P2P network as it changes the objects hash! + @Nullable + @Setter + private String offerFeePaymentTxId; + @Nullable + private final String countryCode; + @Nullable + private final List acceptedCountryCodes; + @Nullable + private final String bankId; + @Nullable + private final List acceptedBankIds; + private final String versionNr; + private final long blockHeightAtOfferCreation; + private final long txFee; + private final long makerFee; + private final boolean isCurrencyForMakerFeeBtc; + private final long buyerSecurityDeposit; + private final long sellerSecurityDeposit; + private final long maxTradeLimit; + private final long maxTradePeriod; + + // reserved for future use cases + // Close offer when certain price is reached + private final boolean useAutoClose; + // If useReOpenAfterAutoClose=true we re-open a new offer with the remaining funds if the trade amount + // was less than the offer's max. trade amount. + private final boolean useReOpenAfterAutoClose; + // Used when useAutoClose is set for canceling the offer when lowerClosePrice is triggered + private final long lowerClosePrice; + // Used when useAutoClose is set for canceling the offer when upperClosePrice is triggered + private final long upperClosePrice; + // 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; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + + // extraDataMap used from v0.6 on for hashOfPaymentAccount + // key ACCOUNT_AGE_WITNESS, value: hex string of hashOfPaymentAccount byte array + @Nullable + private final Map extraDataMap; + private final int protocolVersion; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public OfferPayload(String id, + long date, + NodeAddress ownerNodeAddress, + PubKeyRing pubKeyRing, + Direction direction, + long price, + double marketPriceMargin, + boolean useMarketBasedPrice, + long amount, + long minAmount, + String baseCurrencyCode, + String counterCurrencyCode, + List arbitratorNodeAddresses, + List mediatorNodeAddresses, + String paymentMethodId, + String makerPaymentAccountId, + @Nullable String offerFeePaymentTxId, + @Nullable String countryCode, + @Nullable List acceptedCountryCodes, + @Nullable String bankId, + @Nullable List acceptedBankIds, + String versionNr, + long blockHeightAtOfferCreation, + long txFee, + long makerFee, + boolean isCurrencyForMakerFeeBtc, + long buyerSecurityDeposit, + long sellerSecurityDeposit, + long maxTradeLimit, + long maxTradePeriod, + boolean useAutoClose, + boolean useReOpenAfterAutoClose, + long lowerClosePrice, + long upperClosePrice, + boolean isPrivateOffer, + @Nullable String hashOfChallenge, + @Nullable Map extraDataMap, + int protocolVersion) { + this.id = id; + this.date = date; + this.ownerNodeAddress = ownerNodeAddress; + this.pubKeyRing = pubKeyRing; + this.direction = direction; + this.price = price; + this.marketPriceMargin = marketPriceMargin; + this.useMarketBasedPrice = useMarketBasedPrice; + this.amount = amount; + this.minAmount = minAmount; + this.baseCurrencyCode = baseCurrencyCode; + this.counterCurrencyCode = counterCurrencyCode; + this.arbitratorNodeAddresses = arbitratorNodeAddresses; + this.mediatorNodeAddresses = mediatorNodeAddresses; + this.paymentMethodId = paymentMethodId; + this.makerPaymentAccountId = makerPaymentAccountId; + this.offerFeePaymentTxId = offerFeePaymentTxId; + this.countryCode = countryCode; + this.acceptedCountryCodes = acceptedCountryCodes; + this.bankId = bankId; + this.acceptedBankIds = acceptedBankIds; + this.versionNr = versionNr; + this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; + this.txFee = txFee; + this.makerFee = makerFee; + this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; + this.buyerSecurityDeposit = buyerSecurityDeposit; + this.sellerSecurityDeposit = sellerSecurityDeposit; + this.maxTradeLimit = maxTradeLimit; + this.maxTradePeriod = maxTradePeriod; + this.useAutoClose = useAutoClose; + this.useReOpenAfterAutoClose = useReOpenAfterAutoClose; + this.lowerClosePrice = lowerClosePrice; + this.upperClosePrice = upperClosePrice; + this.isPrivateOffer = isPrivateOffer; + this.hashOfChallenge = hashOfChallenge; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + this.protocolVersion = protocolVersion; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + protobuf.OfferPayload.Builder builder = protobuf.OfferPayload.newBuilder() + .setId(id) + .setDate(date) + .setOwnerNodeAddress(ownerNodeAddress.toProtoMessage()) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .setDirection(Direction.toProtoMessage(direction)) + .setPrice(price) + .setMarketPriceMargin(marketPriceMargin) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setAmount(amount) + .setMinAmount(minAmount) + .setBaseCurrencyCode(baseCurrencyCode) + .setCounterCurrencyCode(counterCurrencyCode) + .addAllArbitratorNodeAddresses(arbitratorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage) + .collect(Collectors.toList())) + .addAllMediatorNodeAddresses(mediatorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage) + .collect(Collectors.toList())) + .setPaymentMethodId(paymentMethodId) + .setMakerPaymentAccountId(makerPaymentAccountId) + .setVersionNr(versionNr) + .setBlockHeightAtOfferCreation(blockHeightAtOfferCreation) + .setTxFee(txFee) + .setMakerFee(makerFee) + .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) + .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setSellerSecurityDeposit(sellerSecurityDeposit) + .setMaxTradeLimit(maxTradeLimit) + .setMaxTradePeriod(maxTradePeriod) + .setUseAutoClose(useAutoClose) + .setUseReOpenAfterAutoClose(useReOpenAfterAutoClose) + .setLowerClosePrice(lowerClosePrice) + .setUpperClosePrice(upperClosePrice) + .setIsPrivateOffer(isPrivateOffer) + .setProtocolVersion(protocolVersion); + + builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId, + "OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network.")); + + Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); + 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(extraDataMap).ifPresent(builder::putAllExtraData); + + return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); + } + + public static OfferPayload fromProto(protobuf.OfferPayload proto) { + checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload"); + List acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? + null : new ArrayList<>(proto.getAcceptedBankIdsList()); + List acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? + null : new ArrayList<>(proto.getAcceptedCountryCodesList()); + String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); + Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap(); + + return new OfferPayload(proto.getId(), + proto.getDate(), + NodeAddress.fromProto(proto.getOwnerNodeAddress()), + PubKeyRing.fromProto(proto.getPubKeyRing()), + OfferPayload.Direction.fromProto(proto.getDirection()), + proto.getPrice(), + proto.getMarketPriceMargin(), + proto.getUseMarketBasedPrice(), + proto.getAmount(), + proto.getMinAmount(), + proto.getBaseCurrencyCode(), + proto.getCounterCurrencyCode(), + proto.getArbitratorNodeAddressesList().stream() + .map(NodeAddress::fromProto) + .collect(Collectors.toList()), + proto.getMediatorNodeAddressesList().stream() + .map(NodeAddress::fromProto) + .collect(Collectors.toList()), + proto.getPaymentMethodId(), + proto.getMakerPaymentAccountId(), + proto.getOfferFeePaymentTxId(), + ProtoUtil.stringOrNullFromProto(proto.getCountryCode()), + acceptedCountryCodes, + ProtoUtil.stringOrNullFromProto(proto.getBankId()), + acceptedBankIds, + proto.getVersionNr(), + proto.getBlockHeightAtOfferCreation(), + proto.getTxFee(), + proto.getMakerFee(), + proto.getIsCurrencyForMakerFeeBtc(), + proto.getBuyerSecurityDeposit(), + proto.getSellerSecurityDeposit(), + proto.getMaxTradeLimit(), + proto.getMaxTradePeriod(), + proto.getUseAutoClose(), + proto.getUseReOpenAfterAutoClose(), + proto.getLowerClosePrice(), + proto.getUpperClosePrice(), + proto.getIsPrivateOffer(), + hashOfChallenge, + extraDataMapMap, + proto.getProtocolVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return pubKeyRing.getSignaturePubKey(); + } + + // In the offer we support base and counter currency + // Fiat offers have base currency BTC and counterCurrency Fiat + // Altcoins have base currency Altcoin and counterCurrency BTC + // The rest of the app does not support yet that concept of base currency and counter currencies + // so we map here for convenience + public String getCurrencyCode() { + return getBaseCurrencyCode().equals("BTC") ? getCounterCurrencyCode() : getBaseCurrencyCode(); + } + + @Override + public String toString() { + return "OfferPayload{" + + "\n id='" + id + '\'' + + ",\n date=" + new Date(date) + + ",\n ownerNodeAddress=" + ownerNodeAddress + + ",\n pubKeyRing=" + pubKeyRing + + ",\n direction=" + direction + + ",\n price=" + price + + ",\n marketPriceMargin=" + marketPriceMargin + + ",\n useMarketBasedPrice=" + useMarketBasedPrice + + ",\n amount=" + amount + + ",\n minAmount=" + minAmount + + ",\n baseCurrencyCode='" + baseCurrencyCode + '\'' + + ",\n counterCurrencyCode='" + counterCurrencyCode + '\'' + + ",\n paymentMethodId='" + paymentMethodId + '\'' + + ",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + + ",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + + ",\n countryCode='" + countryCode + '\'' + + ",\n acceptedCountryCodes=" + acceptedCountryCodes + + ",\n bankId='" + bankId + '\'' + + ",\n acceptedBankIds=" + acceptedBankIds + + ",\n versionNr='" + versionNr + '\'' + + ",\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + + ",\n txFee=" + txFee + + ",\n makerFee=" + makerFee + + ",\n isCurrencyForMakerFeeBtc=" + isCurrencyForMakerFeeBtc + + ",\n buyerSecurityDeposit=" + buyerSecurityDeposit + + ",\n sellerSecurityDeposit=" + sellerSecurityDeposit + + ",\n maxTradeLimit=" + maxTradeLimit + + ",\n maxTradePeriod=" + maxTradePeriod + + ",\n useAutoClose=" + useAutoClose + + ",\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + + ",\n lowerClosePrice=" + lowerClosePrice + + ",\n upperClosePrice=" + upperClosePrice + + ",\n isPrivateOffer=" + isPrivateOffer + + ",\n hashOfChallenge='" + hashOfChallenge + '\'' + + ",\n extraDataMap=" + extraDataMap + + ",\n protocolVersion=" + protocolVersion + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferRestrictions.java b/core/src/main/java/bisq/core/offer/OfferRestrictions.java new file mode 100644 index 0000000000..856b7a6e20 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferRestrictions.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.core.offer; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Map; + +public class OfferRestrictions { + // The date when traders who have not updated cannot take offers from updated clients and their offers become + // invisible for updated clients. + private static final Date REQUIRE_UPDATE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 19); + + static boolean requiresUpdate() { + return new Date().after(REQUIRE_UPDATE_DATE); + } + + public static Coin TOLERATED_SMALL_TRADE_AMOUNT = Coin.parseCoin("0.01"); + + static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) { + Map extraDataMap = offer.getOfferPayload().getExtraDataMap(); + if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) { + String commaSeparatedOrdinals = extraDataMap.get(OfferPayload.CAPABILITIES); + Capabilities capabilities = Capabilities.fromStringList(commaSeparatedOrdinals); + return Capabilities.hasMandatoryCapability(capabilities, mandatoryCapability); + } + return false; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java new file mode 100644 index 0000000000..8ea47fd2be --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -0,0 +1,453 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.payment.CashByMailAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.ReferralIdService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.AutoConfirmSettings; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.CoinUtil; + +import bisq.network.p2p.P2PService; + +import bisq.common.app.Capabilities; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; +import static bisq.core.btc.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; +import static bisq.core.btc.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; +import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput; +import static bisq.core.btc.wallet.Restrictions.isDust; +import static bisq.core.offer.OfferPayload.*; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +/** + * This class holds utility methods for creating, editing and taking an Offer. + */ +@Slf4j +@Singleton +public class OfferUtil { + + private final AccountAgeWitnessService accountAgeWitnessService; + private final BsqWalletService bsqWalletService; + private final FilterManager filterManager; + private final Preferences preferences; + private final PriceFeedService priceFeedService; + private final P2PService p2PService; + private final ReferralIdService referralIdService; + private final TradeStatisticsManager tradeStatisticsManager; + + private final Predicate isValidFeePaymentCurrencyCode = (c) -> + c.equalsIgnoreCase("BSQ") || c.equalsIgnoreCase("BTC"); + + @Inject + public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, + BsqWalletService bsqWalletService, + FilterManager filterManager, + Preferences preferences, + PriceFeedService priceFeedService, + P2PService p2PService, + ReferralIdService referralIdService, + TradeStatisticsManager tradeStatisticsManager) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.bsqWalletService = bsqWalletService; + this.filterManager = filterManager; + this.preferences = preferences; + this.priceFeedService = priceFeedService; + this.p2PService = p2PService; + this.referralIdService = referralIdService; + this.tradeStatisticsManager = tradeStatisticsManager; + } + + public void maybeSetFeePaymentCurrencyPreference(String feeCurrencyCode) { + if (!feeCurrencyCode.isEmpty()) { + if (!isValidFeePaymentCurrencyCode.test(feeCurrencyCode)) + throw new IllegalStateException(format("%s cannot be used to pay trade fees", + feeCurrencyCode.toUpperCase())); + + if (feeCurrencyCode.equalsIgnoreCase("BSQ") && preferences.isPayFeeInBtc()) + preferences.setPayFeeInBtc(false); + else if (feeCurrencyCode.equalsIgnoreCase("BTC") && !preferences.isPayFeeInBtc()) + preferences.setPayFeeInBtc(true); + } + } + + /** + * Given the direction, is this a BUY? + * + * @param direction the offer direction + * @return {@code true} for an offer to buy BTC from the taker, {@code false} for an + * offer to sell BTC to the taker + */ + public boolean isBuyOffer(Direction direction) { + return direction == Direction.BUY; + } + + public long getMaxTradeLimit(PaymentAccount paymentAccount, + String currencyCode, + Direction direction) { + return paymentAccount != null + ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) + : 0; + } + + /** + * Return true if a balance can cover a cost. + * + * @param cost the cost of a trade + * @param balance a wallet balance + * @return true if balance >= cost + */ + public boolean isBalanceSufficient(Coin cost, Coin balance) { + return cost != null && balance.compareTo(cost) >= 0; + } + + /** + * Return the wallet balance shortage for a given trade cost, or zero if there is + * no shortage. + * + * @param cost the cost of a trade + * @param balance a wallet balance + * @return the wallet balance shortage for the given cost, else zero. + */ + public Coin getBalanceShortage(Coin cost, Coin balance) { + if (cost != null) { + Coin shortage = cost.subtract(balance); + return shortage.isNegative() ? Coin.ZERO : shortage; + } else { + return Coin.ZERO; + } + } + + /** + * Returns the usable BSQ balance. + * + * @return Coin the usable BSQ balance + */ + public Coin getUsableBsqBalance() { + // We have to keep a minimum amount of BSQ == bitcoin dust limit, otherwise there + // would be dust violations for change UTXOs; essentially means the minimum usable + // balance of BSQ is 5.46. + Coin usableBsqBalance = bsqWalletService.getAvailableConfirmedBalance().subtract(getMinNonDustOutput()); + return usableBsqBalance.isNegative() ? Coin.ZERO : usableBsqBalance; + } + + public double calculateManualPrice(double volumeAsDouble, double amountAsDouble) { + return volumeAsDouble / amountAsDouble; + } + + public double calculateMarketPriceMargin(double manualPrice, double marketPrice) { + return MathUtils.roundDouble(manualPrice / marketPrice, 4); + } + + /** + * Returns the makerFee as Coin, this can be priced in BTC or BSQ. + * + * @param amount the amount of BTC to trade + * @return the maker fee for the given trade amount, or {@code null} if the amount + * is {@code null} + */ + @Nullable + public Coin getMakerFee(@Nullable Coin amount) { + boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(amount); + return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount); + } + + public Coin getTxFeeByVsize(Coin txFeePerVbyteFromFeeService, int vsizeInVbytes) { + return txFeePerVbyteFromFeeService.multiply(getAverageTakerFeeTxVsize(vsizeInVbytes)); + } + + // We use the sum of the size of the trade fee and the deposit tx to get an average. + // Miners will take the trade fee tx if the total fee of both dependent txs are good + // enough. With that we avoid that we overpay in case that the trade fee has many + // inputs and we would apply that fee for the other 2 txs as well. We still might + // overpay a bit for the payout tx. + public int getAverageTakerFeeTxVsize(int txVsize) { + return (txVsize + 233) / 2; + } + + /** + * Checks if the maker fee should be paid in BTC, this can be the case due to user + * preference or because the user doesn't have enough BSQ. + * + * @param amount the amount of BTC to trade + * @return {@code true} if BTC is preferred or the trade amount is nonnull and there + * isn't enough BSQ for it. + */ + public boolean isCurrencyForMakerFeeBtc(@Nullable Coin amount) { + boolean payFeeInBtc = preferences.getPayFeeInBtc(); + boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(amount); + return payFeeInBtc || !bsqForFeeAvailable; + } + + /** + * Checks if the available BSQ balance is sufficient to pay for the offer's maker fee. + * + * @param amount the amount of BTC to trade + * @return {@code true} if the balance is sufficient, {@code false} otherwise + */ + public boolean isBsqForMakerFeeAvailable(@Nullable Coin amount) { + Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance(); + Coin makerFee = CoinUtil.getMakerFee(false, amount); + + // If we don't know yet the maker fee (amount is not set) we return true, + // otherwise we would disable BSQ fee each time we open the create offer screen + // as there the amount is not set. + if (makerFee == null) + return true; + + Coin surplusFunds = availableBalance.subtract(makerFee); + if (isDust(surplusFunds)) { + return false; // we can't be left with dust + } + return !availableBalance.subtract(makerFee).isNegative(); + } + + + @Nullable + public Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin amount) { + if (amount != null) { + Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getTakerFeePerBtc(isCurrencyForTakerFeeBtc), amount); + return CoinUtil.maxCoin(feePerBtc, FeeService.getMinTakerFee(isCurrencyForTakerFeeBtc)); + } else { + return null; + } + } + + public boolean isCurrencyForTakerFeeBtc(Coin amount) { + boolean payFeeInBtc = preferences.getPayFeeInBtc(); + boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(amount); + return payFeeInBtc || !bsqForFeeAvailable; + } + + public boolean isBsqForTakerFeeAvailable(@Nullable Coin amount) { + Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance(); + Coin takerFee = getTakerFee(false, amount); + + // If we don't know yet the maker fee (amount is not set) we return true, + // otherwise we would disable BSQ fee each time we open the create offer screen + // as there the amount is not set. + if (takerFee == null) + return true; + + Coin surplusFunds = availableBalance.subtract(takerFee); + if (isDust(surplusFunds)) { + return false; // we can't be left with dust + } + return !availableBalance.subtract(takerFee).isNegative(); + } + + public boolean isBlockChainPaymentMethod(Offer offer) { + return offer != null && offer.getPaymentMethod().isAsset(); + } + + public Optional getFeeInUserFiatCurrency(Coin makerFee, + boolean isCurrencyForMakerFeeBtc, + CoinFormatter bsqFormatter) { + String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode(); + if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) { + // In case the user has selected a altcoin as preferredTradeCurrency + // we derive the fiat currency from the user country + String countryCode = preferences.getUserCountry().code; + userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode(); + } + + return getFeeInUserFiatCurrency(makerFee, + isCurrencyForMakerFeeBtc, + userCurrencyCode, + bsqFormatter); + } + + public Map getExtraDataMap(PaymentAccount paymentAccount, + String currencyCode, + Direction direction) { + Map extraDataMap = new HashMap<>(); + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + String myWitnessHashAsHex = accountAgeWitnessService + .getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); + extraDataMap.put(ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); + } + + if (referralIdService.getOptionalReferralId().isPresent()) { + extraDataMap.put(REFERRAL_ID, referralIdService.getOptionalReferralId().get()); + } + + if (paymentAccount instanceof F2FAccount) { + extraDataMap.put(F2F_CITY, ((F2FAccount) paymentAccount).getCity()); + extraDataMap.put(F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); + } + + if (paymentAccount instanceof CashByMailAccount) { + extraDataMap.put(CASH_BY_MAIL_EXTRA_INFO, ((CashByMailAccount) paymentAccount).getExtraInfo()); + } + + extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); + + if (currencyCode.equals("XMR") && direction == Direction.SELL) { + preferences.getAutoConfirmSettingsList().stream() + .filter(e -> e.getCurrencyCode().equals("XMR")) + .filter(AutoConfirmSettings::isEnabled) + .forEach(e -> extraDataMap.put(XMR_AUTO_CONF, XMR_AUTO_CONF_ENABLED_VALUE)); + } + + return extraDataMap.isEmpty() ? null : extraDataMap; + } + + public void validateOfferData(double buyerSecurityDeposit, + PaymentAccount paymentAccount, + String currencyCode, + Coin makerFeeAsCoin) { + checkNotNull(makerFeeAsCoin, "makerFee must not be null"); + checkNotNull(p2PService.getAddress(), "Address must not be null"); + checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), + "securityDeposit must not exceed " + + getMaxBuyerSecurityDepositAsPercent()); + checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), + "securityDeposit must not be less than " + + getMinBuyerSecurityDepositAsPercent()); + checkArgument(!filterManager.isCurrencyBanned(currencyCode), + Res.get("offerbook.warning.currencyBanned")); + checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), + Res.get("offerbook.warning.paymentMethodBanned")); + } + + private Optional getFeeInUserFiatCurrency(Coin makerFee, + boolean isCurrencyForMakerFeeBtc, + String userCurrencyCode, + CoinFormatter bsqFormatter) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); + if (marketPrice != null && makerFee != null) { + long marketPriceAsLong = roundDoubleToLong( + scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); + Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); + + if (isCurrencyForMakerFeeBtc) { + return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee)); + } else { + // We use the current market price for the fiat currency and the 30 day average BSQ price + Tuple2 tuple = AveragePriceUtil.getAveragePriceTuple(preferences, + tradeStatisticsManager, + 30); + Price bsqPrice = tuple.second; + if (bsqPrice.isPositive()) { + String inputValue = bsqFormatter.formatCoin(makerFee); + Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ"); + Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume); + Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc); + return Optional.of(volumeByAmount); + } else { + return Optional.empty(); + } + } + } else { + return Optional.empty(); + } + } + + public static Optional getInvalidMakerFeeTxErrorMessage(Offer offer, BtcWalletService btcWalletService) { + String offerFeePaymentTxId = offer.getOfferFeePaymentTxId(); + if (offerFeePaymentTxId == null) { + return Optional.empty(); + } + + Transaction makerFeeTx = btcWalletService.getTransaction(offerFeePaymentTxId); + if (makerFeeTx == null) { + return Optional.empty(); + } + + String errorMsg = null; + String header = "The offer with offer ID '" + offer.getShortId() + + "' has an invalid maker fee transaction.\n\n"; + String spendingTransaction = null; + String extraString = "\nYou have to remove that offer to avoid failed trades.\n" + + "If this happened because of a bug please contact the Bisq developers " + + "and you can request reimbursement for the lost maker fee."; + if (makerFeeTx.getOutputs().size() > 1) { + // Our output to fund the deposit tx is at index 1 + TransactionOutput output = makerFeeTx.getOutput(1); + TransactionInput spentByTransactionInput = output.getSpentBy(); + if (spentByTransactionInput != null) { + spendingTransaction = spentByTransactionInput.getConnectedTransaction() != null ? + spentByTransactionInput.getConnectedTransaction().toString() : + "null"; + // We this is an exceptional case we do not translate that error msg. + errorMsg = "The output of the maker fee tx is already spent.\n" + + extraString + + "\n\nTransaction input which spent the reserved funds for that offer: '" + + spentByTransactionInput.getConnectedTransaction().getTxId().toString() + ":" + + (spentByTransactionInput.getConnectedOutput() != null ? + spentByTransactionInput.getConnectedOutput().getIndex() + "'" : + "null'"); + log.error("spentByTransactionInput {}", spentByTransactionInput); + } + } else { + errorMsg = "The maker fee tx is invalid as it does not has at least 2 outputs." + extraString + + "\nMakerFeeTx=" + makerFeeTx.toString(); + } + + if (errorMsg == null) { + return Optional.empty(); + } + + errorMsg = header + errorMsg; + log.error(errorMsg); + if (spendingTransaction != null) { + log.error("Spending transaction: {}", spendingTransaction); + } + + return Optional.of(errorMsg); + } +} diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java new file mode 100644 index 0000000000..4c2d06c308 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -0,0 +1,201 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.trade.Tradable; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.proto.ProtoUtil; + +import java.util.Date; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode +@Slf4j +public final class OpenOffer implements Tradable { + // Timeout for offer reservation during takeoffer process. If deposit tx is not completed in that time we reset the offer to AVAILABLE state. + private static final long TIMEOUT = 60; + transient private Timer timeoutTimer; + + public enum State { + AVAILABLE, + RESERVED, + CLOSED, + CANCELED, + DEACTIVATED + } + + @Getter + private final Offer offer; + @Getter + private State state; + @Getter + @Setter + @Nullable + private NodeAddress arbitratorNodeAddress; + @Getter + @Setter + @Nullable + private NodeAddress mediatorNodeAddress; + + // Added v1.2.0 + @Getter + @Setter + @Nullable + private NodeAddress refundAgentNodeAddress; + + // Added in v1.5.3. + // If market price reaches that trigger price the offer gets deactivated + @Getter + private final long triggerPrice; + @Getter + @Setter + transient private long mempoolStatus = -1; + + public OpenOffer(Offer offer) { + this(offer, 0); + } + + public OpenOffer(Offer offer, long triggerPrice) { + this.offer = offer; + this.triggerPrice = triggerPrice; + state = State.AVAILABLE; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private OpenOffer(Offer offer, + State state, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + long triggerPrice) { + this.offer = offer; + this.state = state; + this.arbitratorNodeAddress = arbitratorNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; + this.triggerPrice = triggerPrice; + + if (this.state == State.RESERVED) + setState(State.AVAILABLE); + } + + @Override + public protobuf.Tradable toProtoMessage() { + protobuf.OpenOffer.Builder builder = protobuf.OpenOffer.newBuilder() + .setOffer(offer.toProtoMessage()) + .setTriggerPrice(triggerPrice) + .setState(protobuf.OpenOffer.State.valueOf(state.name())); + + Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage())); + Optional.ofNullable(mediatorNodeAddress).ifPresent(nodeAddress -> builder.setMediatorNodeAddress(nodeAddress.toProtoMessage())); + Optional.ofNullable(refundAgentNodeAddress).ifPresent(nodeAddress -> builder.setRefundAgentNodeAddress(nodeAddress.toProtoMessage())); + + return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); + } + + public static Tradable fromProto(protobuf.OpenOffer proto) { + return new OpenOffer(Offer.fromProto(proto.getOffer()), + ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()), + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + proto.getTriggerPrice()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Date getDate() { + return offer.getDate(); + } + + @Override + public String getId() { + return offer.getId(); + } + + @Override + public String getShortId() { + return offer.getShortId(); + } + + public void setState(State state) { + this.state = state; + + // We keep it reserved for a limited time, if trade preparation fails we revert to available state + if (this.state == State.RESERVED) { + startTimeout(); + } else { + stopTimeout(); + } + } + + public boolean isDeactivated() { + return state == State.DEACTIVATED; + } + + private void startTimeout() { + stopTimeout(); + + timeoutTimer = UserThread.runAfter(() -> { + log.debug("Timeout for resetting State.RESERVED reached"); + if (state == State.RESERVED) { + // we do not need to persist that as at startup any RESERVED state would be reset to AVAILABLE anyway + setState(State.AVAILABLE); + } + }, TIMEOUT); + } + + private void stopTimeout() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } + + + @Override + public String toString() { + return "OpenOffer{" + + ",\n offer=" + offer + + ",\n state=" + state + + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n triggerPrice=" + triggerPrice + + "\n}"; + } +} + diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java new file mode 100644 index 0000000000..bf04e10b19 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -0,0 +1,1052 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.api.CoreContext; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.exceptions.TradePriceOutOfToleranceException; +import bisq.core.filter.FilterManager; +import bisq.core.locale.Res; +import bisq.core.offer.availability.DisputeAgentSelection; +import bisq.core.offer.messages.OfferAvailabilityRequest; +import bisq.core.offer.messages.OfferAvailabilityResponse; +import bisq.core.offer.placeoffer.PlaceOfferModel; +import bisq.core.offer.placeoffer.PlaceOfferProtocol; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.trade.TradableList; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.handlers.TransactionResultHandler; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.Validator; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.DecryptedDirectMessageListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendDirectMessageListener; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMessageListener, PersistedDataHost { + private static final Logger log = LoggerFactory.getLogger(OpenOfferManager.class); + + private static final long RETRY_REPUBLISH_DELAY_SEC = 10; + private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; + private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(40); + private static final long REFRESH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(6); + + private final CoreContext coreContext; + private final CreateOfferService createOfferService; + private final KeyRing keyRing; + private final User user; + private final P2PService p2PService; + private final BtcWalletService btcWalletService; + private final TradeWalletService tradeWalletService; + private final BsqWalletService bsqWalletService; + private final OfferBookService offerBookService; + private final ClosedTradableManager closedTradableManager; + private final PriceFeedService priceFeedService; + private final Preferences preferences; + private final TradeStatisticsManager tradeStatisticsManager; + private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final DaoFacade daoFacade; + private final FilterManager filterManager; + private final Broadcaster broadcaster; + private final PersistenceManager> persistenceManager; + private final Map offersToBeEdited = new HashMap<>(); + private final TradableList openOffers = new TradableList<>(); + private boolean stopped; + private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer; + @Getter + private final ObservableList> invalidOffers = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public OpenOfferManager(CoreContext coreContext, + CreateOfferService createOfferService, + KeyRing keyRing, + User user, + P2PService p2PService, + BtcWalletService btcWalletService, + TradeWalletService tradeWalletService, + BsqWalletService bsqWalletService, + OfferBookService offerBookService, + ClosedTradableManager closedTradableManager, + PriceFeedService priceFeedService, + Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, + FilterManager filterManager, + Broadcaster broadcaster, + PersistenceManager> persistenceManager) { + this.coreContext = coreContext; + this.createOfferService = createOfferService; + this.keyRing = keyRing; + this.user = user; + this.p2PService = p2PService; + this.btcWalletService = btcWalletService; + this.tradeWalletService = tradeWalletService; + this.bsqWalletService = bsqWalletService; + this.offerBookService = offerBookService; + this.closedTradableManager = closedTradableManager; + this.priceFeedService = priceFeedService; + this.preferences = preferences; + this.tradeStatisticsManager = tradeStatisticsManager; + this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.daoFacade = daoFacade; + this.filterManager = filterManager; + this.broadcaster = broadcaster; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); + } + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + openOffers.setAll(persisted.getList()); + openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); + completeHandler.run(); + }, + completeHandler); + } + + public void onAllServicesInitialized() { + p2PService.addDecryptedDirectMessageListener(this); + + if (p2PService.isBootstrapped()) { + onBootstrapComplete(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + onBootstrapComplete(); + } + }); + } + + cleanUpAddressEntries(); + + openOffers.stream() + .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) + .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); + } + + private void cleanUpAddressEntries() { + Set openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + btcWalletService.getAddressEntriesForOpenOffer().stream() + .filter(e -> !openOffersIdSet.contains(e.getOfferId())) + .forEach(e -> { + log.warn("We found an outdated addressEntry for openOffer {} (openOffers does not contain that " + + "offer), offers.size={}", + e.getOfferId(), openOffers.size()); + btcWalletService.resetAddressEntriesForOpenOffer(e.getOfferId()); + }); + } + + public void shutDown(@Nullable Runnable completeHandler) { + stopped = true; + p2PService.getPeerManager().removeListener(this); + p2PService.removeDecryptedDirectMessageListener(this); + + stopPeriodicRefreshOffersTimer(); + stopPeriodicRepublishOffersTimer(); + stopRetryRepublishOffersTimer(); + + // we remove own offers from offerbook when we go offline + // Normally we use a delay for broadcasting to the peers, but at shut down we want to get it fast out + int size = openOffers.size(); + log.info("Remove open offers at shutDown. Number of open offers: {}", size); + if (offerBookService.isBootstrapped() && size > 0) { + UserThread.execute(() -> openOffers.forEach( + openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload()) + )); + + // Force broadcaster to send out immediately, otherwise we could have a 2 sec delay until the + // bundled messages sent out. + broadcaster.flush(); + + if (completeHandler != null) { + // For typical number of offers we are tolerant with delay to give enough time to broadcast. + // If number of offers is very high we limit to 3 sec. to not delay other shutdown routines. + int delay = Math.min(3000, size * 200 + 500); + UserThread.runAfter(completeHandler, delay, TimeUnit.MILLISECONDS); + } + } else { + if (completeHandler != null) + completeHandler.run(); + } + } + + public void removeAllOpenOffers(@Nullable Runnable completeHandler) { + removeOpenOffers(getObservableList(), completeHandler); + } + + private 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 -> removeOpenOffer(openOffer, () -> { + }, errorMessage -> { + })); + if (completeHandler != null) + UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DecryptedDirectMessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peerNodeAddress) { + // Handler for incoming offer availability requests + // We get an encrypted message but don't do the signature check as we don't know the peer yet. + // A basic sig check is in done also at decryption time + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof OfferAvailabilityRequest) { + handleOfferAvailabilityRequest((OfferAvailabilityRequest) networkEnvelope, peerNodeAddress); + } else if (networkEnvelope instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) networkEnvelope; + if (ackMessage.getSourceType() == AckMessageSourceType.OFFER_MESSAGE) { + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} with offerId {} and uid {}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {} with offerId {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); + } + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BootstrapListener delegate + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onBootstrapComplete() { + stopped = false; + + maybeUpdatePersistedOffers(); + + // Republish means we send the complete offer object + republishOffers(); + startPeriodicRepublishOffersTimer(); + + // Refresh is started once we get a success from republish + + // We republish after a bit as it might be that our connected node still has the offer in the data map + // but other peers have it already removed because of expired TTL. + // Those other not directly connected peers would not get the broadcast of the new offer, as the first + // connected peer (seed node) does not broadcast if it has the data in the map. + // To update quickly to the whole network we repeat the republishOffers call after a few seconds when we + // are better connected to the network. There is no guarantee that all peers will receive it but we also + // have our periodic timer, so after that longer interval the offer should be available to all peers. + if (retryRepublishOffersTimer == null) + retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, + REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC); + + p2PService.getPeerManager().addListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PeerManager.Listener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAllConnectionsLost() { + log.info("onAllConnectionsLost"); + stopped = true; + stopPeriodicRefreshOffersTimer(); + stopPeriodicRepublishOffersTimer(); + stopRetryRepublishOffersTimer(); + + restart(); + } + + @Override + public void onNewConnectionAfterAllConnectionsLost() { + log.info("onNewConnectionAfterAllConnectionsLost"); + stopped = false; + restart(); + } + + @Override + public void onAwakeFromStandby() { + log.info("onAwakeFromStandby"); + stopped = false; + if (!p2PService.getNetworkNode().getAllConnections().isEmpty()) + restart(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void placeOffer(Offer offer, + double buyerSecurityDeposit, + boolean useSavingsWallet, + long triggerPrice, + TransactionResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + checkNotNull(offer.getMakerFee(), "makerFee must not be null"); + + Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(), + offer.getAmount(), + buyerSecurityDeposit, + createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit)); + + PlaceOfferModel model = new PlaceOfferModel(offer, + reservedFundsForOffer, + useSavingsWallet, + btcWalletService, + tradeWalletService, + bsqWalletService, + offerBookService, + arbitratorManager, + tradeStatisticsManager, + daoFacade, + user, + filterManager); + PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol( + model, + transaction -> { + OpenOffer openOffer = new OpenOffer(offer, triggerPrice); + openOffers.add(openOffer); + requestPersistence(); + resultHandler.handleResult(transaction); + if (!stopped) { + startPeriodicRepublishOffersTimer(); + startPeriodicRefreshOffersTimer(); + } else { + log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); + } + }, + errorMessageHandler + ); + placeOfferProtocol.placeOffer(); + } + + // Remove from offerbook + public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Optional openOfferOptional = getOpenOfferById(offer.getId()); + if (openOfferOptional.isPresent()) { + removeOpenOffer(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."); + offerBookService.removeOffer(offer.getOfferPayload(), + () -> offer.setState(Offer.State.REMOVED), + null); + } + } + + public void activateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (!offersToBeEdited.containsKey(openOffer.getId())) { + Offer offer = openOffer.getOffer(); + offerBookService.activateOffer(offer, + () -> { + openOffer.setState(OpenOffer.State.AVAILABLE); + requestPersistence(); + log.debug("activateOpenOffer, offerId={}", offer.getId()); + resultHandler.handleResult(); + }, + errorMessageHandler); + } else { + errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + } + } + + public void deactivateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + Offer offer = openOffer.getOffer(); + offerBookService.deactivateOffer(offer.getOfferPayload(), + () -> { + openOffer.setState(OpenOffer.State.DEACTIVATED); + requestPersistence(); + log.debug("deactivateOpenOffer, offerId={}", offer.getId()); + resultHandler.handleResult(); + }, + errorMessageHandler); + } + + public void removeOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (!offersToBeEdited.containsKey(openOffer.getId())) { + Offer offer = openOffer.getOffer(); + if (openOffer.isDeactivated()) { + onRemoved(openOffer, resultHandler, offer); + } else { + offerBookService.removeOffer(offer.getOfferPayload(), + () -> onRemoved(openOffer, resultHandler, offer), + errorMessageHandler); + } + } else { + errorMessageHandler.handleErrorMessage("You can't remove an offer that is currently edited."); + } + } + + public void editOpenOfferStart(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (offersToBeEdited.containsKey(openOffer.getId())) { + log.warn("editOpenOfferStart called for an offer which is already in edit mode."); + resultHandler.handleResult(); + return; + } + + offersToBeEdited.put(openOffer.getId(), openOffer); + + if (openOffer.isDeactivated()) { + resultHandler.handleResult(); + } else { + deactivateOpenOffer(openOffer, + resultHandler, + errorMessage -> { + offersToBeEdited.remove(openOffer.getId()); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + } + + public void editOpenOfferPublish(Offer editedOffer, + long triggerPrice, + OpenOffer.State originalState, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + Optional openOfferOptional = getOpenOfferById(editedOffer.getId()); + + if (openOfferOptional.isPresent()) { + OpenOffer openOffer = openOfferOptional.get(); + + openOffer.getOffer().setState(Offer.State.REMOVED); + openOffer.setState(OpenOffer.State.CANCELED); + openOffers.remove(openOffer); + + OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice); + editedOpenOffer.setState(originalState); + + openOffers.add(editedOpenOffer); + + if (!editedOpenOffer.isDeactivated()) + republishOffer(editedOpenOffer); + + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); + } + } + + public void editOpenOfferCancel(OpenOffer openOffer, + OpenOffer.State originalState, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (offersToBeEdited.containsKey(openOffer.getId())) { + offersToBeEdited.remove(openOffer.getId()); + if (originalState.equals(OpenOffer.State.AVAILABLE)) { + activateOpenOffer(openOffer, resultHandler, errorMessageHandler); + } else { + resultHandler.handleResult(); + } + } else { + errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); + } + } + + private void onRemoved(@NotNull OpenOffer openOffer, ResultHandler resultHandler, Offer offer) { + offer.setState(Offer.State.REMOVED); + openOffer.setState(OpenOffer.State.CANCELED); + openOffers.remove(openOffer); + closedTradableManager.add(openOffer); + log.info("onRemoved offerId={}", offer.getId()); + btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); + requestPersistence(); + resultHandler.handleResult(); + } + + // Close openOffer after deposit published + public void closeOpenOffer(Offer offer) { + getOpenOfferById(offer.getId()).ifPresent(openOffer -> { + openOffers.remove(openOffer); + openOffer.setState(OpenOffer.State.CLOSED); + offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), + () -> log.trace("Successful removed offer"), + log::error); + requestPersistence(); + }); + } + + public void reserveOpenOffer(OpenOffer openOffer) { + openOffer.setState(OpenOffer.State.RESERVED); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); + } + + public ObservableList getObservableList() { + return openOffers.getObservableList(); + } + + public Optional getOpenOfferById(String offerId) { + return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // OfferPayload Availability + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, NodeAddress peer) { + log.info("Received OfferAvailabilityRequest from {} with offerId {} and uid {}", + peer, request.getOfferId(), request.getUid()); + + boolean result = false; + String errorMessage = null; + + if (!p2PService.isBootstrapped()) { + errorMessage = "We got a handleOfferAvailabilityRequest but we have not bootstrapped yet."; + log.info(errorMessage); + sendAckMessage(request, peer, false, errorMessage); + return; + } + + // Don't allow trade start if BitcoinJ is not fully synced (bisq issue #4764) + if (!btcWalletService.isChainHeightSyncedWithinTolerance()) { + errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced."; + log.info(errorMessage); + sendAckMessage(request, peer, false, errorMessage); + return; + } + + if (stopped) { + errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; + log.debug(errorMessage); + sendAckMessage(request, peer, false, errorMessage); + return; + } + + try { + Validator.nonEmptyStringOf(request.offerId); + checkNotNull(request.getPubKeyRing()); + } catch (Throwable t) { + errorMessage = "Message validation failed. Error=" + t.toString() + ", Message=" + request.toString(); + log.warn(errorMessage); + sendAckMessage(request, peer, false, errorMessage); + return; + } + + try { + Optional openOfferOptional = getOpenOfferById(request.offerId); + AvailabilityResult availabilityResult; + NodeAddress arbitratorNodeAddress = null; + NodeAddress mediatorNodeAddress = null; + NodeAddress refundAgentNodeAddress = null; + if (openOfferOptional.isPresent()) { + OpenOffer openOffer = openOfferOptional.get(); + if (!apiUserDeniedByOffer(request)) { + if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + Offer offer = openOffer.getOffer(); + if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { + mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); + openOffer.setMediatorNodeAddress(mediatorNodeAddress); + + refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress(); + openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress); + + try { + // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference + // in trade price between the peers. Also here poor connectivity might cause market price API connection + // losses and therefore an outdated market price. + offer.checkTradePriceTolerance(request.getTakersTradePrice()); + availabilityResult = AvailabilityResult.AVAILABLE; + } catch (TradePriceOutOfToleranceException e) { + log.warn("Trade price check failed because takers price is outside out tolerance."); + availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; + } catch (MarketPriceNotAvailableException e) { + log.warn(e.getMessage()); + availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; + } catch (Throwable e) { + log.warn("Trade price check failed. " + e.getMessage()); + if (coreContext.isApiUser()) + // Give api user something more than 'unknown_failure'. + availabilityResult = AvailabilityResult.PRICE_CHECK_FAILED; + else + availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + } + } else { + availabilityResult = AvailabilityResult.USER_IGNORED; + } + } else { + availabilityResult = AvailabilityResult.OFFER_TAKEN; + } + } else { + availabilityResult = AvailabilityResult.MAKER_DENIED_API_USER; + } + } else { + log.warn("handleOfferAvailabilityRequest: openOffer not found."); + availabilityResult = AvailabilityResult.OFFER_TAKEN; + } + + if (btcWalletService.isUnconfirmedTransactionsLimitHit() || bsqWalletService.isUnconfirmedTransactionsLimitHit()) { + errorMessage = Res.get("shared.unconfirmedTransactionsLimitReached"); + log.warn(errorMessage); + availabilityResult = AvailabilityResult.UNCONF_TX_LIMIT_HIT; + } + + OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, + availabilityResult, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress); + log.info("Send {} with offerId {} and uid {} to peer {}", + offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), + offerAvailabilityResponse.getUid(), peer); + p2PService.sendEncryptedDirectMessage(peer, + request.getPubKeyRing(), + offerAvailabilityResponse, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer: offerId={}; uid={}", + offerAvailabilityResponse.getClass().getSimpleName(), + offerAvailabilityResponse.getOfferId(), + offerAvailabilityResponse.getUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", + offerAvailabilityResponse.getClass().getSimpleName(), + offerAvailabilityResponse.getUid(), + peer, + errorMessage); + } + }); + result = true; + } catch (Throwable t) { + errorMessage = "Exception at handleRequestIsOfferAvailableMessage " + t.getMessage(); + log.error(errorMessage); + t.printStackTrace(); + } finally { + sendAckMessage(request, peer, result, errorMessage); + } + } + + private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) { + return preferences.isDenyApiTaker() && request.isTakerApiUser(); + } + + private void sendAckMessage(OfferAvailabilityRequest message, + NodeAddress sender, + boolean result, + String errorMessage) { + String offerId = message.getOfferId(); + String sourceUid = message.getUid(); + AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), + AckMessageSourceType.OFFER_MESSAGE, + message.getClass().getSimpleName(), + sourceUid, + offerId, + result, + errorMessage); + + final NodeAddress takersNodeAddress = sender; + PubKeyRing takersPubKeyRing = message.getPubKeyRing(); + log.info("Send AckMessage for OfferAvailabilityRequest to peer {} with offerId {} and sourceUid {}", + takersNodeAddress, offerId, ackMessage.getSourceUid()); + p2PService.sendEncryptedDirectMessage( + takersNodeAddress, + takersPubKeyRing, + ackMessage, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for OfferAvailabilityRequest arrived at takersNodeAddress {}. offerId={}, sourceUid={}", + takersNodeAddress, offerId, ackMessage.getSourceUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for OfferAvailabilityRequest failed. AckMessage={}, takersNodeAddress={}, errorMessage={}", + ackMessage, takersNodeAddress, errorMessage); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Update persisted offer if a new capability is required after a software update + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeUpdatePersistedOffers() { + // We need to clone to avoid ConcurrentModificationException + ArrayList openOffersClone = new ArrayList<>(openOffers.getList()); + openOffersClone.forEach(originalOpenOffer -> { + Offer originalOffer = originalOpenOffer.getOffer(); + + OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); + // We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and + // Capability.REFUND_AGENT in v1.2.0 and want to rewrite a + // persisted offer after the user has updated to 1.2.0 so their offer will be accepted by the network. + + if (originalOfferPayload.getProtocolVersion() < Version.TRADE_PROTOCOL_VERSION || + !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION) || + !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.REFUND_AGENT) || + !originalOfferPayload.getOwnerNodeAddress().equals(p2PService.getAddress())) { + + // - Capabilities changed? + // We rewrite our offer with the additional capabilities entry + Map updatedExtraDataMap = new HashMap<>(); + if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION) || + !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.REFUND_AGENT)) { + Map originalExtraDataMap = originalOfferPayload.getExtraDataMap(); + + if (originalExtraDataMap != null) { + updatedExtraDataMap.putAll(originalExtraDataMap); + } + + // We overwrite any entry with our current capabilities + updatedExtraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); + + log.info("Converted offer to support new Capability.MEDIATION and Capability.REFUND_AGENT capability. id={}", originalOffer.getId()); + } else { + updatedExtraDataMap = originalOfferPayload.getExtraDataMap(); + } + + // - Protocol version changed? + int protocolVersion = originalOfferPayload.getProtocolVersion(); + if (protocolVersion < Version.TRADE_PROTOCOL_VERSION) { + // We update the trade protocol version + protocolVersion = Version.TRADE_PROTOCOL_VERSION; + log.info("Updated the protocol version of offer id={}", originalOffer.getId()); + } + + // - node address changed? (due to a faulty tor dir) + NodeAddress ownerNodeAddress = originalOfferPayload.getOwnerNodeAddress(); + if (!ownerNodeAddress.equals(p2PService.getAddress())) { + ownerNodeAddress = p2PService.getAddress(); + log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId()); + } + + OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), + originalOfferPayload.getDate(), + ownerNodeAddress, + originalOfferPayload.getPubKeyRing(), + originalOfferPayload.getDirection(), + originalOfferPayload.getPrice(), + originalOfferPayload.getMarketPriceMargin(), + originalOfferPayload.isUseMarketBasedPrice(), + originalOfferPayload.getAmount(), + originalOfferPayload.getMinAmount(), + originalOfferPayload.getBaseCurrencyCode(), + originalOfferPayload.getCounterCurrencyCode(), + originalOfferPayload.getArbitratorNodeAddresses(), + originalOfferPayload.getMediatorNodeAddresses(), + originalOfferPayload.getPaymentMethodId(), + originalOfferPayload.getMakerPaymentAccountId(), + originalOfferPayload.getOfferFeePaymentTxId(), + originalOfferPayload.getCountryCode(), + originalOfferPayload.getAcceptedCountryCodes(), + originalOfferPayload.getBankId(), + originalOfferPayload.getAcceptedBankIds(), + originalOfferPayload.getVersionNr(), + originalOfferPayload.getBlockHeightAtOfferCreation(), + originalOfferPayload.getTxFee(), + originalOfferPayload.getMakerFee(), + originalOfferPayload.isCurrencyForMakerFeeBtc(), + originalOfferPayload.getBuyerSecurityDeposit(), + originalOfferPayload.getSellerSecurityDeposit(), + originalOfferPayload.getMaxTradeLimit(), + originalOfferPayload.getMaxTradePeriod(), + originalOfferPayload.isUseAutoClose(), + originalOfferPayload.isUseReOpenAfterAutoClose(), + originalOfferPayload.getLowerClosePrice(), + originalOfferPayload.getUpperClosePrice(), + originalOfferPayload.isPrivateOffer(), + originalOfferPayload.getHashOfChallenge(), + updatedExtraDataMap, + protocolVersion); + + // Save states from original data to use for the updated + Offer.State originalOfferState = originalOffer.getState(); + OpenOffer.State originalOpenOfferState = originalOpenOffer.getState(); + + // remove old offer + originalOffer.setState(Offer.State.REMOVED); + originalOpenOffer.setState(OpenOffer.State.CANCELED); + openOffers.remove(originalOpenOffer); + + // Create new Offer + Offer updatedOffer = new Offer(updatedPayload); + updatedOffer.setPriceFeedService(priceFeedService); + updatedOffer.setState(originalOfferState); + + OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); + updatedOpenOffer.setState(originalOpenOfferState); + openOffers.add(updatedOpenOffer); + requestPersistence(); + + log.info("Updating offer completed. id={}", originalOffer.getId()); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // RepublishOffers, refreshOffers + /////////////////////////////////////////////////////////////////////////////////////////// + + private void republishOffers() { + if (stopped) { + return; + } + + stopPeriodicRefreshOffersTimer(); + + List openOffersList = new ArrayList<>(openOffers.getList()); + processListForRepublishOffers(openOffersList); + } + + private void processListForRepublishOffers(List list) { + if (list.isEmpty()) { + return; + } + + OpenOffer openOffer = list.remove(0); + if (openOffers.contains(openOffer) && !openOffer.isDeactivated()) { + // TODO It is not clear yet if it is better for the node and the network to send out all add offer + // messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have + // some significant impact to user experience and the network + republishOffer(openOffer, () -> processListForRepublishOffers(list)); + + /* republishOffer(openOffer, + () -> UserThread.runAfter(() -> processListForRepublishOffers(list), + 30, TimeUnit.MILLISECONDS));*/ + } else { + // If the offer was removed in the meantime or if its deactivated we skip and call + // processListForRepublishOffers again with the list where we removed the offer already. + processListForRepublishOffers(list); + } + } + + private void republishOffer(OpenOffer openOffer) { + republishOffer(openOffer, null); + } + + private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { + 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(); + } + } + }); + } + + private void startPeriodicRepublishOffersTimer() { + stopped = false; + if (periodicRepublishOffersTimer == null) { + periodicRepublishOffersTimer = UserThread.runPeriodically(() -> { + if (!stopped) { + republishOffers(); + } + }, + REPUBLISH_INTERVAL_MS, + TimeUnit.MILLISECONDS); + } + } + + private void startPeriodicRefreshOffersTimer() { + stopped = false; + // refresh sufficiently before offer would expire + 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 + if (openOffers.contains(openOffer) && !openOffer.isDeactivated()) + refreshOffer(openOffer); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + } + } else { + log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); + } + }, + REFRESH_INTERVAL_MS, + TimeUnit.MILLISECONDS); + else + log.trace("periodicRefreshOffersTimer already stated"); + } + + private void refreshOffer(OpenOffer openOffer) { + offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(), + () -> log.debug("Successful refreshed TTL for offer"), + log::warn); + } + + private void restart() { + log.debug("Restart after connection loss"); + if (retryRepublishOffersTimer == null) + retryRepublishOffersTimer = UserThread.runAfter(() -> { + stopped = false; + stopRetryRepublishOffersTimer(); + republishOffers(); + }, RETRY_REPUBLISH_DELAY_SEC); + + startPeriodicRepublishOffersTimer(); + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void stopPeriodicRefreshOffersTimer() { + if (periodicRefreshOffersTimer != null) { + periodicRefreshOffersTimer.stop(); + periodicRefreshOffersTimer = null; + } + } + + private void stopPeriodicRepublishOffersTimer() { + if (periodicRepublishOffersTimer != null) { + periodicRepublishOffersTimer.stop(); + periodicRepublishOffersTimer = null; + } + } + + private void stopRetryRepublishOffersTimer() { + if (retryRepublishOffersTimer != null) { + retryRepublishOffersTimer.stop(); + retryRepublishOffersTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/offer/TriggerPriceService.java b/core/src/main/java/bisq/core/offer/TriggerPriceService.java new file mode 100644 index 0000000000..99defe325b --- /dev/null +++ b/core/src/main/java/bisq/core/offer/TriggerPriceService.java @@ -0,0 +1,201 @@ +/* + * 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 bisq.core.offer; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.provider.mempool.MempoolService; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ListChangeListener; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; + +@Slf4j +@Singleton +public class TriggerPriceService { + private final P2PService p2PService; + private final OpenOfferManager openOfferManager; + private final MempoolService mempoolService; + private final PriceFeedService priceFeedService; + private final Map> openOffersByCurrency = new HashMap<>(); + + @Inject + public TriggerPriceService(P2PService p2PService, + OpenOfferManager openOfferManager, + MempoolService mempoolService, + PriceFeedService priceFeedService) { + this.p2PService = p2PService; + this.openOfferManager = openOfferManager; + this.mempoolService = mempoolService; + this.priceFeedService = priceFeedService; + } + + public void onAllServicesInitialized() { + if (p2PService.isBootstrapped()) { + onBootstrapComplete(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + onBootstrapComplete(); + } + }); + } + } + + private void onBootstrapComplete() { + openOfferManager.getObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + onAddedOpenOffers(c.getAddedSubList()); + } + if (c.wasRemoved()) { + onRemovedOpenOffers(c.getRemoved()); + } + }); + onAddedOpenOffers(openOfferManager.getObservableList()); + + priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> onPriceFeedChanged()); + onPriceFeedChanged(); + } + + private void onPriceFeedChanged() { + openOffersByCurrency.keySet().stream() + .map(priceFeedService::getMarketPrice) + .filter(Objects::nonNull) + .filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) + .forEach(marketPrice -> { + openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() + .filter(openOffer -> !openOffer.isDeactivated()) + .forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); + }); + } + + public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) { + Price price = openOffer.getOffer().getPrice(); + if (price == null || marketPrice == null) { + return false; + } + + String currencyCode = openOffer.getOffer().getCurrencyCode(); + boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); + int smallestUnitExponent = cryptoCurrency ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long marketPriceAsLong = roundDoubleToLong( + scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent)); + long triggerPrice = openOffer.getTriggerPrice(); + if (triggerPrice <= 0) { + return false; + } + + OfferPayload.Direction direction = openOffer.getOffer().getDirection(); + boolean isSellOffer = direction == OfferPayload.Direction.SELL; + boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; + return condition ? + marketPriceAsLong < triggerPrice : + marketPriceAsLong > triggerPrice; + } + + private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { + if (wasTriggered(marketPrice, openOffer)) { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long triggerPrice = openOffer.getTriggerPrice(); + + 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: {}", + openOffer.getOffer().getShortId(), + currencyCode, + openOffer.getOffer().getDirection(), + marketPrice.getPrice(), + MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) + ); + + openOfferManager.deactivateOpenOffer(openOffer, () -> { + }, errorMessage -> { + }); + } else if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + // check the mempool if it has not been done before + if (openOffer.getMempoolStatus() < 0 && mempoolService.canRequestBeMade(openOffer.getOffer().getOfferPayload())) { + mempoolService.validateOfferMakerTx(openOffer.getOffer().getOfferPayload(), (txValidator -> { + openOffer.setMempoolStatus(txValidator.isFail() ? 0 : 1); + })); + } + // if the mempool indicated failure then deactivate the open offer + if (openOffer.getMempoolStatus() == 0) { + log.info("Deactivating open offer {} due to mempool validation", openOffer.getOffer().getShortId()); + openOfferManager.deactivateOpenOffer(openOffer, () -> { + }, errorMessage -> { + }); + } + } + } + + private void onAddedOpenOffers(List openOffers) { + openOffers.forEach(openOffer -> { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>()); + openOffersByCurrency.get(currencyCode).add(openOffer); + + MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()); + if (marketPrice != null) { + checkPriceThreshold(marketPrice, openOffer); + } + }); + } + + private void onRemovedOpenOffers(List openOffers) { + openOffers.forEach(openOffer -> { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + if (openOffersByCurrency.containsKey(currencyCode)) { + Set set = openOffersByCurrency.get(currencyCode); + set.remove(openOffer); + if (set.isEmpty()) { + openOffersByCurrency.remove(currencyCode); + } + } + }); + } +} diff --git a/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java new file mode 100644 index 0000000000..eaec9dcbe9 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java @@ -0,0 +1,110 @@ +/* + * 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 bisq.core.offer.availability; + +import bisq.core.support.dispute.agent.DisputeAgent; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.common.util.Tuple2; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class DisputeAgentSelection { + public static final int LOOK_BACK_RANGE = 100; + + public static T getLeastUsedMediator(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager) { + return getLeastUsedDisputeAgent(tradeStatisticsManager, + disputeAgentManager, + true); + } + + public static T getLeastUsedRefundAgent(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager) { + return getLeastUsedDisputeAgent(tradeStatisticsManager, + disputeAgentManager, + false); + } + + private static T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager, + boolean isMediator) { + // We take last 100 entries from trade statistics + List list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); + list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); + Collections.reverse(list); + if (!list.isEmpty()) { + int max = Math.min(list.size(), LOOK_BACK_RANGE); + list = list.subList(0, max); + } + + // We stored only first 4 chars of disputeAgents onion address + List lastAddressesUsedInTrades = list.stream() + .map(tradeStatistics3 -> isMediator ? tradeStatistics3.getMediator() : tradeStatistics3.getRefundAgent()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + Set disputeAgents = disputeAgentManager.getObservableMap().values().stream() + .map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress()) + .collect(Collectors.toSet()); + + String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents); + + Optional optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream() + .filter(e -> e.getNodeAddress().getFullAddress().equals(result)) + .findAny(); + checkArgument(optionalDisputeAgent.isPresent(), "optionalDisputeAgent has to be present"); + return optionalDisputeAgent.get(); + } + + @VisibleForTesting + static String getLeastUsedDisputeAgent(List lastAddressesUsedInTrades, Set disputeAgents) { + checkArgument(!disputeAgents.isEmpty(), "disputeAgents must not be empty"); + List> disputeAgentTuples = disputeAgents.stream() + .map(e -> new Tuple2<>(e, new AtomicInteger(0))) + .collect(Collectors.toList()); + disputeAgentTuples.forEach(tuple -> { + int count = (int) lastAddressesUsedInTrades.stream() + .filter(tuple.first::startsWith) // we use only first 4 chars for comparing + .mapToInt(e -> 1) + .count(); + tuple.second.set(count); + }); + + disputeAgentTuples.sort(Comparator.comparing(e -> e.first)); + disputeAgentTuples.sort(Comparator.comparingInt(e -> e.second.get())); + return disputeAgentTuples.get(0).first; + } +} diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java new file mode 100644 index 0000000000..c1559cec8d --- /dev/null +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.offer.availability; + +import bisq.core.offer.Offer; +import bisq.core.offer.messages.OfferAvailabilityResponse; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.Model; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +public class OfferAvailabilityModel implements Model { + @Getter + private final Offer offer; + @Getter + private final PubKeyRing pubKeyRing; // takers PubKey (my pubkey) + @Getter + private final P2PService p2PService; + @Getter + final private User user; + @Getter + private final MediatorManager mediatorManager; + @Getter + private final TradeStatisticsManager tradeStatisticsManager; + private NodeAddress peerNodeAddress; // maker + private OfferAvailabilityResponse message; + @Nullable + @Setter + @Getter + private NodeAddress selectedArbitrator; + + // Added in v1.1.6 + @Nullable + @Setter + @Getter + private NodeAddress selectedMediator; + + // Added in v1.2.0 + @Nullable + @Setter + @Getter + private NodeAddress selectedRefundAgent; + + // Added in v1.5.5 + @Getter + private final boolean isTakerApiUser; + + public OfferAvailabilityModel(Offer offer, + PubKeyRing pubKeyRing, + P2PService p2PService, + User user, + MediatorManager mediatorManager, + TradeStatisticsManager tradeStatisticsManager, + boolean isTakerApiUser) { + this.offer = offer; + this.pubKeyRing = pubKeyRing; + this.p2PService = p2PService; + this.user = user; + this.mediatorManager = mediatorManager; + this.tradeStatisticsManager = tradeStatisticsManager; + this.isTakerApiUser = isTakerApiUser; + } + + public NodeAddress getPeerNodeAddress() { + return peerNodeAddress; + } + + void setPeerNodeAddress(NodeAddress peerNodeAddress) { + this.peerNodeAddress = peerNodeAddress; + } + + public void setMessage(OfferAvailabilityResponse message) { + this.message = message; + } + + public OfferAvailabilityResponse getMessage() { + return message; + } + + public long getTakersTradePrice() { + return offer.getPrice() != null ? offer.getPrice().getValue() : 0; + } + + @Override + public void onComplete() { + } +} diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java new file mode 100644 index 0000000000..218abca67b --- /dev/null +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java @@ -0,0 +1,208 @@ +/* + * 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 bisq.core.offer.availability; + +import bisq.core.offer.Offer; +import bisq.core.offer.availability.tasks.ProcessOfferAvailabilityResponse; +import bisq.core.offer.availability.tasks.SendOfferAvailabilityRequest; +import bisq.core.offer.messages.OfferAvailabilityResponse; +import bisq.core.offer.messages.OfferMessage; +import bisq.core.util.Validator; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.DecryptedDirectMessageListener; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class OfferAvailabilityProtocol { + private static final long TIMEOUT = 90; + + private final OfferAvailabilityModel model; + private final ResultHandler resultHandler; + private final ErrorMessageHandler errorMessageHandler; + private final DecryptedDirectMessageListener decryptedDirectMessageListener; + + private TaskRunner taskRunner; + private Timer timeoutTimer; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public OfferAvailabilityProtocol(OfferAvailabilityModel model, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + this.model = model; + this.resultHandler = resultHandler; + this.errorMessageHandler = errorMessageHandler; + + decryptedDirectMessageListener = (decryptedMessageWithPubKey, peersNodeAddress) -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof OfferMessage) { + OfferMessage offerMessage = (OfferMessage) networkEnvelope; + Validator.nonEmptyStringOf(offerMessage.offerId); + if (networkEnvelope instanceof OfferAvailabilityResponse + && model.getOffer().getId().equals(offerMessage.offerId)) { + handleOfferAvailabilityResponse((OfferAvailabilityResponse) networkEnvelope, peersNodeAddress); + } + } + }; + } + + private void cleanup() { + stopTimeout(); + model.getP2PService().removeDecryptedDirectMessageListener(decryptedDirectMessageListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Called from UI + /////////////////////////////////////////////////////////////////////////////////////////// + + public void sendOfferAvailabilityRequest() { + // reset + model.getOffer().setState(Offer.State.UNKNOWN); + + model.getP2PService().addDecryptedDirectMessageListener(decryptedDirectMessageListener); + model.setPeerNodeAddress(model.getOffer().getMakerNodeAddress()); + + taskRunner = new TaskRunner<>(model, + () -> handleTaskRunnerSuccess("TaskRunner at sendOfferAvailabilityRequest completed", null), + errorMessage -> handleTaskRunnerFault(errorMessage, null) + ); + taskRunner.addTasks(SendOfferAvailabilityRequest.class); + startTimeout(); + taskRunner.run(); + } + + public void cancel() { + taskRunner.cancel(); + cleanup(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message handling + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handleOfferAvailabilityResponse(OfferAvailabilityResponse message, NodeAddress peersNodeAddress) { + log.info("Received handleOfferAvailabilityResponse from {} with offerId {} and uid {}", + peersNodeAddress, message.getOfferId(), message.getUid()); + + stopTimeout(); + startTimeout(); + model.setMessage(message); + + taskRunner = new TaskRunner<>(model, + () -> { + handleTaskRunnerSuccess("TaskRunner at handle OfferAvailabilityResponse completed", message); + + stopTimeout(); + resultHandler.handleResult(); + }, + errorMessage -> handleTaskRunnerFault(errorMessage, message)); + taskRunner.addTasks(ProcessOfferAvailabilityResponse.class); + taskRunner.run(); + } + + private void startTimeout() { + if (timeoutTimer == null) { + timeoutTimer = UserThread.runAfter(() -> { + log.debug("Timeout reached at " + this); + model.getOffer().setState(Offer.State.MAKER_OFFLINE); + errorMessageHandler.handleErrorMessage("Timeout reached: Peer has not responded."); + }, TIMEOUT); + } else { + log.warn("timeoutTimer already created. That must not happen."); + } + } + + private void stopTimeout() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } + + private void handleTaskRunnerSuccess(String info, @Nullable OfferAvailabilityResponse message) { + log.debug("handleTaskRunnerSuccess " + info); + + if (message != null) + sendAckMessage(message, true, null); + } + + private void handleTaskRunnerFault(String errorMessage, @Nullable OfferAvailabilityResponse message) { + log.error(errorMessage); + + stopTimeout(); + errorMessageHandler.handleErrorMessage(errorMessage); + + if (message != null) + sendAckMessage(message, false, errorMessage); + } + + private void sendAckMessage(OfferAvailabilityResponse message, boolean result, @Nullable String errorMessage) { + String offerId = message.getOfferId(); + String sourceUid = message.getUid(); + final NodeAddress makersNodeAddress = model.getPeerNodeAddress(); + PubKeyRing makersPubKeyRing = model.getOffer().getPubKeyRing(); + log.info("Send AckMessage for OfferAvailabilityResponse to peer {} with offerId {} and sourceUid {}", + makersNodeAddress, offerId, sourceUid); + + AckMessage ackMessage = new AckMessage(model.getP2PService().getNetworkNode().getNodeAddress(), + AckMessageSourceType.OFFER_MESSAGE, + message.getClass().getSimpleName(), + sourceUid, + offerId, + result, + errorMessage); + model.getP2PService().sendEncryptedDirectMessage( + makersNodeAddress, + makersPubKeyRing, + ackMessage, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for OfferAvailabilityResponse arrived at makersNodeAddress {}. " + + "offerId={}, sourceUid={}", + makersNodeAddress, offerId, ackMessage.getSourceUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for OfferAvailabilityResponse failed. AckMessage={}, " + + "makersNodeAddress={}, errorMessage={}", + ackMessage, makersNodeAddress, errorMessage); + } + } + ); + } +} diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java new file mode 100644 index 0000000000..f6c04fa198 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.offer.availability.tasks; + +import bisq.core.offer.AvailabilityResult; +import bisq.core.offer.Offer; +import bisq.core.offer.availability.DisputeAgentSelection; +import bisq.core.offer.availability.OfferAvailabilityModel; +import bisq.core.offer.messages.OfferAvailabilityResponse; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class ProcessOfferAvailabilityResponse extends Task { + public ProcessOfferAvailabilityResponse(TaskRunner taskHandler, + OfferAvailabilityModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + Offer offer = model.getOffer(); + try { + runInterceptHook(); + + checkArgument(offer.getState() != Offer.State.REMOVED, "Offer state must not be Offer.State.REMOVED"); + + OfferAvailabilityResponse offerAvailabilityResponse = model.getMessage(); + + if (offerAvailabilityResponse.getAvailabilityResult() != AvailabilityResult.AVAILABLE) { + offer.setState(Offer.State.NOT_AVAILABLE); + failed("Take offer attempt rejected because of: " + offerAvailabilityResponse.getAvailabilityResult()); + return; + } + + offer.setState(Offer.State.AVAILABLE); + + model.setSelectedArbitrator(offerAvailabilityResponse.getArbitrator()); + + NodeAddress mediator = offerAvailabilityResponse.getMediator(); + if (mediator == null) { + // We do not get a mediator from old clients so we need to handle the null case. + mediator = DisputeAgentSelection.getLeastUsedMediator(model.getTradeStatisticsManager(), model.getMediatorManager()).getNodeAddress(); + } + model.setSelectedMediator(mediator); + + model.setSelectedRefundAgent(offerAvailabilityResponse.getRefundAgent()); + + complete(); + } catch (Throwable t) { + offer.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java new file mode 100644 index 0000000000..0dbc8e69ea --- /dev/null +++ b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.offer.availability.tasks; + +import bisq.core.offer.Offer; +import bisq.core.offer.availability.OfferAvailabilityModel; +import bisq.core.offer.messages.OfferAvailabilityRequest; + +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SendOfferAvailabilityRequest extends Task { + public SendOfferAvailabilityRequest(TaskRunner taskHandler, OfferAvailabilityModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), + model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser()); + log.info("Send {} with offerId {} and uid {} to peer {}", + message.getClass().getSimpleName(), message.getOfferId(), + message.getUid(), model.getPeerNodeAddress()); + + model.getP2PService().sendEncryptedDirectMessage(model.getPeerNodeAddress(), + model.getOffer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer: offerId={}; uid={}", + message.getClass().getSimpleName(), message.getOfferId(), message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", + message.getClass().getSimpleName(), message.getUid(), + model.getPeerNodeAddress(), errorMessage); + model.getOffer().setState(Offer.State.MAKER_OFFLINE); + } + } + ); + } catch (Throwable t) { + model.getOffer().setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java new file mode 100644 index 0000000000..6d9d14eaf6 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java @@ -0,0 +1,104 @@ +/* + * 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 bisq.core.offer.messages; + +import bisq.network.p2p.SupportedCapabilitiesMessage; + +import bisq.common.app.Capabilities; +import bisq.common.app.Version; +import bisq.common.crypto.PubKeyRing; + +import java.util.Optional; +import java.util.UUID; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +// Here we add the SupportedCapabilitiesMessage interface as that message always predates a direct connection +// to the trading peer +@EqualsAndHashCode(callSuper = true) +@Value +@Slf4j +public final class OfferAvailabilityRequest extends OfferMessage implements SupportedCapabilitiesMessage { + private final PubKeyRing pubKeyRing; + private final long takersTradePrice; + @Nullable + private final Capabilities supportedCapabilities; + private final boolean isTakerApiUser; + + public OfferAvailabilityRequest(String offerId, + PubKeyRing pubKeyRing, + long takersTradePrice, + boolean isTakerApiUser) { + this(offerId, + pubKeyRing, + takersTradePrice, + isTakerApiUser, + Capabilities.app, + Version.getP2PMessageVersion(), + UUID.randomUUID().toString()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private OfferAvailabilityRequest(String offerId, + PubKeyRing pubKeyRing, + long takersTradePrice, + boolean isTakerApiUser, + @Nullable Capabilities supportedCapabilities, + int messageVersion, + @Nullable String uid) { + super(messageVersion, offerId, uid); + this.pubKeyRing = pubKeyRing; + this.takersTradePrice = takersTradePrice; + this.isTakerApiUser = isTakerApiUser; + this.supportedCapabilities = supportedCapabilities; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.OfferAvailabilityRequest.Builder builder = protobuf.OfferAvailabilityRequest.newBuilder() + .setOfferId(offerId) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .setTakersTradePrice(takersTradePrice) + .setIsTakerApiUser(isTakerApiUser); + + Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); + Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); + + return getNetworkEnvelopeBuilder() + .setOfferAvailabilityRequest(builder) + .build(); + } + + public static OfferAvailabilityRequest fromProto(protobuf.OfferAvailabilityRequest proto, int messageVersion) { + return new OfferAvailabilityRequest(proto.getOfferId(), + PubKeyRing.fromProto(proto.getPubKeyRing()), + proto.getTakersTradePrice(), + proto.getIsTakerApiUser(), + Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), + messageVersion, + proto.getUid().isEmpty() ? null : proto.getUid()); + } +} diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java new file mode 100644 index 0000000000..41b5aa0fcc --- /dev/null +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java @@ -0,0 +1,119 @@ +/* + * 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 bisq.core.offer.messages; + + +import bisq.core.offer.AvailabilityResult; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SupportedCapabilitiesMessage; + +import bisq.common.app.Capabilities; +import bisq.common.app.Version; +import bisq.common.proto.ProtoUtil; + +import java.util.Optional; +import java.util.UUID; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import javax.annotation.Nullable; + +// We add here the SupportedCapabilitiesMessage interface as that message always predates a direct connection +// to the trading peer +@EqualsAndHashCode(callSuper = true) +@Value +public final class OfferAvailabilityResponse extends OfferMessage implements SupportedCapabilitiesMessage { + private final AvailabilityResult availabilityResult; + @Nullable + private final Capabilities supportedCapabilities; + + private final NodeAddress arbitrator; + // Was introduced in v 1.1.6. Might be null if msg received from node with old version + @Nullable + private final NodeAddress mediator; + + // Added v1.2.0 + @Nullable + private final NodeAddress refundAgent; + + public OfferAvailabilityResponse(String offerId, + AvailabilityResult availabilityResult, + NodeAddress arbitrator, + NodeAddress mediator, + NodeAddress refundAgent) { + this(offerId, + availabilityResult, + Capabilities.app, + Version.getP2PMessageVersion(), + UUID.randomUUID().toString(), + arbitrator, + mediator, + refundAgent); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private OfferAvailabilityResponse(String offerId, + AvailabilityResult availabilityResult, + @Nullable Capabilities supportedCapabilities, + int messageVersion, + @Nullable String uid, + NodeAddress arbitrator, + @Nullable NodeAddress mediator, + @Nullable NodeAddress refundAgent) { + super(messageVersion, offerId, uid); + this.availabilityResult = availabilityResult; + this.supportedCapabilities = supportedCapabilities; + this.arbitrator = arbitrator; + this.mediator = mediator; + this.refundAgent = refundAgent; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder() + .setOfferId(offerId) + .setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name())); + + Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); + Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); + Optional.ofNullable(mediator).ifPresent(e -> builder.setMediator(mediator.toProtoMessage())); + Optional.ofNullable(refundAgent).ifPresent(e -> builder.setRefundAgent(refundAgent.toProtoMessage())); + Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator(arbitrator.toProtoMessage())); + + return getNetworkEnvelopeBuilder() + .setOfferAvailabilityResponse(builder) + .build(); + } + + public static OfferAvailabilityResponse fromProto(protobuf.OfferAvailabilityResponse proto, int messageVersion) { + return new OfferAvailabilityResponse(proto.getOfferId(), + ProtoUtil.enumFromProto(AvailabilityResult.class, proto.getAvailabilityResult().name()), + Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), + messageVersion, + proto.getUid().isEmpty() ? null : proto.getUid(), + proto.hasArbitrator() ? NodeAddress.fromProto(proto.getArbitrator()) : null, + proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null, + proto.hasRefundAgent() ? NodeAddress.fromProto(proto.getRefundAgent()) : null); + } +} diff --git a/core/src/main/java/bisq/core/offer/messages/OfferMessage.java b/core/src/main/java/bisq/core/offer/messages/OfferMessage.java new file mode 100644 index 0000000000..d72fdc4a27 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/messages/OfferMessage.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.offer.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.UidMessage; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Getter +@ToString +public abstract class OfferMessage extends NetworkEnvelope implements DirectMessage, UidMessage { + public final String offerId; + + // Added in version 0.7.1. Can be null if we receive the msg from a peer with an older version + @Nullable + protected final String uid; + + protected OfferMessage(int messageVersion, String offerId, @Nullable String uid) { + super(messageVersion); + this.offerId = offerId; + this.uid = uid; + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java new file mode 100644 index 0000000000..0c54d733ed --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.offer.placeoffer; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.filter.FilterManager; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; + +import bisq.common.taskrunner.Model; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +public class PlaceOfferModel implements Model { + // Immutable + private final Offer offer; + private final Coin reservedFundsForOffer; + private final boolean useSavingsWallet; + private final BtcWalletService walletService; + private final TradeWalletService tradeWalletService; + private final BsqWalletService bsqWalletService; + private final OfferBookService offerBookService; + private final ArbitratorManager arbitratorManager; + private final TradeStatisticsManager tradeStatisticsManager; + private final DaoFacade daoFacade; + private final User user; + @Getter + private final FilterManager filterManager; + + // Mutable + @Setter + private boolean offerAddedToOfferBook; + @Setter + private Transaction transaction; + + public PlaceOfferModel(Offer offer, + Coin reservedFundsForOffer, + boolean useSavingsWallet, + BtcWalletService walletService, + TradeWalletService tradeWalletService, + BsqWalletService bsqWalletService, + OfferBookService offerBookService, + ArbitratorManager arbitratorManager, + TradeStatisticsManager tradeStatisticsManager, + DaoFacade daoFacade, + User user, + FilterManager filterManager) { + this.offer = offer; + this.reservedFundsForOffer = reservedFundsForOffer; + this.useSavingsWallet = useSavingsWallet; + this.walletService = walletService; + this.tradeWalletService = tradeWalletService; + this.bsqWalletService = bsqWalletService; + this.offerBookService = offerBookService; + this.arbitratorManager = arbitratorManager; + this.tradeStatisticsManager = tradeStatisticsManager; + this.daoFacade = daoFacade; + this.user = user; + this.filterManager = filterManager; + } + + @Override + public void onComplete() { + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java new file mode 100644 index 0000000000..cdf394435f --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java @@ -0,0 +1,88 @@ +/* + * 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 bisq.core.offer.placeoffer; + +import bisq.core.offer.placeoffer.tasks.AddToOfferBook; +import bisq.core.offer.placeoffer.tasks.CheckNumberOfUnconfirmedTransactions; +import bisq.core.offer.placeoffer.tasks.CreateMakerFeeTx; +import bisq.core.offer.placeoffer.tasks.ValidateOffer; +import bisq.core.trade.handlers.TransactionResultHandler; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.taskrunner.TaskRunner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PlaceOfferProtocol { + private static final Logger log = LoggerFactory.getLogger(PlaceOfferProtocol.class); + + private final PlaceOfferModel model; + private final TransactionResultHandler resultHandler; + private final ErrorMessageHandler errorMessageHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public PlaceOfferProtocol(PlaceOfferModel model, + TransactionResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + this.model = model; + this.resultHandler = resultHandler; + this.errorMessageHandler = errorMessageHandler; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Called from UI + /////////////////////////////////////////////////////////////////////////////////////////// + + public void placeOffer() { + log.debug("model.offer.id" + model.getOffer().getId()); + TaskRunner taskRunner = new TaskRunner<>(model, + () -> { + log.debug("sequence at handleRequestTakeOfferMessage completed"); + resultHandler.handleResult(model.getTransaction()); + }, + (errorMessage) -> { + log.error(errorMessage); + + if (model.isOfferAddedToOfferBook()) { + model.getOfferBookService().removeOffer(model.getOffer().getOfferPayload(), + () -> { + model.setOfferAddedToOfferBook(false); + log.debug("OfferPayload removed from offer book."); + }, + log::error); + } + model.getOffer().setErrorMessage(errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + } + ); + taskRunner.addTasks( + ValidateOffer.class, + CheckNumberOfUnconfirmedTransactions.class, + CreateMakerFeeTx.class, + AddToOfferBook.class + ); + + taskRunner.run(); + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/AddToOfferBook.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/AddToOfferBook.java new file mode 100644 index 0000000000..16def612ba --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/AddToOfferBook.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.offer.placeoffer.tasks; + +import bisq.core.offer.placeoffer.PlaceOfferModel; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +public class AddToOfferBook extends Task { + + public AddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + try { + runInterceptHook(); + model.getOfferBookService().addOffer(model.getOffer(), + () -> { + model.setOfferAddedToOfferBook(true); + complete(); + }, + errorMessage -> { + model.getOffer().setErrorMessage("Could not add offer to offerbook.\n" + + "Please check your network connection and try again."); + + failed(errorMessage); + }); + } catch (Throwable t) { + model.getOffer().setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/CheckNumberOfUnconfirmedTransactions.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CheckNumberOfUnconfirmedTransactions.java new file mode 100644 index 0000000000..31e8d30003 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CheckNumberOfUnconfirmedTransactions.java @@ -0,0 +1,20 @@ +package bisq.core.offer.placeoffer.tasks; + +import bisq.core.locale.Res; +import bisq.core.offer.placeoffer.PlaceOfferModel; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +public class CheckNumberOfUnconfirmedTransactions extends Task { + public CheckNumberOfUnconfirmedTransactions(TaskRunner taskHandler, PlaceOfferModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + if (model.getWalletService().isUnconfirmedTransactionsLimitHit() || model.getBsqWalletService().isUnconfirmedTransactionsLimitHit()) + failed(Res.get("shared.unconfirmedTransactionsLimitReached")); + complete(); + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java new file mode 100644 index 0000000000..df69713431 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java @@ -0,0 +1,172 @@ +/* + * 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 bisq.core.offer.placeoffer.tasks; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletService; +import bisq.core.dao.exceptions.DaoDisabledException; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.offer.Offer; +import bisq.core.offer.placeoffer.PlaceOfferModel; +import bisq.core.util.FeeReceiverSelector; + +import bisq.common.UserThread; +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +public class CreateMakerFeeTx extends Task { + private static final Logger log = LoggerFactory.getLogger(CreateMakerFeeTx.class); + + @SuppressWarnings({"unused"}) + public CreateMakerFeeTx(TaskRunner taskHandler, PlaceOfferModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + Offer offer = model.getOffer(); + + try { + runInterceptHook(); + + String id = offer.getId(); + BtcWalletService walletService = model.getWalletService(); + + Address fundingAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.OFFER_FUNDING).getAddress(); + Address reservedForTradeAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + Address changeAddress = walletService.getFreshAddressEntry().getAddress(); + + TradeWalletService tradeWalletService = model.getTradeWalletService(); + + String feeReceiver = FeeReceiverSelector.getAddress(model.getDaoFacade(), model.getFilterManager()); + + if (offer.isCurrencyForMakerFeeBtc()) { + tradeWalletService.createBtcTradingFeeTx( + fundingAddress, + reservedForTradeAddress, + changeAddress, + model.getReservedFundsForOffer(), + model.isUseSavingsWallet(), + offer.getMakerFee(), + offer.getTxFee(), + feeReceiver, + true, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + // we delay one render frame to be sure we don't get called before the method call has + // returned (tradeFeeTx would be null in that case) + UserThread.execute(() -> { + if (!completed) { + offer.setOfferFeePaymentTxId(transaction.getTxId().toString()); + model.setTransaction(transaction); + walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.OFFER_FUNDING); + + model.getOffer().setState(Offer.State.OFFER_FEE_PAID); + + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + }); + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + } else { + BsqWalletService bsqWalletService = model.getBsqWalletService(); + Transaction preparedBurnFeeTx = model.getBsqWalletService().getPreparedTradeFeeTx(offer.getMakerFee()); + Transaction txWithBsqFee = tradeWalletService.completeBsqTradingFeeTx(preparedBurnFeeTx, + fundingAddress, + reservedForTradeAddress, + changeAddress, + model.getReservedFundsForOffer(), + model.isUseSavingsWallet(), + offer.getTxFee()); + + Transaction signedTx = model.getBsqWalletService().signTx(txWithBsqFee); + WalletService.checkAllScriptSignaturesForTx(signedTx); + bsqWalletService.commitTx(signedTx, TxType.PAY_TRADE_FEE); + // We need to create another instance, otherwise the tx would trigger an invalid state exception + // if it gets committed 2 times + tradeWalletService.commitTx(tradeWalletService.getClonedTransaction(signedTx)); + + // We use a short timeout as there are issues with BSQ txs. See comment in TxBroadcaster + bsqWalletService.broadcastTx(signedTx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(@Nullable Transaction transaction) { + if (transaction != null) { + offer.setOfferFeePaymentTxId(transaction.getTxId().toString()); + model.setTransaction(transaction); + log.debug("onSuccess, offerId={}, OFFER_FUNDING", id); + walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.OFFER_FUNDING); + + log.debug("Successfully sent tx with id " + transaction.getTxId().toString()); + model.getOffer().setState(Offer.State.OFFER_FEE_PAID); + + complete(); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error(exception.toString()); + exception.printStackTrace(); + offer.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + exception.getMessage()); + failed(exception); + } + }, + 1); + } + } catch (Throwable t) { + if (t instanceof DaoDisabledException) { + offer.setErrorMessage("You cannot pay the trade fee in BSQ at the moment because the DAO features have been " + + "disabled due technical problems. Please use the BTC fee option until the issues are resolved. " + + "For more information please visit the Bisq Forum."); + } else { + offer.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + } + + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java new file mode 100644 index 0000000000..35d619feb4 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java @@ -0,0 +1,135 @@ +/* + * 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 bisq.core.offer.placeoffer.tasks; + +import bisq.core.offer.Offer; +import bisq.core.offer.placeoffer.PlaceOfferModel; +import bisq.core.trade.messages.TradeMessage; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ValidateOffer extends Task { + public ValidateOffer(TaskRunner taskHandler, PlaceOfferModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + Offer offer = model.getOffer(); + try { + runInterceptHook(); + + // Coins + checkCoinNotNullOrZero(offer.getAmount(), "Amount"); + checkCoinNotNullOrZero(offer.getMinAmount(), "MinAmount"); + checkCoinNotNullOrZero(offer.getMakerFee(), "MakerFee"); + checkCoinNotNullOrZero(offer.getBuyerSecurityDeposit(), "buyerSecurityDeposit"); + checkCoinNotNullOrZero(offer.getSellerSecurityDeposit(), "sellerSecurityDeposit"); + checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); + checkCoinNotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); + + // 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());*/ + + checkArgument(offer.getAmount().compareTo(offer.getPaymentMethod().getMaxTradeLimitAsCoin(offer.getCurrencyCode())) <= 0, + "Amount is larger than " + offer.getPaymentMethod().getMaxTradeLimitAsCoin(offer.getCurrencyCode()).toFriendlyString()); + checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); + + checkNotNull(offer.getPrice(), "Price is null"); + checkArgument(offer.getPrice().isPositive(), + "Price must be positive. 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.getTxFee(), "txFee is null"); + checkNotNull(offer.getMakerFee(), "MakerFee 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 + // TODO check for account age witness base tradeLimit is missing + + complete(); + } catch (Exception e) { + offer.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + e.getMessage()); + failed(e); + } + } + + public 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) { + checkNotNull(value); + checkArgument(value.length() > 0); + return value; + } + + public static long nonNegativeLongOf(long value) { + checkArgument(value >= 0); + return value; + } + + public static Coin nonZeroCoinOf(Coin value) { + checkNotNull(value); + checkArgument(!value.isZero()); + return value; + } + + public static Coin positiveCoinOf(Coin value) { + checkNotNull(value); + checkArgument(value.isPositive()); + return value; + } + + public static void checkTradeId(String tradeId, TradeMessage tradeMessage) { + checkArgument(tradeId.equals(tradeMessage.getTradeId())); + } +} diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java new file mode 100644 index 0000000000..8e345b8142 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -0,0 +1,316 @@ +/* + * 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 bisq.core.offer.takeoffer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferUtil; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.PriceFeedService; + +import bisq.common.taskrunner.Model; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static bisq.core.btc.model.AddressEntry.Context.OFFER_FUNDING; +import static bisq.core.offer.OfferPayload.Direction.SELL; +import static bisq.core.util.VolumeUtil.getAdjustedVolumeForHalCash; +import static bisq.core.util.VolumeUtil.getRoundedFiatVolume; +import static bisq.core.util.coin.CoinUtil.minCoin; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.Coin.ZERO; +import static org.bitcoinj.core.Coin.valueOf; + +@Slf4j +public class TakeOfferModel implements Model { + // Immutable + private final AccountAgeWitnessService accountAgeWitnessService; + private final BtcWalletService btcWalletService; + private final FeeService feeService; + private final OfferUtil offerUtil; + private final PriceFeedService priceFeedService; + + // Mutable + @Getter + private AddressEntry addressEntry; + @Getter + private Coin amount; + @Getter + private boolean isCurrencyForTakerFeeBtc; + private Offer offer; + private PaymentAccount paymentAccount; + @Getter + private Coin securityDeposit; + private boolean useSavingsWallet; + + // Use an average of a typical trade fee tx with 1 input, deposit tx and payout tx. + private final int feeTxVsize = 192; // (175+233+169)/3 + private Coin txFeePerVbyteFromFeeService; + @Getter + private Coin txFeeFromFeeService; + @Getter + private Coin takerFee; + @Getter + private Coin totalToPayAsCoin; + @Getter + private Coin missingCoin = ZERO; + @Getter + private Coin totalAvailableBalance; + @Getter + private Coin balance; + @Getter + private boolean isBtcWalletFunded; + @Getter + private Volume volume; + + @Inject + public TakeOfferModel(AccountAgeWitnessService accountAgeWitnessService, + BtcWalletService btcWalletService, + FeeService feeService, + OfferUtil offerUtil, + PriceFeedService priceFeedService) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.btcWalletService = btcWalletService; + this.feeService = feeService; + this.offerUtil = offerUtil; + this.priceFeedService = priceFeedService; + } + + public void initModel(Offer offer, + PaymentAccount paymentAccount, + boolean useSavingsWallet) { + this.clearModel(); + this.offer = offer; + this.paymentAccount = paymentAccount; + this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); + validateModelInputs(); + + this.useSavingsWallet = useSavingsWallet; + this.amount = valueOf(Math.min(offer.getAmount().value, getMaxTradeLimit())); + this.securityDeposit = offer.getDirection() == SELL + ? offer.getBuyerSecurityDeposit() + : offer.getSellerSecurityDeposit(); + this.isCurrencyForTakerFeeBtc = offerUtil.isCurrencyForTakerFeeBtc(amount); + this.takerFee = offerUtil.getTakerFee(isCurrencyForTakerFeeBtc, amount); + + calculateTxFees(); + calculateVolume(); + calculateTotalToPay(); + offer.resetState(); + + priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + } + + @Override + public void onComplete() { + // empty + } + + private void calculateTxFees() { + // Taker pays 3 times the tx fee (taker fee, deposit, payout) because the mining + // fee might be different when maker created the offer and reserved his funds. + // Taker creates at least taker fee and deposit tx at nearly the same moment. + // Just the payout will be later and still could lead to issues if the required + // fee changed a lot in the meantime. using RBF and/or multiple batch-signed + // payout tx with different fees might be an option but RBF is not supported yet + // in BitcoinJ and batched txs would add more complexity to the trade protocol. + + // A typical trade fee tx has about 175 vbytes (if one input). The trade txs has + // about 169-263 vbytes. We use 192 as a average value. + + // Fee calculations: + // Trade fee tx: 175 vbytes (1 input) + // Deposit tx: 233 vbytes (1 MS output+ OP_RETURN) - 263 vbytes + // (1 MS output + OP_RETURN + change in case of smaller trade amount) + // Payout tx: 169 vbytes + // Disputed payout tx: 139 vbytes + + txFeePerVbyteFromFeeService = getTxFeePerVbyte(); + txFeeFromFeeService = offerUtil.getTxFeeByVsize(txFeePerVbyteFromFeeService, feeTxVsize); + log.info("{} txFeePerVbyte = {}", feeService.getClass().getSimpleName(), txFeePerVbyteFromFeeService); + } + + private Coin getTxFeePerVbyte() { + try { + CompletableFuture feeRequestFuture = CompletableFuture.runAsync(feeService::requestFees); + feeRequestFuture.get(); // Block until async fee request is complete. + return feeService.getTxFeePerVbyte(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException("Could not request fees from fee service.", e); + } + } + + private void calculateTotalToPay() { + // Taker pays 2 times the tx fee because the mining fee might be different when + // maker created the offer and reserved his funds, so that would not work well + // with dynamic fees. The mining fee for the takeOfferFee tx is deducted from + // the createOfferFee and not visible to the trader. + Coin feeAndSecDeposit = getTotalTxFee().add(securityDeposit); + if (isCurrencyForTakerFeeBtc) + feeAndSecDeposit = feeAndSecDeposit.add(takerFee); + + totalToPayAsCoin = offer.isBuyOffer() + ? feeAndSecDeposit.add(amount) + : feeAndSecDeposit; + + updateBalance(); + } + + private void calculateVolume() { + Price tradePrice = offer.getPrice(); + Volume volumeByAmount = Objects.requireNonNull(tradePrice).getVolumeByAmount(amount); + + if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) + volumeByAmount = getRoundedFiatVolume(volumeByAmount); + + volume = volumeByAmount; + + updateBalance(); + } + + private void updateBalance() { + Coin tradeWalletBalance = btcWalletService.getBalanceForAddress(addressEntry.getAddress()); + if (useSavingsWallet) { + Coin savingWalletBalance = btcWalletService.getSavingWalletBalance(); + totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); + if (totalToPayAsCoin != null) + balance = minCoin(totalToPayAsCoin, totalAvailableBalance); + + } else { + balance = tradeWalletBalance; + } + missingCoin = offerUtil.getBalanceShortage(totalToPayAsCoin, balance); + isBtcWalletFunded = offerUtil.isBalanceSufficient(totalToPayAsCoin, balance); + } + + private long getMaxTradeLimit() { + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), + offer.getMirroredDirection()); + } + + public Coin getTotalTxFee() { + Coin totalTxFees = txFeeFromFeeService.add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx()); + if (isCurrencyForTakerFeeBtc) + return totalTxFees; + else + return totalTxFees.subtract(takerFee); + } + + @NotNull + public Coin getFundsNeededForTrade() { + // If taking a buy offer, taker needs to reserve the offer.amt too. + return securityDeposit + .add(getTxFeeForDepositTx()) + .add(getTxFeeForPayoutTx()) + .add(offer.isBuyOffer() ? amount : ZERO); + } + + private Coin getTxFeeForDepositTx() { + // TODO fix with new trade protocol! + // Unfortunately we cannot change that to the correct fees as it would break + // backward compatibility. We still might find a way with offer version or app + // version checks so lets keep that commented out code as that shows how it + // should be. + return txFeeFromFeeService; + } + + private Coin getTxFeeForPayoutTx() { + // TODO fix with new trade protocol! + // Unfortunately we cannot change that to the correct fees as it would break + // backward compatibility. We still might find a way with offer version or app + // version checks so lets keep that commented out code as that shows how it + // should be. + return txFeeFromFeeService; + } + + private void validateModelInputs() { + checkNotNull(offer, "offer must not be null"); + checkNotNull(offer.getAmount(), "offer amount must not be null"); + checkArgument(offer.getAmount().value > 0, "offer amount must not be zero"); + checkNotNull(offer.getPrice(), "offer price must not be null"); + checkNotNull(paymentAccount, "payment account must not be null"); + checkNotNull(addressEntry, "address entry must not be null"); + } + + private void clearModel() { + this.addressEntry = null; + this.amount = null; + this.balance = null; + this.isBtcWalletFunded = false; + this.isCurrencyForTakerFeeBtc = false; + this.missingCoin = ZERO; + this.offer = null; + this.paymentAccount = null; + this.securityDeposit = null; + this.takerFee = null; + this.totalAvailableBalance = null; + this.totalToPayAsCoin = null; + this.txFeeFromFeeService = null; + this.txFeePerVbyteFromFeeService = null; + this.useSavingsWallet = true; + this.volume = null; + } + + @Override + public String toString() { + return "TakeOfferModel{" + + " offer.id=" + offer.getId() + "\n" + + " offer.state=" + offer.getState() + "\n" + + ", paymentAccount.id=" + paymentAccount.getId() + "\n" + + ", paymentAccount.method.id=" + paymentAccount.getPaymentMethod().getId() + "\n" + + ", useSavingsWallet=" + useSavingsWallet + "\n" + + ", addressEntry=" + addressEntry + "\n" + + ", amount=" + amount + "\n" + + ", securityDeposit=" + securityDeposit + "\n" + + ", feeTxVsize=" + feeTxVsize + "\n" + + ", txFeePerVbyteFromFeeService=" + txFeePerVbyteFromFeeService + "\n" + + ", txFeeFromFeeService=" + txFeeFromFeeService + "\n" + + ", takerFee=" + takerFee + "\n" + + ", totalToPayAsCoin=" + totalToPayAsCoin + "\n" + + ", missingCoin=" + missingCoin + "\n" + + ", totalAvailableBalance=" + totalAvailableBalance + "\n" + + ", balance=" + balance + "\n" + + ", volume=" + volume + "\n" + + ", fundsNeededForTrade=" + getFundsNeededForTrade() + "\n" + + ", isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + "\n" + + ", isBtcWalletFunded=" + isBtcWalletFunded + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java b/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java new file mode 100644 index 0000000000..688d1c5307 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.payload.AdvancedCashAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class AdvancedCashAccount extends PaymentAccount { + public AdvancedCashAccount() { + super(PaymentMethod.ADVANCED_CASH); + tradeCurrencies.addAll(CurrencyUtil.getAllAdvancedCashCurrencies()); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AdvancedCashAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountNr(String accountNr) { + ((AdvancedCashAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((AdvancedCashAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/AliPayAccount.java b/core/src/main/java/bisq/core/payment/AliPayAccount.java new file mode 100644 index 0000000000..b01b71bbfe --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AliPayAccount.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.AliPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class AliPayAccount extends PaymentAccount { + public AliPayAccount() { + super(PaymentMethod.ALI_PAY); + setSingleTradeCurrency(new FiatCurrency("CNY")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AliPayAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountNr(String accountNr) { + ((AliPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((AliPayAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java b/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java new file mode 100644 index 0000000000..b65cbe6cbd --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.payment.payload.AmazonGiftCardAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +public final class AmazonGiftCardAccount extends PaymentAccount { + + @Nullable + private Country country; + + public AmazonGiftCardAccount() { + super(PaymentMethod.AMAZON_GIFT_CARD); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AmazonGiftCardAccountPayload(paymentMethod.getId(), id); + } + + public String getEmailOrMobileNr() { + return getAmazonGiftCardAccountPayload().getEmailOrMobileNr(); + } + + public void setEmailOrMobileNr(String emailOrMobileNr) { + getAmazonGiftCardAccountPayload().setEmailOrMobileNr(emailOrMobileNr); + } + + public boolean countryNotSet() { + return (getAmazonGiftCardAccountPayload()).countryNotSet(); + } + + @Nullable + public Country getCountry() { + if (country == null) { + final String countryCode = getAmazonGiftCardAccountPayload().getCountryCode(); + CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); + } + return country; + } + + public void setCountry(@NotNull Country country) { + this.country = country; + getAmazonGiftCardAccountPayload().setCountryCode(country.code); + } + + private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() { + return (AmazonGiftCardAccountPayload) paymentAccountPayload; + } +} diff --git a/core/src/main/java/bisq/core/payment/AssetAccount.java b/core/src/main/java/bisq/core/payment/AssetAccount.java new file mode 100644 index 0000000000..de7c2e72d2 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AssetAccount.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +public abstract class AssetAccount extends PaymentAccount { + protected AssetAccount(PaymentMethod paymentMethod) { + super(paymentMethod); + } + + public void setAddress(String address) { + ((AssetsAccountPayload) paymentAccountPayload).setAddress(address); + } + + public String getAddress() { + return ((AssetsAccountPayload) paymentAccountPayload).getAddress(); + } +} diff --git a/core/src/main/java/bisq/core/payment/AustraliaPayid.java b/core/src/main/java/bisq/core/payment/AustraliaPayid.java new file mode 100644 index 0000000000..a64c62b42a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AustraliaPayid.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +public final class AustraliaPayid extends PaymentAccount { + public AustraliaPayid() { + super(PaymentMethod.AUSTRALIA_PAYID); + setSingleTradeCurrency(new FiatCurrency("AUD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AustraliaPayidPayload(paymentMethod.getId(), id); + } + + public String getPayid() { + return ((AustraliaPayidPayload) paymentAccountPayload).getPayid(); + } + + public void setPayid(String payid) { + if (payid == null) payid = ""; + ((AustraliaPayidPayload) paymentAccountPayload).setPayid(payid); + } + + public String getBankAccountName() { + return ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName(); + } + + public void setBankAccountName(String bankAccountName) { + if (bankAccountName == null) bankAccountName = ""; + ((AustraliaPayidPayload) paymentAccountPayload).setBankAccountName(bankAccountName); + } +} diff --git a/core/src/main/java/bisq/core/payment/BankAccount.java b/core/src/main/java/bisq/core/payment/BankAccount.java new file mode 100644 index 0000000000..b5fa712846 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/BankAccount.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.core.payment; + +import javax.annotation.Nullable; + +public interface BankAccount { + @Nullable + String getBankId(); +} diff --git a/core/src/main/java/bisq/core/payment/BankNameRestrictedBankAccount.java b/core/src/main/java/bisq/core/payment/BankNameRestrictedBankAccount.java new file mode 100644 index 0000000000..fe25a65f8e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/BankNameRestrictedBankAccount.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.core.payment; + +public interface BankNameRestrictedBankAccount extends BankAccount { + +} diff --git a/core/src/main/java/bisq/core/payment/CashAppAccount.java b/core/src/main/java/bisq/core/payment/CashAppAccount.java new file mode 100644 index 0000000000..acd4f6b6a5 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CashAppAccount.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.CashAppAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +// Removed due too high chargeback risk +// Cannot be deleted as it would break old trade history entries +@Deprecated +@EqualsAndHashCode(callSuper = true) +public final class CashAppAccount extends PaymentAccount { + public CashAppAccount() { + super(PaymentMethod.CASH_APP); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CashAppAccountPayload(paymentMethod.getId(), id); + } + + public void setCashTag(String cashTag) { + ((CashAppAccountPayload) paymentAccountPayload).setCashTag(cashTag); + } + + public String getCashTag() { + return ((CashAppAccountPayload) paymentAccountPayload).getCashTag(); + } +} diff --git a/core/src/main/java/bisq/core/payment/CashByMailAccount.java b/core/src/main/java/bisq/core/payment/CashByMailAccount.java new file mode 100644 index 0000000000..d7d1b85ecc --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CashByMailAccount.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.CashByMailAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +public final class CashByMailAccount extends PaymentAccount { + + public CashByMailAccount() { + super(PaymentMethod.CASH_BY_MAIL); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CashByMailAccountPayload(paymentMethod.getId(), id); + } + + public void setPostalAddress(String postalAddress) { + ((CashByMailAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); + } + + public String getPostalAddress() { + return ((CashByMailAccountPayload) paymentAccountPayload).getPostalAddress(); + } + + public void setContact(String contact) { + ((CashByMailAccountPayload) paymentAccountPayload).setContact(contact); + } + + public String getContact() { + return ((CashByMailAccountPayload) paymentAccountPayload).getContact(); + } + + public void setExtraInfo(String extraInfo) { + ((CashByMailAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((CashByMailAccountPayload) paymentAccountPayload).getExtraInfo(); + } +} diff --git a/core/src/main/java/bisq/core/payment/CashDepositAccount.java b/core/src/main/java/bisq/core/payment/CashDepositAccount.java new file mode 100644 index 0000000000..f39de46b57 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CashDepositAccount.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.CashDepositAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import javax.annotation.Nullable; + +public final class CashDepositAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + public CashDepositAccount() { + super(PaymentMethod.CASH_DEPOSIT); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CashDepositAccountPayload(paymentMethod.getId(), id); + } + + @Override + public String getBankId() { + return ((CashDepositAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } + + @Nullable + public String getRequirements() { + return ((CashDepositAccountPayload) paymentAccountPayload).getRequirements(); + } + + public void setRequirements(String requirements) { + ((CashDepositAccountPayload) paymentAccountPayload).setRequirements(requirements); + } +} diff --git a/core/src/main/java/bisq/core/payment/ChargeBackRisk.java b/core/src/main/java/bisq/core/payment/ChargeBackRisk.java new file mode 100644 index 0000000000..6faa91c940 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ChargeBackRisk.java @@ -0,0 +1,29 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import javax.inject.Singleton; + +@Singleton +public class ChargeBackRisk { + public boolean hasChargebackRisk(String id, String currencyCode) { + return PaymentMethod.hasChargebackRisk(id, currencyCode); + } +} diff --git a/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java b/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java new file mode 100644 index 0000000000..085aff85ec --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.ChaseQuickPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class ChaseQuickPayAccount extends PaymentAccount { + public ChaseQuickPayAccount() { + super(PaymentMethod.CHASE_QUICK_PAY); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new ChaseQuickPayAccountPayload(paymentMethod.getId(), id); + } + + public void setEmail(String email) { + ((ChaseQuickPayAccountPayload) paymentAccountPayload).setEmail(email); + } + + public String getEmail() { + return ((ChaseQuickPayAccountPayload) paymentAccountPayload).getEmail(); + } + + public void setHolderName(String holderName) { + ((ChaseQuickPayAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((ChaseQuickPayAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java b/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java new file mode 100644 index 0000000000..397aeb44f0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.ClearXchangeAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class ClearXchangeAccount extends PaymentAccount { + public ClearXchangeAccount() { + super(PaymentMethod.CLEAR_X_CHANGE); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new ClearXchangeAccountPayload(paymentMethod.getId(), id); + } + + public void setEmailOrMobileNr(String mobileNr) { + ((ClearXchangeAccountPayload) paymentAccountPayload).setEmailOrMobileNr(mobileNr); + } + + public String getEmailOrMobileNr() { + return ((ClearXchangeAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); + } + + public void setHolderName(String holderName) { + ((ClearXchangeAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((ClearXchangeAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/CountryBasedPaymentAccount.java b/core/src/main/java/bisq/core/payment/CountryBasedPaymentAccount.java new file mode 100644 index 0000000000..6bb2a8656b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CountryBasedPaymentAccount.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.payment.payload.CountryBasedPaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +public abstract class CountryBasedPaymentAccount extends PaymentAccount { + @Nullable + protected Country country; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + + protected CountryBasedPaymentAccount(PaymentMethod paymentMethod) { + super(paymentMethod); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter, Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + public Country getCountry() { + if (country == null) { + final String countryCode = ((CountryBasedPaymentAccountPayload) paymentAccountPayload).getCountryCode(); + CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); + } + return country; + } + + public void setCountry(@NotNull Country country) { + this.country = country; + ((CountryBasedPaymentAccountPayload) paymentAccountPayload).setCountryCode(country.code); + } +} diff --git a/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java new file mode 100644 index 0000000000..534157bd1f --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.CryptoCurrencyAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class CryptoCurrencyAccount extends AssetAccount { + + public CryptoCurrencyAccount() { + super(PaymentMethod.BLOCK_CHAINS); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CryptoCurrencyAccountPayload(paymentMethod.getId(), id); + } +} diff --git a/core/src/main/java/bisq/core/payment/F2FAccount.java b/core/src/main/java/bisq/core/payment/F2FAccount.java new file mode 100644 index 0000000000..03554c3eb0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/F2FAccount.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.F2FAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class F2FAccount extends CountryBasedPaymentAccount { + public F2FAccount() { + super(PaymentMethod.F2F); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new F2FAccountPayload(paymentMethod.getId(), id); + } + + public void setContact(String contact) { + ((F2FAccountPayload) paymentAccountPayload).setContact(contact); + } + + public String getContact() { + return ((F2FAccountPayload) paymentAccountPayload).getContact(); + } + + public void setCity(String city) { + ((F2FAccountPayload) paymentAccountPayload).setCity(city); + } + + public String getCity() { + return ((F2FAccountPayload) paymentAccountPayload).getCity(); + } + + public void setExtraInfo(String extraInfo) { + ((F2FAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((F2FAccountPayload) paymentAccountPayload).getExtraInfo(); + } +} diff --git a/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java b/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java new file mode 100644 index 0000000000..3c4f124c66 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.FasterPaymentsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class FasterPaymentsAccount extends PaymentAccount { + public FasterPaymentsAccount() { + super(PaymentMethod.FASTER_PAYMENTS); + setSingleTradeCurrency(new FiatCurrency("GBP")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new FasterPaymentsAccountPayload(paymentMethod.getId(), id); + } + + public void setHolderName(String value) { + ((FasterPaymentsAccountPayload) paymentAccountPayload).setHolderName(value); + } + + public String getHolderName() { + return ((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setSortCode(String value) { + ((FasterPaymentsAccountPayload) paymentAccountPayload).setSortCode(value); + } + + public String getSortCode() { + return ((FasterPaymentsAccountPayload) paymentAccountPayload).getSortCode(); + } + + public void setAccountNr(String value) { + ((FasterPaymentsAccountPayload) paymentAccountPayload).setAccountNr(value); + } + + public String getAccountNr() { + return ((FasterPaymentsAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/HalCashAccount.java b/core/src/main/java/bisq/core/payment/HalCashAccount.java new file mode 100644 index 0000000000..dba8ee82f1 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/HalCashAccount.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.HalCashAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class HalCashAccount extends PaymentAccount { + public HalCashAccount() { + super(PaymentMethod.HAL_CASH); + setSingleTradeCurrency(new FiatCurrency("EUR")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new HalCashAccountPayload(paymentMethod.getId(), id); + } + + public void setMobileNr(String mobileNr) { + ((HalCashAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); + } + + public String getMobileNr() { + return ((HalCashAccountPayload) paymentAccountPayload).getMobileNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java b/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java new file mode 100644 index 0000000000..17ddf5eca7 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.InstantCryptoCurrencyPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class InstantCryptoCurrencyAccount extends AssetAccount { + + public InstantCryptoCurrencyAccount() { + super(PaymentMethod.BLOCK_CHAINS_INSTANT); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new InstantCryptoCurrencyPayload(paymentMethod.getId(), id); + } +} diff --git a/core/src/main/java/bisq/core/payment/InteracETransferAccount.java b/core/src/main/java/bisq/core/payment/InteracETransferAccount.java new file mode 100644 index 0000000000..b4b5f8310a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/InteracETransferAccount.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.InteracETransferAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class InteracETransferAccount extends PaymentAccount { + public InteracETransferAccount() { + super(PaymentMethod.INTERAC_E_TRANSFER); + setSingleTradeCurrency(new FiatCurrency("CAD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new InteracETransferAccountPayload(paymentMethod.getId(), id); + } + + public void setEmail(String email) { + ((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email); + } + + public String getEmail() { + return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail(); + } + + public void setAnswer(String answer) { + ((InteracETransferAccountPayload) paymentAccountPayload).setAnswer(answer); + } + + public String getAnswer() { + return ((InteracETransferAccountPayload) paymentAccountPayload).getAnswer(); + } + + public void setQuestion(String question) { + ((InteracETransferAccountPayload) paymentAccountPayload).setQuestion(question); + } + + public String getQuestion() { + return ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion(); + } + + public void setHolderName(String holderName) { + ((InteracETransferAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/JapanBankAccount.java b/core/src/main/java/bisq/core/payment/JapanBankAccount.java new file mode 100644 index 0000000000..b2d9fede11 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/JapanBankAccount.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.JapanBankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.JapanBankAccountPayload; + +import org.jetbrains.annotations.NotNull; + +import lombok.Getter; +import lombok.Setter; + +import bisq.core.locale.Country; +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.JapanBankAccountPayload; + +public final class JapanBankAccount extends PaymentAccount +{ + public JapanBankAccount() + { + super(PaymentMethod.JAPAN_BANK); + setSingleTradeCurrency(new FiatCurrency("JPY")); + } + + @Override + protected PaymentAccountPayload createPayload() + { + return new JapanBankAccountPayload(paymentMethod.getId(), id); + } + + // bank code + public String getBankCode() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankCode(); + } + public void setBankCode(String bankCode) + { + if (bankCode == null) bankCode = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankCode(bankCode); + } + + // bank name + public String getBankName() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankName(); + } + public void setBankName(String bankName) + { + if (bankName == null) bankName = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankName(bankName); + } + + // branch code + public String getBankBranchCode() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchCode(); + } + public void setBankBranchCode(String bankBranchCode) + { + if (bankBranchCode == null) bankBranchCode = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchCode(bankBranchCode); + } + + // branch name + public String getBankBranchName() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchName(); + } + public void setBankBranchName(String bankBranchName) + { + if (bankBranchName == null) bankBranchName = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchName(bankBranchName); + } + + // account type + public String getBankAccountType() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountType(); + } + public void setBankAccountType(String bankAccountType) + { + if (bankAccountType == null) bankAccountType = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountType(bankAccountType); + } + + // account number + public String getBankAccountNumber() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountNumber(); + } + public void setBankAccountNumber(String bankAccountNumber) + { + if (bankAccountNumber == null) bankAccountNumber = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountNumber(bankAccountNumber); + } + + // account name + public String getBankAccountName() + { + return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountName(); + } + public void setBankAccountName(String bankAccountName) + { + if (bankAccountName == null) bankAccountName = ""; + ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountName(bankAccountName); + } +} diff --git a/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java b/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java new file mode 100644 index 0000000000..4e67c33473 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.MoneyBeamAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +//TODO missing support for selected trade currency +@EqualsAndHashCode(callSuper = true) +public final class MoneyBeamAccount extends PaymentAccount { + public MoneyBeamAccount() { + super(PaymentMethod.MONEY_BEAM); + setSingleTradeCurrency(new FiatCurrency("EUR")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new MoneyBeamAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountId(String accountId) { + ((MoneyBeamAccountPayload) paymentAccountPayload).setAccountId(accountId); + } + + public String getAccountId() { + return ((MoneyBeamAccountPayload) paymentAccountPayload).getAccountId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/MoneyGramAccount.java b/core/src/main/java/bisq/core/payment/MoneyGramAccount.java new file mode 100644 index 0000000000..996fa01378 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/MoneyGramAccount.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.payload.MoneyGramAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +public final class MoneyGramAccount extends PaymentAccount { + + @Nullable + private Country country; + + + public MoneyGramAccount() { + super(PaymentMethod.MONEY_GRAM); + tradeCurrencies.addAll(CurrencyUtil.getAllMoneyGramCurrencies()); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new MoneyGramAccountPayload(paymentMethod.getId(), id); + } + + @Nullable + public Country getCountry() { + if (country == null) { + final String countryCode = ((MoneyGramAccountPayload) paymentAccountPayload).getCountryCode(); + CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); + } + return country; + } + + public void setCountry(@NotNull Country country) { + this.country = country; + ((MoneyGramAccountPayload) paymentAccountPayload).setCountryCode(country.code); + } + + public String getEmail() { + return ((MoneyGramAccountPayload) paymentAccountPayload).getEmail(); + } + + public void setEmail(String email) { + ((MoneyGramAccountPayload) paymentAccountPayload).setEmail(email); + } + + public String getFullName() { + return ((MoneyGramAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setFullName(String email) { + ((MoneyGramAccountPayload) paymentAccountPayload).setHolderName(email); + } + + public String getState() { + return ((MoneyGramAccountPayload) paymentAccountPayload).getState(); + } + + public void setState(String email) { + ((MoneyGramAccountPayload) paymentAccountPayload).setState(email); + } +} diff --git a/core/src/main/java/bisq/core/payment/NationalBankAccount.java b/core/src/main/java/bisq/core/payment/NationalBankAccount.java new file mode 100644 index 0000000000..bccad09e17 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/NationalBankAccount.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.NationalBankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class NationalBankAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + public NationalBankAccount() { + super(PaymentMethod.NATIONAL_BANK); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new NationalBankAccountPayload(paymentMethod.getId(), id); + } + + @Override + public String getBankId() { + return ((BankAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } +} diff --git a/core/src/main/java/bisq/core/payment/OKPayAccount.java b/core/src/main/java/bisq/core/payment/OKPayAccount.java new file mode 100644 index 0000000000..2473018170 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/OKPayAccount.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.payload.OKPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +// Cannot be deleted as it would break old trade history entries +@Deprecated +@EqualsAndHashCode(callSuper = true) +public final class OKPayAccount extends PaymentAccount { + public OKPayAccount() { + super(PaymentMethod.OK_PAY); + + // Incorrect call but we don't want to keep Deprecated code in CurrencyUtil if not needed... + tradeCurrencies.addAll(CurrencyUtil.getAllUpholdCurrencies()); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new OKPayAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountNr(String accountNr) { + ((OKPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((OKPayAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java new file mode 100644 index 0000000000..1e481e4458 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -0,0 +1,229 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@EqualsAndHashCode +@ToString +@Getter +@Slf4j +public abstract class PaymentAccount implements PersistablePayload { + protected final PaymentMethod paymentMethod; + @Setter + protected String id; + @Setter + protected long creationDate; + @Setter + public PaymentAccountPayload paymentAccountPayload; + @Setter + protected String accountName; + protected final List tradeCurrencies = new ArrayList<>(); + @Setter + @Nullable + protected TradeCurrency selectedTradeCurrency; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + protected PaymentAccount(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public void init() { + id = UUID.randomUUID().toString(); + creationDate = new Date().getTime(); + paymentAccountPayload = createPayload(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PaymentAccount toProtoMessage() { + checkNotNull(accountName, "accountName must not be null"); + protobuf.PaymentAccount.Builder builder = protobuf.PaymentAccount.newBuilder() + .setPaymentMethod(paymentMethod.toProtoMessage()) + .setId(id) + .setCreationDate(creationDate) + .setPaymentAccountPayload((protobuf.PaymentAccountPayload) paymentAccountPayload.toProtoMessage()) + .setAccountName(accountName) + .addAllTradeCurrencies(ProtoUtil.collectionToProto(tradeCurrencies, protobuf.TradeCurrency.class)); + Optional.ofNullable(selectedTradeCurrency).ifPresent(selectedTradeCurrency -> builder.setSelectedTradeCurrency((protobuf.TradeCurrency) selectedTradeCurrency.toProtoMessage())); + return builder.build(); + } + + public static PaymentAccount fromProto(protobuf.PaymentAccount proto, CoreProtoResolver coreProtoResolver) { + String paymentMethodId = proto.getPaymentMethod().getId(); + List tradeCurrencies = proto.getTradeCurrenciesList().stream() + .map(TradeCurrency::fromProto) + .collect(Collectors.toList()); + + // We need to remove NGN for Transferwise + Optional ngnTwOptional = tradeCurrencies.stream() + .filter(e -> paymentMethodId.equals(PaymentMethod.TRANSFERWISE_ID)) + .filter(e -> e.getCode().equals("NGN")) + .findAny(); + // We cannot remove it in the stream as it would cause a concurrentModificationException + ngnTwOptional.ifPresent(tradeCurrencies::remove); + + PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethodById(paymentMethodId)); + account.getTradeCurrencies().clear(); + account.setId(proto.getId()); + account.setCreationDate(proto.getCreationDate()); + account.setAccountName(proto.getAccountName()); + account.getTradeCurrencies().addAll(tradeCurrencies); + account.setPaymentAccountPayload(coreProtoResolver.fromProto(proto.getPaymentAccountPayload())); + + if (proto.hasSelectedTradeCurrency()) + account.setSelectedTradeCurrency(TradeCurrency.fromProto(proto.getSelectedTradeCurrency())); + + return account; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Date getCreationDate() { + return new Date(creationDate); + } + + public void addCurrency(TradeCurrency tradeCurrency) { + if (!tradeCurrencies.contains(tradeCurrency)) + tradeCurrencies.add(tradeCurrency); + } + + public void removeCurrency(TradeCurrency tradeCurrency) { + tradeCurrencies.remove(tradeCurrency); + } + + public boolean hasMultipleCurrencies() { + return tradeCurrencies.size() > 1; + } + + public void setSingleTradeCurrency(TradeCurrency tradeCurrency) { + tradeCurrencies.clear(); + tradeCurrencies.add(tradeCurrency); + setSelectedTradeCurrency(tradeCurrency); + } + + @Nullable + public TradeCurrency getSingleTradeCurrency() { + if (tradeCurrencies.size() == 1) + return tradeCurrencies.get(0); + else + return null; + } + + public long getMaxTradePeriod() { + return paymentMethod.getMaxTradePeriod(); + } + + protected abstract PaymentAccountPayload createPayload(); + + public void setSalt(byte[] salt) { + paymentAccountPayload.setSalt(salt); + } + + public byte[] getSalt() { + return paymentAccountPayload.getSalt(); + } + + public void setSaltAsHex(String saltAsHex) { + setSalt(Utilities.decodeFromHex(saltAsHex)); + } + + public String getSaltAsHex() { + return Utilities.bytesAsHexString(getSalt()); + } + + public String getOwnerId() { + return paymentAccountPayload.getOwnerId(); + } + + public boolean isCountryBasedPaymentAccount() { + return this instanceof CountryBasedPaymentAccount; + } + + public boolean isHalCashAccount() { + return this instanceof HalCashAccount; + } + + public boolean isMoneyGramAccount() { + return this instanceof MoneyGramAccount; + } + + public boolean isTransferwiseAccount() { + return this instanceof TransferwiseAccount; + } + + /** + * Return an Optional of the trade currency for this payment account, or + * Optional.empty() if none is found. If this payment account has a selected + * trade currency, that is returned, else its single trade currency is returned, + * else the first trade currency in the this payment account's tradeCurrencies + * list is returned. + * + * @return Optional of the trade currency for the given payment account + */ + public Optional getTradeCurrency() { + if (this.getSelectedTradeCurrency() != null) + return Optional.of(this.getSelectedTradeCurrency()); + else if (this.getSingleTradeCurrency() != null) + return Optional.of(this.getSingleTradeCurrency()); + else if (!this.getTradeCurrencies().isEmpty()) + return Optional.of(this.getTradeCurrencies().get(0)); + else + return Optional.empty(); + } + + public void onAddToUser() { + // We are in the process to get added to the user. This is called just before saving the account and the + // last moment we could apply some special handling if needed (e.g. as it happens for Revolut) + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java new file mode 100644 index 0000000000..faed57acd3 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.PaymentMethod; + +public class PaymentAccountFactory { + public static PaymentAccount getPaymentAccount(PaymentMethod paymentMethod) { + switch (paymentMethod.getId()) { + case PaymentMethod.UPHOLD_ID: + return new UpholdAccount(); + case PaymentMethod.MONEY_BEAM_ID: + return new MoneyBeamAccount(); + case PaymentMethod.POPMONEY_ID: + return new PopmoneyAccount(); + case PaymentMethod.REVOLUT_ID: + return new RevolutAccount(); + case PaymentMethod.PERFECT_MONEY_ID: + return new PerfectMoneyAccount(); + case PaymentMethod.SEPA_ID: + return new SepaAccount(); + case PaymentMethod.SEPA_INSTANT_ID: + return new SepaInstantAccount(); + case PaymentMethod.FASTER_PAYMENTS_ID: + return new FasterPaymentsAccount(); + case PaymentMethod.NATIONAL_BANK_ID: + return new NationalBankAccount(); + case PaymentMethod.SAME_BANK_ID: + return new SameBankAccount(); + case PaymentMethod.SPECIFIC_BANKS_ID: + return new SpecificBanksAccount(); + case PaymentMethod.JAPAN_BANK_ID: + return new JapanBankAccount(); + case PaymentMethod.AUSTRALIA_PAYID_ID: + return new AustraliaPayid(); + case PaymentMethod.ALI_PAY_ID: + return new AliPayAccount(); + case PaymentMethod.WECHAT_PAY_ID: + return new WeChatPayAccount(); + case PaymentMethod.SWISH_ID: + return new SwishAccount(); + case PaymentMethod.CLEAR_X_CHANGE_ID: + return new ClearXchangeAccount(); + case PaymentMethod.CHASE_QUICK_PAY_ID: + return new ChaseQuickPayAccount(); + case PaymentMethod.INTERAC_E_TRANSFER_ID: + return new InteracETransferAccount(); + case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: + return new USPostalMoneyOrderAccount(); + case PaymentMethod.CASH_DEPOSIT_ID: + return new CashDepositAccount(); + case PaymentMethod.BLOCK_CHAINS_ID: + return new CryptoCurrencyAccount(); + case PaymentMethod.MONEY_GRAM_ID: + return new MoneyGramAccount(); + case PaymentMethod.WESTERN_UNION_ID: + return new WesternUnionAccount(); + case PaymentMethod.HAL_CASH_ID: + return new HalCashAccount(); + case PaymentMethod.F2F_ID: + return new F2FAccount(); + case PaymentMethod.CASH_BY_MAIL_ID: + return new CashByMailAccount(); + case PaymentMethod.PROMPT_PAY_ID: + return new PromptPayAccount(); + case PaymentMethod.ADVANCED_CASH_ID: + return new AdvancedCashAccount(); + case PaymentMethod.TRANSFERWISE_ID: + return new TransferwiseAccount(); + case PaymentMethod.AMAZON_GIFT_CARD_ID: + return new AmazonGiftCardAccount(); + case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: + return new InstantCryptoCurrencyAccount(); + + // Cannot be deleted as it would break old trade history entries + case PaymentMethod.OK_PAY_ID: + return new OKPayAccount(); + case PaymentMethod.CASH_APP_ID: + return new CashAppAccount(); + case PaymentMethod.VENMO_ID: + return new VenmoAccount(); + + default: + throw new RuntimeException("Not supported PaymentMethod: " + paymentMethod); + } + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountList.java b/core/src/main/java/bisq/core/payment/PaymentAccountList.java new file mode 100644 index 0000000000..a35801705c --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccountList.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.proto.persistable.PersistableList; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public class PaymentAccountList extends PersistableList { + + public PaymentAccountList(List list) { + super(list); + } + + @Override + public Message toProtoMessage() { + 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) { + return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream() + .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) + .collect(Collectors.toList()))); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java new file mode 100644 index 0000000000..fc810b017d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -0,0 +1,147 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.user.User; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class PaymentAccountUtil { + + public static boolean isAnyPaymentAccountValidForOffer(Offer offer, + Collection paymentAccounts) { + for (PaymentAccount paymentAccount : paymentAccounts) { + if (isPaymentAccountValidForOffer(offer, paymentAccount)) + return true; + } + return false; + } + + public static ObservableList getPossiblePaymentAccounts(Offer offer, + Set paymentAccounts, + AccountAgeWitnessService accountAgeWitnessService) { + ObservableList result = FXCollections.observableArrayList(); + result.addAll(paymentAccounts.stream() + .filter(paymentAccount -> isPaymentAccountValidForOffer(offer, paymentAccount)) + .filter(paymentAccount -> isAmountValidForOffer(offer, paymentAccount, accountAgeWitnessService)) + .collect(Collectors.toList())); + return result; + } + + // Return true if paymentAccount can take this offer + public static boolean isAmountValidForOffer(Offer offer, + PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService) { + boolean hasChargebackRisk = PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); + boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().value; + return !hasChargebackRisk || hasValidAccountAgeWitness; + } + + // TODO might be used to show more details if we get payment methods updates with diff. limits + public static String getInfoForMismatchingPaymentMethodLimits(Offer offer, PaymentAccount paymentAccount) { + // don't translate atm as it is not used so far in the UI just for logs + return "Payment methods have different trade limits or trade periods.\n" + + "Our local Payment method: " + paymentAccount.getPaymentMethod().toString() + "\n" + + "Payment method from offer: " + offer.getPaymentMethod().toString(); + } + + public static boolean isPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) { + return new ReceiptValidator(offer, paymentAccount).isValid(); + } + + public static Optional getMostMaturePaymentAccountForOffer(Offer offer, + Set paymentAccounts, + AccountAgeWitnessService service) { + PaymentAccounts accounts = new PaymentAccounts(paymentAccounts, service); + return Optional.ofNullable(accounts.getOldestPaymentAccountForOffer(offer)); + } + + @Nullable + public static ArrayList getAcceptedCountryCodes(PaymentAccount paymentAccount) { + ArrayList acceptedCountryCodes = null; + if (paymentAccount instanceof SepaAccount) { + acceptedCountryCodes = new ArrayList<>(((SepaAccount) paymentAccount).getAcceptedCountryCodes()); + } else if (paymentAccount instanceof SepaInstantAccount) { + acceptedCountryCodes = new ArrayList<>(((SepaInstantAccount) paymentAccount).getAcceptedCountryCodes()); + } else if (paymentAccount instanceof CountryBasedPaymentAccount) { + acceptedCountryCodes = new ArrayList<>(); + Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); + if (country != null) + acceptedCountryCodes.add(country.code); + } + return acceptedCountryCodes; + } + + @Nullable + public static List getAcceptedBanks(PaymentAccount paymentAccount) { + List acceptedBanks = null; + if (paymentAccount instanceof SpecificBanksAccount) { + acceptedBanks = new ArrayList<>(((SpecificBanksAccount) paymentAccount).getAcceptedBanks()); + } else if (paymentAccount instanceof SameBankAccount) { + acceptedBanks = new ArrayList<>(); + acceptedBanks.add(((SameBankAccount) paymentAccount).getBankId()); + } + return acceptedBanks; + } + + @Nullable + public static String getBankId(PaymentAccount paymentAccount) { + return paymentAccount instanceof BankAccount ? ((BankAccount) paymentAccount).getBankId() : null; + } + + @Nullable + public static String getCountryCode(PaymentAccount paymentAccount) { + // That is optional and set to null if not supported (AltCoins,...) + if (paymentAccount instanceof CountryBasedPaymentAccount) { + Country country = (((CountryBasedPaymentAccount) paymentAccount)).getCountry(); + return country != null ? country.code : null; + } + return null; + } + + public static boolean isCryptoCurrencyAccount(PaymentAccount paymentAccount) { + return (paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS) || + paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT)); + } + + public static Optional findPaymentAccount(PaymentAccountPayload paymentAccountPayload, + User user) { + return user.getPaymentAccountsAsObservable().stream(). + filter(e -> e.getPaymentAccountPayload().equals(paymentAccountPayload)) + .findAny(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccounts.java b/core/src/main/java/bisq/core/payment/PaymentAccounts.java new file mode 100644 index 0000000000..6f871ce20f --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccounts.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.offer.Offer; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +class PaymentAccounts { + private static final Logger log = LoggerFactory.getLogger(PaymentAccounts.class); + + private final Set accounts; + private final AccountAgeWitnessService accountAgeWitnessService; + private final BiFunction validator; + + PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService) { + this(accounts, accountAgeWitnessService, PaymentAccountUtil::isPaymentAccountValidForOffer); + } + + PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService, + BiFunction validator) { + this.accounts = accounts; + this.accountAgeWitnessService = accountAgeWitnessService; + this.validator = validator; + } + + @Nullable + PaymentAccount getOldestPaymentAccountForOffer(Offer offer) { + List sortedValidAccounts = sortValidAccounts(offer); + + logAccounts(sortedValidAccounts); + + return firstOrNull(sortedValidAccounts); + } + + private List sortValidAccounts(Offer offer) { + Comparator comparator = this::compareByTradeLimit; + return accounts.stream() + .filter(account -> validator.apply(offer, account)) + .sorted(comparator.reversed()) + .collect(Collectors.toList()); + } + + @Nullable + private PaymentAccount firstOrNull(List accounts) { + return accounts.isEmpty() ? null : accounts.get(0); + } + + private void logAccounts(List accounts) { + if (log.isDebugEnabled()) { + StringBuilder message = new StringBuilder("Valid accounts: \n"); + for (PaymentAccount account : accounts) { + String accountName = account.getAccountName(); + String witnessHex = accountAgeWitnessService.getMyWitnessHashAsHex(account.getPaymentAccountPayload()); + + message.append("name = ") + .append(accountName) + .append("; witness hex = ") + .append(witnessHex) + .append(";\n"); + } + + log.debug(message.toString()); + } + } + + // Accounts ranked by trade limit + private int compareByTradeLimit(PaymentAccount left, PaymentAccount right) { + // Mature accounts count as infinite sign age + if (accountAgeWitnessService.myHasTradeLimitException(left)) { + return !accountAgeWitnessService.myHasTradeLimitException(right) ? 1 : 0; + } + if (accountAgeWitnessService.myHasTradeLimitException(right)) { + return -1; + } + + AccountAgeWitness leftWitness = accountAgeWitnessService.getMyWitness(left.getPaymentAccountPayload()); + AccountAgeWitness rightWitness = accountAgeWitnessService.getMyWitness(right.getPaymentAccountPayload()); + + Date now = new Date(); + + long leftSignAge = accountAgeWitnessService.getWitnessSignAge(leftWitness, now); + long rightSignAge = accountAgeWitnessService.getWitnessSignAge(rightWitness, now); + + return Long.compare(leftSignAge, rightSignAge); + } +} diff --git a/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java b/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java new file mode 100644 index 0000000000..0abc12f367 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PerfectMoneyAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class PerfectMoneyAccount extends PaymentAccount { + public PerfectMoneyAccount() { + super(PaymentMethod.PERFECT_MONEY); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PerfectMoneyAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountNr(String accountNr) { + ((PerfectMoneyAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((PerfectMoneyAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PopmoneyAccount.java b/core/src/main/java/bisq/core/payment/PopmoneyAccount.java new file mode 100644 index 0000000000..c6f534c24e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PopmoneyAccount.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PopmoneyAccountPayload; + +import lombok.EqualsAndHashCode; + +//TODO missing support for selected trade currency +@EqualsAndHashCode(callSuper = true) +public final class PopmoneyAccount extends PaymentAccount { + public PopmoneyAccount() { + super(PaymentMethod.POPMONEY); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PopmoneyAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountId(String accountId) { + ((PopmoneyAccountPayload) paymentAccountPayload).setAccountId(accountId); + } + + public String getAccountId() { + return ((PopmoneyAccountPayload) paymentAccountPayload).getAccountId(); + } + + public void setHolderName(String holderName) { + ((PopmoneyAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((PopmoneyAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PromptPayAccount.java b/core/src/main/java/bisq/core/payment/PromptPayAccount.java new file mode 100644 index 0000000000..89ed2102d3 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PromptPayAccount.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PromptPayAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class PromptPayAccount extends PaymentAccount { + public PromptPayAccount() { + super(PaymentMethod.PROMPT_PAY); + setSingleTradeCurrency(new FiatCurrency("THB")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PromptPayAccountPayload(paymentMethod.getId(), id); + } + + public void setPromptPayId(String promptPayId) { + ((PromptPayAccountPayload) paymentAccountPayload).setPromptPayId(promptPayId); + } + + public String getPromptPayId() { + return ((PromptPayAccountPayload) paymentAccountPayload).getPromptPayId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/ReceiptPredicates.java b/core/src/main/java/bisq/core/payment/ReceiptPredicates.java new file mode 100644 index 0000000000..e103335ced --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ReceiptPredicates.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; + +import com.google.common.base.Preconditions; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class ReceiptPredicates { + boolean isEqualPaymentMethods(Offer offer, PaymentAccount account) { + // check if we have a matching payment method or if its a bank account payment method which is treated special + PaymentMethod accountPaymentMethod = account.getPaymentMethod(); + PaymentMethod offerPaymentMethod = offer.getPaymentMethod(); + + boolean arePaymentMethodsEqual = accountPaymentMethod.equals(offerPaymentMethod); + + if (log.isWarnEnabled()) { + String accountPaymentMethodId = accountPaymentMethod.getId(); + String offerPaymentMethodId = offerPaymentMethod.getId(); + if (!arePaymentMethodsEqual && accountPaymentMethodId.equals(offerPaymentMethodId)) { + log.warn(PaymentAccountUtil.getInfoForMismatchingPaymentMethodLimits(offer, account)); + } + } + + return arePaymentMethodsEqual; + } + + boolean isOfferRequireSameOrSpecificBank(Offer offer, PaymentAccount account) { + PaymentMethod paymentMethod = offer.getPaymentMethod(); + boolean isSameOrSpecificBank = paymentMethod.equals(PaymentMethod.SAME_BANK) + || paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS); + return (account instanceof BankAccount) && isSameOrSpecificBank; + } + + boolean isMatchingBankId(Offer offer, PaymentAccount account) { + final List acceptedBanksForOffer = offer.getAcceptedBankIds(); + Preconditions.checkNotNull(acceptedBanksForOffer, "offer.getAcceptedBankIds() must not be null"); + + final String accountBankId = ((BankAccount) account).getBankId(); + + if (account instanceof SpecificBanksAccount) { + // check if we have a matching bank + boolean offerSideMatchesBank = (accountBankId != null) && acceptedBanksForOffer.contains(accountBankId); + List acceptedBanksForAccount = ((SpecificBanksAccount) account).getAcceptedBanks(); + boolean paymentAccountSideMatchesBank = acceptedBanksForAccount.contains(offer.getBankId()); + + return offerSideMatchesBank && paymentAccountSideMatchesBank; + } else { + // national or same bank + return (accountBankId != null) && acceptedBanksForOffer.contains(accountBankId); + } + } + + boolean isMatchingCountryCodes(Offer offer, PaymentAccount account) { + List acceptedCodes = Optional.ofNullable(offer.getAcceptedCountryCodes()) + .orElse(Collections.emptyList()); + + String code = Optional.of(account) + .map(CountryBasedPaymentAccount.class::cast) + .map(CountryBasedPaymentAccount::getCountry) + .map(country -> country.code) + .orElse("undefined"); + + return acceptedCodes.contains(code); + } + + boolean isMatchingCurrency(Offer offer, PaymentAccount account) { + List currencies = account.getTradeCurrencies(); + + Set codes = currencies.stream() + .map(TradeCurrency::getCode) + .collect(Collectors.toSet()); + + return codes.contains(offer.getCurrencyCode()); + } + + boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) { + boolean isSepa = account instanceof SepaAccount; + boolean isSepaInstant = account instanceof SepaInstantAccount; + return offer.getPaymentMethod().equals(PaymentMethod.SEPA) && (isSepa || isSepaInstant); + } + + boolean isMatchingSepaInstant(Offer offer, PaymentAccount account) { + return offer.getPaymentMethod().equals(PaymentMethod.SEPA_INSTANT) && account instanceof SepaInstantAccount; + } +} diff --git a/core/src/main/java/bisq/core/payment/ReceiptValidator.java b/core/src/main/java/bisq/core/payment/ReceiptValidator.java new file mode 100644 index 0000000000..e2175b6650 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ReceiptValidator.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.offer.Offer; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class ReceiptValidator { + private final ReceiptPredicates predicates; + private final PaymentAccount account; + private final Offer offer; + + ReceiptValidator(Offer offer, PaymentAccount account) { + this(offer, account, new ReceiptPredicates()); + } + + ReceiptValidator(Offer offer, PaymentAccount account, ReceiptPredicates predicates) { + this.offer = offer; + this.account = account; + this.predicates = predicates; + } + + boolean isValid() { + // We only support trades with the same currencies + if (!predicates.isMatchingCurrency(offer, account)) { + return false; + } + + boolean isEqualPaymentMethods = predicates.isEqualPaymentMethods(offer, account); + + // All non-CountryBasedPaymentAccount need to have same payment methods + if (!(account instanceof CountryBasedPaymentAccount)) { + return isEqualPaymentMethods; + } + + // We have a CountryBasedPaymentAccount, countries need to match + if (!predicates.isMatchingCountryCodes(offer, account)) { + return false; + } + + // We have same country + if (predicates.isMatchingSepaOffer(offer, account)) { + // Sepa offer and taker account is Sepa or Sepa Instant + return true; + } + + if (predicates.isMatchingSepaInstant(offer, account)) { + // Sepa Instant offer and taker account + return true; + } + + // Aside from Sepa or Sepa Instant, payment methods need to match + if (!isEqualPaymentMethods) { + return false; + } + + if (predicates.isOfferRequireSameOrSpecificBank(offer, account)) { + return predicates.isMatchingBankId(offer, account); + } + + return true; + } +} diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java new file mode 100644 index 0000000000..93191bbac0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.RevolutAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class RevolutAccount extends PaymentAccount { + public RevolutAccount() { + super(PaymentMethod.REVOLUT); + tradeCurrencies.addAll(CurrencyUtil.getAllRevolutCurrencies()); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new RevolutAccountPayload(paymentMethod.getId(), id); + } + + public void setUserName(String userName) { + revolutAccountPayload().setUserName(userName); + } + + public String getUserName() { + return (revolutAccountPayload()).getUserName(); + } + + public String getAccountId() { + return (revolutAccountPayload()).getAccountId(); + } + + public boolean userNameNotSet() { + return (revolutAccountPayload()).userNameNotSet(); + } + + public boolean hasOldAccountId() { + return (revolutAccountPayload()).hasOldAccountId(); + } + + private RevolutAccountPayload revolutAccountPayload() { + return (RevolutAccountPayload) paymentAccountPayload; + } + + @Override + public void onAddToUser() { + super.onAddToUser(); + + // At save we apply the userName to accountId in case it is empty for backward compatibility + revolutAccountPayload().maybeApplyUserNameToAccountId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/SameBankAccount.java b/core/src/main/java/bisq/core/payment/SameBankAccount.java new file mode 100644 index 0000000000..d561cf582b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SameBankAccount.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SameBankAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class SameBankAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { + public SameBankAccount() { + super(PaymentMethod.SAME_BANK); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SameBankAccountPayload(paymentMethod.getId(), id); + } + + @Override + public String getBankId() { + return ((BankAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } +} diff --git a/core/src/main/java/bisq/core/payment/SameCountryRestrictedBankAccount.java b/core/src/main/java/bisq/core/payment/SameCountryRestrictedBankAccount.java new file mode 100644 index 0000000000..aa1526d2ac --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SameCountryRestrictedBankAccount.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.core.payment; + +public interface SameCountryRestrictedBankAccount extends BankAccount { + String getCountryCode(); +} diff --git a/core/src/main/java/bisq/core/payment/SepaAccount.java b/core/src/main/java/bisq/core/payment/SepaAccount.java new file mode 100644 index 0000000000..d7c551f3f1 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SepaAccount.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SepaAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class SepaAccount extends CountryBasedPaymentAccount implements BankAccount { + public SepaAccount() { + super(PaymentMethod.SEPA); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SepaAccountPayload(paymentMethod.getId(), id, + CountryUtil.getAllSepaCountries()); + } + + @Override + public String getBankId() { + return ((SepaAccountPayload) paymentAccountPayload).getBic(); + } + + public void setHolderName(String holderName) { + ((SepaAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((SepaAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setIban(String iban) { + ((SepaAccountPayload) paymentAccountPayload).setIban(iban); + } + + public String getIban() { + return ((SepaAccountPayload) paymentAccountPayload).getIban(); + } + + public void setBic(String bic) { + ((SepaAccountPayload) paymentAccountPayload).setBic(bic); + } + + public String getBic() { + return ((SepaAccountPayload) paymentAccountPayload).getBic(); + } + + public List getAcceptedCountryCodes() { + return ((SepaAccountPayload) paymentAccountPayload).getAcceptedCountryCodes(); + } + + public void addAcceptedCountry(String countryCode) { + ((SepaAccountPayload) paymentAccountPayload).addAcceptedCountry(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + ((SepaAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); + } +} diff --git a/core/src/main/java/bisq/core/payment/SepaInstantAccount.java b/core/src/main/java/bisq/core/payment/SepaInstantAccount.java new file mode 100644 index 0000000000..9479a1b6a5 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SepaInstantAccount.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SepaInstantAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class SepaInstantAccount extends CountryBasedPaymentAccount implements BankAccount { + public SepaInstantAccount() { + super(PaymentMethod.SEPA_INSTANT); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SepaInstantAccountPayload(paymentMethod.getId(), id, + CountryUtil.getAllSepaInstantCountries()); + } + + @Override + public String getBankId() { + return ((SepaInstantAccountPayload) paymentAccountPayload).getBic(); + } + + public void setHolderName(String holderName) { + ((SepaInstantAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((SepaInstantAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setIban(String iban) { + ((SepaInstantAccountPayload) paymentAccountPayload).setIban(iban); + } + + public String getIban() { + return ((SepaInstantAccountPayload) paymentAccountPayload).getIban(); + } + + public void setBic(String bic) { + ((SepaInstantAccountPayload) paymentAccountPayload).setBic(bic); + } + + public String getBic() { + return ((SepaInstantAccountPayload) paymentAccountPayload).getBic(); + } + + public List getAcceptedCountryCodes() { + return ((SepaInstantAccountPayload) paymentAccountPayload).getAcceptedCountryCodes(); + } + + public void addAcceptedCountry(String countryCode) { + ((SepaInstantAccountPayload) paymentAccountPayload).addAcceptedCountry(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + ((SepaInstantAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); + } +} diff --git a/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java b/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java new file mode 100644 index 0000000000..106d1f8164 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SpecificBanksAccountPayload; + +import java.util.ArrayList; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class SpecificBanksAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { + public SpecificBanksAccount() { + super(PaymentMethod.SPECIFIC_BANKS); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SpecificBanksAccountPayload(paymentMethod.getId(), id); + } + + // TODO change to List + public ArrayList getAcceptedBanks() { + return ((SpecificBanksAccountPayload) paymentAccountPayload).getAcceptedBanks(); + } + + @Override + public String getBankId() { + return ((SpecificBanksAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } +} diff --git a/core/src/main/java/bisq/core/payment/SwishAccount.java b/core/src/main/java/bisq/core/payment/SwishAccount.java new file mode 100644 index 0000000000..e323630040 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SwishAccount.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SwishAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class SwishAccount extends PaymentAccount { + public SwishAccount() { + super(PaymentMethod.SWISH); + setSingleTradeCurrency(new FiatCurrency("SEK")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SwishAccountPayload(paymentMethod.getId(), id); + } + + public void setMobileNr(String mobileNr) { + ((SwishAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); + } + + public String getMobileNr() { + return ((SwishAccountPayload) paymentAccountPayload).getMobileNr(); + } + + public void setHolderName(String holderName) { + ((SwishAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((SwishAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/TradeLimits.java b/core/src/main/java/bisq/core/payment/TradeLimits.java new file mode 100644 index 0000000000..2d3c5c5688 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/TradeLimits.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public class TradeLimits { + @Nullable + @Getter + private static TradeLimits INSTANCE; + + private final DaoStateService daoStateService; + private final PeriodService periodService; + + @Inject + public TradeLimits(DaoStateService daoStateService, PeriodService periodService) { + this.daoStateService = daoStateService; + this.periodService = periodService; + INSTANCE = this; + } + + public void onAllServicesInitialized() { + // Do nothing but required to enforce class creation by guice. + // The TradeLimits is used by PaymentMethod via the static INSTANCE and this would not trigger class creation by + // guice. + } + + + /** + * The default trade limits defined as statics in PaymentMethod are only used until the DAO + * is fully synchronized. + * + * @see bisq.core.payment.payload.PaymentMethod + * @return the maximum trade limit set by the DAO. + */ + public Coin getMaxTradeLimit() { + return daoStateService.getParamValueAsCoin(Param.MAX_TRADE_LIMIT, periodService.getChainHeight()); + } + + // 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). + + /** + * + * @param maxLimit Satoshi value of max trade limit + * @param riskFactor Risk factor to decrease trade limit for higher risk payment methods + * @return Possibly adjusted trade limit to avoid that in first month trade limit get precision < 4. + */ + public long getRoundedRiskBasedTradeLimit(long maxLimit, long riskFactor) { + return getFirstMonthRiskBasedTradeLimit(maxLimit, riskFactor) * 4; + } + + // The first month we allow only 0.25% of the trade limit. We want to ensure that precision is <=4 otherwise we round. + + /** + * + * @param maxLimit Satoshi value of max trade limit + * @param riskFactor Risk factor to decrease trade limit for higher risk payment methods + * @return Rounded trade limit for first month to avoid BTC value with precision < 4. + */ + @VisibleForTesting + long getFirstMonthRiskBasedTradeLimit(long maxLimit, long riskFactor) { + // The first month we use 1/4 of the max limit. We multiply with riskFactor, so 1/ (4 * 8) is smallest limit in + // first month of a maxTradeLimitHighRisk method + long smallestLimit = maxLimit / (4 * riskFactor); // e.g. 100000000 / 32 = 3125000 + // We want to avoid more than 4 decimal places (100000000 / 32 = 3125000 or 1 BTC / 32 = 0.03125 BTC). + // We want rounding to 0.0313 BTC + double decimalForm = MathUtils.scaleDownByPowerOf10((double) smallestLimit, 8); + double rounded = MathUtils.roundDouble(decimalForm, 4); + return MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(rounded, 8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/TransferwiseAccount.java b/core/src/main/java/bisq/core/payment/TransferwiseAccount.java new file mode 100644 index 0000000000..50a3379c4a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/TransferwiseAccount.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.TransferwiseAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class TransferwiseAccount extends PaymentAccount { + public TransferwiseAccount() { + super(PaymentMethod.TRANSFERWISE); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new TransferwiseAccountPayload(paymentMethod.getId(), id); + } + + public void setEmail(String accountId) { + ((TransferwiseAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((TransferwiseAccountPayload) paymentAccountPayload).getEmail(); + } +} diff --git a/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java b/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java new file mode 100644 index 0000000000..af8f577e65 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class USPostalMoneyOrderAccount extends PaymentAccount { + public USPostalMoneyOrderAccount() { + super(PaymentMethod.US_POSTAL_MONEY_ORDER); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new USPostalMoneyOrderAccountPayload(paymentMethod.getId(), id); + } + + public void setPostalAddress(String postalAddress) { + ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); + } + + public String getPostalAddress() { + return ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getPostalAddress(); + } + + public void setHolderName(String holderName) { + ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/UpholdAccount.java b/core/src/main/java/bisq/core/payment/UpholdAccount.java new file mode 100644 index 0000000000..af11de6be6 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/UpholdAccount.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.UpholdAccountPayload; + +import lombok.EqualsAndHashCode; + +//TODO missing support for selected trade currency +@EqualsAndHashCode(callSuper = true) +public final class UpholdAccount extends PaymentAccount { + public UpholdAccount() { + super(PaymentMethod.UPHOLD); + tradeCurrencies.addAll(CurrencyUtil.getAllUpholdCurrencies()); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new UpholdAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountId(String accountId) { + ((UpholdAccountPayload) paymentAccountPayload).setAccountId(accountId); + } + + public String getAccountId() { + return ((UpholdAccountPayload) paymentAccountPayload).getAccountId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/VenmoAccount.java b/core/src/main/java/bisq/core/payment/VenmoAccount.java new file mode 100644 index 0000000000..cd54660144 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/VenmoAccount.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.VenmoAccountPayload; + +import lombok.EqualsAndHashCode; + +// Removed due too high chargeback risk +// Cannot be deleted as it would break old trade history entries +@Deprecated +@EqualsAndHashCode(callSuper = true) +public final class VenmoAccount extends PaymentAccount { + public VenmoAccount() { + super(PaymentMethod.VENMO); + setSingleTradeCurrency(new FiatCurrency("USD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new VenmoAccountPayload(paymentMethod.getId(), id); + } + + public void setVenmoUserName(String venmoUserName) { + ((VenmoAccountPayload) paymentAccountPayload).setVenmoUserName(venmoUserName); + } + + public String getVenmoUserName() { + return ((VenmoAccountPayload) paymentAccountPayload).getVenmoUserName(); + } + + public void setHolderName(String holderName) { + ((VenmoAccountPayload) paymentAccountPayload).setHolderName(holderName); + } + + public String getHolderName() { + return ((VenmoAccountPayload) paymentAccountPayload).getHolderName(); + } +} diff --git a/core/src/main/java/bisq/core/payment/WeChatPayAccount.java b/core/src/main/java/bisq/core/payment/WeChatPayAccount.java new file mode 100644 index 0000000000..f4ef6f6440 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/WeChatPayAccount.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.WeChatPayAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class WeChatPayAccount extends PaymentAccount { + + public WeChatPayAccount() { + super(PaymentMethod.WECHAT_PAY); + setSingleTradeCurrency(new FiatCurrency("CNY")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new WeChatPayAccountPayload(paymentMethod.getId(), id); + } + + public void setAccountNr(String accountNr) { + ((WeChatPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((WeChatPayAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/WesternUnionAccount.java b/core/src/main/java/bisq/core/payment/WesternUnionAccount.java new file mode 100644 index 0000000000..18c1e8cb11 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/WesternUnionAccount.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.WesternUnionAccountPayload; + +public final class WesternUnionAccount extends CountryBasedPaymentAccount { + public WesternUnionAccount() { + super(PaymentMethod.WESTERN_UNION); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new WesternUnionAccountPayload(paymentMethod.getId(), id); + } + + public String getEmail() { + return ((WesternUnionAccountPayload) paymentAccountPayload).getEmail(); + } + + public void setEmail(String email) { + ((WesternUnionAccountPayload) paymentAccountPayload).setEmail(email); + } + + public String getFullName() { + return ((WesternUnionAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setFullName(String email) { + ((WesternUnionAccountPayload) paymentAccountPayload).setHolderName(email); + } + + public String getCity() { + return ((WesternUnionAccountPayload) paymentAccountPayload).getCity(); + } + + public void setCity(String email) { + ((WesternUnionAccountPayload) paymentAccountPayload).setCity(email); + } + + public String getState() { + return ((WesternUnionAccountPayload) paymentAccountPayload).getState(); + } + + public void setState(String email) { + ((WesternUnionAccountPayload) paymentAccountPayload).setState(email); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AdvancedCashAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AdvancedCashAccountPayload.java new file mode 100644 index 0000000000..cea2a0546a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AdvancedCashAccountPayload.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class AdvancedCashAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public AdvancedCashAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AdvancedCashAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setAdvancedCashAccountPayload(protobuf.AdvancedCashAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static AdvancedCashAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new AdvancedCashAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getAdvancedCashAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.wallet") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AliPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AliPayAccountPayload.java new file mode 100644 index 0000000000..e0dd58eaa5 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AliPayAccountPayload.java @@ -0,0 +1,97 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@ToString +public final class AliPayAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public AliPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AliPayAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setAliPayAccountPayload(protobuf.AliPayAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static AliPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new AliPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getAliPayAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AmazonGiftCardAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AmazonGiftCardAccountPayload.java new file mode 100644 index 0000000000..92ffaf302d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AmazonGiftCardAccountPayload.java @@ -0,0 +1,116 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; +import bisq.common.util.JsonExclude; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public class AmazonGiftCardAccountPayload extends PaymentAccountPayload { + private String emailOrMobileNr; + // For backward compatibility we need to exclude the new field for the contract json. + // We can remove that after a while when risk that users with pre 1.5.5 version is very low. + @JsonExclude + private String countryCode = ""; + + public AmazonGiftCardAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AmazonGiftCardAccountPayload(String paymentMethodName, + String id, + String emailOrMobileNr, + String countryCode, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.emailOrMobileNr = emailOrMobileNr; + this.countryCode = countryCode; + } + + @Override + public Message toProtoMessage() { + protobuf.AmazonGiftCardAccountPayload.Builder builder = + protobuf.AmazonGiftCardAccountPayload.newBuilder() + .setCountryCode(countryCode) + .setEmailOrMobileNr(emailOrMobileNr); + return getPaymentAccountPayloadBuilder() + .setAmazonGiftCardAccountPayload(builder) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.AmazonGiftCardAccountPayload amazonGiftCardAccountPayload = proto.getAmazonGiftCardAccountPayload(); + return new AmazonGiftCardAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + amazonGiftCardAccountPayload.getEmailOrMobileNr(), + amazonGiftCardAccountPayload.getCountryCode(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.email.mobile") + " " + emailOrMobileNr; + } + + @Override + public byte[] getAgeWitnessInputData() { + String data = "AmazonGiftCard" + emailOrMobileNr; + return super.getAgeWitnessInputData(data.getBytes(StandardCharsets.UTF_8)); + } + + public boolean countryNotSet() { + return countryCode.isEmpty(); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java new file mode 100644 index 0000000000..e35dd4c7bc --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import java.nio.charset.StandardCharsets; + +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public abstract class AssetsAccountPayload extends PaymentAccountPayload { + protected String address = ""; + + protected AssetsAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected AssetsAccountPayload(String paymentMethod, + String id, + String address, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.address = address; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.getWithCol("payment.altcoin.receiver.address") + " " + address; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(address.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java new file mode 100644 index 0000000000..50539cd4c0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class AustraliaPayidPayload extends PaymentAccountPayload { + private String payid = ""; + private String bankAccountName = ""; + + public AustraliaPayidPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AustraliaPayidPayload(String paymentMethod, + String id, + String payid, + String bankAccountName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.payid = payid; + this.bankAccountName = bankAccountName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setAustraliaPayidPayload( + protobuf.AustraliaPayidPayload.newBuilder() + .setPayid(payid) + .setBankAccountName(bankAccountName) + ).build(); + } + + public static AustraliaPayidPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.AustraliaPayidPayload AustraliaPayidPayload = proto.getAustraliaPayidPayload(); + return new AustraliaPayidPayload(proto.getPaymentMethodId(), + proto.getId(), + AustraliaPayidPayload.getPayid(), + AustraliaPayidPayload.getBankAccountName(), + proto.getMaxTradePeriod(), + CollectionUtils.isEmpty(proto.getExcludeFromJsonDataMap()) ? null : new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return + Res.get("payment.australia.payid") + ": " + payid + "\n" + + Res.get("payment.account.owner") + ": " + bankAccountName; + } + + + @Override + public byte[] getAgeWitnessInputData() { + String all = this.payid + this.bankAccountName; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java new file mode 100644 index 0000000000..3abfa8b2ae --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java @@ -0,0 +1,187 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.BankUtil; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import java.nio.charset.StandardCharsets; + +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Setter +@Getter +@ToString +@Slf4j +public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + protected String holderName = ""; + @Nullable + protected String bankName; + @Nullable + protected String branchId; + @Nullable + protected String accountNr; + @Nullable + protected String accountType; + @Nullable + protected String holderTaxId; + @Nullable + protected String bankId; + @Nullable + protected String nationalAccountId; + + protected BankAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected BankAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + @Nullable String bankName, + @Nullable String branchId, + @Nullable String accountNr, + @Nullable String accountType, + @Nullable String holderTaxId, + @Nullable String bankId, + @Nullable String nationalAccountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.bankName = bankName; + this.branchId = branchId; + this.accountNr = accountNr; + this.accountType = accountType; + this.holderTaxId = holderTaxId; + this.bankId = bankId; + this.nationalAccountId = nationalAccountId; + } + + @Override + public protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { + protobuf.BankAccountPayload.Builder builder = + protobuf.BankAccountPayload.newBuilder() + .setHolderName(holderName); + Optional.ofNullable(holderTaxId).ifPresent(builder::setHolderTaxId); + Optional.ofNullable(bankName).ifPresent(builder::setBankName); + Optional.ofNullable(branchId).ifPresent(builder::setBranchId); + Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); + Optional.ofNullable(accountType).ifPresent(builder::setAccountType); + Optional.ofNullable(bankId).ifPresent(builder::setBankId); + Optional.ofNullable(nationalAccountId).ifPresent(builder::setNationalAccountId); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = super.getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(builder); + return super.getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder); + } + + + @Override + public String getPaymentDetails() { + return "Bank account transfer - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + String bankName = BankUtil.isBankNameRequired(countryCode) ? + BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + "\n" : ""; + String bankId = BankUtil.isBankIdRequired(countryCode) ? + BankUtil.getBankIdLabel(countryCode) + ": " + this.bankId + "\n" : ""; + String branchId = BankUtil.isBranchIdRequired(countryCode) ? + BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + "\n" : ""; + String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? + BankUtil.getNationalAccountIdLabel(countryCode) + ": " + this.nationalAccountId + "\n" : ""; + String accountNr = BankUtil.isAccountNrRequired(countryCode) ? + BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr + "\n" : ""; + String accountType = BankUtil.isAccountTypeRequired(countryCode) ? + BankUtil.getAccountTypeLabel(countryCode) + ": " + this.accountType + "\n" : ""; + String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? + (BankUtil.getHolderIdLabel(countryCode) + ": " + holderTaxId + "\n") : ""; + + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + bankName + + bankId + + branchId + + nationalAccountId + + accountNr + + accountType + + holderTaxIdString + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); + } + + protected String getHolderIdLabel() { + return BankUtil.getHolderIdLabel(countryCode); + } + + @Nullable + public String getBankId() { + return BankUtil.isBankIdRequired(countryCode) ? bankId : bankName; + } + + @Override + public byte[] getAgeWitnessInputData() { + String bankName = BankUtil.isBankNameRequired(countryCode) ? this.bankName : ""; + String bankId = BankUtil.isBankIdRequired(countryCode) ? this.bankId : ""; + String branchId = BankUtil.isBranchIdRequired(countryCode) ? this.branchId : ""; + String accountNr = BankUtil.isAccountNrRequired(countryCode) ? this.accountNr : ""; + String accountType = BankUtil.isAccountTypeRequired(countryCode) ? this.accountType : ""; + String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? + (BankUtil.getHolderIdLabel(countryCode) + " " + holderTaxId + "\n") : ""; + String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? this.nationalAccountId : ""; + + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + + String all = bankName + + bankId + + branchId + + accountNr + + accountType + + holderTaxIdString + + nationalAccountId; + + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CashAppAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashAppAccountPayload.java new file mode 100644 index 0000000000..718cd43e10 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CashAppAccountPayload.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +// Cannot be deleted as it would break old trade history entries +// Removed due too high chargeback risk +@Deprecated +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class CashAppAccountPayload extends PaymentAccountPayload { + private String cashTag = ""; + + public CashAppAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CashAppAccountPayload(String paymentMethod, + String id, + String cashTag, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.cashTag = cashTag; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder() + .setCashTag(cashTag)) + .build(); + } + + public static CashAppAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new CashAppAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getCashAppAccountPayload().getCashTag(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + cashTag; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(cashTag.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CashByMailAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashByMailAccountPayload.java new file mode 100644 index 0000000000..f1b906f90d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CashByMailAccountPayload.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class CashByMailAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String postalAddress = ""; + private String contact = ""; + private String extraInfo = ""; + + public CashByMailAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CashByMailAccountPayload(String paymentMethod, String id, + String postalAddress, + String contact, + String extraInfo, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.postalAddress = postalAddress; + this.contact = contact; + this.extraInfo = extraInfo; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setCashByMailAccountPayload(protobuf.CashByMailAccountPayload.newBuilder() + .setPostalAddress(postalAddress) + .setContact(contact) + .setExtraInfo(extraInfo)) + .build(); + } + + public static CashByMailAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new CashByMailAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getCashByMailAccountPayload().getPostalAddress(), + proto.getCashByMailAccountPayload().getContact(), + proto.getCashByMailAccountPayload().getExtraInfo(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + contact + ", " + + Res.getWithCol("payment.postal.address") + " " + postalAddress + ", " + + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + contact + "\n" + + Res.getWithCol("payment.postal.address") + " " + postalAddress; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We use here the contact because the address alone seems to be too weak + return super.getAgeWitnessInputData(ArrayUtils.addAll(contact.getBytes(StandardCharsets.UTF_8), + postalAddress.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public String getOwnerId() { + return contact; + } + @Override + public String getHolderName() { + return contact; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java new file mode 100644 index 0000000000..afa97764a9 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java @@ -0,0 +1,232 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.BankUtil; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + private String holderName = ""; + @Nullable + private String holderEmail; + @Nullable + private String bankName; + @Nullable + private String branchId; + @Nullable + private String accountNr; + @Nullable + private String accountType; + @Nullable + private String requirements; + @Nullable + private String holderTaxId; + @Nullable + private String bankId; + @Nullable + protected String nationalAccountId; + + public CashDepositAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CashDepositAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + @Nullable String holderEmail, + @Nullable String bankName, + @Nullable String branchId, + @Nullable String accountNr, + @Nullable String accountType, + @Nullable String requirements, + @Nullable String holderTaxId, + @Nullable String bankId, + @Nullable String nationalAccountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + this.holderName = holderName; + this.holderEmail = holderEmail; + this.bankName = bankName; + this.branchId = branchId; + this.accountNr = accountNr; + this.accountType = accountType; + this.requirements = requirements; + this.holderTaxId = holderTaxId; + this.bankId = bankId; + this.nationalAccountId = nationalAccountId; + } + + @Override + public Message toProtoMessage() { + protobuf.CashDepositAccountPayload.Builder builder = + protobuf.CashDepositAccountPayload.newBuilder() + .setHolderName(holderName); + Optional.ofNullable(holderEmail).ifPresent(builder::setHolderEmail); + Optional.ofNullable(bankName).ifPresent(builder::setBankName); + Optional.ofNullable(branchId).ifPresent(builder::setBranchId); + Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); + Optional.ofNullable(accountType).ifPresent(builder::setAccountType); + Optional.ofNullable(requirements).ifPresent(builder::setRequirements); + Optional.ofNullable(holderTaxId).ifPresent(builder::setHolderTaxId); + Optional.ofNullable(bankId).ifPresent(builder::setBankId); + Optional.ofNullable(nationalAccountId).ifPresent(builder::setNationalAccountId); + + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setCashDepositAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.CashDepositAccountPayload cashDepositAccountPayload = countryBasedPaymentAccountPayload.getCashDepositAccountPayload(); + return new CashDepositAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + cashDepositAccountPayload.getHolderName(), + cashDepositAccountPayload.getHolderEmail().isEmpty() ? null : cashDepositAccountPayload.getHolderEmail(), + cashDepositAccountPayload.getBankName().isEmpty() ? null : cashDepositAccountPayload.getBankName(), + cashDepositAccountPayload.getBranchId().isEmpty() ? null : cashDepositAccountPayload.getBranchId(), + cashDepositAccountPayload.getAccountNr().isEmpty() ? null : cashDepositAccountPayload.getAccountNr(), + cashDepositAccountPayload.getAccountType().isEmpty() ? null : cashDepositAccountPayload.getAccountType(), + cashDepositAccountPayload.getRequirements().isEmpty() ? null : cashDepositAccountPayload.getRequirements(), + cashDepositAccountPayload.getHolderTaxId().isEmpty() ? null : cashDepositAccountPayload.getHolderTaxId(), + cashDepositAccountPayload.getBankId().isEmpty() ? null : cashDepositAccountPayload.getBankId(), + cashDepositAccountPayload.getNationalAccountId().isEmpty() ? null : cashDepositAccountPayload.getNationalAccountId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return "Cash deposit - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + String bankName = BankUtil.isBankNameRequired(countryCode) ? + BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + "\n" : ""; + String bankId = BankUtil.isBankIdRequired(countryCode) ? + BankUtil.getBankIdLabel(countryCode) + ": " + this.bankId + "\n" : ""; + String branchId = BankUtil.isBranchIdRequired(countryCode) ? + BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + "\n" : ""; + String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? + BankUtil.getNationalAccountIdLabel(countryCode) + ": " + this.nationalAccountId + "\n" : ""; + String accountNr = BankUtil.isAccountNrRequired(countryCode) ? + BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr + "\n" : ""; + String accountType = BankUtil.isAccountTypeRequired(countryCode) ? + BankUtil.getAccountTypeLabel(countryCode) + ": " + this.accountType + "\n" : ""; + String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? + (BankUtil.getHolderIdLabel(countryCode) + ": " + holderTaxId + "\n") : ""; + String requirementsString = requirements != null && !requirements.isEmpty() ? + (Res.getWithCol("payment.extras") + " " + requirements + "\n") : ""; + String emailString = holderEmail != null ? + (Res.getWithCol("payment.email") + " " + holderEmail + "\n") : ""; + + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + emailString + + bankName + + bankId + + branchId + + nationalAccountId + + accountNr + + accountType + + holderTaxIdString + + requirementsString + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); + } + + public String getHolderIdLabel() { + return BankUtil.getHolderIdLabel(countryCode); + } + + @Nullable + public String getBankId() { + return BankUtil.isBankIdRequired(countryCode) ? bankId : bankName; + } + + @Override + public byte[] getAgeWitnessInputData() { + String bankName = BankUtil.isBankNameRequired(countryCode) ? this.bankName : ""; + String bankId = BankUtil.isBankIdRequired(countryCode) ? this.bankId : ""; + String branchId = BankUtil.isBranchIdRequired(countryCode) ? this.branchId : ""; + String accountNr = BankUtil.isAccountNrRequired(countryCode) ? this.accountNr : ""; + String accountType = BankUtil.isAccountTypeRequired(countryCode) ? this.accountType : ""; + String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? + (BankUtil.getHolderIdLabel(countryCode) + " " + holderTaxId + "\n") : ""; + String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? this.nationalAccountId : ""; + + // We don't add holderName and holderEmail because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + + String all = bankName + + bankId + + branchId + + accountNr + + accountType + + holderTaxIdString + + nationalAccountId; + + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java new file mode 100644 index 0000000000..a320c36679 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String email = ""; + private String holderName = ""; + + public ChaseQuickPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ChaseQuickPayAccountPayload(String paymentMethod, + String id, + String email, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setChaseQuickPayAccountPayload(protobuf.ChaseQuickPayAccountPayload.newBuilder() + .setEmail(email) + .setHolderName(holderName)) + .build(); + } + + public static ChaseQuickPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new ChaseQuickPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getChaseQuickPayAccountPayload().getEmail(), + proto.getChaseQuickPayAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.get("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java new file mode 100644 index 0000000000..f437d5d672 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class ClearXchangeAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String emailOrMobileNr = ""; + private String holderName = ""; + + public ClearXchangeAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ClearXchangeAccountPayload(String paymentMethod, + String id, + String emailOrMobileNr, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.emailOrMobileNr = emailOrMobileNr; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setClearXchangeAccountPayload(protobuf.ClearXchangeAccountPayload.newBuilder() + .setEmailOrMobileNr(emailOrMobileNr) + .setHolderName(holderName)) + .build(); + } + + public static ClearXchangeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new ClearXchangeAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getClearXchangeAccountPayload().getEmailOrMobileNr(), + proto.getClearXchangeAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.emailOrMobile") + " " + emailOrMobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + Res.getWithCol("payment.emailOrMobile") + " " + emailOrMobileNr; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + return super.getAgeWitnessInputData(emailOrMobileNr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java new file mode 100644 index 0000000000..41b2a5245d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.payment.payload; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public abstract class CountryBasedPaymentAccountPayload extends PaymentAccountPayload { + protected String countryCode = ""; + + CountryBasedPaymentAccountPayload(String paymentMethodName, String id) { + super(paymentMethodName, id); + } + + protected CountryBasedPaymentAccountPayload(String paymentMethodName, + String id, + String countryCode, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.countryCode = countryCode; + } + + @Override + protected protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { + protobuf.CountryBasedPaymentAccountPayload.Builder builder = protobuf.CountryBasedPaymentAccountPayload.newBuilder() + .setCountryCode(countryCode); + return super.getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(builder); + } + + public abstract String getPaymentDetails(); + + public abstract String getPaymentDetailsForTradePopup(); + + @Override + protected byte[] getAgeWitnessInputData(byte[] data) { + return super.getAgeWitnessInputData(ArrayUtils.addAll(countryCode.getBytes(StandardCharsets.UTF_8), data)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CryptoCurrencyAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CryptoCurrencyAccountPayload.java new file mode 100644 index 0000000000..3557a83f56 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CryptoCurrencyAccountPayload.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.core.payment.payload; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class CryptoCurrencyAccountPayload extends AssetsAccountPayload { + + public CryptoCurrencyAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CryptoCurrencyAccountPayload(String paymentMethod, + String id, + String address, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + address, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setCryptoCurrencyAccountPayload(protobuf.CryptoCurrencyAccountPayload.newBuilder() + .setAddress(address)) + .build(); + } + + public static CryptoCurrencyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new CryptoCurrencyAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getCryptoCurrencyAccountPayload().getAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/F2FAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/F2FAccountPayload.java new file mode 100644 index 0000000000..ec3246dd50 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/F2FAccountPayload.java @@ -0,0 +1,127 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class F2FAccountPayload extends CountryBasedPaymentAccountPayload { + private String contact = ""; + private String city = ""; + private String extraInfo = ""; + + public F2FAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private F2FAccountPayload(String paymentMethodName, + String id, + String countryCode, + String contact, + String city, + String extraInfo, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + this.contact = contact; + this.city = city; + this.extraInfo = extraInfo; + } + + @Override + public Message toProtoMessage() { + protobuf.F2FAccountPayload.Builder builder = protobuf.F2FAccountPayload.newBuilder() + .setContact(contact) + .setCity(city) + .setExtraInfo(extraInfo); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setF2FAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.F2FAccountPayload f2fAccountPayloadPB = countryBasedPaymentAccountPayload.getF2FAccountPayload(); + return new F2FAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + f2fAccountPayloadPB.getContact(), + f2fAccountPayloadPB.getCity(), + f2fAccountPayloadPB.getExtraInfo(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.f2f.contact") + " " + contact + ", " + + Res.getWithCol("payment.f2f.city") + " " + city + + ", " + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; + } + + + @Override + public String getPaymentDetailsForTradePopup() { + // We don't show here more as the makers extra data are the relevant for the trade. City has to be anyway the + // same for maker and taker. + return Res.getWithCol("payment.f2f.contact") + " " + contact; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We use here the city because the address alone seems to be too weak + return super.getAgeWitnessInputData(ArrayUtils.addAll(contact.getBytes(StandardCharsets.UTF_8), + city.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/FasterPaymentsAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/FasterPaymentsAccountPayload.java new file mode 100644 index 0000000000..1c14c47faf --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/FasterPaymentsAccountPayload.java @@ -0,0 +1,125 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import com.google.common.base.Strings; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Slf4j +public final class FasterPaymentsAccountPayload extends PaymentAccountPayload { + @Setter + private String sortCode = ""; + @Setter + private String accountNr = ""; + private String email = "";// not used anymore but need to keep it for backward compatibility, must not be null but empty string, otherwise hash check fails for contract + + public FasterPaymentsAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private FasterPaymentsAccountPayload(String paymentMethod, + String id, + String sortCode, + String accountNr, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.sortCode = sortCode; + this.accountNr = accountNr; + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setFasterPaymentsAccountPayload(protobuf.FasterPaymentsAccountPayload.newBuilder() + .setSortCode(sortCode) + .setAccountNr(accountNr) + .setEmail(email)) + .build(); + } + + public static FasterPaymentsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new FasterPaymentsAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getFasterPaymentsAccountPayload().getSortCode(), + proto.getFasterPaymentsAccountPayload().getAccountNr(), + proto.getFasterPaymentsAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getHolderName() { + return excludeFromJsonDataMap.getOrDefault(HOLDER_NAME, ""); + } + + public void setHolderName(String holderName) { + excludeFromJsonDataMap.compute(HOLDER_NAME, (k, v) -> Strings.emptyToNull(holderName)); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return (getHolderName().isEmpty() ? "" : Res.getWithCol("payment.account.owner") + " " + getHolderName() + "\n") + + "UK Sort code: " + sortCode + "\n" + + Res.getWithCol("payment.accountNr") + " " + accountNr; + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(ArrayUtils.addAll(sortCode.getBytes(StandardCharsets.UTF_8), + accountNr.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/HalCashAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/HalCashAccountPayload.java new file mode 100644 index 0000000000..c991f0e8a7 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/HalCashAccountPayload.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class HalCashAccountPayload extends PaymentAccountPayload { + private String mobileNr = ""; + + public HalCashAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private HalCashAccountPayload(String paymentMethod, String id, + String mobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.mobileNr = mobileNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setHalCashAccountPayload(protobuf.HalCashAccountPayload.newBuilder() + .setMobileNr(mobileNr)) + .build(); + } + + public static HalCashAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new HalCashAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getHalCashAccountPayload().getMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/InstantCryptoCurrencyPayload.java b/core/src/main/java/bisq/core/payment/payload/InstantCryptoCurrencyPayload.java new file mode 100644 index 0000000000..5c7cb03fe5 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/InstantCryptoCurrencyPayload.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.core.payment.payload; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class InstantCryptoCurrencyPayload extends AssetsAccountPayload { + + public InstantCryptoCurrencyPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private InstantCryptoCurrencyPayload(String paymentMethod, + String id, + String address, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + address, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setInstantCryptoCurrencyAccountPayload(protobuf.InstantCryptoCurrencyAccountPayload.newBuilder() + .setAddress(address)) + .build(); + } + + public static InstantCryptoCurrencyPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new InstantCryptoCurrencyPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getInstantCryptoCurrencyAccountPayload().getAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java new file mode 100644 index 0000000000..88b2042af8 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java @@ -0,0 +1,128 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String email = ""; + private String holderName = ""; + private String question = ""; + private String answer = ""; + + public InteracETransferAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private InteracETransferAccountPayload(String paymentMethod, + String id, + String email, + String holderName, + String question, + String answer, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.email = email; + this.holderName = holderName; + this.question = question; + this.answer = answer; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder() + .setEmail(email) + .setHolderName(holderName) + .setQuestion(question) + .setAnswer(answer)) + .build(); + } + + public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new InteracETransferAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getInteracETransferAccountPayload().getEmail(), + proto.getInteracETransferAccountPayload().getHolderName(), + proto.getInteracETransferAccountPayload().getQuestion(), + proto.getInteracETransferAccountPayload().getAnswer(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.get("payment.email") + " " + email + ", " + Res.getWithCol("payment.secret") + " " + + question + ", " + Res.getWithCol("payment.answer") + " " + answer; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + Res.getWithCol("payment.email") + " " + email + "\n" + + Res.getWithCol("payment.secret") + " " + question + "\n" + + Res.getWithCol("payment.answer") + " " + answer; + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(ArrayUtils.addAll(email.getBytes(StandardCharsets.UTF_8), + ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8), + answer.getBytes(StandardCharsets.UTF_8)))); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java new file mode 100644 index 0000000000..807915b2bd --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java @@ -0,0 +1,145 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class JapanBankAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + // bank + private String bankName = ""; + private String bankCode = ""; + // branch + private String bankBranchName = ""; + private String bankBranchCode = ""; + // account + private String bankAccountType = ""; + private String bankAccountName = ""; + private String bankAccountNumber = ""; + + public JapanBankAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private JapanBankAccountPayload(String paymentMethod, + String id, + String bankName, + String bankCode, + String bankBranchName, + String bankBranchCode, + String bankAccountType, + String bankAccountName, + String bankAccountNumber, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.bankName = bankName; + this.bankCode = bankCode; + this.bankBranchName = bankBranchName; + this.bankBranchCode = bankBranchCode; + this.bankAccountType = bankAccountType; + this.bankAccountName = bankAccountName; + this.bankAccountNumber = bankAccountNumber; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setJapanBankAccountPayload( + protobuf.JapanBankAccountPayload.newBuilder() + .setBankName(bankName) + .setBankCode(bankCode) + .setBankBranchName(bankBranchName) + .setBankBranchCode(bankBranchCode) + .setBankAccountType(bankAccountType) + .setBankAccountName(bankAccountName) + .setBankAccountNumber(bankAccountNumber) + ).build(); + } + + public static JapanBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.JapanBankAccountPayload japanBankAccountPayload = proto.getJapanBankAccountPayload(); + return new JapanBankAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + japanBankAccountPayload.getBankName(), + japanBankAccountPayload.getBankCode(), + japanBankAccountPayload.getBankBranchName(), + japanBankAccountPayload.getBankBranchCode(), + japanBankAccountPayload.getBankAccountType(), + japanBankAccountPayload.getBankAccountName(), + japanBankAccountPayload.getBankAccountNumber(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.get("payment.japan.bank") + ": " + bankName + "(" + bankCode + ")\n" + + Res.get("payment.japan.branch") + ": " + bankBranchName + "(" + bankBranchCode + ")\n" + + Res.get("payment.japan.account") + ": " + bankAccountType + " " + bankAccountNumber + "\n" + + Res.get("payment.japan.recipient") + ": " + bankAccountName; + } + + + @Override + public byte[] getAgeWitnessInputData() { + String all = this.bankName + this.bankBranchName + this.bankAccountType + this.bankAccountNumber + this.bankAccountName; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getHolderName() { + return bankAccountName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/MoneyBeamAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/MoneyBeamAccountPayload.java new file mode 100644 index 0000000000..ffb6ad7aa7 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/MoneyBeamAccountPayload.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class MoneyBeamAccountPayload extends PaymentAccountPayload { + private String accountId = ""; + + public MoneyBeamAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MoneyBeamAccountPayload(String paymentMethod, + String id, + String accountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountId = accountId; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setMoneyBeamAccountPayload(protobuf.MoneyBeamAccountPayload.newBuilder() + .setAccountId(accountId)) + .build(); + } + + public static MoneyBeamAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new MoneyBeamAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getMoneyBeamAccountPayload().getAccountId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java new file mode 100644 index 0000000000..03734c68f6 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java @@ -0,0 +1,129 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.BankUtil; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public class MoneyGramAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String holderName; + private String countryCode = ""; + private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. + private String email; + + public MoneyGramAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MoneyGramAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String state, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.holderName = holderName; + this.countryCode = countryCode; + this.state = state; + this.email = email; + } + + @Override + public Message toProtoMessage() { + protobuf.MoneyGramAccountPayload.Builder builder = + protobuf.MoneyGramAccountPayload.newBuilder() + .setHolderName(holderName) + .setCountryCode(countryCode) + .setState(state) + .setEmail(email); + + return getPaymentAccountPayloadBuilder() + .setMoneyGramAccountPayload(builder) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.MoneyGramAccountPayload moneyGramAccountPayload = proto.getMoneyGramAccountPayload(); + return new MoneyGramAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + moneyGramAccountPayload.getCountryCode(), + moneyGramAccountPayload.getHolderName(), + moneyGramAccountPayload.getState(), + moneyGramAccountPayload.getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + String state = BankUtil.isStateRequired(countryCode) ? (Res.getWithCol("payment.account.state") + + " " + this.state + "\n") : ""; + return Res.getWithCol("payment.account.fullName") + " " + holderName + "\n" + + state + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode) + "\n" + + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public byte[] getAgeWitnessInputData() { + String all = this.countryCode + + this.state + + this.holderName + + this.email; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/NationalBankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/NationalBankAccountPayload.java new file mode 100644 index 0000000000..194f4a2aab --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/NationalBankAccountPayload.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Slf4j +public final class NationalBankAccountPayload extends BankAccountPayload { + + public NationalBankAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NationalBankAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String bankName, + String branchId, + String accountNr, + String accountType, + String holderTaxId, + String bankId, + String nationalAccountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + holderName, + bankName, + branchId, + accountNr, + accountType, + holderTaxId, + bankId, + nationalAccountId, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getBankAccountPayloadBuilder() + .setNationalBankAccountPayload(protobuf.NationalBankAccountPayload.newBuilder()); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(bankAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static NationalBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); + return new NationalBankAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + bankAccountPayloadPB.getHolderName(), + bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), + bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), + bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), + bankAccountPayloadPB.getAccountType().isEmpty() ? null : bankAccountPayloadPB.getAccountType(), + bankAccountPayloadPB.getHolderTaxId().isEmpty() ? null : bankAccountPayloadPB.getHolderTaxId(), + bankAccountPayloadPB.getBankId().isEmpty() ? null : bankAccountPayloadPB.getBankId(), + bankAccountPayloadPB.getNationalAccountId().isEmpty() ? null : bankAccountPayloadPB.getNationalAccountId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/OKPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/OKPayAccountPayload.java new file mode 100644 index 0000000000..b13e83d172 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/OKPayAccountPayload.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +// Cannot be deleted as it would break old trade history entries +@Deprecated +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class OKPayAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public OKPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private OKPayAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setOKPayAccountPayload(protobuf.OKPayAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static OKPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new OKPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getOKPayAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java new file mode 100644 index 0000000000..25efe937f4 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.core.payment.payload; + +public interface PayloadWithHolderName { + String getHolderName(); +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java new file mode 100644 index 0000000000..7fa67beb03 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java @@ -0,0 +1,142 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.common.consensus.UsedForTradeContractJson; +import bisq.common.crypto.CryptoUtils; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +// That class is used in the contract for creating the contract json. Any change will break the contract. +// If a field gets added it need to be be annotated with @JsonExclude (excluded from contract). +// We should add an extraDataMap as in StoragePayload objects + +@Getter +@EqualsAndHashCode +@ToString +@Slf4j +public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTradeContractJson { + + // Keys for excludeFromJsonDataMap + public static final String SALT = "salt"; + public static final String HOLDER_NAME = "holderName"; + + protected final String paymentMethodId; + protected final String id; + + // Is just kept for not breaking backward compatibility. Set to -1 to indicate it is no used anymore. + protected final long maxTradePeriod; + + // In v0.6 we removed maxTradePeriod but we need to keep it in the PB file for backward compatibility + // protected final long maxTradePeriod; + + // Used for new data (e.g. salt introduced in v0.6) which would break backward compatibility as + // PaymentAccountPayload is used for the json contract and a trade with a user who has an older version would + // fail the contract verification. + @JsonExclude + protected final Map excludeFromJsonDataMap; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + PaymentAccountPayload(String paymentMethodId, String id) { + this(paymentMethodId, + id, + -1, + new HashMap<>()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected PaymentAccountPayload(String paymentMethodId, + String id, + long maxTradePeriod, + Map excludeFromJsonDataMapParam) { + this.paymentMethodId = paymentMethodId; + this.id = id; + this.maxTradePeriod = maxTradePeriod; + this.excludeFromJsonDataMap = excludeFromJsonDataMapParam; + + // If not set (old versions) we set by default a random 256 bit salt. + // User can set salt as well by hex string. + // Persisted value will overwrite that + if (!this.excludeFromJsonDataMap.containsKey(SALT)) + this.excludeFromJsonDataMap.put(SALT, Utilities.encodeToHex(CryptoUtils.getRandomBytes(32))); + } + + protected protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { + final protobuf.PaymentAccountPayload.Builder builder = protobuf.PaymentAccountPayload.newBuilder() + .setPaymentMethodId(paymentMethodId) + .setMaxTradePeriod(maxTradePeriod) + .setId(id); + + builder.putAllExcludeFromJsonData(excludeFromJsonDataMap); + + return builder; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public abstract String getPaymentDetails(); + + public abstract String getPaymentDetailsForTradePopup(); + + public byte[] getSalt() { + checkArgument(excludeFromJsonDataMap.containsKey(SALT), "Salt must have been set in excludeFromJsonDataMap."); + return Utilities.decodeFromHex(excludeFromJsonDataMap.get(SALT)); + } + + public void setSalt(byte[] salt) { + excludeFromJsonDataMap.put(SALT, Utilities.encodeToHex(salt)); + } + + // Identifying data of payment account (e.g. IBAN). + // This is critical code for verifying age of payment account. + // Any change would break validation of historical data! + public abstract byte[] getAgeWitnessInputData(); + + protected byte[] getAgeWitnessInputData(byte[] data) { + return ArrayUtils.addAll(paymentMethodId.getBytes(StandardCharsets.UTF_8), data); + } + + public String getOwnerId() { + return null; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java new file mode 100644 index 0000000000..5498b7da17 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java @@ -0,0 +1,384 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.TradeLimits; + +import bisq.common.proto.persistable.PersistablePayload; + +import org.bitcoinj.core.Coin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static com.google.common.base.Preconditions.checkNotNull; + +@EqualsAndHashCode(exclude = {"maxTradePeriod", "maxTradeLimit"}) +@ToString +@Slf4j +public final class PaymentMethod implements PersistablePayload, Comparable { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + // time in blocks (average 10 min for one block confirmation + private static final long DAY = TimeUnit.HOURS.toMillis(24); + + // Default trade limits. + // We initialize very early before reading persisted data. We will apply later the limit from + // the DAO param (Param.MAX_TRADE_LIMIT) but that can be only done after the dao is initialized. + // The default values will be used for deriving the + // risk factor so the relation between the risk categories stays the same as with the default values. + // We must not change those values as it could lead to invalid offers if amount becomes lower then new trade limit. + // Increasing might be ok, but needs more thought as well... + private static final Coin DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = Coin.parseCoin("1"); + private static final Coin DEFAULT_TRADE_LIMIT_LOW_RISK = Coin.parseCoin("0.5"); + private static final Coin DEFAULT_TRADE_LIMIT_MID_RISK = Coin.parseCoin("0.25"); + private static final Coin DEFAULT_TRADE_LIMIT_HIGH_RISK = Coin.parseCoin("0.125"); + + public static final String UPHOLD_ID = "UPHOLD"; + public static final String MONEY_BEAM_ID = "MONEY_BEAM"; + public static final String POPMONEY_ID = "POPMONEY"; + public static final String REVOLUT_ID = "REVOLUT"; + public static final String PERFECT_MONEY_ID = "PERFECT_MONEY"; + public static final String SEPA_ID = "SEPA"; + public static final String SEPA_INSTANT_ID = "SEPA_INSTANT"; + public static final String FASTER_PAYMENTS_ID = "FASTER_PAYMENTS"; + public static final String NATIONAL_BANK_ID = "NATIONAL_BANK"; + public static final String JAPAN_BANK_ID = "JAPAN_BANK"; + public static final String AUSTRALIA_PAYID_ID = "AUSTRALIA_PAYID"; + public static final String SAME_BANK_ID = "SAME_BANK"; + public static final String SPECIFIC_BANKS_ID = "SPECIFIC_BANKS"; + public static final String SWISH_ID = "SWISH"; + public static final String ALI_PAY_ID = "ALI_PAY"; + public static final String WECHAT_PAY_ID = "WECHAT_PAY"; + public static final String CLEAR_X_CHANGE_ID = "CLEAR_X_CHANGE"; + public static final String CHASE_QUICK_PAY_ID = "CHASE_QUICK_PAY"; + public static final String INTERAC_E_TRANSFER_ID = "INTERAC_E_TRANSFER"; + public static final String US_POSTAL_MONEY_ORDER_ID = "US_POSTAL_MONEY_ORDER"; + public static final String CASH_DEPOSIT_ID = "CASH_DEPOSIT"; + public static final String MONEY_GRAM_ID = "MONEY_GRAM"; + public static final String WESTERN_UNION_ID = "WESTERN_UNION"; + public static final String HAL_CASH_ID = "HAL_CASH"; + public static final String F2F_ID = "F2F"; + public static final String BLOCK_CHAINS_ID = "BLOCK_CHAINS"; + public static final String PROMPT_PAY_ID = "PROMPT_PAY"; + public static final String ADVANCED_CASH_ID = "ADVANCED_CASH"; + public static final String TRANSFERWISE_ID = "TRANSFERWISE"; + public static final String AMAZON_GIFT_CARD_ID = "AMAZON_GIFT_CARD"; + public static final String BLOCK_CHAINS_INSTANT_ID = "BLOCK_CHAINS_INSTANT"; + public static final String CASH_BY_MAIL_ID = "CASH_BY_MAIL"; + + // Cannot be deleted as it would break old trade history entries + @Deprecated + public static final String OK_PAY_ID = "OK_PAY"; + @Deprecated + public static final String CASH_APP_ID = "CASH_APP"; // Removed due too high chargeback risk + @Deprecated + public static final String VENMO_ID = "VENMO"; // Removed due too high chargeback risk + + public static PaymentMethod UPHOLD; + public static PaymentMethod MONEY_BEAM; + public static PaymentMethod POPMONEY; + public static PaymentMethod REVOLUT; + public static PaymentMethod PERFECT_MONEY; + public static PaymentMethod SEPA; + public static PaymentMethod SEPA_INSTANT; + public static PaymentMethod FASTER_PAYMENTS; + public static PaymentMethod NATIONAL_BANK; + public static PaymentMethod JAPAN_BANK; + public static PaymentMethod AUSTRALIA_PAYID; + public static PaymentMethod SAME_BANK; + public static PaymentMethod SPECIFIC_BANKS; + public static PaymentMethod SWISH; + public static PaymentMethod ALI_PAY; + public static PaymentMethod WECHAT_PAY; + public static PaymentMethod CLEAR_X_CHANGE; + public static PaymentMethod CHASE_QUICK_PAY; + public static PaymentMethod INTERAC_E_TRANSFER; + public static PaymentMethod US_POSTAL_MONEY_ORDER; + public static PaymentMethod CASH_DEPOSIT; + public static PaymentMethod MONEY_GRAM; + public static PaymentMethod WESTERN_UNION; + public static PaymentMethod F2F; + public static PaymentMethod HAL_CASH; + public static PaymentMethod BLOCK_CHAINS; + public static PaymentMethod PROMPT_PAY; + public static PaymentMethod ADVANCED_CASH; + public static PaymentMethod TRANSFERWISE; + public static PaymentMethod AMAZON_GIFT_CARD; + public static PaymentMethod BLOCK_CHAINS_INSTANT; + public static PaymentMethod CASH_BY_MAIL; + + // Cannot be deleted as it would break old trade history entries + @Deprecated + public static PaymentMethod OK_PAY = getDummyPaymentMethod(OK_PAY_ID); + @Deprecated + public static PaymentMethod CASH_APP = getDummyPaymentMethod(CASH_APP_ID); // Removed due too high chargeback risk + @Deprecated + public static PaymentMethod VENMO = getDummyPaymentMethod(VENMO_ID); // Removed due too high chargeback risk + + // The limit and duration assignment must not be changed as that could break old offers (if amount would be higher + // than new trade limit) and violate the maker expectation when he created the offer (duration). + @Getter + private final static List paymentMethods = new ArrayList<>(Arrays.asList( + // EUR + SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + MONEY_BEAM = new PaymentMethod(MONEY_BEAM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // UK + FASTER_PAYMENTS = new PaymentMethod(FASTER_PAYMENTS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // Sweden + SWISH = new PaymentMethod(SWISH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + + // US + CLEAR_X_CHANGE = new PaymentMethod(CLEAR_X_CHANGE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + POPMONEY = new PaymentMethod(POPMONEY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + CHASE_QUICK_PAY = new PaymentMethod(CHASE_QUICK_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // Canada + INTERAC_E_TRANSFER = new PaymentMethod(INTERAC_E_TRANSFER_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // Global + CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + CASH_BY_MAIL = new PaymentMethod(CASH_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK), + WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK), + NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + SAME_BANK = new PaymentMethod(SAME_BANK_ID, 2 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + HAL_CASH = new PaymentMethod(HAL_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + F2F = new PaymentMethod(F2F_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + AMAZON_GIFT_CARD = new PaymentMethod(AMAZON_GIFT_CARD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // Trans national + UPHOLD = new PaymentMethod(UPHOLD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + REVOLUT = new PaymentMethod(REVOLUT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + ADVANCED_CASH = new PaymentMethod(ADVANCED_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK), + TRANSFERWISE = new PaymentMethod(TRANSFERWISE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), + + // Japan + JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + + // Australia + AUSTRALIA_PAYID = new PaymentMethod(AUSTRALIA_PAYID_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + + // China + ALI_PAY = new PaymentMethod(ALI_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + WECHAT_PAY = new PaymentMethod(WECHAT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + + // Thailand + PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK), + + // Altcoins + BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK), + // Altcoins with 1 hour trade period + BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK) + )); + + static { + paymentMethods.sort((o1, o2) -> { + String id1 = o1.getId(); + if (id1.equals(CLEAR_X_CHANGE_ID)) + id1 = "ZELLE"; + String id2 = o2.getId(); + if (id2.equals(CLEAR_X_CHANGE_ID)) + id2 = "ZELLE"; + return id1.compareTo(id2); + }); + } + + public static PaymentMethod getDummyPaymentMethod(String id) { + return new PaymentMethod(id, 0, Coin.ZERO); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + @Getter + private final String id; + + // Must not change as old offers would get a new period then and that would violate the makers "contract" or + // expectation when he created the offer. + @Getter + private final long maxTradePeriod; + + // With v0.9.4 we changed context of that field. Before it was the hard coded trade limit. Now it is the default + // limit which will be used just in time to adjust the real trade limit based on the DAO param value and risk factor. + // The risk factor is derived from the maxTradeLimit. + // As that field is used in protobuffer definitions we cannot change it to reflect better the new context. We prefer + // to keep the convention that PB fields has the same name as the Java class field (as we could rename it in + // Java without breaking PB). + private final long maxTradeLimit; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @param id against charge back risk. If Bank do the charge back quickly the Arbitrator and the seller can push another + * double spend tx to invalidate the time locked payout tx. For the moment we set all to 0 but will have it in + * place when needed. + * @param maxTradePeriod The min. period a trader need to wait until he gets displayed the contact form for opening a dispute. + * @param maxTradeLimit The max. allowed trade amount in Bitcoin for that payment method (depending on charge back risk) + */ + private PaymentMethod(String id, long maxTradePeriod, Coin maxTradeLimit) { + this.id = id; + this.maxTradePeriod = maxTradePeriod; + this.maxTradeLimit = maxTradeLimit.value; + } + + // Used for dummy entries in payment methods list (SHOW_ALL) + private PaymentMethod(String id) { + this(id, 0, Coin.ZERO); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.PaymentMethod toProtoMessage() { + return protobuf.PaymentMethod.newBuilder() + .setId(id) + .setMaxTradePeriod(maxTradePeriod) + .setMaxTradeLimit(maxTradeLimit) + .build(); + } + + public static PaymentMethod fromProto(protobuf.PaymentMethod proto) { + return new PaymentMethod(proto.getId(), + proto.getMaxTradePeriod(), + Coin.valueOf(proto.getMaxTradeLimit())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public static PaymentMethod getPaymentMethodById(String id) { + return paymentMethods.stream() + .filter(e -> e.getId().equals(id)) + .findFirst() + .orElseGet(() -> new PaymentMethod(Res.get("shared.na"))); + } + + public Coin getMaxTradeLimitAsCoin(String currencyCode) { + // Hack for SF as the smallest unit is 1 SF ;-( and price is about 3 BTC! + if (currencyCode.equals("SF")) + return Coin.parseCoin("4"); + + // We use the class field maxTradeLimit only for mapping the risk factor. + long riskFactor; + if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.value) + riskFactor = 1; + else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.value) + riskFactor = 2; + else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.value) + riskFactor = 4; + else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.value) + riskFactor = 8; + else { + riskFactor = 8; + log.error("maxTradeLimit is not matching one of our default values. maxTradeLimit=" + Coin.valueOf(maxTradeLimit).toFriendlyString()); + } + + TradeLimits tradeLimits = TradeLimits.getINSTANCE(); + checkNotNull(tradeLimits, "tradeLimits must not be null"); + long maxTradeLimit = tradeLimits.getMaxTradeLimit().value; + return Coin.valueOf(tradeLimits.getRoundedRiskBasedTradeLimit(maxTradeLimit, riskFactor)); + } + + public String getShortName() { + // in cases where translation is not found, Res.get() simply returns the key string + // so no need for special error-handling code. + return Res.get(this.id + "_SHORT"); + } + + @Override + public int compareTo(@NotNull PaymentMethod other) { + return Res.get(id).compareTo(Res.get(other.id)); + } + + public String getDisplayString() { + return Res.get(id); + } + + public boolean isAsset() { + return this.equals(BLOCK_CHAINS_INSTANT) || this.equals(BLOCK_CHAINS); + } + + public static boolean hasChargebackRisk(PaymentMethod paymentMethod, List tradeCurrencies) { + return tradeCurrencies.stream() + .anyMatch(tradeCurrency -> hasChargebackRisk(paymentMethod, tradeCurrency.getCode())); + } + + public static boolean hasChargebackRisk(PaymentMethod paymentMethod) { + return hasChargebackRisk(paymentMethod, CurrencyUtil.getMatureMarketCurrencies()); + } + + public static boolean hasChargebackRisk(PaymentMethod paymentMethod, String currencyCode) { + if (paymentMethod == null) + return false; + + String id = paymentMethod.getId(); + return hasChargebackRisk(id, currencyCode); + } + + public static boolean hasChargebackRisk(String id, String currencyCode) { + if (CurrencyUtil.getMatureMarketCurrencies().stream() + .noneMatch(c -> c.getCode().equals(currencyCode))) + return false; + + return id.equals(PaymentMethod.SEPA_ID) || + id.equals(PaymentMethod.SEPA_INSTANT_ID) || + id.equals(PaymentMethod.INTERAC_E_TRANSFER_ID) || + id.equals(PaymentMethod.CLEAR_X_CHANGE_ID) || + id.equals(PaymentMethod.REVOLUT_ID) || + id.equals(PaymentMethod.NATIONAL_BANK_ID) || + id.equals(PaymentMethod.SAME_BANK_ID) || + id.equals(PaymentMethod.SPECIFIC_BANKS_ID) || + id.equals(PaymentMethod.CHASE_QUICK_PAY_ID) || + id.equals(PaymentMethod.POPMONEY_ID) || + id.equals(PaymentMethod.MONEY_BEAM_ID) || + id.equals(PaymentMethod.UPHOLD_ID); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PerfectMoneyAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PerfectMoneyAccountPayload.java new file mode 100644 index 0000000000..9ffbeb4837 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PerfectMoneyAccountPayload.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PerfectMoneyAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public PerfectMoneyAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PerfectMoneyAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPerfectMoneyAccountPayload(protobuf.PerfectMoneyAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static PerfectMoneyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PerfectMoneyAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPerfectMoneyAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java new file mode 100644 index 0000000000..3a451bda61 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PopmoneyAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String accountId = ""; + private String holderName = ""; + + public PopmoneyAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PopmoneyAccountPayload(String paymentMethod, + String id, + String accountId, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountId = accountId; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPopmoneyAccountPayload(protobuf.PopmoneyAccountPayload.newBuilder() + .setAccountId(accountId) + .setHolderName(holderName)) + .build(); + } + + public static PopmoneyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PopmoneyAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPopmoneyAccountPayload().getAccountId(), + proto.getPopmoneyAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.popmoney.accountId") + " " + accountId; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PromptPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PromptPayAccountPayload.java new file mode 100644 index 0000000000..bf3c7c976d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PromptPayAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PromptPayAccountPayload extends PaymentAccountPayload { + private String promptPayId = ""; + + public PromptPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PromptPayAccountPayload(String paymentMethod, String id, + String promptPayId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.promptPayId = promptPayId; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPromptPayAccountPayload(protobuf.PromptPayAccountPayload.newBuilder() + .setPromptPayId(promptPayId)) + .build(); + } + + public static PromptPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PromptPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPromptPayAccountPayload().getPromptPayId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.getWithCol("payment.promptPay.promptPayId") + " " + promptPayId; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(promptPayId.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java new file mode 100644 index 0000000000..19050325cf --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -0,0 +1,181 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import bisq.common.util.JsonExclude; +import bisq.common.util.Tuple2; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Slf4j +public final class RevolutAccountPayload extends PaymentAccountPayload { + // Only used as internal Id to not break existing account witness objects + // We still show it in case it is different to the userName for additional security + @Getter + private String accountId = ""; + + // Was added in 1.3.8 + // To not break signed accounts we keep accountId as internal id used for signing. + // Old accounts get a popup to add the new required field userName but accountId is + // left unchanged. Newly created accounts fill accountId with the value of userName. + // In the UI we only use userName. + + // For backward compatibility we need to exclude the new field for the contract json. + // We can remove that after a while when risk that users with pre 1.3.8 version trade with updated + // users is very low. + @JsonExclude + @Getter + private String userName = ""; + + public RevolutAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RevolutAccountPayload(String paymentMethod, + String id, + String accountId, + @Nullable String userName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountId = accountId; + this.userName = userName; + } + + @Override + public Message toProtoMessage() { + protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() + .setAccountId(accountId) + .setUserName(userName); + return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); + } + + + public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload(); + return new RevolutAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + revolutAccountPayload.getAccountId(), + revolutAccountPayload.getUserName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + Tuple2 tuple = getLabelValueTuple(); + return Res.get(paymentMethodId) + " - " + tuple.first + ": " + tuple.second; + } + + private Tuple2 getLabelValueTuple() { + String label; + String value; + checkArgument(!userName.isEmpty() || hasOldAccountId(), + "Either username must be set or we have an old account with accountId"); + if (!userName.isEmpty()) { + label = Res.get("payment.account.userName"); + value = userName; + + if (hasOldAccountId()) { + label += "/" + Res.get("payment.account.phoneNr"); + value += "/" + accountId; + } + } else { + label = Res.get("payment.account.phoneNr"); + value = accountId; + } + return new Tuple2<>(label, value); + } + + public Tuple2 getRecipientsAccountData() { + Tuple2 tuple = getLabelValueTuple(); + String label = Res.get("portfolio.pending.step2_buyer.recipientsAccountData", tuple.first); + return new Tuple2<>(label, tuple.second); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + // getAgeWitnessInputData is called at new account creation when accountId is empty string. + if (hasOldAccountId()) { + // If the accountId was already in place (updated user who had used accountId for account age) we keep the + // old accountId to not invalidate the existing account age witness. + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + + } else { + // If a new account was registered from version 1.3.8 or later we use the userName. + return super.getAgeWitnessInputData(userName.getBytes(StandardCharsets.UTF_8)); + } + } + + public boolean userNameNotSet() { + return userName.isEmpty(); + } + + public boolean hasOldAccountId() { + return !accountId.equals(userName); + } + + public void setUserName(String userName) { + this.userName = userName; + } + + // In case it is a new account we need to fill the accountId field to support not-updated traders who are not + // aware of the new userName field + public void maybeApplyUserNameToAccountId() { + if (accountId.isEmpty()) { + accountId = userName; + } + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SameBankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SameBankAccountPayload.java new file mode 100644 index 0000000000..d10d384219 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SameBankAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Slf4j +public final class SameBankAccountPayload extends BankAccountPayload { + + public SameBankAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SameBankAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String bankName, + String branchId, + String accountNr, + String accountType, + String holderTaxId, + String bankId, + String nationalAccountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + holderName, + bankName, + branchId, + accountNr, + accountType, + holderTaxId, + bankId, + nationalAccountId, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getBankAccountPayloadBuilder() + .setSameBankAccontPayload(protobuf.SameBankAccountPayload.newBuilder()); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(bankAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static SameBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BankAccountPayload bankAccountPayload = countryBasedPaymentAccountPayload.getBankAccountPayload(); + return new SameBankAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + bankAccountPayload.getHolderName(), + bankAccountPayload.getBankName().isEmpty() ? null : bankAccountPayload.getBankName(), + bankAccountPayload.getBranchId().isEmpty() ? null : bankAccountPayload.getBranchId(), + bankAccountPayload.getAccountNr().isEmpty() ? null : bankAccountPayload.getAccountNr(), + bankAccountPayload.getAccountType().isEmpty() ? null : bankAccountPayload.getAccountType(), + bankAccountPayload.getHolderTaxId().isEmpty() ? null : bankAccountPayload.getHolderTaxId(), + bankAccountPayload.getBankId().isEmpty() ? null : bankAccountPayload.getBankId(), + bankAccountPayload.getNationalAccountId().isEmpty() ? null : bankAccountPayload.getNationalAccountId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java new file mode 100644 index 0000000000..59425e1bcf --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java @@ -0,0 +1,166 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Slf4j +public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + @Setter + private String holderName = ""; + @Setter + private String iban = ""; + @Setter + private String bic = ""; + private String email = ""; // not used anymore but need to keep it for backward compatibility, must not be null but empty string, otherwise hash check fails for contract + + // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match + private final List acceptedCountryCodes; + + public SepaAccountPayload(String paymentMethod, String id, List acceptedCountries) { + super(paymentMethod, id); + acceptedCountryCodes = acceptedCountries.stream() + .map(e -> e.code) + .sorted() + .distinct() + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SepaAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String iban, + String bic, + String email, + List acceptedCountryCodes, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.iban = iban; + this.bic = bic; + this.email = email; + this.acceptedCountryCodes = acceptedCountryCodes; + } + + @Override + public Message toProtoMessage() { + protobuf.SepaAccountPayload.Builder builder = + protobuf.SepaAccountPayload.newBuilder() + .setHolderName(holderName) + .setIban(iban) + .setBic(bic) + .setEmail(email) + .addAllAcceptedCountryCodes(acceptedCountryCodes); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setSepaAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.SepaAccountPayload sepaAccountPayloadPB = countryBasedPaymentAccountPayload.getSepaAccountPayload(); + return new SepaAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + sepaAccountPayloadPB.getHolderName(), + sepaAccountPayloadPB.getIban(), + sepaAccountPayloadPB.getBic(), + sepaAccountPayloadPB.getEmail(), + new ArrayList<>(sepaAccountPayloadPB.getAcceptedCountryCodesList()), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addAcceptedCountry(String countryCode) { + if (!acceptedCountryCodes.contains(countryCode)) + acceptedCountryCodes.add(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + acceptedCountryCodes.remove(countryCode); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", IBAN: " + + iban + ", BIC: " + bic + ", " + Res.getWithCol("payment.bank.country") + " " + getCountryCode(); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + "IBAN: " + iban + "\n" + + "BIC: " + bic + "\n" + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java new file mode 100644 index 0000000000..54cffcd78d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java @@ -0,0 +1,161 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Slf4j +public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + @Setter + private String holderName = ""; + @Setter + private String iban = ""; + @Setter + private String bic = ""; + + // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match + private final List acceptedCountryCodes; + + public SepaInstantAccountPayload(String paymentMethod, String id, List acceptedCountries) { + super(paymentMethod, id); + acceptedCountryCodes = acceptedCountries.stream() + .map(e -> e.code) + .sorted() + .distinct() + .collect(Collectors.toList()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SepaInstantAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String iban, + String bic, + List acceptedCountryCodes, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.iban = iban; + this.bic = bic; + this.acceptedCountryCodes = acceptedCountryCodes; + } + + @Override + public Message toProtoMessage() { + protobuf.SepaInstantAccountPayload.Builder builder = + protobuf.SepaInstantAccountPayload.newBuilder() + .setHolderName(holderName) + .setIban(iban) + .setBic(bic) + .addAllAcceptedCountryCodes(acceptedCountryCodes); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setSepaInstantAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.SepaInstantAccountPayload sepaInstantAccountPayloadPB = countryBasedPaymentAccountPayload.getSepaInstantAccountPayload(); + return new SepaInstantAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + sepaInstantAccountPayloadPB.getHolderName(), + sepaInstantAccountPayloadPB.getIban(), + sepaInstantAccountPayloadPB.getBic(), + new ArrayList<>(sepaInstantAccountPayloadPB.getAcceptedCountryCodesList()), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addAcceptedCountry(String countryCode) { + if (!acceptedCountryCodes.contains(countryCode)) + acceptedCountryCodes.add(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + acceptedCountryCodes.remove(countryCode); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", IBAN: " + + iban + ", BIC: " + bic + ", " + Res.getWithCol("payment.bank.country") + " " + getCountryCode(); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + "IBAN: " + iban + "\n" + + "BIC: " + bic + "\n" + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SpecificBanksAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SpecificBanksAccountPayload.java new file mode 100644 index 0000000000..ac32af5d47 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SpecificBanksAccountPayload.java @@ -0,0 +1,146 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import com.google.common.base.Joiner; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Slf4j +public final class SpecificBanksAccountPayload extends BankAccountPayload { + // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match + private ArrayList acceptedBanks = new ArrayList<>(); + + public SpecificBanksAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SpecificBanksAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String bankName, + String branchId, + String accountNr, + String accountType, + String holderTaxId, + String bankId, + String nationalAccountId, + ArrayList acceptedBanks, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + holderName, + bankName, + branchId, + accountNr, + accountType, + holderTaxId, + bankId, + nationalAccountId, + maxTradePeriod, + excludeFromJsonDataMap); + + this.acceptedBanks = acceptedBanks; + } + + @Override + public Message toProtoMessage() { + final protobuf.SpecificBanksAccountPayload.Builder builder = protobuf.SpecificBanksAccountPayload.newBuilder() + .addAllAcceptedBanks(acceptedBanks); + + protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getBankAccountPayloadBuilder() + .setSpecificBanksAccountPayload(builder); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(bankAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static SpecificBanksAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BankAccountPayload bankAccountPayload = countryBasedPaymentAccountPayload.getBankAccountPayload(); + protobuf.SpecificBanksAccountPayload specificBanksAccountPayload = bankAccountPayload.getSpecificBanksAccountPayload(); + return new SpecificBanksAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + bankAccountPayload.getHolderName(), + bankAccountPayload.getBankName().isEmpty() ? null : bankAccountPayload.getBankName(), + bankAccountPayload.getBranchId().isEmpty() ? null : bankAccountPayload.getBranchId(), + bankAccountPayload.getAccountNr().isEmpty() ? null : bankAccountPayload.getAccountNr(), + bankAccountPayload.getAccountType().isEmpty() ? null : bankAccountPayload.getAccountType(), + bankAccountPayload.getHolderTaxId().isEmpty() ? null : bankAccountPayload.getHolderTaxId(), + bankAccountPayload.getBankId().isEmpty() ? null : bankAccountPayload.getBankId(), + bankAccountPayload.getNationalAccountId().isEmpty() ? null : bankAccountPayload.getNationalAccountId(), + new ArrayList<>(specificBanksAccountPayload.getAcceptedBanksList()), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void clearAcceptedBanks() { + acceptedBanks = new ArrayList<>(); + } + + public void addAcceptedBank(String bankName) { + if (!acceptedBanks.contains(bankName)) + acceptedBanks.add(bankName); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return super.getPaymentDetailsForTradePopup() + "\n" + + Res.getWithCol("payment.accepted.banks") + " " + Joiner.on(", ").join(acceptedBanks); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java new file mode 100644 index 0000000000..f14eafb92e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class SwishAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String mobileNr = ""; + private String holderName = ""; + + public SwishAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private SwishAccountPayload(String paymentMethod, String id, + String mobileNr, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.mobileNr = mobileNr; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setSwishAccountPayload(protobuf.SwishAccountPayload.newBuilder() + .setMobileNr(mobileNr) + .setHolderName(holderName)) + .build(); + } + + public static SwishAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new SwishAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getSwishAccountPayload().getMobileNr(), + proto.getSwishAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + + ", " + Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/TransferwiseAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/TransferwiseAccountPayload.java new file mode 100644 index 0000000000..472f64f28b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/TransferwiseAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class TransferwiseAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public TransferwiseAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TransferwiseAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setTransferwiseAccountPayload(protobuf.TransferwiseAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static TransferwiseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new TransferwiseAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getTransferwiseAccountPayload().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/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java new file mode 100644 index 0000000000..96a8dec520 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { + private String postalAddress = ""; + private String holderName = ""; + + public USPostalMoneyOrderAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private USPostalMoneyOrderAccountPayload(String paymentMethod, String id, + String postalAddress, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.postalAddress = postalAddress; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setUSPostalMoneyOrderAccountPayload(protobuf.USPostalMoneyOrderAccountPayload.newBuilder() + .setPostalAddress(postalAddress) + .setHolderName(holderName)) + .build(); + } + + public static USPostalMoneyOrderAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new USPostalMoneyOrderAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getUSPostalMoneyOrderAccountPayload().getPostalAddress(), + proto.getUSPostalMoneyOrderAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.postal.address") + " " + postalAddress; + } + + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + Res.getWithCol("payment.postal.address") + " " + postalAddress; + } + + @Override + public byte[] getAgeWitnessInputData() { + // We use here the holderName because the address alone seems to be too weak + return super.getAgeWitnessInputData(ArrayUtils.addAll(holderName.getBytes(StandardCharsets.UTF_8), + postalAddress.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java new file mode 100644 index 0000000000..eab4293f80 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class UpholdAccountPayload extends PaymentAccountPayload { + private String accountId = ""; + + public UpholdAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private UpholdAccountPayload(String paymentMethod, + String id, + String accountId, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountId = accountId; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setUpholdAccountPayload(protobuf.UpholdAccountPayload.newBuilder() + .setAccountId(accountId)) + .build(); + } + + public static UpholdAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new UpholdAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getUpholdAccountPayload().getAccountId(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/VenmoAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/VenmoAccountPayload.java new file mode 100644 index 0000000000..230ccdd9a5 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/VenmoAccountPayload.java @@ -0,0 +1,114 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +// Cannot be deleted as it would break old trade history entries +// Removed due too high chargeback risk +@Deprecated +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class VenmoAccountPayload extends PaymentAccountPayload { + private String venmoUserName = ""; + private String holderName = ""; + + public VenmoAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private VenmoAccountPayload(String paymentMethod, + String id, + String venmoUserName, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.venmoUserName = venmoUserName; + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setVenmoAccountPayload(protobuf.VenmoAccountPayload.newBuilder() + .setVenmoUserName(venmoUserName) + .setHolderName(holderName)) + .build(); + } + + public static VenmoAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new VenmoAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getVenmoAccountPayload().getVenmoUserName(), + proto.getVenmoAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.venmo.venmoUserName") + " " + venmoUserName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(venmoUserName.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/WeChatPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/WeChatPayAccountPayload.java new file mode 100644 index 0000000000..b4afe51bd4 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/WeChatPayAccountPayload.java @@ -0,0 +1,97 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@ToString +public final class WeChatPayAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public WeChatPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private WeChatPayAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setWeChatPayAccountPayload(protobuf.WeChatPayAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static WeChatPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new WeChatPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getWeChatPayAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java new file mode 100644 index 0000000000..45f33186cc --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java @@ -0,0 +1,136 @@ +/* + * 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 bisq.core.payment.payload; + +import bisq.core.locale.BankUtil; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + private String holderName; + private String city; + private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. + private String email; + + public WesternUnionAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private WesternUnionAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String city, + String state, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + this.holderName = holderName; + this.city = city; + this.state = state; + this.email = email; + } + + @Override + public Message toProtoMessage() { + protobuf.WesternUnionAccountPayload.Builder builder = + protobuf.WesternUnionAccountPayload.newBuilder() + .setHolderName(holderName) + .setCity(city) + .setState(state) + .setEmail(email); + + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setWesternUnionAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.WesternUnionAccountPayload westernUnionAccountPayload = countryBasedPaymentAccountPayload.getWesternUnionAccountPayload(); + return new WesternUnionAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + westernUnionAccountPayload.getHolderName(), + westernUnionAccountPayload.getCity(), + westernUnionAccountPayload.getState(), + westernUnionAccountPayload.getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + String cityState = BankUtil.isStateRequired(countryCode) + ? (Res.get("payment.account.city") + " / " + Res.getWithCol("payment.account.state") + " " + city + " / " + state + "\n") + : (Res.getWithCol("payment.account.city") + " " + city + "\n"); + return Res.getWithCol("payment.account.fullName") + " " + holderName + "\n" + + cityState + + Res.getWithCol("payment.country") + " " + CountryUtil.getNameByCode(countryCode) + "\n" + + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public byte[] getAgeWitnessInputData() { + String all = this.countryCode + + this.holderName + + this.email; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java b/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java new file mode 100644 index 0000000000..e4646ea4d0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.payment.validation; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.util.validation.InputValidator; + +import bisq.asset.AddressValidationResult; +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; + +import bisq.common.app.DevEnv; +import bisq.common.config.Config; + +import com.google.inject.Inject; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class AltCoinAddressValidator extends InputValidator { + + private final AssetRegistry assetRegistry; + private String currencyCode; + + @Inject + public AltCoinAddressValidator(AssetRegistry assetRegistry) { + this.assetRegistry = assetRegistry; + } + + public void setCurrencyCode(String currencyCode) { + this.currencyCode = currencyCode; + } + + @Override + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid || currencyCode == null) + return validationResult; + + Optional optionalAsset = CurrencyUtil.findAsset(assetRegistry, currencyCode, + Config.baseCurrencyNetwork(), DevEnv.isDaoTradingActivated()); + if (optionalAsset.isPresent()) { + Asset asset = optionalAsset.get(); + AddressValidationResult result = asset.validateAddress(input); + if (!result.isValid()) { + return new ValidationResult(false, Res.get(result.getI18nKey(), asset.getTickerSymbol(), + result.getMessage())); + } + + return new ValidationResult(true); + } else { + return new ValidationResult(false); + } + } +} diff --git a/core/src/main/java/bisq/core/presentation/BalancePresentation.java b/core/src/main/java/bisq/core/presentation/BalancePresentation.java new file mode 100644 index 0000000000..a7a035e1a7 --- /dev/null +++ b/core/src/main/java/bisq/core/presentation/BalancePresentation.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.core.presentation; + +import bisq.core.btc.Balances; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BalancePresentation { + @Getter + private final StringProperty availableBalance = new SimpleStringProperty(); + @Getter + private final StringProperty reservedBalance = new SimpleStringProperty(); + @Getter + private final StringProperty lockedBalance = new SimpleStringProperty(); + + @Inject + public BalancePresentation(Balances balances, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { + balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> { + String value = formatter.formatCoinWithCode(newValue); + // If we get full precision the BTC postfix breaks layout so we omit it + if (value.length() > 11) + value = formatter.formatCoin(newValue); + availableBalance.set(value); + }); + + balances.getReservedBalance().addListener((observable, oldValue, newValue) -> { + reservedBalance.set(formatter.formatCoinWithCode(newValue)); + }); + balances.getLockedBalance().addListener((observable, oldValue, newValue) -> { + lockedBalance.set(formatter.formatCoinWithCode(newValue)); + }); + } +} diff --git a/core/src/main/java/bisq/core/presentation/CorePresentationModule.java b/core/src/main/java/bisq/core/presentation/CorePresentationModule.java new file mode 100644 index 0000000000..94444d625c --- /dev/null +++ b/core/src/main/java/bisq/core/presentation/CorePresentationModule.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.core.presentation; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +public class CorePresentationModule extends AppModule { + + public CorePresentationModule(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(BalancePresentation.class).in(Singleton.class); + bind(TradePresentation.class).in(Singleton.class); + bind(SupportTicketsPresentation.class).in(Singleton.class); + } +} + diff --git a/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java b/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java new file mode 100644 index 0000000000..8e1764f635 --- /dev/null +++ b/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.core.presentation; + +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; + +import javax.inject.Inject; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import lombok.Getter; + +public class SupportTicketsPresentation { + @Getter + private final StringProperty numOpenSupportTickets = new SimpleStringProperty(); + @Getter + private final BooleanProperty showOpenSupportTicketsNotification = new SimpleBooleanProperty(); + + @org.jetbrains.annotations.NotNull + private final ArbitrationManager arbitrationManager; + @org.jetbrains.annotations.NotNull + private final MediationManager mediationManager; + @org.jetbrains.annotations.NotNull + private final RefundManager refundManager; + + @Inject + public SupportTicketsPresentation(ArbitrationManager arbitrationManager, + MediationManager mediationManager, + RefundManager refundManager) { + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + this.refundManager = refundManager; + + arbitrationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); + mediationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); + refundManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); + } + + private void onChange() { + int supportTickets = arbitrationManager.getNumOpenDisputes().get() + + mediationManager.getNumOpenDisputes().get() + + refundManager.getNumOpenDisputes().get(); + + numOpenSupportTickets.set(String.valueOf(supportTickets)); + showOpenSupportTicketsNotification.set(supportTickets > 0); + } +} diff --git a/core/src/main/java/bisq/core/presentation/TradePresentation.java b/core/src/main/java/bisq/core/presentation/TradePresentation.java new file mode 100644 index 0000000000..d8dca362f3 --- /dev/null +++ b/core/src/main/java/bisq/core/presentation/TradePresentation.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.presentation; + +import bisq.core.trade.TradeManager; + +import javax.inject.Inject; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import lombok.Getter; + +public class TradePresentation { + @Getter + private final StringProperty numPendingTrades = new SimpleStringProperty(); + @Getter + private final BooleanProperty showPendingTradesNotification = new SimpleBooleanProperty(); + + @Inject + public TradePresentation(TradeManager tradeManager) { + tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> { + long numPendingTrades = (long) newValue; + if (numPendingTrades > 0) + this.numPendingTrades.set(String.valueOf(numPendingTrades)); + + showPendingTradesNotification.set(numPendingTrades > 0); + }); + } +} diff --git a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java new file mode 100644 index 0000000000..3841a0e051 --- /dev/null +++ b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java @@ -0,0 +1,204 @@ +/* + * 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 bisq.core.proto; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; +import bisq.core.payment.payload.AdvancedCashAccountPayload; +import bisq.core.payment.payload.AliPayAccountPayload; +import bisq.core.payment.payload.AmazonGiftCardAccountPayload; +import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.CashAppAccountPayload; +import bisq.core.payment.payload.CashByMailAccountPayload; +import bisq.core.payment.payload.CashDepositAccountPayload; +import bisq.core.payment.payload.ChaseQuickPayAccountPayload; +import bisq.core.payment.payload.ClearXchangeAccountPayload; +import bisq.core.payment.payload.CryptoCurrencyAccountPayload; +import bisq.core.payment.payload.F2FAccountPayload; +import bisq.core.payment.payload.FasterPaymentsAccountPayload; +import bisq.core.payment.payload.HalCashAccountPayload; +import bisq.core.payment.payload.InstantCryptoCurrencyPayload; +import bisq.core.payment.payload.InteracETransferAccountPayload; +import bisq.core.payment.payload.JapanBankAccountPayload; +import bisq.core.payment.payload.MoneyBeamAccountPayload; +import bisq.core.payment.payload.MoneyGramAccountPayload; +import bisq.core.payment.payload.NationalBankAccountPayload; +import bisq.core.payment.payload.OKPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PerfectMoneyAccountPayload; +import bisq.core.payment.payload.PopmoneyAccountPayload; +import bisq.core.payment.payload.PromptPayAccountPayload; +import bisq.core.payment.payload.RevolutAccountPayload; +import bisq.core.payment.payload.SameBankAccountPayload; +import bisq.core.payment.payload.SepaAccountPayload; +import bisq.core.payment.payload.SepaInstantAccountPayload; +import bisq.core.payment.payload.SpecificBanksAccountPayload; +import bisq.core.payment.payload.SwishAccountPayload; +import bisq.core.payment.payload.TransferwiseAccountPayload; +import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; +import bisq.core.payment.payload.UpholdAccountPayload; +import bisq.core.payment.payload.VenmoAccountPayload; +import bisq.core.payment.payload.WeChatPayAccountPayload; +import bisq.core.payment.payload.WesternUnionAccountPayload; +import bisq.core.trade.statistics.TradeStatistics2; +import bisq.core.trade.statistics.TradeStatistics3; + +import bisq.common.proto.ProtoResolver; +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.common.proto.persistable.PersistablePayload; + +import java.time.Clock; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CoreProtoResolver implements ProtoResolver { + @Getter + protected Clock clock; + + @Override + public PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + if (proto != null) { + final protobuf.PaymentAccountPayload.MessageCase messageCase = proto.getMessageCase(); + switch (messageCase) { + case ALI_PAY_ACCOUNT_PAYLOAD: + return AliPayAccountPayload.fromProto(proto); + case WE_CHAT_PAY_ACCOUNT_PAYLOAD: + return WeChatPayAccountPayload.fromProto(proto); + case CHASE_QUICK_PAY_ACCOUNT_PAYLOAD: + return ChaseQuickPayAccountPayload.fromProto(proto); + case CLEAR_XCHANGE_ACCOUNT_PAYLOAD: + return ClearXchangeAccountPayload.fromProto(proto); + case COUNTRY_BASED_PAYMENT_ACCOUNT_PAYLOAD: + final protobuf.CountryBasedPaymentAccountPayload.MessageCase messageCaseCountry = proto.getCountryBasedPaymentAccountPayload().getMessageCase(); + switch (messageCaseCountry) { + case BANK_ACCOUNT_PAYLOAD: + final protobuf.BankAccountPayload.MessageCase messageCaseBank = proto.getCountryBasedPaymentAccountPayload().getBankAccountPayload().getMessageCase(); + switch (messageCaseBank) { + case NATIONAL_BANK_ACCOUNT_PAYLOAD: + return NationalBankAccountPayload.fromProto(proto); + case SAME_BANK_ACCONT_PAYLOAD: + return SameBankAccountPayload.fromProto(proto); + case SPECIFIC_BANKS_ACCOUNT_PAYLOAD: + return SpecificBanksAccountPayload.fromProto(proto); + default: + throw new ProtobufferRuntimeException("Unknown proto message case" + + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload.BankAccountPayload). " + + "messageCase=" + messageCaseBank); + } + case WESTERN_UNION_ACCOUNT_PAYLOAD: + return WesternUnionAccountPayload.fromProto(proto); + case CASH_DEPOSIT_ACCOUNT_PAYLOAD: + return CashDepositAccountPayload.fromProto(proto); + case SEPA_ACCOUNT_PAYLOAD: + return SepaAccountPayload.fromProto(proto); + case SEPA_INSTANT_ACCOUNT_PAYLOAD: + return SepaInstantAccountPayload.fromProto(proto); + case F2F_ACCOUNT_PAYLOAD: + return F2FAccountPayload.fromProto(proto); + default: + throw new ProtobufferRuntimeException("Unknown proto message case" + + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload)." + + " messageCase=" + messageCaseCountry); + } + case CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: + return CryptoCurrencyAccountPayload.fromProto(proto); + case FASTER_PAYMENTS_ACCOUNT_PAYLOAD: + return FasterPaymentsAccountPayload.fromProto(proto); + case INTERAC_E_TRANSFER_ACCOUNT_PAYLOAD: + return InteracETransferAccountPayload.fromProto(proto); + case JAPAN_BANK_ACCOUNT_PAYLOAD: + return JapanBankAccountPayload.fromProto(proto); + case AUSTRALIA_PAYID_PAYLOAD: + return AustraliaPayidPayload.fromProto(proto); + case UPHOLD_ACCOUNT_PAYLOAD: + return UpholdAccountPayload.fromProto(proto); + case MONEY_BEAM_ACCOUNT_PAYLOAD: + return MoneyBeamAccountPayload.fromProto(proto); + case MONEY_GRAM_ACCOUNT_PAYLOAD: + return MoneyGramAccountPayload.fromProto(proto); + case POPMONEY_ACCOUNT_PAYLOAD: + return PopmoneyAccountPayload.fromProto(proto); + case REVOLUT_ACCOUNT_PAYLOAD: + return RevolutAccountPayload.fromProto(proto); + case PERFECT_MONEY_ACCOUNT_PAYLOAD: + return PerfectMoneyAccountPayload.fromProto(proto); + case SWISH_ACCOUNT_PAYLOAD: + return SwishAccountPayload.fromProto(proto); + case HAL_CASH_ACCOUNT_PAYLOAD: + return HalCashAccountPayload.fromProto(proto); + case U_S_POSTAL_MONEY_ORDER_ACCOUNT_PAYLOAD: + return USPostalMoneyOrderAccountPayload.fromProto(proto); + case CASH_BY_MAIL_ACCOUNT_PAYLOAD: + return CashByMailAccountPayload.fromProto(proto); + case PROMPT_PAY_ACCOUNT_PAYLOAD: + return PromptPayAccountPayload.fromProto(proto); + case ADVANCED_CASH_ACCOUNT_PAYLOAD: + return AdvancedCashAccountPayload.fromProto(proto); + case TRANSFERWISE_ACCOUNT_PAYLOAD: + return TransferwiseAccountPayload.fromProto(proto); + case AMAZON_GIFT_CARD_ACCOUNT_PAYLOAD: + return AmazonGiftCardAccountPayload.fromProto(proto); + case INSTANT_CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: + return InstantCryptoCurrencyPayload.fromProto(proto); + + // Cannot be deleted as it would break old trade history entries + case O_K_PAY_ACCOUNT_PAYLOAD: + return OKPayAccountPayload.fromProto(proto); + case CASH_APP_ACCOUNT_PAYLOAD: + return CashAppAccountPayload.fromProto(proto); + case VENMO_ACCOUNT_PAYLOAD: + return VenmoAccountPayload.fromProto(proto); + + default: + throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase); + } + } else { + log.error("PersistableEnvelope.fromProto: PB.PaymentAccountPayload is null"); + throw new ProtobufferRuntimeException("PB.PaymentAccountPayload is null"); + } + } + + @Override + public PersistablePayload fromProto(protobuf.PersistableNetworkPayload proto) { + if (proto != null) { + switch (proto.getMessageCase()) { + case ACCOUNT_AGE_WITNESS: + return AccountAgeWitness.fromProto(proto.getAccountAgeWitness()); + case TRADE_STATISTICS2: + return TradeStatistics2.fromProto(proto.getTradeStatistics2()); + case PROPOSAL_PAYLOAD: + return ProposalPayload.fromProto(proto.getProposalPayload()); + case BLIND_VOTE_PAYLOAD: + return BlindVotePayload.fromProto(proto.getBlindVotePayload()); + case SIGNED_WITNESS: + return SignedWitness.fromProto(proto.getSignedWitness()); + case TRADE_STATISTICS3: + return TradeStatistics3.fromProto(proto.getTradeStatistics3()); + default: + throw new ProtobufferRuntimeException("Unknown proto message case (PB.PersistableNetworkPayload). messageCase=" + proto.getMessageCase()); + } + } else { + log.error("PB.PersistableNetworkPayload is null"); + throw new ProtobufferRuntimeException("PB.PersistableNetworkPayload is null"); + } + } +} diff --git a/core/src/main/java/bisq/core/proto/ProtoDevUtil.java b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java new file mode 100644 index 0000000000..6b70441a86 --- /dev/null +++ b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java @@ -0,0 +1,169 @@ +/* + * 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 bisq.core.proto; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.offer.AvailabilityResult; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOffer; +import bisq.core.trade.Trade; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProtoDevUtil { + // Util for auto generating enum values used in pb definition + public static void printAllEnumsForPB() { + StringBuilder sb = new StringBuilder("\n enum State {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < Trade.State.values().length; i++) { + Trade.State s = Trade.State.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n"); + + sb.append(" enum Phase {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < Trade.Phase.values().length; i++) { + Trade.Phase s = Trade.Phase.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum DisputeState {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < Trade.DisputeState.values().length; i++) { + Trade.DisputeState s = Trade.DisputeState.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum TradePeriodState {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < Trade.TradePeriodState.values().length; i++) { + Trade.TradePeriodState s = Trade.TradePeriodState.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum Direction {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < OfferPayload.Direction.values().length; i++) { + OfferPayload.Direction s = OfferPayload.Direction.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum Winner {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < DisputeResult.Winner.values().length; i++) { + DisputeResult.Winner s = DisputeResult.Winner.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum Reason {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < DisputeResult.Reason.values().length; i++) { + DisputeResult.Reason s = DisputeResult.Reason.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum AvailabilityResult {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < AvailabilityResult.values().length; i++) { + AvailabilityResult s = AvailabilityResult.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum Context {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < AddressEntry.Context.values().length; i++) { + AddressEntry.Context s = AddressEntry.Context.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum State {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < Offer.State.values().length; i++) { + Offer.State s = Offer.State.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + sb.append(" enum State {\n"); + sb.append(" PB_ERROR = 0;\n"); + for (int i = 0; i < OpenOffer.State.values().length; i++) { + OpenOffer.State s = OpenOffer.State.values()[i]; + sb.append(" "); + sb.append(s.toString()); + sb.append(" = "); + sb.append(s.ordinal() + 1); + sb.append(";\n"); + } + sb.append(" }\n\n\n"); + + log.info(sb.toString()); + } + +} diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java new file mode 100644 index 0000000000..d30a671bc8 --- /dev/null +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -0,0 +1,289 @@ +/* + * 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 bisq.core.proto.network; + +import bisq.core.alert.Alert; +import bisq.core.alert.PrivateNotificationMessage; +import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; +import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; +import bisq.core.dao.node.messages.GetBlocksRequest; +import bisq.core.dao.node.messages.GetBlocksResponse; +import bisq.core.dao.node.messages.NewBlockBroadcastMessage; +import bisq.core.filter.Filter; +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.messages.OfferAvailabilityRequest; +import bisq.core.offer.messages.OfferAvailabilityResponse; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; +import bisq.core.trade.messages.RefreshTradeStateRequest; +import bisq.core.trade.messages.TraderSignedWitnessMessage; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.BundleOfEnvelopes; +import bisq.network.p2p.CloseConnectionMessage; +import bisq.network.p2p.PrefixedSealedAndSignedMessage; +import bisq.network.p2p.peers.getdata.messages.GetDataResponse; +import bisq.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; +import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; +import bisq.network.p2p.peers.keepalive.messages.Ping; +import bisq.network.p2p.peers.keepalive.messages.Pong; +import bisq.network.p2p.peers.peerexchange.messages.GetPeersRequest; +import bisq.network.p2p.peers.peerexchange.messages.GetPeersResponse; +import bisq.network.p2p.storage.messages.AddDataMessage; +import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; +import bisq.network.p2p.storage.messages.RefreshOfferMessage; +import bisq.network.p2p.storage.messages.RemoveDataMessage; +import bisq.network.p2p.storage.messages.RemoveMailboxDataMessage; +import bisq.network.p2p.storage.payload.MailboxStoragePayload; +import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.proto.ProtobufferException; +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.network.NetworkProtoResolver; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.time.Clock; + +import lombok.extern.slf4j.Slf4j; + +// TODO Use ProtobufferException instead of ProtobufferRuntimeException +@Slf4j +@Singleton +public class CoreNetworkProtoResolver extends CoreProtoResolver implements NetworkProtoResolver { + @Inject + public CoreNetworkProtoResolver(Clock clock) { + this.clock = clock; + } + + @Override + public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException { + if (proto != null) { + final int messageVersion = proto.getMessageVersion(); + switch (proto.getMessageCase()) { + case PRELIMINARY_GET_DATA_REQUEST: + return PreliminaryGetDataRequest.fromProto(proto.getPreliminaryGetDataRequest(), messageVersion); + case GET_DATA_RESPONSE: + return GetDataResponse.fromProto(proto.getGetDataResponse(), this, messageVersion); + case GET_UPDATED_DATA_REQUEST: + return GetUpdatedDataRequest.fromProto(proto.getGetUpdatedDataRequest(), messageVersion); + + case GET_PEERS_REQUEST: + return GetPeersRequest.fromProto(proto.getGetPeersRequest(), messageVersion); + case GET_PEERS_RESPONSE: + return GetPeersResponse.fromProto(proto.getGetPeersResponse(), messageVersion); + case PING: + return Ping.fromProto(proto.getPing(), messageVersion); + case PONG: + return Pong.fromProto(proto.getPong(), messageVersion); + + case OFFER_AVAILABILITY_REQUEST: + return OfferAvailabilityRequest.fromProto(proto.getOfferAvailabilityRequest(), messageVersion); + case OFFER_AVAILABILITY_RESPONSE: + return OfferAvailabilityResponse.fromProto(proto.getOfferAvailabilityResponse(), messageVersion); + case REFRESH_OFFER_MESSAGE: + return RefreshOfferMessage.fromProto(proto.getRefreshOfferMessage(), messageVersion); + + case ADD_DATA_MESSAGE: + return AddDataMessage.fromProto(proto.getAddDataMessage(), this, messageVersion); + case REMOVE_DATA_MESSAGE: + return RemoveDataMessage.fromProto(proto.getRemoveDataMessage(), this, messageVersion); + case REMOVE_MAILBOX_DATA_MESSAGE: + return RemoveMailboxDataMessage.fromProto(proto.getRemoveMailboxDataMessage(), this, messageVersion); + + case CLOSE_CONNECTION_MESSAGE: + return CloseConnectionMessage.fromProto(proto.getCloseConnectionMessage(), messageVersion); + case PREFIXED_SEALED_AND_SIGNED_MESSAGE: + return PrefixedSealedAndSignedMessage.fromProto(proto.getPrefixedSealedAndSignedMessage(), messageVersion); + + // trade protocol messages + case REFRESH_TRADE_STATE_REQUEST: + return RefreshTradeStateRequest.fromProto(proto.getRefreshTradeStateRequest(), messageVersion); + case INPUTS_FOR_DEPOSIT_TX_REQUEST: + return InputsForDepositTxRequest.fromProto(proto.getInputsForDepositTxRequest(), this, messageVersion); + case INPUTS_FOR_DEPOSIT_TX_RESPONSE: + return InputsForDepositTxResponse.fromProto(proto.getInputsForDepositTxResponse(), this, messageVersion); + case DEPOSIT_TX_MESSAGE: + return DepositTxMessage.fromProto(proto.getDepositTxMessage(), messageVersion); + case DELAYED_PAYOUT_TX_SIGNATURE_REQUEST: + return DelayedPayoutTxSignatureRequest.fromProto(proto.getDelayedPayoutTxSignatureRequest(), messageVersion); + case DELAYED_PAYOUT_TX_SIGNATURE_RESPONSE: + return DelayedPayoutTxSignatureResponse.fromProto(proto.getDelayedPayoutTxSignatureResponse(), messageVersion); + case DEPOSIT_TX_AND_DELAYED_PAYOUT_TX_MESSAGE: + return DepositTxAndDelayedPayoutTxMessage.fromProto(proto.getDepositTxAndDelayedPayoutTxMessage(), messageVersion); + + case COUNTER_CURRENCY_TRANSFER_STARTED_MESSAGE: + return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion); + + case PAYOUT_TX_PUBLISHED_MESSAGE: + return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion); + case PEER_PUBLISHED_DELAYED_PAYOUT_TX_MESSAGE: + return PeerPublishedDelayedPayoutTxMessage.fromProto(proto.getPeerPublishedDelayedPayoutTxMessage(), messageVersion); + case TRADER_SIGNED_WITNESS_MESSAGE: + return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion); + + case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE: + return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion); + case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: + return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion); + + case OPEN_NEW_DISPUTE_MESSAGE: + return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion); + case PEER_OPENED_DISPUTE_MESSAGE: + return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion); + case CHAT_MESSAGE: + return ChatMessage.fromProto(proto.getChatMessage(), messageVersion); + case DISPUTE_RESULT_MESSAGE: + return DisputeResultMessage.fromProto(proto.getDisputeResultMessage(), messageVersion); + case PEER_PUBLISHED_DISPUTE_PAYOUT_TX_MESSAGE: + return PeerPublishedDisputePayoutTxMessage.fromProto(proto.getPeerPublishedDisputePayoutTxMessage(), messageVersion); + + case PRIVATE_NOTIFICATION_MESSAGE: + return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion); + + case GET_BLOCKS_REQUEST: + return GetBlocksRequest.fromProto(proto.getGetBlocksRequest(), messageVersion); + case GET_BLOCKS_RESPONSE: + return GetBlocksResponse.fromProto(proto.getGetBlocksResponse(), messageVersion); + case NEW_BLOCK_BROADCAST_MESSAGE: + return NewBlockBroadcastMessage.fromProto(proto.getNewBlockBroadcastMessage(), messageVersion); + case ADD_PERSISTABLE_NETWORK_PAYLOAD_MESSAGE: + return AddPersistableNetworkPayloadMessage.fromProto(proto.getAddPersistableNetworkPayloadMessage(), this, messageVersion); + case ACK_MESSAGE: + return AckMessage.fromProto(proto.getAckMessage(), messageVersion); + case REPUBLISH_GOVERNANCE_DATA_REQUEST: + return RepublishGovernanceDataRequest.fromProto(proto.getRepublishGovernanceDataRequest(), messageVersion); + + case NEW_DAO_STATE_HASH_MESSAGE: + return NewDaoStateHashMessage.fromProto(proto.getNewDaoStateHashMessage(), messageVersion); + case GET_DAO_STATE_HASHES_REQUEST: + return GetDaoStateHashesRequest.fromProto(proto.getGetDaoStateHashesRequest(), messageVersion); + case GET_DAO_STATE_HASHES_RESPONSE: + return GetDaoStateHashesResponse.fromProto(proto.getGetDaoStateHashesResponse(), messageVersion); + + case NEW_PROPOSAL_STATE_HASH_MESSAGE: + return NewProposalStateHashMessage.fromProto(proto.getNewProposalStateHashMessage(), messageVersion); + case GET_PROPOSAL_STATE_HASHES_REQUEST: + return GetProposalStateHashesRequest.fromProto(proto.getGetProposalStateHashesRequest(), messageVersion); + case GET_PROPOSAL_STATE_HASHES_RESPONSE: + return GetProposalStateHashesResponse.fromProto(proto.getGetProposalStateHashesResponse(), messageVersion); + + case NEW_BLIND_VOTE_STATE_HASH_MESSAGE: + return NewBlindVoteStateHashMessage.fromProto(proto.getNewBlindVoteStateHashMessage(), messageVersion); + case GET_BLIND_VOTE_STATE_HASHES_REQUEST: + return GetBlindVoteStateHashesRequest.fromProto(proto.getGetBlindVoteStateHashesRequest(), messageVersion); + case GET_BLIND_VOTE_STATE_HASHES_RESPONSE: + return GetBlindVoteStateHashesResponse.fromProto(proto.getGetBlindVoteStateHashesResponse(), messageVersion); + + case BUNDLE_OF_ENVELOPES: + return BundleOfEnvelopes.fromProto(proto.getBundleOfEnvelopes(), this, messageVersion); + + case GET_INVENTORY_REQUEST: + return GetInventoryRequest.fromProto(proto.getGetInventoryRequest(), messageVersion); + case GET_INVENTORY_RESPONSE: + return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion); + + default: + throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" + + proto.getMessageCase() + "; proto raw data=" + proto.toString()); + } + } else { + log.error("PersistableEnvelope.fromProto: PB.NetworkEnvelope is null"); + throw new ProtobufferException("PB.NetworkEnvelope is null"); + } + } + + public NetworkPayload fromProto(protobuf.StorageEntryWrapper proto) { + if (proto != null) { + switch (proto.getMessageCase()) { + case PROTECTED_MAILBOX_STORAGE_ENTRY: + return ProtectedMailboxStorageEntry.fromProto(proto.getProtectedMailboxStorageEntry(), this); + case PROTECTED_STORAGE_ENTRY: + return ProtectedStorageEntry.fromProto(proto.getProtectedStorageEntry(), this); + default: + throw new ProtobufferRuntimeException("Unknown proto message case(PB.StorageEntryWrapper). " + + "messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); + } + } else { + log.error("PersistableEnvelope.fromProto: PB.StorageEntryWrapper is null"); + throw new ProtobufferRuntimeException("PB.StorageEntryWrapper is null"); + } + } + + public NetworkPayload fromProto(protobuf.StoragePayload proto) { + if (proto != null) { + switch (proto.getMessageCase()) { + case ALERT: + return Alert.fromProto(proto.getAlert()); + case ARBITRATOR: + return Arbitrator.fromProto(proto.getArbitrator()); + case MEDIATOR: + return Mediator.fromProto(proto.getMediator()); + case REFUND_AGENT: + return RefundAgent.fromProto(proto.getRefundAgent()); + case FILTER: + return Filter.fromProto(proto.getFilter()); + case MAILBOX_STORAGE_PAYLOAD: + return MailboxStoragePayload.fromProto(proto.getMailboxStoragePayload()); + case OFFER_PAYLOAD: + return OfferPayload.fromProto(proto.getOfferPayload()); + case TEMP_PROPOSAL_PAYLOAD: + return TempProposalPayload.fromProto(proto.getTempProposalPayload()); + default: + throw new ProtobufferRuntimeException("Unknown proto message case (PB.StoragePayload). messageCase=" + + proto.getMessageCase() + "; proto raw data=" + proto.toString()); + } + } else { + log.error("PersistableEnvelope.fromProto: PB.StoragePayload is null"); + throw new ProtobufferRuntimeException("PB.StoragePayload is null"); + } + } +} diff --git a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java new file mode 100644 index 0000000000..f42670d76d --- /dev/null +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -0,0 +1,150 @@ +/* + * 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 bisq.core.proto.persistable; + +import bisq.core.account.sign.SignedWitnessStore; +import bisq.core.account.witness.AccountAgeWitnessStore; +import bisq.core.btc.model.AddressEntryList; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.governance.blindvote.MyBlindVoteList; +import bisq.core.dao.governance.blindvote.storage.BlindVoteStore; +import bisq.core.dao.governance.bond.reputation.MyReputationList; +import bisq.core.dao.governance.myvote.MyVoteList; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnList; +import bisq.core.dao.governance.proposal.MyProposalList; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStore; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalStore; +import bisq.core.dao.state.model.governance.BallotList; +import bisq.core.dao.state.storage.DaoStateStore; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputList; +import bisq.core.payment.PaymentAccountList; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.ArbitrationDisputeList; +import bisq.core.support.dispute.mediation.MediationDisputeList; +import bisq.core.support.dispute.refund.RefundDisputeList; +import bisq.core.trade.TradableList; +import bisq.core.trade.statistics.TradeStatistics2Store; +import bisq.core.trade.statistics.TradeStatistics3Store; +import bisq.core.user.PreferencesPayload; +import bisq.core.user.UserPayload; + +import bisq.network.p2p.mailbox.IgnoredMailboxMap; +import bisq.network.p2p.mailbox.MailboxMessageList; +import bisq.network.p2p.peers.peerexchange.PeerList; +import bisq.network.p2p.storage.persistence.RemovedPayloadsMap; +import bisq.network.p2p.storage.persistence.SequenceNumberMap; + +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.common.proto.network.NetworkProtoResolver; +import bisq.common.proto.persistable.NavigationPath; +import bisq.common.proto.persistable.PersistableEnvelope; +import bisq.common.proto.persistable.PersistenceProtoResolver; + +import com.google.inject.Provider; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +// TODO Use ProtobufferException instead of ProtobufferRuntimeException +@Slf4j +@Singleton +public class CorePersistenceProtoResolver extends CoreProtoResolver implements PersistenceProtoResolver { + private final Provider btcWalletService; + private final NetworkProtoResolver networkProtoResolver; + + @Inject + public CorePersistenceProtoResolver(Provider btcWalletService, + NetworkProtoResolver networkProtoResolver) { + this.btcWalletService = btcWalletService; + this.networkProtoResolver = networkProtoResolver; + } + + @Override + public PersistableEnvelope fromProto(protobuf.PersistableEnvelope proto) { + if (proto != null) { + switch (proto.getMessageCase()) { + case SEQUENCE_NUMBER_MAP: + return SequenceNumberMap.fromProto(proto.getSequenceNumberMap()); + case PEER_LIST: + return PeerList.fromProto(proto.getPeerList()); + case ADDRESS_ENTRY_LIST: + return AddressEntryList.fromProto(proto.getAddressEntryList()); + case TRADABLE_LIST: + return TradableList.fromProto(proto.getTradableList(), this, btcWalletService.get()); + case ARBITRATION_DISPUTE_LIST: + return ArbitrationDisputeList.fromProto(proto.getArbitrationDisputeList(), this); + case MEDIATION_DISPUTE_LIST: + return MediationDisputeList.fromProto(proto.getMediationDisputeList(), this); + case REFUND_DISPUTE_LIST: + return RefundDisputeList.fromProto(proto.getRefundDisputeList(), this); + case PREFERENCES_PAYLOAD: + return PreferencesPayload.fromProto(proto.getPreferencesPayload(), this); + case USER_PAYLOAD: + return UserPayload.fromProto(proto.getUserPayload(), this); + case NAVIGATION_PATH: + return NavigationPath.fromProto(proto.getNavigationPath()); + case PAYMENT_ACCOUNT_LIST: + return PaymentAccountList.fromProto(proto.getPaymentAccountList(), this); + case ACCOUNT_AGE_WITNESS_STORE: + return AccountAgeWitnessStore.fromProto(proto.getAccountAgeWitnessStore()); + case TRADE_STATISTICS2_STORE: + return TradeStatistics2Store.fromProto(proto.getTradeStatistics2Store()); + case BLIND_VOTE_STORE: + return BlindVoteStore.fromProto(proto.getBlindVoteStore()); + case PROPOSAL_STORE: + return ProposalStore.fromProto(proto.getProposalStore()); + case TEMP_PROPOSAL_STORE: + return TempProposalStore.fromProto(proto.getTempProposalStore(), networkProtoResolver); + case MY_PROPOSAL_LIST: + return MyProposalList.fromProto(proto.getMyProposalList()); + case BALLOT_LIST: + return BallotList.fromProto(proto.getBallotList()); + case MY_VOTE_LIST: + return MyVoteList.fromProto(proto.getMyVoteList()); + case MY_BLIND_VOTE_LIST: + return MyBlindVoteList.fromProto(proto.getMyBlindVoteList()); + case DAO_STATE_STORE: + return DaoStateStore.fromProto(proto.getDaoStateStore()); + case MY_REPUTATION_LIST: + return MyReputationList.fromProto(proto.getMyReputationList()); + case MY_PROOF_OF_BURN_LIST: + return MyProofOfBurnList.fromProto(proto.getMyProofOfBurnList()); + case UNCONFIRMED_BSQ_CHANGE_OUTPUT_LIST: + return UnconfirmedBsqChangeOutputList.fromProto(proto.getUnconfirmedBsqChangeOutputList()); + case SIGNED_WITNESS_STORE: + return SignedWitnessStore.fromProto(proto.getSignedWitnessStore()); + case TRADE_STATISTICS3_STORE: + return TradeStatistics3Store.fromProto(proto.getTradeStatistics3Store()); + case MAILBOX_MESSAGE_LIST: + return MailboxMessageList.fromProto(proto.getMailboxMessageList(), networkProtoResolver); + case IGNORED_MAILBOX_MAP: + return IgnoredMailboxMap.fromProto(proto.getIgnoredMailboxMap()); + case REMOVED_PAYLOADS_MAP: + return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap()); + default: + throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " + + "messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); + } + } else { + log.error("PersistableEnvelope.fromProto: PB.PersistableEnvelope is null"); + throw new ProtobufferRuntimeException("PB.PersistableEnvelope is null"); + } + } +} diff --git a/core/src/main/java/bisq/core/provider/FeeHttpClient.java b/core/src/main/java/bisq/core/provider/FeeHttpClient.java new file mode 100644 index 0000000000..2579c6ffb2 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/FeeHttpClient.java @@ -0,0 +1,34 @@ +/* + * 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 bisq.core.provider; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javax.annotation.Nullable; + +@Singleton +public class FeeHttpClient extends HttpClientImpl { + @Inject + public FeeHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } +} diff --git a/core/src/main/java/bisq/core/provider/HttpClientProvider.java b/core/src/main/java/bisq/core/provider/HttpClientProvider.java new file mode 100644 index 0000000000..d977f7de70 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/HttpClientProvider.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.provider; + +import bisq.network.http.HttpClient; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class HttpClientProvider { + protected final HttpClient httpClient; + + public HttpClientProvider(HttpClient httpClient, String baseUrl) { + this(httpClient, baseUrl, false); + } + + public HttpClientProvider(HttpClient httpClient, String baseUrl, @SuppressWarnings("SameParameterValue") boolean ignoreSocks5Proxy) { + this.httpClient = httpClient; + log.debug("{} with baseUrl {}", this.getClass().getSimpleName(), baseUrl); + httpClient.setBaseUrl(baseUrl); + + httpClient.setIgnoreSocks5Proxy(ignoreSocks5Proxy); + } + + @Override + public String toString() { + return "HttpClientProvider{" + + "\n httpClient=" + httpClient + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/provider/MempoolHttpClient.java b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java new file mode 100644 index 0000000000..18d4001664 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.provider; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import bisq.common.app.Version; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.IOException; + +import javax.annotation.Nullable; + +@Singleton +public class MempoolHttpClient extends HttpClientImpl { + @Inject + public MempoolHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } + + // returns JSON of the transaction details + public String getTxDetails(String txId) throws IOException { + super.shutDown(); // close any prior incomplete request + String api = "/" + txId; + return get(api, "User-Agent", "bisq/" + Version.VERSION); + } +} diff --git a/core/src/main/java/bisq/core/provider/PriceHttpClient.java b/core/src/main/java/bisq/core/provider/PriceHttpClient.java new file mode 100644 index 0000000000..1f0b47d8e8 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/PriceHttpClient.java @@ -0,0 +1,34 @@ +/* + * 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 bisq.core.provider; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javax.annotation.Nullable; + +@Singleton +public class PriceHttpClient extends HttpClientImpl { + @Inject + public PriceHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } +} diff --git a/core/src/main/java/bisq/core/provider/ProvidersRepository.java b/core/src/main/java/bisq/core/provider/ProvidersRepository.java new file mode 100644 index 0000000000..9a9d3b23f5 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/ProvidersRepository.java @@ -0,0 +1,128 @@ +/* + * 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 bisq.core.provider; + +import bisq.common.config.Config; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class ProvidersRepository { + private static final List DEFAULT_NODES = Arrays.asList( + "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/", // @wiz + "http://emzypricpidesmyqg2hc6dkwitqzaxrqnpkdg3ae2wef5znncu2ambqd.onion/", // @emzy + "http://aprcndeiwdrkbf4fq7iozxbd27dl72oeo76n7zmjwdi4z34agdrnheyd.onion/", // @mrosseel + "http://devinpndvdwll4wiqcyq5e7itezmarg7rzicrvf6brzkwxdm374kmmyd.onion/", // @devinbileck + "http://ro7nv73awqs3ga2qtqeqawrjpbxwarsazznszvr6whv7tes5ehffopid.onion/" // @alexej996 + ); + + private final Config config; + private final List providersFromProgramArgs; + private final boolean useLocalhostForP2P; + + private List providerList; + @Getter + private String baseUrl = ""; + @Getter + @Nullable + private List bannedNodes; + private int index = 0; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProvidersRepository(Config config, + @Named(Config.PROVIDERS) List providers, + @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P) { + + this.config = config; + this.providersFromProgramArgs = providers; + this.useLocalhostForP2P = useLocalhostForP2P; + + Collections.shuffle(DEFAULT_NODES); + + applyBannedNodes(config.bannedPriceRelayNodes); + } + + public void applyBannedNodes(@Nullable List bannedNodes) { + this.bannedNodes = bannedNodes; + fillProviderList(); + selectNextProviderBaseUrl(); + + if (bannedNodes != null && !bannedNodes.isEmpty()) { + log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}", + bannedNodes, baseUrl, providerList); + } + } + + public void selectNextProviderBaseUrl() { + if (!providerList.isEmpty()) { + if (index >= providerList.size()) + index = 0; + + baseUrl = providerList.get(index); + index++; + + if (providerList.size() == 1 && config.baseCurrencyNetwork.isMainnet()) + log.warn("We only have one provider"); + } else { + baseUrl = ""; + log.warn("We do not have any providers. That can be if all providers are filtered or providersFromProgramArgs is set but empty. " + + "bannedNodes={}. providersFromProgramArgs={}", bannedNodes, providersFromProgramArgs); + } + } + + private void fillProviderList() { + List providers; + if (providersFromProgramArgs.isEmpty()) { + if (useLocalhostForP2P) { + // 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 + // providerAsString = Collections.singletonList("http://localhost:8080/"); + providers = Collections.singletonList("https://price.bisq.wiz.biz/"); // @wiz + } else { + providers = DEFAULT_NODES; + } + } else { + providers = providersFromProgramArgs; + } + providerList = providers.stream() + .filter(e -> bannedNodes == null || + !bannedNodes.contains(e.replace("http://", "") + .replace("/", "") + .replace(".onion", ""))) + .map(e -> e.endsWith("/") ? e : e + "/") + .map(e -> e.startsWith("http") ? e : "http://" + e) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/bisq/core/provider/fee/FeeProvider.java b/core/src/main/java/bisq/core/provider/fee/FeeProvider.java new file mode 100644 index 0000000000..0e410204b8 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/fee/FeeProvider.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.provider.fee; + +import bisq.core.provider.FeeHttpClient; +import bisq.core.provider.HttpClientProvider; +import bisq.core.provider.ProvidersRepository; + +import bisq.network.http.HttpClient; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.util.Tuple2; + +import com.google.gson.Gson; +import com.google.gson.internal.LinkedTreeMap; + +import com.google.inject.Inject; + +import java.io.IOException; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeeProvider extends HttpClientProvider { + + @Inject + public FeeProvider(FeeHttpClient httpClient, ProvidersRepository providersRepository) { + super(httpClient, providersRepository.getBaseUrl(), false); + } + + public Tuple2, Map> getFees() throws IOException { + String json = httpClient.get("getFees", "User-Agent", "bisq/" + Version.VERSION); + + LinkedTreeMap linkedTreeMap = new Gson().fromJson(json, LinkedTreeMap.class); + Map tsMap = new HashMap<>(); + tsMap.put(Config.BTC_FEES_TS, ((Double) linkedTreeMap.get(Config.BTC_FEES_TS)).longValue()); + + Map map = new HashMap<>(); + + try { + LinkedTreeMap dataMap = (LinkedTreeMap) linkedTreeMap.get("dataMap"); + Long btcTxFee = ((Double) dataMap.get(Config.BTC_TX_FEE)).longValue(); + Long btcMinTxFee = dataMap.get(Config.BTC_MIN_TX_FEE) != null ? + ((Double) dataMap.get(Config.BTC_MIN_TX_FEE)).longValue() : Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte(); + + map.put(Config.BTC_TX_FEE, btcTxFee); + map.put(Config.BTC_MIN_TX_FEE, btcMinTxFee); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + } + return new Tuple2<>(tsMap, map); + } + + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/core/src/main/java/bisq/core/provider/fee/FeeRequest.java b/core/src/main/java/bisq/core/provider/fee/FeeRequest.java new file mode 100644 index 0000000000..007a5d3959 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/fee/FeeRequest.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.provider.fee; + +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jetbrains.annotations.NotNull; + +public class FeeRequest { + private static final Logger log = LoggerFactory.getLogger(FeeRequest.class); + + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("FeeRequest", 3, 5, 10 * 60); + + public FeeRequest() { + } + + public SettableFuture, Map>> getFees(FeeProvider provider) { + final SettableFuture, Map>> resultFuture = SettableFuture.create(); + ListenableFuture, Map>> future = executorService.submit(() -> { + Thread.currentThread().setName("FeeRequest @ " + provider.getHttpClient().getBaseUrl()); + return provider.getFees(); + }); + + Futures.addCallback(future, new FutureCallback, Map>>() { + public void onSuccess(Tuple2, Map> feeData) { + log.debug("Received feeData of {}\nfrom provider {}", feeData, provider); + resultFuture.set(feeData); + } + + public void onFailure(@NotNull Throwable throwable) { + resultFuture.setException(throwable); + } + }, MoreExecutors.directExecutor()); + + return resultFuture; + } +} diff --git a/core/src/main/java/bisq/core/provider/fee/FeeService.java b/core/src/main/java/bisq/core/provider/fee/FeeService.java new file mode 100644 index 0000000000..0c68293565 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/fee/FeeService.java @@ -0,0 +1,206 @@ +/* + * 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 bisq.core.provider.fee; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.handlers.FaultHandler; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import java.time.Instant; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class FeeService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + // Miner fees are between 1-600 sat/vbyte. We try to stay on the safe side. BTC_DEFAULT_TX_FEE is only used if our + // fee service would not deliver data. + private static final long BTC_DEFAULT_TX_FEE = 50; + private static final long MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN = 2; + private static DaoStateService daoStateService; + private static PeriodService periodService; + + private static Coin getFeeFromParamAsCoin(Param parm) { + return daoStateService != null && periodService != null ? daoStateService.getParamValueAsCoin(parm, periodService.getChainHeight()) : Coin.ZERO; + } + + public static Coin getMakerFeePerBtc(boolean currencyForFeeIsBtc) { + return currencyForFeeIsBtc ? getFeeFromParamAsCoin(Param.DEFAULT_MAKER_FEE_BTC) : getFeeFromParamAsCoin(Param.DEFAULT_MAKER_FEE_BSQ); + } + + public static Coin getMinMakerFee(boolean currencyForFeeIsBtc) { + return currencyForFeeIsBtc ? getFeeFromParamAsCoin(Param.MIN_MAKER_FEE_BTC) : getFeeFromParamAsCoin(Param.MIN_MAKER_FEE_BSQ); + } + + public static Coin getTakerFeePerBtc(boolean currencyForFeeIsBtc) { + return currencyForFeeIsBtc ? getFeeFromParamAsCoin(Param.DEFAULT_TAKER_FEE_BTC) : getFeeFromParamAsCoin(Param.DEFAULT_TAKER_FEE_BSQ); + } + + public static Coin getMinTakerFee(boolean currencyForFeeIsBtc) { + return currencyForFeeIsBtc ? getFeeFromParamAsCoin(Param.MIN_TAKER_FEE_BTC) : getFeeFromParamAsCoin(Param.MIN_TAKER_FEE_BSQ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final FeeProvider feeProvider; + private final IntegerProperty feeUpdateCounter = new SimpleIntegerProperty(0); + private long txFeePerVbyte = BTC_DEFAULT_TX_FEE; + private Map timeStampMap; + @Getter + private long lastRequest; + @Getter + private long minFeePerVByte; + private long epochInSecondAtLastRequest; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public FeeService(FeeProvider feeProvider, DaoStateService daoStateService, PeriodService periodService) { + this.feeProvider = feeProvider; + FeeService.daoStateService = daoStateService; + FeeService.periodService = periodService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + minFeePerVByte = Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte(); + + requestFees(); + + // We update all 5 min. + UserThread.runPeriodically(this::requestFees, 5, TimeUnit.MINUTES); + } + + + public void requestFees() { + requestFees(null, null); + } + + public void requestFees(Runnable resultHandler) { + requestFees(resultHandler, null); + } + + public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) { + if (feeProvider.getHttpClient().hasPendingRequest()) { + log.warn("We have a pending request open. We ignore that request. httpClient {}", feeProvider.getHttpClient()); + return; + } + + long now = Instant.now().getEpochSecond(); + // We all requests only each 2 minutes + if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) { + lastRequest = now; + FeeRequest feeRequest = new FeeRequest(); + SettableFuture, Map>> future = feeRequest.getFees(feeProvider); + Futures.addCallback(future, new FutureCallback, Map>>() { + @Override + public void onSuccess(@Nullable Tuple2, Map> result) { + UserThread.execute(() -> { + checkNotNull(result, "Result must not be null at getFees"); + timeStampMap = result.first; + epochInSecondAtLastRequest = timeStampMap.get(Config.BTC_FEES_TS); + final Map map = result.second; + txFeePerVbyte = map.get(Config.BTC_TX_FEE); + minFeePerVByte = map.get(Config.BTC_MIN_TX_FEE); + + if (txFeePerVbyte < minFeePerVByte) { + log.warn("The delivered fee of {} sat/vbyte is smaller than the min. default fee of {} sat/vbyte", txFeePerVbyte, minFeePerVByte); + txFeePerVbyte = minFeePerVByte; + } + + feeUpdateCounter.set(feeUpdateCounter.get() + 1); + log.info("BTC tx fee: txFeePerVbyte={} minFeePerVbyte={}", txFeePerVbyte, minFeePerVByte); + if (resultHandler != null) + resultHandler.run(); + }); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.warn("Could not load fees. feeProvider={}, error={}", feeProvider.toString(), throwable.toString()); + if (faultHandler != null) + UserThread.execute(() -> faultHandler.handleFault("Could not load fees", throwable)); + } + }, MoreExecutors.directExecutor()); + } else { + log.debug("We got a requestFees called again before min pause of {} minutes has passed.", MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN); + UserThread.execute(() -> { + if (resultHandler != null) + resultHandler.run(); + }); + } + } + + public Coin getTxFee(int vsizeInVbytes) { + return getTxFeePerVbyte().multiply(vsizeInVbytes); + } + + public Coin getTxFeePerVbyte() { + return Coin.valueOf(txFeePerVbyte); + } + + public ReadOnlyIntegerProperty feeUpdateCounterProperty() { + return feeUpdateCounter; + } + + public boolean isFeeAvailable() { + return feeUpdateCounter.get() > 0; + } +} diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java new file mode 100644 index 0000000000..fcfdeb4a45 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.provider.mempool; + +import bisq.core.provider.MempoolHttpClient; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MempoolRequest { + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("MempoolRequest", 3, 5, 10 * 60); + private final List txBroadcastServices = new ArrayList<>(); + private final MempoolHttpClient mempoolHttpClient; + + public MempoolRequest(Preferences preferences, Socks5ProxyProvider socks5ProxyProvider) { + this.txBroadcastServices.addAll(preferences.getDefaultTxBroadcastServices()); + this.mempoolHttpClient = new MempoolHttpClient(socks5ProxyProvider); + } + + public void getTxStatus(SettableFuture mempoolServiceCallback, String txId) { + mempoolHttpClient.setBaseUrl(getRandomServiceAddress(txBroadcastServices)); + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName("MempoolRequest @ " + mempoolHttpClient.getBaseUrl()); + log.info("Making http request for information on txId: {}", txId); + return mempoolHttpClient.getTxDetails(txId); + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(String mempoolData) { + log.info("Received mempoolData of [{}] from provider", mempoolData); + mempoolServiceCallback.set(mempoolData); + } + public void onFailure(@NotNull Throwable throwable) { + mempoolServiceCallback.setException(throwable); + } + }, MoreExecutors.directExecutor()); + } + + public boolean switchToAnotherProvider() { + txBroadcastServices.remove(mempoolHttpClient.getBaseUrl()); + return txBroadcastServices.size() > 0; + } + + @Nullable + private static String getRandomServiceAddress(List txBroadcastServices) { + List list = checkNotNull(txBroadcastServices); + return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null; + } +} + diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java new file mode 100644 index 0000000000..7894b55f08 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java @@ -0,0 +1,275 @@ +/* + * 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 bisq.core.provider.mempool; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateService; +import bisq.core.filter.FilterManager; +import bisq.core.offer.OfferPayload; +import bisq.core.trade.Trade; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.UserThread; +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; + +import javax.inject.Singleton; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public class MempoolService { + private final Socks5ProxyProvider socks5ProxyProvider; + private final Config config; + private final Preferences preferences; + private final FilterManager filterManager; + private final DaoFacade daoFacade; + private final DaoStateService daoStateService; + private final List btcFeeReceivers = new ArrayList<>(); + @Getter + private int outstandingRequests = 0; + + @Inject + public MempoolService(Socks5ProxyProvider socks5ProxyProvider, + Config config, + Preferences preferences, + FilterManager filterManager, + DaoFacade daoFacade, + DaoStateService daoStateService) { + this.socks5ProxyProvider = socks5ProxyProvider; + this.config = config; + this.preferences = preferences; + this.filterManager = filterManager; + this.daoFacade = daoFacade; + this.daoStateService = daoStateService; + } + + public void onAllServicesInitialized() { + btcFeeReceivers.addAll(getAllBtcFeeReceivers()); + } + + public boolean canRequestBeMade() { + return outstandingRequests < 5; // limit max simultaneous lookups + } + + public boolean canRequestBeMade(OfferPayload offerPayload) { + // when validating a new offer, wait 1 block for the tx to propagate + return offerPayload.getBlockHeightAtOfferCreation() < daoStateService.getChainHeight() && canRequestBeMade(); + } + + public void validateOfferMakerTx(OfferPayload offerPayload, Consumer resultHandler) { + validateOfferMakerTx(new TxValidator(daoStateService, offerPayload.getOfferFeePaymentTxId(), Coin.valueOf(offerPayload.getAmount()), + offerPayload.isCurrencyForMakerFeeBtc()), resultHandler); + } + + public void validateOfferMakerTx(TxValidator txValidator, Consumer resultHandler) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferMakerTx(mempoolRequest, txValidator, resultHandler); + } + + public void validateOfferTakerTx(Trade trade, Consumer resultHandler) { + validateOfferTakerTx(new TxValidator(daoStateService, trade.getTakerFeeTxId(), trade.getTradeAmount(), + trade.isCurrencyForTakerFeeBtc()), resultHandler); + } + + public void validateOfferTakerTx(TxValidator txValidator, Consumer resultHandler) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferTakerTx(mempoolRequest, txValidator, resultHandler); + } + + public void checkTxIsConfirmed(String txId, Consumer resultHandler) { + TxValidator txValidator = new TxValidator(daoStateService, txId); + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForTxRequest(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txId); + } + + // /////////////////////////// + + private void validateOfferMakerTx(MempoolRequest mempoolRequest, + TxValidator txValidator, + Consumer resultHandler) { + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForMakerTxValidation(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txValidator.getTxId()); + } + + private void validateOfferTakerTx(MempoolRequest mempoolRequest, + TxValidator txValidator, + Consumer resultHandler) { + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForTakerTxValidation(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txValidator.getTxId()); + } + + private FutureCallback callbackForMakerTxValidation(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers)); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + if (theRequest.switchToAnotherProvider()) { + validateOfferMakerTx(theRequest, txValidator, resultHandler); + } else { + // exhausted all providers, let user know of failure + resultHandler.accept(txValidator.endResult("Tx not found", false)); + } + }); + } + }; + return myCallback; + } + + private FutureCallback callbackForTakerTxValidation(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.parseJsonValidateTakerFeeTx(jsonTxt, btcFeeReceivers)); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + if (theRequest.switchToAnotherProvider()) { + validateOfferTakerTx(theRequest, txValidator, resultHandler); + } else { + // exhausted all providers, let user know of failure + resultHandler.accept(txValidator.endResult("Tx not found", false)); + } + }); + } + }; + return myCallback; + } + + private FutureCallback callbackForTxRequest(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + txValidator.setJsonTxt(jsonTxt); + resultHandler.accept(txValidator); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.endResult("Tx not found", false)); + }); + + } + }; + return myCallback; + } + + // ///////////////////////////// + + private List getAllBtcFeeReceivers() { + List btcFeeReceivers = new ArrayList<>(); + // fee receivers from filter ref: bisq-network/bisq/pull/4294 + List feeReceivers = Optional.ofNullable(filterManager.getFilter()) + .flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses())) + .orElse(List.of()); + feeReceivers.forEach(e -> { + try { + btcFeeReceivers.add(e.split("#")[0]); // victim's receiver address + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + }); + btcFeeReceivers.addAll(daoFacade.getAllDonationAddresses()); + log.info("Known BTC fee receivers: {}", btcFeeReceivers.toString()); + + return btcFeeReceivers; + } + + private boolean isServiceSupported() { + if (filterManager.getFilter() != null && filterManager.getFilter().isDisableMempoolValidation()) { + log.info("MempoolService bypassed by filter setting disableMempoolValidation=true"); + return false; + } + if (config.bypassMempoolValidation) { + log.info("MempoolService bypassed by config setting bypassMempoolValidation=true"); + return false; + } + if (!Config.baseCurrencyNetwork().isMainnet()) { + log.info("MempoolService only supports mainnet"); + return false; + } + return true; + } +} diff --git a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java new file mode 100644 index 0000000000..7a70f54363 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java @@ -0,0 +1,394 @@ +/* + * 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 bisq.core.provider.mempool; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +import static bisq.core.util.coin.CoinUtil.maxCoin; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Getter +public class TxValidator { + private final static double FEE_TOLERANCE = 0.95; // we expect fees to be at least 95% of target + private final static long BLOCK_TOLERANCE = 599999L; // allow really old offers with weird fee addresses + + private final DaoStateService daoStateService; + private final List errorList; + private final String txId; + private Coin amount; + @Nullable + private Boolean isFeeCurrencyBtc = null; + @Nullable + private Long chainHeight; + @Setter + private String jsonTxt; + + + public TxValidator(DaoStateService daoStateService, String txId, Coin amount, @Nullable Boolean isFeeCurrencyBtc) { + this.daoStateService = daoStateService; + this.txId = txId; + this.amount = amount; + this.isFeeCurrencyBtc = isFeeCurrencyBtc; + this.errorList = new ArrayList<>(); + this.jsonTxt = ""; + } + + public TxValidator(DaoStateService daoStateService, String txId) { + this.daoStateService = daoStateService; + this.txId = txId; + this.chainHeight = (long) daoStateService.getChainHeight(); + this.errorList = new ArrayList<>(); + this.jsonTxt = ""; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxValidator parseJsonValidateMakerFeeTx(String jsonTxt, List btcFeeReceivers) { + this.jsonTxt = jsonTxt; + boolean status = initialSanityChecks(txId, jsonTxt); + try { + if (status) { + if (checkNotNull(isFeeCurrencyBtc)) { + status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) + && checkFeeAmountBTC(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); + } else { + status = checkFeeAmountBSQ(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); + } + } + } catch (JsonSyntaxException e) { + String s = "The maker fee tx JSON validation failed with reason: " + e.toString(); + log.info(s); + errorList.add(s); + status = false; + } + return endResult("Maker tx validation", status); + } + + public TxValidator parseJsonValidateTakerFeeTx(String jsonTxt, List btcFeeReceivers) { + this.jsonTxt = jsonTxt; + boolean status = initialSanityChecks(txId, jsonTxt); + try { + if (status) { + if (isFeeCurrencyBtc == null) { + isFeeCurrencyBtc = checkFeeAddressBTC(jsonTxt, btcFeeReceivers); + } + if (isFeeCurrencyBtc) { + status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) + && checkFeeAmountBTC(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); + } else { + status = checkFeeAmountBSQ(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); + } + } + } catch (JsonSyntaxException e) { + String s = "The taker fee tx JSON validation failed with reason: " + e.toString(); + log.info(s); + errorList.add(s); + status = false; + } + return endResult("Taker tx validation", status); + } + + public long parseJsonValidateTx() { + if (!initialSanityChecks(txId, jsonTxt)) { + return -1; + } + return getTxConfirms(jsonTxt, chainHeight); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean checkFeeAddressBTC(String jsonTxt, List btcFeeReceivers) { + try { + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonFeeAddress = jsonVout0.get("scriptpubkey_address"); + log.debug("fee address: {}", jsonFeeAddress.getAsString()); + if (btcFeeReceivers.contains(jsonFeeAddress.getAsString())) { + return true; + } else if (getBlockHeightForFeeCalculation(jsonTxt) < BLOCK_TOLERANCE) { + log.info("Leniency rule, unrecognised fee receiver but its a really old offer so let it pass, {}", jsonFeeAddress.getAsString()); + return true; + } else { + String error = "fee address: " + jsonFeeAddress.getAsString() + " was not a known BTC fee receiver"; + errorList.add(error); + log.info(error); + } + } catch (JsonSyntaxException e) { + errorList.add(e.toString()); + log.warn(e.toString()); + } + return false; + } + + private boolean checkFeeAmountBTC(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { + JsonArray jsonVin = getVinAndVout(jsonTxt).first; + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); + JsonElement jsonFeeValue = jsonVout0.get("value"); + if (jsonVIn0Value == null || jsonFeeValue == null) { + throw new JsonSyntaxException("vin/vout missing data"); + } + long feeValue = jsonFeeValue.getAsLong(); + log.debug("BTC fee: {}", feeValue); + Coin expectedFee = getFeeHistorical(tradeAmount, + isMaker ? getMakerFeeRateBtc(blockHeight) : getTakerFeeRateBtc(blockHeight), + isMaker ? Param.MIN_MAKER_FEE_BTC : Param.MIN_TAKER_FEE_BTC); + double leniencyCalc = feeValue / (double) expectedFee.getValue(); + String description = "Expected BTC fee: " + expectedFee.toString() + " sats , actual fee paid: " + Coin.valueOf(feeValue).toString() + " sats"; + if (expectedFee.getValue() == feeValue) { + log.debug("The fee matched what we expected"); + return true; + } else if (expectedFee.getValue() < feeValue) { + log.info("The fee was more than what we expected: " + description); + return true; + } else if (leniencyCalc > FEE_TOLERANCE) { + log.info("Leniency rule: the fee was low, but above {} of what was expected {} {}", FEE_TOLERANCE, leniencyCalc, description); + return true; + } else if (feeExistsUsingDifferentDaoParam(tradeAmount, Coin.valueOf(feeValue), + isMaker ? Param.DEFAULT_MAKER_FEE_BTC : Param.DEFAULT_TAKER_FEE_BTC, + isMaker ? Param.MIN_MAKER_FEE_BTC : Param.MIN_TAKER_FEE_BTC)) { + log.info("Leniency rule: the fee matches a different DAO parameter {}", description); + return true; + } else { + String feeUnderpaidMessage = "UNDERPAID. " + description; + errorList.add(feeUnderpaidMessage); + log.info(feeUnderpaidMessage); + } + return false; + } + + // I think its better to postpone BSQ fee check once the BSQ trade fee tx is confirmed and then use the BSQ explorer to request the + // BSQ fee to check if it is correct. + // Otherwise the requirements here become very complicated and potentially impossible to verify as we don't know + // if inputs and outputs are valid BSQ without the BSQ parser and confirmed transactions. + private boolean checkFeeAmountBSQ(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { + JsonArray jsonVin = getVinAndVout(jsonTxt).first; + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); + JsonElement jsonFeeValue = jsonVout0.get("value"); + if (jsonVIn0Value == null || jsonFeeValue == null) { + throw new JsonSyntaxException("vin/vout missing data"); + } + Coin expectedFee = getFeeHistorical(tradeAmount, + isMaker ? getMakerFeeRateBsq(blockHeight) : getTakerFeeRateBsq(blockHeight), + isMaker ? Param.MIN_MAKER_FEE_BSQ : Param.MIN_TAKER_FEE_BSQ); + long feeValue = jsonVIn0Value.getAsLong() - jsonFeeValue.getAsLong(); + // if the first output (BSQ) is greater than the first input (BSQ) include the second input (presumably BSQ) + if (jsonFeeValue.getAsLong() > jsonVIn0Value.getAsLong()) { + // in this case 2 or more UTXOs were spent to pay the fee: + //TODO missing handling of > 2 BSQ inputs + JsonObject jsonVin1 = jsonVin.get(1).getAsJsonObject(); + JsonElement jsonVIn1Value = jsonVin1.getAsJsonObject("prevout").get("value"); + feeValue += jsonVIn1Value.getAsLong(); + } + log.debug("BURNT BSQ maker fee: {} BSQ ({} sats)", (double) feeValue / 100.0, feeValue); + double leniencyCalc = feeValue / (double) expectedFee.getValue(); + String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ", + (double) expectedFee.getValue() / 100.0, (double) feeValue / 100.0); + if (expectedFee.getValue() == feeValue) { + log.debug("The fee matched what we expected"); + return true; + } else if (expectedFee.getValue() < feeValue) { + log.info("The fee was more than what we expected. " + description); + return true; + } else if (leniencyCalc > FEE_TOLERANCE) { + log.info("Leniency rule: the fee was low, but above {} of what was expected {} {}", FEE_TOLERANCE, leniencyCalc, description); + return true; + } else if (feeExistsUsingDifferentDaoParam(tradeAmount, Coin.valueOf(feeValue), + isMaker ? Param.DEFAULT_MAKER_FEE_BSQ : Param.DEFAULT_TAKER_FEE_BSQ, + isMaker ? Param.MIN_MAKER_FEE_BSQ : Param.MIN_TAKER_FEE_BSQ)) { + log.info("Leniency rule: the fee matches a different DAO parameter {}", description); + return true; + } else { + errorList.add(description); + log.info(description); + } + return false; + } + + private static Tuple2 getVinAndVout(String jsonTxt) throws JsonSyntaxException { + // there should always be "vout" at the top level + // check that there are 2 or 3 vout elements: the fee, the reserved for trade, optional change + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("vin") == null || json.get("vout") == null) { + throw new JsonSyntaxException("missing vin/vout"); + } + JsonArray jsonVin = json.get("vin").getAsJsonArray(); + JsonArray jsonVout = json.get("vout").getAsJsonArray(); + if (jsonVin == null || jsonVout == null || jsonVin.size() < 1 || jsonVout.size() < 2) { + throw new JsonSyntaxException("not enough vins/vouts"); + } + return new Tuple2<>(jsonVin, jsonVout); + } + + private static boolean initialSanityChecks(String txId, String jsonTxt) { + // there should always be "status" container element at the top level + if (jsonTxt == null || jsonTxt.length() == 0) { + return false; + } + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("status") == null) { + return false; + } + // there should always be "txid" string element at the top level + if (json.get("txid") == null) { + return false; + } + // txid should match what we requested + if (!txId.equals(json.get("txid").getAsString())) { + return false; + } + JsonObject jsonStatus = json.get("status").getAsJsonObject(); + JsonElement jsonConfirmed = jsonStatus.get("confirmed"); + return (jsonConfirmed != null); + // the json is valid and it contains a "confirmed" field then tx is known to mempool.space + // we don't care if it is confirmed or not, just that it exists. + } + + private static long getTxConfirms(String jsonTxt, long chainHeight) { + long blockHeight = getTxBlockHeight(jsonTxt); + if (blockHeight > 0) { + return (chainHeight - blockHeight) + 1; // if it is in the current block it has 1 conf + } + return 0; // 0 indicates unconfirmed + } + + // we want the block height applicable for calculating the appropriate expected trading fees + // if the tx is not yet confirmed, use current block tip, if tx is confirmed use the block it was confirmed at. + private long getBlockHeightForFeeCalculation(String jsonTxt) { + long txBlockHeight = getTxBlockHeight(jsonTxt); + if (txBlockHeight > 0) { + return txBlockHeight; + } + return daoStateService.getChainHeight(); + } + + // this would be useful for the arbitrator verifying that the delayed payout tx is confirmed + private static long getTxBlockHeight(String jsonTxt) { + // there should always be "status" container element at the top level + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("status") == null) { + return -1L; + } + JsonObject jsonStatus = json.get("status").getAsJsonObject(); + JsonElement jsonConfirmed = jsonStatus.get("confirmed"); + if (jsonConfirmed == null) { + return -1L; + } + if (jsonConfirmed.getAsBoolean()) { + // it is confirmed, lets get the block height + JsonElement jsonBlockHeight = jsonStatus.get("block_height"); + if (jsonBlockHeight == null) { + return -1L; // block height error + } + return (jsonBlockHeight.getAsLong()); + } + return 0L; // in mempool, not confirmed yet + } + + private Coin getFeeHistorical(Coin amount, Coin feeRatePerBtc, Param minFeeParam) { + double feePerBtcAsDouble = (double) feeRatePerBtc.value; + double amountAsDouble = amount != null ? (double) amount.value : 0; + double btcAsDouble = (double) Coin.COIN.value; + double fact = amountAsDouble / btcAsDouble; + Coin feePerBtc = Coin.valueOf(Math.round(feePerBtcAsDouble * fact)); + Coin minFee = daoStateService.getParamValueAsCoin(minFeeParam, minFeeParam.getDefaultValue()); + return maxCoin(feePerBtc, minFee); + } + + private Coin getMakerFeeRateBsq(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_MAKER_FEE_BSQ, (int) blockHeight); + } + + private Coin getTakerFeeRateBsq(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_TAKER_FEE_BSQ, (int) blockHeight); + } + + private Coin getMakerFeeRateBtc(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_MAKER_FEE_BTC, (int) blockHeight); + } + + private Coin getTakerFeeRateBtc(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_TAKER_FEE_BTC, (int) blockHeight); + } + + // implements leniency rule of accepting old DAO rate parameters: https://github.com/bisq-network/bisq/issues/5329#issuecomment-803223859 + // We iterate over all past dao param values and if one of those matches we consider it valid. That covers the non-in-sync cases. + private boolean feeExistsUsingDifferentDaoParam(Coin tradeAmount, Coin actualFeeValue, Param defaultFeeParam, Param minFeeParam) { + for (Coin daoHistoricalRate : daoStateService.getParamChangeList(defaultFeeParam)) { + if (actualFeeValue.equals(getFeeHistorical(tradeAmount, daoHistoricalRate, minFeeParam))) { + return true; + } + } + // finally check the default rate used when we ask for the fee rate at block height 0 (it is hard coded in the Param enum) + Coin defaultRate = daoStateService.getParamValueAsCoin(defaultFeeParam, 0); + return actualFeeValue.equals(getFeeHistorical(tradeAmount, defaultRate, minFeeParam)); + } + + public TxValidator endResult(String title, boolean status) { + log.info("{} : {}", title, status ? "SUCCESS" : "FAIL"); + if (!status) { + errorList.add(title); + } + return this; + } + + public boolean isFail() { + return errorList.size() > 0; + } + + public boolean getResult() { + return errorList.size() == 0; + } + + public String errorSummary() { + return errorList.toString().substring(0, Math.min(85, errorList.toString().length())); + } + + public String toString() { + return errorList.toString(); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/MarketPrice.java b/core/src/main/java/bisq/core/provider/price/MarketPrice.java new file mode 100644 index 0000000000..26111f2cdf --- /dev/null +++ b/core/src/main/java/bisq/core/provider/price/MarketPrice.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.provider.price; + +import java.time.Instant; + +import lombok.Getter; +import lombok.Value; + +@Value +public class MarketPrice { + public static final long MARKET_PRICE_MAX_AGE_SEC = 1800; // 30 min + + private final String currencyCode; + private final double price; + private final long timestampSec; + @Getter + private final boolean isExternallyProvidedPrice; + + public MarketPrice(String currencyCode, double price, long timestampSec, boolean isExternallyProvidedPrice) { + this.currencyCode = currencyCode; + this.price = price; + this.timestampSec = timestampSec; + this.isExternallyProvidedPrice = isExternallyProvidedPrice; + } + + public boolean isPriceAvailable() { + return price > 0; + } + + public boolean isRecentPriceAvailable() { + return isPriceAvailable() && timestampSec > (Instant.now().getEpochSecond() - MARKET_PRICE_MAX_AGE_SEC); + } + + public boolean isRecentExternalPriceAvailable() { + return isExternallyProvidedPrice && isRecentPriceAvailable(); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java new file mode 100644 index 0000000000..57e9a5197d --- /dev/null +++ b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java @@ -0,0 +1,427 @@ +/* + * 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 bisq.core.provider.price; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.provider.PriceHttpClient; +import bisq.core.provider.ProvidersRepository; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.user.Preferences; + +import bisq.network.http.HttpClient; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.handlers.FaultHandler; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import com.google.inject.Inject; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.time.Instant; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class PriceFeedService { + private final HttpClient httpClient; + private final ProvidersRepository providersRepository; + private final Preferences preferences; + + private static final long PERIOD_SEC = 60; + + private final Map cache = new HashMap<>(); + private PriceProvider priceProvider; + @Nullable + private Consumer priceConsumer; + @Nullable + private FaultHandler faultHandler; + private String currencyCode; + private final StringProperty currencyCodeProperty = new SimpleStringProperty(); + private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); + private long epochInMillisAtLastRequest; + private long retryDelay = 1; + private long requestTs; + @Nullable + private String baseUrlOfRespondingProvider; + @Nullable + private Timer requestTimer; + @Nullable + private PriceRequest priceRequest; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PriceFeedService(PriceHttpClient httpClient, + @SuppressWarnings("SameParameterValue") ProvidersRepository providersRepository, + @SuppressWarnings("SameParameterValue") Preferences preferences) { + this.httpClient = httpClient; + this.providersRepository = providersRepository; + this.preferences = preferences; + + // Do not use Guice for PriceProvider as we might create multiple instances + this.priceProvider = new PriceProvider(httpClient, providersRepository.getBaseUrl()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown() { + if (requestTimer != null) { + requestTimer.stop(); + requestTimer = null; + } + if (priceRequest != null) { + priceRequest.shutDown(); + } + } + + public void setCurrencyCodeOnInit() { + if (getCurrencyCode() == null) { + TradeCurrency preferredTradeCurrency = preferences.getPreferredTradeCurrency(); + String code = preferredTradeCurrency != null ? preferredTradeCurrency.getCode() : "USD"; + setCurrencyCode(code); + } + } + + public void initialRequestPriceFeed() { + request(false); + } + + public boolean hasPrices() { + return !cache.isEmpty(); + } + + public void requestPriceFeed(Consumer resultHandler, FaultHandler faultHandler) { + this.priceConsumer = resultHandler; + this.faultHandler = faultHandler; + + request(true); + } + + public String getProviderNodeAddress() { + return httpClient.getBaseUrl(); + } + + private void request(boolean repeatRequests) { + if (requestTs == 0) + log.debug("request from provider {}", + providersRepository.getBaseUrl()); + else + log.debug("request from provider {} {} sec. after last request", + providersRepository.getBaseUrl(), + (System.currentTimeMillis() - requestTs) / 1000d); + + requestTs = System.currentTimeMillis(); + + baseUrlOfRespondingProvider = null; + + requestAllPrices(priceProvider, () -> { + baseUrlOfRespondingProvider = priceProvider.getBaseUrl(); + + // At applyPriceToConsumer we also check if price is not exceeding max. age for price data. + boolean success = applyPriceToConsumer(); + if (success) { + MarketPrice marketPrice = cache.get(currencyCode); + if (marketPrice != null) + log.debug("Received new {} from provider {} after {} sec.", + marketPrice, + baseUrlOfRespondingProvider, + (System.currentTimeMillis() - requestTs) / 1000d); + else + log.debug("Received new data from provider {} after {} sec. " + + "Requested market price for currency {} was not provided. " + + "That is expected if currency is not listed at provider.", + baseUrlOfRespondingProvider, + (System.currentTimeMillis() - requestTs) / 1000d, + currencyCode); + } else { + log.warn("applyPriceToConsumer was not successful. We retry with a new provider."); + retryWithNewProvider(); + } + }, (errorMessage, throwable) -> { + if (throwable instanceof PriceRequestException) { + String baseUrlOfFaultyRequest = ((PriceRequestException) throwable).priceProviderBaseUrl; + String baseUrlOfCurrentRequest = priceProvider.getBaseUrl(); + if (baseUrlOfCurrentRequest.equals(baseUrlOfFaultyRequest)) { + log.warn("We received an error: baseUrlOfCurrentRequest={}, baseUrlOfFaultyRequest={}, error={}", + baseUrlOfCurrentRequest, baseUrlOfFaultyRequest, throwable.toString()); + retryWithNewProvider(); + } else { + log.debug("We received an error from an earlier request. We have started a new request already so we ignore that error. " + + "baseUrlOfCurrentRequest={}, baseUrlOfFaultyRequest={}", + baseUrlOfCurrentRequest, baseUrlOfFaultyRequest); + } + } else { + log.warn("We received an error with throwable={}", throwable.toString()); + retryWithNewProvider(); + } + + if (faultHandler != null) + faultHandler.handleFault(errorMessage, throwable); + }); + + if (repeatRequests) { + if (requestTimer != null) + requestTimer.stop(); + + long delay = PERIOD_SEC + new Random().nextInt(5); + requestTimer = UserThread.runAfter(() -> { + // If we have not received a result from the last request. We try a new provider. + if (baseUrlOfRespondingProvider == null) { + final String oldBaseUrl = priceProvider.getBaseUrl(); + setNewPriceProvider(); + log.warn("We did not received a response from provider {}. " + + "We select the new provider {} and use that for a new request.", oldBaseUrl, priceProvider.getBaseUrl()); + } + request(true); + }, delay); + } + } + + private void retryWithNewProvider() { + // We increase retry delay each time until we reach PERIOD_SEC to not exceed requests. + UserThread.runAfter(() -> { + retryDelay = Math.min(retryDelay + 5, PERIOD_SEC); + + String oldBaseUrl = priceProvider.getBaseUrl(); + setNewPriceProvider(); + log.warn("We received an error at the request from provider {}. " + + "We select the new provider {} and use that for a new request. retryDelay was {} sec.", oldBaseUrl, priceProvider.getBaseUrl(), retryDelay); + + request(true); + }, retryDelay); + } + + private void setNewPriceProvider() { + providersRepository.selectNextProviderBaseUrl(); + if (!providersRepository.getBaseUrl().isEmpty()) + priceProvider = new PriceProvider(httpClient, providersRepository.getBaseUrl()); + else + log.warn("We cannot create a new priceProvider because new base url is empty."); + } + + @Nullable + public MarketPrice getMarketPrice(String currencyCode) { + return cache.getOrDefault(currencyCode, null); + } + + private void setBisqMarketPrice(String currencyCode, Price price) { + if (!cache.containsKey(currencyCode) || !cache.get(currencyCode).isExternallyProvidedPrice()) { + cache.put(currencyCode, new MarketPrice(currencyCode, + MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? 8 : 4), + 0, + false)); + updateCounter.set(updateCounter.get() + 1); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setCurrencyCode(String currencyCode) { + if (this.currencyCode == null || !this.currencyCode.equals(currencyCode)) { + this.currencyCode = currencyCode; + currencyCodeProperty.set(currencyCode); + if (priceConsumer != null) + applyPriceToConsumer(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getCurrencyCode() { + return currencyCode; + } + + public StringProperty currencyCodeProperty() { + return currencyCodeProperty; + } + + public ReadOnlyIntegerProperty updateCounterProperty() { + return updateCounter; + } + + public Date getLastRequestTimeStamp() { + return new Date(epochInMillisAtLastRequest); + } + + public void applyLatestBisqMarketPrice(Set tradeStatisticsSet) { + // takes about 10 ms for 5000 items + Map> mapByCurrencyCode = new HashMap<>(); + tradeStatisticsSet.forEach(e -> { + List list; + String currencyCode = e.getCurrency(); + if (mapByCurrencyCode.containsKey(currencyCode)) { + list = mapByCurrencyCode.get(currencyCode); + } else { + list = new ArrayList<>(); + mapByCurrencyCode.put(currencyCode, list); + } + list.add(e); + }); + + mapByCurrencyCode.values().stream() + .filter(list -> !list.isEmpty()) + .forEach(list -> { + list.sort(Comparator.comparing(TradeStatistics3::getDate)); + TradeStatistics3 tradeStatistics = list.get(list.size() - 1); + setBisqMarketPrice(tradeStatistics.getCurrency(), tradeStatistics.getTradePrice()); + }); + } + + public Optional getBsqPrice() { + MarketPrice bsqMarketPrice = getMarketPrice("BSQ"); + if (bsqMarketPrice != null) { + long bsqPriceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(bsqMarketPrice.getPrice(), Altcoin.SMALLEST_UNIT_EXPONENT)); + return Optional.of(Price.valueOf("BSQ", bsqPriceAsLong)); + } else { + return Optional.empty(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean applyPriceToConsumer() { + boolean result = false; + String errorMessage = null; + if (currencyCode != null) { + String baseUrl = priceProvider.getBaseUrl(); + if (cache.containsKey(currencyCode)) { + try { + MarketPrice marketPrice = cache.get(currencyCode); + if (marketPrice.isExternallyProvidedPrice()) { + if (marketPrice.isRecentPriceAvailable()) { + if (priceConsumer != null) + priceConsumer.accept(marketPrice.getPrice()); + result = true; + } else { + errorMessage = "Price for currency " + currencyCode + " is outdated by " + + (Instant.now().getEpochSecond() - marketPrice.getTimestampSec()) / 60 + " minutes. " + + "Max. allowed age of price is " + MarketPrice.MARKET_PRICE_MAX_AGE_SEC / 60 + " minutes. " + + "priceProvider=" + baseUrl + ". " + + "marketPrice= " + marketPrice; + } + } else { + if (baseUrlOfRespondingProvider == null) + log.debug("Market price for currency " + currencyCode + " was not delivered by provider " + + baseUrl + ". That is expected at startup."); + else + log.debug("Market price for currency " + currencyCode + " is not provided by the provider " + + baseUrl + ". That is expected for currencies not listed at providers."); + result = true; + } + } catch (Throwable t) { + errorMessage = "Exception at applyPriceToConsumer for currency " + currencyCode + + ". priceProvider=" + baseUrl + ". Exception=" + t; + } + } else { + log.debug("We don't have a price for currency " + currencyCode + ". priceProvider=" + baseUrl + + ". That is expected for currencies not listed at providers."); + result = true; + } + } else { + errorMessage = "We don't have a currency yet set. That should never happen"; + } + + if (errorMessage != null) { + log.warn(errorMessage); + if (faultHandler != null) + faultHandler.handleFault(errorMessage, new PriceRequestException(errorMessage)); + } + + updateCounter.set(updateCounter.get() + 1); + + return result; + } + + private void requestAllPrices(PriceProvider provider, Runnable resultHandler, FaultHandler faultHandler) { + if (httpClient.hasPendingRequest()) { + log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient); + return; + } + + priceRequest = new PriceRequest(); + SettableFuture, Map>> future = priceRequest.requestAllPrices(provider); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Tuple2, Map> result) { + UserThread.execute(() -> { + checkNotNull(result, "Result must not be null at requestAllPrices"); + // Each currency rate has a different timestamp, depending on when + // the priceNode aggregate rate was calculated + // However, the request timestamp is when the pricenode was queried + epochInMillisAtLastRequest = System.currentTimeMillis(); + + Map priceMap = result.second; + + cache.putAll(priceMap); + + resultHandler.run(); + }); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> faultHandler.handleFault("Could not load marketPrices", throwable)); + } + }, MoreExecutors.directExecutor()); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java new file mode 100644 index 0000000000..3a69bb0f61 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -0,0 +1,97 @@ +/* + * 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 bisq.core.provider.price; + +import bisq.core.provider.HttpClientProvider; + +import bisq.network.http.HttpClient; +import bisq.network.p2p.P2PService; + +import bisq.common.app.Version; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import com.google.gson.Gson; +import com.google.gson.internal.LinkedTreeMap; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceProvider extends HttpClientProvider { + + private boolean shutDownRequested; + + // Do not use Guice here as we might create multiple instances + public PriceProvider(HttpClient httpClient, String baseUrl) { + super(httpClient, baseUrl, false); + } + + public Tuple2, Map> getAll() throws IOException { + if (shutDownRequested) { + return new Tuple2<>(new HashMap<>(), new HashMap<>()); + } + + Map marketPriceMap = new HashMap<>(); + String hsVersion = ""; + if (P2PService.getMyNodeAddress() != null) + hsVersion = P2PService.getMyNodeAddress().getHostName().length() > 22 ? ", HSv3" : ", HSv2"; + + String json = httpClient.get("getAllMarketPrices", "User-Agent", "bisq/" + + Version.VERSION + hsVersion); + + + LinkedTreeMap map = new Gson().fromJson(json, LinkedTreeMap.class); + Map tsMap = new HashMap<>(); + tsMap.put("btcAverageTs", ((Double) map.get("btcAverageTs")).longValue()); + tsMap.put("poloniexTs", ((Double) map.get("poloniexTs")).longValue()); + tsMap.put("coinmarketcapTs", ((Double) map.get("coinmarketcapTs")).longValue()); + + List list = (ArrayList) map.get("data"); + list.forEach(obj -> { + try { + LinkedTreeMap treeMap = (LinkedTreeMap) obj; + String currencyCode = (String) treeMap.get("currencyCode"); + double price = (Double) treeMap.get("price"); + // json uses double for our timestampSec long value... + long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); + marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + } + + }); + return new Tuple2<>(tsMap, marketPriceMap); + } + + public String getBaseUrl() { + return httpClient.getBaseUrl(); + } + + public void shutDown() { + shutDownRequested = true; + httpClient.shutDown(); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/PriceRequest.java b/core/src/main/java/bisq/core/provider/price/PriceRequest.java new file mode 100644 index 0000000000..fa9134099c --- /dev/null +++ b/core/src/main/java/bisq/core/provider/price/PriceRequest.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.core.provider.price; + +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Slf4j +public class PriceRequest { + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("PriceRequest", 3, 5, 10 * 60); + @Nullable + private PriceProvider provider; + private boolean shutDownRequested; + + public PriceRequest() { + } + + public SettableFuture, Map>> requestAllPrices(PriceProvider provider) { + this.provider = provider; + String baseUrl = provider.getBaseUrl(); + SettableFuture, Map>> resultFuture = SettableFuture.create(); + ListenableFuture, Map>> future = executorService.submit(() -> { + Thread.currentThread().setName("PriceRequest @ " + baseUrl); + return provider.getAll(); + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(Tuple2, Map> marketPriceTuple) { + log.trace("Received marketPriceTuple of {}\nfrom provider {}", marketPriceTuple, provider); + if (!shutDownRequested) { + resultFuture.set(marketPriceTuple); + } + + } + + public void onFailure(@NotNull Throwable throwable) { + if (!shutDownRequested) { + resultFuture.setException(new PriceRequestException(throwable, baseUrl)); + } + } + }, MoreExecutors.directExecutor()); + + return resultFuture; + } + + public void shutDown() { + shutDownRequested = true; + if (provider != null) { + provider.shutDown(); + } + Utilities.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/PriceRequestException.java b/core/src/main/java/bisq/core/provider/price/PriceRequestException.java new file mode 100644 index 0000000000..ac11e03ce8 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/price/PriceRequestException.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.provider.price; + + +import javax.annotation.Nullable; + +public class PriceRequestException extends Exception { + @Nullable + public String priceProviderBaseUrl; + + public PriceRequestException(String errorMessage) { + super(errorMessage); + } + + public PriceRequestException(Throwable throwable, String priceProviderBaseUrl) { + super(throwable); + this.priceProviderBaseUrl = priceProviderBaseUrl; + } +} diff --git a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java new file mode 100644 index 0000000000..69485612c7 --- /dev/null +++ b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java @@ -0,0 +1,66 @@ +/* + * 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 bisq.core.setup; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.config.Config; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CoreNetworkCapabilities { + + static void setSupportedCapabilities(Config config) { + Capabilities.app.addAll( + Capability.TRADE_STATISTICS, + Capability.TRADE_STATISTICS_2, + Capability.ACCOUNT_AGE_WITNESS, + Capability.ACK_MSG, + Capability.PROPOSAL, + Capability.BLIND_VOTE, + Capability.DAO_STATE, + Capability.BUNDLE_OF_ENVELOPES, + Capability.MEDIATION, + Capability.SIGNED_ACCOUNT_AGE_WITNESS, + Capability.REFUND_AGENT, + Capability.TRADE_STATISTICS_HASH_UPDATE, + Capability.NO_ADDRESS_PRE_FIX, + Capability.TRADE_STATISTICS_3 + ); + + if (config.daoActivated) { + maybeApplyDaoFullMode(config); + } + + log.info(Capabilities.app.prettyPrint()); + } + + public static void maybeApplyDaoFullMode(Config config) { + // If we set dao full mode at the preferences view we add the capability there. We read the preferences a + // bit later than we call that method so we have to add DAO_FULL_NODE Capability at preferences as well to + // be sure it is set in both cases. + if (config.fullDaoNode) { + Capabilities.app.addAll(Capability.DAO_FULL_NODE); + } else { + // A lite node has the capability to receive bsq blocks. We do not want to send BSQ blocks to full nodes + // as they ignore them anyway. + Capabilities.app.addAll(Capability.RECEIVE_BSQ_BLOCK); + } + } +} diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java new file mode 100644 index 0000000000..0e54e5527c --- /dev/null +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.core.setup; + +import bisq.core.btc.model.AddressEntryList; +import bisq.core.dao.governance.ballot.BallotListService; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.bond.reputation.MyReputationListService; +import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proposal.MyProposalListService; +import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService; +import bisq.core.support.dispute.mediation.MediationDisputeListService; +import bisq.core.support.dispute.refund.RefundDisputeListService; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.mailbox.IgnoredMailboxService; +import bisq.network.p2p.mailbox.MailboxMessageService; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.persistence.RemovedPayloadsService; + +import bisq.common.config.Config; +import bisq.common.proto.persistable.PersistedDataHost; + +import com.google.inject.Injector; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CorePersistedDataHost { + + // All classes which are persisting objects need to be added here + public static List getPersistedDataHosts(Injector injector) { + List persistedDataHosts = new ArrayList<>(); + persistedDataHosts.add(injector.getInstance(Preferences.class)); + persistedDataHosts.add(injector.getInstance(User.class)); + persistedDataHosts.add(injector.getInstance(AddressEntryList.class)); + persistedDataHosts.add(injector.getInstance(OpenOfferManager.class)); + persistedDataHosts.add(injector.getInstance(TradeManager.class)); + persistedDataHosts.add(injector.getInstance(ClosedTradableManager.class)); + persistedDataHosts.add(injector.getInstance(FailedTradesManager.class)); + persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class)); + persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class)); + persistedDataHosts.add(injector.getInstance(RefundDisputeListService.class)); + persistedDataHosts.add(injector.getInstance(P2PDataStorage.class)); + persistedDataHosts.add(injector.getInstance(PeerManager.class)); + persistedDataHosts.add(injector.getInstance(MailboxMessageService.class)); + persistedDataHosts.add(injector.getInstance(IgnoredMailboxService.class)); + persistedDataHosts.add(injector.getInstance(RemovedPayloadsService.class)); + + if (injector.getInstance(Config.class).daoActivated) { + persistedDataHosts.add(injector.getInstance(BallotListService.class)); + persistedDataHosts.add(injector.getInstance(MyBlindVoteListService.class)); + persistedDataHosts.add(injector.getInstance(MyVoteListService.class)); + persistedDataHosts.add(injector.getInstance(MyProposalListService.class)); + persistedDataHosts.add(injector.getInstance(MyReputationListService.class)); + persistedDataHosts.add(injector.getInstance(MyProofOfBurnListService.class)); + persistedDataHosts.add(injector.getInstance(UnconfirmedBsqChangeOutputListService.class)); + } + return persistedDataHosts; + } +} diff --git a/core/src/main/java/bisq/core/setup/CoreSetup.java b/core/src/main/java/bisq/core/setup/CoreSetup.java new file mode 100644 index 0000000000..8f5eea38aa --- /dev/null +++ b/core/src/main/java/bisq/core/setup/CoreSetup.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.setup; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; + +import bisq.common.config.Config; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CoreSetup { + + public static void setup(Config config) { + CoreNetworkCapabilities.setSupportedCapabilities(config); + Res.setup(); + CurrencyUtil.setup(); + } + +} diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java new file mode 100644 index 0000000000..d353ed0606 --- /dev/null +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -0,0 +1,331 @@ +/* + * 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 bisq.core.support; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; +import bisq.network.p2p.mailbox.MailboxMessageService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArraySet; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class SupportManager { + protected final P2PService p2PService; + protected final WalletsSetup walletsSetup; + protected final Map delayMsgMap = new HashMap<>(); + private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); + protected final MailboxMessageService mailboxMessageService; + private boolean allServicesInitialized; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public SupportManager(P2PService p2PService, WalletsSetup walletsSetup) { + this.p2PService = p2PService; + mailboxMessageService = p2PService.getMailboxMessageService(); + + this.walletsSetup = walletsSetup; + + // We get first the message handler called then the onBootstrapped + p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored + decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored + decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void onSupportMessage(SupportMessage networkEnvelope); + + public abstract NodeAddress getPeerNodeAddress(ChatMessage message); + + public abstract PubKeyRing getPeerPubKeyRing(ChatMessage message); + + public abstract SupportType getSupportType(); + + public abstract boolean channelOpen(ChatMessage message); + + public abstract List getAllChatMessages(); + + public abstract void addAndPersistChatMessage(ChatMessage message); + + public abstract void requestPersistence(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegates p2pService + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isBootstrapped() { + return p2PService.isBootstrapped(); + } + + public NodeAddress getMyAddress() { + return p2PService.getAddress(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + allServicesInitialized = true; + } + + public void tryApplyMessages() { + if (isReady()) + applyMessages(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void onChatMessage(ChatMessage chatMessage) { + final String tradeId = chatMessage.getTradeId(); + final String uid = chatMessage.getUid(); + boolean channelOpen = channelOpen(chatMessage); + if (!channelOpen) { + log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + Timer timer = UserThread.runAfter(() -> onChatMessage(chatMessage), 1); + delayMsgMap.put(uid, timer); + } else { + String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; + log.warn(msg); + } + return; + } + + cleanupRetryMap(uid); + PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage); + + addAndPersistChatMessage(chatMessage); + + // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we + // cannot send it anyway) + if (receiverPubKeyRing != null) + sendAckMessage(chatMessage, receiverPubKeyRing, true, null); + } + + private void onAckMessage(AckMessage ackMessage) { + if (ackMessage.getSourceType() == getAckMessageSourceType()) { + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} with tradeId {} and uid {}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); + } + + getAllChatMessages().stream() + .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) + .forEach(msg -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); + requestPersistence(); + } + } + + protected abstract AckMessageSourceType getAckMessageSourceType(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send message + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChatMessage sendChatMessage(ChatMessage message) { + NodeAddress peersNodeAddress = getPeerNodeAddress(message); + PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(message); + if (peersNodeAddress == null || receiverPubKeyRing == null) { + UserThread.runAfter(() -> + message.setSendMessageError(Res.get("support.receiverNotKnown")), 1); + } else { + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + receiverPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setArrived(true); + requestPersistence(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setStoredInMailbox(true); + requestPersistence(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + message.setSendMessageError(errorMessage); + requestPersistence(); + } + } + ); + } + + return message; + } + + protected void sendAckMessage(SupportMessage supportMessage, PubKeyRing peersPubKeyRing, + boolean result, @Nullable String errorMessage) { + String tradeId = supportMessage.getTradeId(); + String uid = supportMessage.getUid(); + AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), + getAckMessageSourceType(), + supportMessage.getClass().getSimpleName(), + uid, + tradeId, + result, + errorMessage); + final NodeAddress peersNodeAddress = supportMessage.getSenderNodeAddress(); + log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + mailboxMessageService.sendEncryptedMailboxMessage( + peersNodeAddress, + peersPubKeyRing, + ackMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onStoredInMailbox() { + log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected boolean canProcessMessage(SupportMessage message) { + return message.getSupportType() == getSupportType(); + } + + protected void cleanupRetryMap(String uid) { + if (delayMsgMap.containsKey(uid)) { + Timer timer = delayMsgMap.remove(uid); + if (timer != null) + timer.stop(); + } + } + + private boolean isReady() { + return allServicesInitialized && + p2PService.isBootstrapped() && + walletsSetup.isDownloadComplete() && + walletsSetup.hasSufficientPeersForBroadcast(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyMessages() { + decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof SupportMessage) { + onSupportMessage((SupportMessage) networkEnvelope); + } else if (networkEnvelope instanceof AckMessage) { + onAckMessage((AckMessage) networkEnvelope); + } + }); + decryptedDirectMessageWithPubKeys.clear(); + + decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName()); + if (networkEnvelope instanceof SupportMessage) { + SupportMessage supportMessage = (SupportMessage) networkEnvelope; + onSupportMessage(supportMessage); + mailboxMessageService.removeMailboxMsg(supportMessage); + } else if (networkEnvelope instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) networkEnvelope; + onAckMessage(ackMessage); + mailboxMessageService.removeMailboxMsg(ackMessage); + } + }); + decryptedMailboxMessageWithPubKeys.clear(); + } +} diff --git a/core/src/main/java/bisq/core/support/SupportSession.java b/core/src/main/java/bisq/core/support/SupportSession.java new file mode 100644 index 0000000000..0354ddba7f --- /dev/null +++ b/core/src/main/java/bisq/core/support/SupportSession.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.core.support; + +import bisq.core.support.messages.ChatMessage; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +/** + * A Support session is using a trade or a dispute to implement the methods. + * It keeps the ChatView transparent if used in dispute or trade chat context. + */ +public abstract class SupportSession { + // todo refactor ui so that can be converted to isTrader + private boolean isClient; + + + protected SupportSession(boolean isClient) { + this.isClient = isClient; + } + + protected SupportSession() { + } + + // todo refactor ui so that can be converted to isTrader + public boolean isClient() { + return isClient; + } + + public abstract String getTradeId(); + + public abstract int getClientId(); + + public abstract ObservableList getObservableChatMessageList(); + + public abstract boolean chatIsOpen(); + + public abstract boolean isDisputeAgent(); +} diff --git a/core/src/main/java/bisq/core/support/SupportType.java b/core/src/main/java/bisq/core/support/SupportType.java new file mode 100644 index 0000000000..9444e7381d --- /dev/null +++ b/core/src/main/java/bisq/core/support/SupportType.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.support; + +import bisq.common.proto.ProtoUtil; + +public enum SupportType { + ARBITRATION, // Need to be at index 0 to be the fallback for old clients + MEDIATION, + TRADE, + REFUND; + + public static SupportType fromProto( + protobuf.SupportType type) { + return ProtoUtil.enumFromProto(SupportType.class, type.name()); + } + + public static protobuf.SupportType toProtoMessage(SupportType supportType) { + return protobuf.SupportType.valueOf(supportType.name()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/Attachment.java b/core/src/main/java/bisq/core/support/dispute/Attachment.java new file mode 100644 index 0000000000..1f425da2bf --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/Attachment.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.common.proto.network.NetworkPayload; + +import com.google.protobuf.ByteString; + +import lombok.Value; + +@Value +public final class Attachment implements NetworkPayload { + private final String fileName; + private final byte[] bytes; + + public Attachment(String fileName, byte[] bytes) { + this.fileName = fileName; + this.bytes = bytes; + } + + @Override + public protobuf.Attachment toProtoMessage() { + return protobuf.Attachment.newBuilder() + .setFileName(fileName) + .setBytes(ByteString.copyFrom(bytes)) + .build(); + } + + public static Attachment fromProto(protobuf.Attachment proto) { + return new Attachment(proto.getFileName(), proto.getBytes().toByteArray()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java new file mode 100644 index 0000000000..98ed154fbe --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -0,0 +1,477 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.core.locale.Res; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Contract; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@EqualsAndHashCode +@Getter +public final class Dispute implements NetworkPayload, PersistablePayload { + + public enum State { + NEEDS_UPGRADE, + NEW, + OPEN, + REOPENED, + CLOSED; + + public static Dispute.State fromProto(protobuf.Dispute.State state) { + return ProtoUtil.enumFromProto(Dispute.State.class, state.name()); + } + + public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { + return protobuf.Dispute.State.valueOf(state.name()); + } + } + + private final String tradeId; + private final String id; + private final int traderId; + private final boolean disputeOpenerIsBuyer; + private final boolean disputeOpenerIsMaker; + // PubKeyRing of trader who opened the dispute + private final PubKeyRing traderPubKeyRing; + private final long tradeDate; + private final long tradePeriodEnd; + private final Contract contract; + @Nullable + private final byte[] contractHash; + @Nullable + private final byte[] depositTxSerialized; + @Nullable + private final byte[] payoutTxSerialized; + @Nullable + private final String depositTxId; + @Nullable + private final String payoutTxId; + private final String contractAsJson; + @Nullable + private final String makerContractSignature; + @Nullable + private final String takerContractSignature; + private final PubKeyRing agentPubKeyRing; // dispute agent + private final boolean isSupportTicket; + private final ObservableList chatMessages = FXCollections.observableArrayList(); + // disputeResultProperty.get is Nullable! + private final ObjectProperty disputeResultProperty = new SimpleObjectProperty<>(); + private final long openingDate; + @Nullable + @Setter + private String disputePayoutTxId; + @Setter + // Added v1.2.0 + private SupportType supportType; + // Only used at refundAgent so that he knows how the mediator resolved the case + @Setter + @Nullable + private String mediatorsDisputeResult; + @Setter + @Nullable + private String delayedPayoutTxId; + + // Added at v1.4.0 + @Setter + @Nullable + private String donationAddressOfDelayedPayoutTx; + // Added at v1.6.0 + private Dispute.State disputeState = State.NEW; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + @Setter + private Map extraDataMap; + + // We do not persist uid, it is only used by dispute agents to guarantee an uid. + @Setter + @Nullable + private transient String uid; + @Setter + private transient long payoutTxConfirms = -1; + + private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); + private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Dispute(long openingDate, + String tradeId, + int traderId, + boolean disputeOpenerIsBuyer, + boolean disputeOpenerIsMaker, + PubKeyRing traderPubKeyRing, + long tradeDate, + long tradePeriodEnd, + Contract contract, + @Nullable byte[] contractHash, + @Nullable byte[] depositTxSerialized, + @Nullable byte[] payoutTxSerialized, + @Nullable String depositTxId, + @Nullable String payoutTxId, + String contractAsJson, + @Nullable String makerContractSignature, + @Nullable String takerContractSignature, + PubKeyRing agentPubKeyRing, + boolean isSupportTicket, + SupportType supportType) { + this.openingDate = openingDate; + this.tradeId = tradeId; + this.traderId = traderId; + this.disputeOpenerIsBuyer = disputeOpenerIsBuyer; + this.disputeOpenerIsMaker = disputeOpenerIsMaker; + this.traderPubKeyRing = traderPubKeyRing; + this.tradeDate = tradeDate; + this.tradePeriodEnd = tradePeriodEnd; + this.contract = contract; + this.contractHash = contractHash; + this.depositTxSerialized = depositTxSerialized; + this.payoutTxSerialized = payoutTxSerialized; + this.depositTxId = depositTxId; + this.payoutTxId = payoutTxId; + this.contractAsJson = contractAsJson; + this.makerContractSignature = makerContractSignature; + this.takerContractSignature = takerContractSignature; + this.agentPubKeyRing = agentPubKeyRing; + this.isSupportTicket = isSupportTicket; + this.supportType = supportType; + + id = tradeId + "_" + traderId; + uid = UUID.randomUUID().toString(); + refreshAlertLevel(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Dispute toProtoMessage() { + // Needed to avoid ConcurrentModificationException + List clonedChatMessages = new ArrayList<>(chatMessages); + protobuf.Dispute.Builder builder = protobuf.Dispute.newBuilder() + .setTradeId(tradeId) + .setTraderId(traderId) + .setDisputeOpenerIsBuyer(disputeOpenerIsBuyer) + .setDisputeOpenerIsMaker(disputeOpenerIsMaker) + .setTraderPubKeyRing(traderPubKeyRing.toProtoMessage()) + .setTradeDate(tradeDate) + .setTradePeriodEnd(tradePeriodEnd) + .setContract(contract.toProtoMessage()) + .setContractAsJson(contractAsJson) + .setAgentPubKeyRing(agentPubKeyRing.toProtoMessage()) + .setIsSupportTicket(isSupportTicket) + .addAllChatMessage(clonedChatMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) + .collect(Collectors.toList())) + .setIsClosed(this.isClosed()) + .setOpeningDate(openingDate) + .setState(Dispute.State.toProtoMessage(disputeState)) + .setId(id); + + Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e))); + Optional.ofNullable(payoutTxSerialized).ifPresent(e -> builder.setPayoutTxSerialized(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); + Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); + Optional.ofNullable(disputePayoutTxId).ifPresent(builder::setDisputePayoutTxId); + Optional.ofNullable(makerContractSignature).ifPresent(builder::setMakerContractSignature); + Optional.ofNullable(takerContractSignature).ifPresent(builder::setTakerContractSignature); + Optional.ofNullable(disputeResultProperty.get()).ifPresent(result -> builder.setDisputeResult(disputeResultProperty.get().toProtoMessage())); + Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType))); + Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); + Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); + Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx)); + Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData); + return builder.build(); + } + + public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver coreProtoResolver) { + Dispute dispute = new Dispute(proto.getOpeningDate(), + proto.getTradeId(), + proto.getTraderId(), + proto.getDisputeOpenerIsBuyer(), + proto.getDisputeOpenerIsMaker(), + PubKeyRing.fromProto(proto.getTraderPubKeyRing()), + proto.getTradeDate(), + proto.getTradePeriodEnd(), + Contract.fromProto(proto.getContract(), coreProtoResolver), + ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()), + ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()), + ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSerialized()), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxId()), + ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()), + proto.getContractAsJson(), + ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature()), + ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature()), + PubKeyRing.fromProto(proto.getAgentPubKeyRing()), + proto.getIsSupportTicket(), + SupportType.fromProto(proto.getSupportType())); + + dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap())); + + dispute.chatMessages.addAll(proto.getChatMessageList().stream() + .map(ChatMessage::fromPayloadProto) + .collect(Collectors.toList())); + + if (proto.hasDisputeResult()) + dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult())); + dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId()); + + String mediatorsDisputeResult = proto.getMediatorsDisputeResult(); + if (!mediatorsDisputeResult.isEmpty()) { + dispute.setMediatorsDisputeResult(mediatorsDisputeResult); + } + + String delayedPayoutTxId = proto.getDelayedPayoutTxId(); + if (!delayedPayoutTxId.isEmpty()) { + dispute.setDelayedPayoutTxId(delayedPayoutTxId); + } + + String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx(); + if (!donationAddressOfDelayedPayoutTx.isEmpty()) { + dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx); + } + + if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) { + // old disputes did not have a state field, so choose an appropriate state: + dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN); + if (dispute.getDisputeState() == State.CLOSED) { + // mark chat messages as read for pre-existing CLOSED disputes + // otherwise at upgrade, all old disputes would have 1 unread chat message + // because currently when a dispute is closed, the last chat message is not marked read + dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); + } + } else { + dispute.setState(Dispute.State.fromProto(proto.getState())); + } + + dispute.refreshAlertLevel(true); + return dispute; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addAndPersistChatMessage(ChatMessage chatMessage) { + if (!chatMessages.contains(chatMessage)) { + chatMessages.add(chatMessage); + } else { + log.error("disputeDirectMessage already exists"); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setIsClosed() { + setState(State.CLOSED); + } + + public void reOpen() { + setState(State.REOPENED); + } + + public void setState(Dispute.State disputeState) { + this.disputeState = disputeState; + this.isClosedProperty.set(disputeState == State.CLOSED); + } + + public void setDisputeResult(DisputeResult disputeResult) { + disputeResultProperty.set(disputeResult); + } + + public void setExtraData(String key, String value) { + if (key == null || value == null) { + return; + } + if (extraDataMap == null) { + extraDataMap = new HashMap<>(); + } + extraDataMap.put(key, value); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getShortTradeId() { + return Utilities.getShortId(tradeId); + } + + public ReadOnlyBooleanProperty isClosedProperty() { + return isClosedProperty; + } + public ReadOnlyIntegerProperty getBadgeCountProperty() { + return badgeCountProperty; + } + public ReadOnlyObjectProperty disputeResultProperty() { + return disputeResultProperty; + } + + public Date getTradeDate() { + return new Date(tradeDate); + } + + public Date getTradePeriodEnd() { + return new Date(tradePeriodEnd); + } + + public Date getOpeningDate() { + return new Date(openingDate); + } + + public boolean isNew() { + return this.disputeState == State.NEW; + } + + public boolean isClosed() { + return this.disputeState == State.CLOSED; + } + + public void refreshAlertLevel(boolean senderFlag) { + // if the dispute is "new" that is 1 alert that has to be propagated upstream + // or if there are unread messages that is 1 alert that has to be propagated upstream + if (isNew() || unreadMessageCount(senderFlag) > 0) { + badgeCountProperty.setValue(1); + } else { + badgeCountProperty.setValue(0); + } + } + + public long unreadMessageCount(boolean senderFlag) { + return chatMessages.stream() + .filter(m -> m.isSenderIsTrader() == senderFlag || m.isSystemMessage()) + .filter(m -> !m.isWasDisplayed()) + .count(); + } + + public void setDisputeSeen(boolean senderFlag) { + if (this.disputeState == State.NEW) + setState(State.OPEN); + refreshAlertLevel(senderFlag); + } + + public void setChatMessagesSeen(boolean senderFlag) { + getChatMessages().forEach(m -> m.setWasDisplayed(true)); + refreshAlertLevel(senderFlag); + } + + public String getRoleString() { + if (disputeOpenerIsMaker) { + if (disputeOpenerIsBuyer) + return Res.get("support.buyerOfferer"); + else + return Res.get("support.sellerOfferer"); + } else { + if (disputeOpenerIsBuyer) + return Res.get("support.buyerTaker"); + else + return Res.get("support.sellerTaker"); + } + } + + @Override + public String toString() { + return "Dispute{" + + "\n tradeId='" + tradeId + '\'' + + ",\n id='" + id + '\'' + + ",\n uid='" + uid + '\'' + + ",\n state=" + disputeState + + ",\n traderId=" + traderId + + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + + ",\n traderPubKeyRing=" + traderPubKeyRing + + ",\n tradeDate=" + tradeDate + + ",\n tradePeriodEnd=" + tradePeriodEnd + + ",\n contract=" + contract + + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + + ",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) + + ",\n payoutTxSerialized=" + Utilities.bytesAsHexString(payoutTxSerialized) + + ",\n depositTxId='" + depositTxId + '\'' + + ",\n payoutTxId='" + payoutTxId + '\'' + + ",\n contractAsJson='" + contractAsJson + '\'' + + ",\n makerContractSignature='" + makerContractSignature + '\'' + + ",\n takerContractSignature='" + takerContractSignature + '\'' + + ",\n agentPubKeyRing=" + agentPubKeyRing + + ",\n isSupportTicket=" + isSupportTicket + + ",\n chatMessages=" + chatMessages + + ",\n isClosedProperty=" + isClosedProperty + + ",\n disputeResultProperty=" + disputeResultProperty + + ",\n disputePayoutTxId='" + disputePayoutTxId + '\'' + + ",\n openingDate=" + openingDate + + ",\n supportType=" + supportType + + ",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' + + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + + ",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java b/core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java new file mode 100644 index 0000000000..04e2cdd978 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.support.dispute; + +public class DisputeAlreadyOpenException extends Exception { + public DisputeAlreadyOpenException() { + super(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeList.java b/core/src/main/java/bisq/core/support/dispute/DisputeList.java new file mode 100644 index 0000000000..1506964956 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeList.java @@ -0,0 +1,44 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.common.proto.persistable.PersistableListAsObservable; +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Collection; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ToString +/* + * Holds a List of Dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public abstract class DisputeList extends PersistableListAsObservable { + + public DisputeList() { + } + + protected DisputeList(Collection collection) { + super(collection); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java new file mode 100644 index 0000000000..b38cf7fa4f --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java @@ -0,0 +1,174 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.core.trade.Contract; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.UserThread; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import org.fxmisc.easybind.EasyBind; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import javafx.collections.ObservableList; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class DisputeListService> implements PersistedDataHost { + @Getter + protected final PersistenceManager persistenceManager; + @Getter + private final T disputeList; + @Getter + private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); + @Getter + private final Set disputedTradeIds = new HashSet<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeListService(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + disputeList = getConcreteDisputeList(); + + this.persistenceManager.initialize(disputeList, getFileName(), PersistenceManager.Source.PRIVATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract T getConcreteDisputeList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(getFileName(), persisted -> { + disputeList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } + + protected String getFileName() { + return disputeList.getDefaultStorageFileName(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public + /////////////////////////////////////////////////////////////////////////////////////////// + + public void cleanupDisputes(@Nullable Consumer closedDisputeHandler) { + disputeList.stream().forEach(dispute -> { + String tradeId = dispute.getTradeId(); + if (dispute.isClosed() && closedDisputeHandler != null) { + closedDisputeHandler.accept(tradeId); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Package scope + /////////////////////////////////////////////////////////////////////////////////////////// + + void onAllServicesInitialized() { + disputeList.addListener(change -> { + change.next(); + onDisputesChangeListener(change.getAddedSubList(), change.getRemoved()); + }); + onDisputesChangeListener(disputeList.getList(), null); + } + + String getNrOfDisputes(boolean isBuyer, Contract contract) { + return String.valueOf(getObservableList().stream() + .filter(e -> { + Contract contract1 = e.getContract(); + if (contract1 == null) + return false; + + if (isBuyer) { + NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress(); + return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress()); + } else { + NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress(); + return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress()); + } + }) + .collect(Collectors.toSet()).size()); + } + + ObservableList getObservableList() { + return disputeList.getObservableList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onDisputesChangeListener(List addedList, + @Nullable List removedList) { + if (removedList != null) { + removedList.forEach(dispute -> { + disputedTradeIds.remove(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(() -> { + int numAlerts = (int) disputeList.getList().stream() + .mapToLong(x -> x.getBadgeCountProperty().getValue()) + .sum(); + numOpenDisputes.set(numAlerts); + }); + }); + disputedTradeIds.add(dispute.getTradeId()); + }); + } + + public void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java new file mode 100644 index 0000000000..6abb55ffcb --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -0,0 +1,967 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.SupportManager; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.FaultHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + +import javafx.beans.property.IntegerProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.security.KeyPair; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class DisputeManager> extends SupportManager { + protected final TradeWalletService tradeWalletService; + protected final BtcWalletService btcWalletService; + protected final TradeManager tradeManager; + protected final ClosedTradableManager closedTradableManager; + protected final OpenOfferManager openOfferManager; + protected final PubKeyRing pubKeyRing; + protected final DisputeListService disputeListService; + private final Config config; + private final PriceFeedService priceFeedService; + protected final DaoFacade daoFacade; + + @Getter + protected final ObservableList validationExceptions = + FXCollections.observableArrayList(); + @Getter + private final KeyPair signatureKeyPair; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService btcWalletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + DaoFacade daoFacade, + KeyRing keyRing, + DisputeListService disputeListService, + Config config, + PriceFeedService priceFeedService) { + super(p2PService, walletsSetup); + + this.tradeWalletService = tradeWalletService; + this.btcWalletService = btcWalletService; + this.tradeManager = tradeManager; + this.closedTradableManager = closedTradableManager; + this.openOfferManager = openOfferManager; + this.daoFacade = daoFacade; + this.pubKeyRing = keyRing.getPubKeyRing(); + signatureKeyPair = keyRing.getSignatureKeyPair(); + this.disputeListService = disputeListService; + this.config = config; + this.priceFeedService = priceFeedService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void requestPersistence() { + disputeListService.requestPersistence(); + } + + @Override + public NodeAddress getPeerNodeAddress(ChatMessage message) { + Optional disputeOptional = findDispute(message); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + return getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; + } + + @Override + public PubKeyRing getPeerPubKeyRing(ChatMessage message) { + Optional disputeOptional = findDispute(message); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + + return getNodeAddressPubKeyRingTuple(disputeOptional.get()).second; + } + + @Override + public List getAllChatMessages() { + return getDisputeList().stream() + .flatMap(dispute -> dispute.getChatMessages().stream()) + .collect(Collectors.toList()); + } + + @Override + public boolean channelOpen(ChatMessage message) { + return findDispute(message).isPresent(); + } + + @Override + public void addAndPersistChatMessage(ChatMessage message) { + findDispute(message).ifPresent(dispute -> { + if (dispute.getChatMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { + dispute.addAndPersistChatMessage(message); + requestPersistence(); + } else { + log.warn("We got a chatMessage that we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + // We get that message at both peers. The dispute object is in context of the trader + public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage); + + @Nullable + public abstract NodeAddress getAgentNodeAddress(Dispute dispute); + + protected abstract Trade.DisputeState getDisputeStateStartedByPeer(); + + public abstract void cleanupDisputes(); + + protected abstract String getDisputeInfo(Dispute dispute); + + protected abstract String getDisputeIntroForPeer(String disputeInfo); + + protected abstract String getDisputeIntroForDisputeCreator(String disputeInfo); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegates for disputeListService + /////////////////////////////////////////////////////////////////////////////////////////// + + public IntegerProperty getNumOpenDisputes() { + return disputeListService.getNumOpenDisputes(); + } + + public ObservableList getDisputesAsObservableList() { + return disputeListService.getObservableList(); + } + + public String getNrOfDisputes(boolean isBuyer, Contract contract) { + return disputeListService.getNrOfDisputes(isBuyer, contract); + } + + protected T getDisputeList() { + return disputeListService.getDisputeList(); + } + + public Set getDisputedTradeIds() { + return disputeListService.getDisputedTradeIds(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + super.onAllServicesInitialized(); + disputeListService.onAllServicesInitialized(); + + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + tryApplyMessages(); + } + }); + + walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.isDownloadComplete()) + tryApplyMessages(); + }); + + walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.hasSufficientPeersForBroadcast()) + tryApplyMessages(); + }); + + tryApplyMessages(); + cleanupDisputes(); + + List disputes = getDisputeList().getList(); + disputes.forEach(dispute -> { + try { + TradeDataValidation.validateDonationAddress(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config); + } catch (TradeDataValidation.AddressException | TradeDataValidation.NodeAddressException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + }); + + TradeDataValidation.testIfAnyDisputeTriedReplay(disputes, + disputeReplayException -> { + log.error(disputeReplayException.toString()); + validationExceptions.add(disputeReplayException); + }); + } + + public boolean isTrader(Dispute dispute) { + return pubKeyRing.equals(dispute.getTraderPubKeyRing()); + } + + + public Optional findOwnDispute(String tradeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + // dispute agent receives that from trader who opens dispute + protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + String errorMessage = null; + Dispute dispute = openNewDisputeMessage.getDispute(); + // Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before + dispute.setSupportType(openNewDisputeMessage.getSupportType()); + // disputes from clients < 1.6.0 have state not set as the field didn't exist before + dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release + + Contract contract = dispute.getContract(); + addPriceInfoMessage(dispute, 0); + + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + if (isAgent(dispute)) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + disputeList.add(dispute); + sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } + } else { + errorMessage = "Trader received openNewDisputeMessage. That must never happen."; + log.error(errorMessage); + } + + // We use the ChatMessage not the openNewDisputeMessage for the ACK + ObservableList messages = dispute.getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage chatMessage = messages.get(0); + PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + sendAckMessage(chatMessage, sendersPubKeyRing, errorMessage == null, errorMessage); + } + + addMediationResultMessage(dispute); + + try { + TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); + TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config); + } catch (TradeDataValidation.AddressException | + TradeDataValidation.DisputeReplayException | + TradeDataValidation.NodeAddressException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + requestPersistence(); + } + + // Not-dispute-requester receives that msg from dispute agent + protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + String errorMessage = null; + Dispute dispute = peerOpenedDisputeMessage.getDispute(); + + Optional optionalTrade = tradeManager.getTradeById(dispute.getTradeId()); + if (!optionalTrade.isPresent()) { + return; + } + + Trade trade = optionalTrade.get(); + try { + TradeDataValidation.validateDelayedPayoutTx(trade, + trade.getDelayedPayoutTx(), + dispute, + daoFacade, + btcWalletService); + } catch (TradeDataValidation.ValidationException e) { + // The peer sent us an invalid donation address. We do not return here as we don't want to break + // mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get + // a popup displayed to react. + log.warn("Donation address is invalid. {}", e.toString()); + } + + if (!isAgent(dispute)) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + disputeList.add(dispute); + trade.setDisputeState(getDisputeStateStartedByPeer()); + tradeManager.requestPersistence(); + errorMessage = null; + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } + } else { + errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen."; + log.error(errorMessage); + } + + // We use the ChatMessage not the peerOpenedDisputeMessage for the ACK + ObservableList messages = peerOpenedDisputeMessage.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + } + + sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send message + /////////////////////////////////////////////////////////////////////////////////////////// + + public void sendOpenNewDisputeMessage(Dispute dispute, + boolean reOpen, + ResultHandler resultHandler, + FaultHandler faultHandler) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + if (disputeList.contains(dispute)) { + String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + return; + } + + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent() || reOpen) { + String disputeInfo = getDisputeInfo(dispute); + String disputeMessage = getDisputeIntroForDisputeCreator(disputeInfo); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) + : disputeMessage; + + String message = Res.get("support.systemMsg", sysMsg); + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + message, + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + if (!reOpen) { + disputeList.add(dispute); + } + + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + if (agentNodeAddress == null) { + return; + } + + OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), + agentNodeAddress, + openNewDisputeMessage.getTradeId(), + openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, + dispute.getAgentPubKeyRing(), + openNewDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // 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); + requestPersistence(); + faultHandler.handleFault("Sending dispute message failed: " + + errorMessage, new DisputeMessageDeliveryFailedException()); + } + } + ); + } else { + String msg = "We got a dispute already open for that trade and trading peer.\n" + + "TradeId = " + dispute.getTradeId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + } + requestPersistence(); + } + + // Dispute agent sends that to trading peer when he received openDispute request + private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, + Contract contractFromOpener, + PubKeyRing pubKeyRing) { + // We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is + // being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct + // message and not skip the system message of the peer as it would be the case if we have created the system msg + // from the code below. + UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener, + contractFromOpener, + pubKeyRing), + 100, TimeUnit.MILLISECONDS); + } + + private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, + Contract contractFromOpener, + PubKeyRing pubKeyRing) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + Dispute dispute = new Dispute(new Date().getTime(), + disputeFromOpener.getTradeId(), + pubKeyRing.hashCode(), + !disputeFromOpener.isDisputeOpenerIsBuyer(), + !disputeFromOpener.isDisputeOpenerIsMaker(), + pubKeyRing, + disputeFromOpener.getTradeDate().getTime(), + disputeFromOpener.getTradePeriodEnd().getTime(), + contractFromOpener, + disputeFromOpener.getContractHash(), + disputeFromOpener.getDepositTxSerialized(), + disputeFromOpener.getPayoutTxSerialized(), + disputeFromOpener.getDepositTxId(), + disputeFromOpener.getPayoutTxId(), + disputeFromOpener.getContractAsJson(), + disputeFromOpener.getMakerContractSignature(), + disputeFromOpener.getTakerContractSignature(), + disputeFromOpener.getAgentPubKeyRing(), + disputeFromOpener.isSupportTicket(), + disputeFromOpener.getSupportType()); + dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap()); + dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); + dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx()); + + Optional storedDisputeOptional = findDispute(dispute); + + // Valid case if both have opened a dispute and agent was not online. + if (storedDisputeOptional.isPresent()) { + log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); + return; + } + + String disputeInfo = getDisputeInfo(dispute); + String disputeMessage = getDisputeIntroForPeer(disputeInfo); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) + : disputeMessage; + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + + addPriceInfoMessage(dispute, 0); + + disputeList.add(dispute); + + // We mirrored dispute already! + Contract contract = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); + PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + + log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + peerOpenedDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + requestPersistence(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + requestPersistence(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + requestPersistence(); + } + } + ); + requestPersistence(); + } + + // dispute agent send result to trader + public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + dispute.getTraderPubKeyRing().hashCode(), + false, + summaryText, + p2PService.getAddress()); + + disputeResult.setChatMessage(chatMessage); + dispute.addAndPersistChatMessage(chatMessage); + + NodeAddress peersNodeAddress; + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) + peersNodeAddress = contract.getBuyerNodeAddress(); + else + peersNodeAddress = contract.getSellerNodeAddress(); + DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(), + disputeResultMessage.getUid(), chatMessage.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + dispute.getTraderPubKeyRing(), + disputeResultMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + requestPersistence(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + requestPersistence(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + requestPersistence(); + } + } + ); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { + PubKeyRing receiverPubKeyRing = null; + NodeAddress peerNodeAddress = null; + if (isTrader(dispute)) { + receiverPubKeyRing = dispute.getAgentPubKeyRing(); + peerNodeAddress = getAgentNodeAddress(dispute); + } else if (isAgent(dispute)) { + receiverPubKeyRing = dispute.getTraderPubKeyRing(); + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing)) + peerNodeAddress = contract.getBuyerNodeAddress(); + else + peerNodeAddress = contract.getSellerNodeAddress(); + } else { + log.error("That must not happen. Trader cannot communicate to other trader."); + } + return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); + } + + private boolean isAgent(Dispute dispute) { + return pubKeyRing.equals(dispute.getAgentPubKeyRing()); + } + + private Optional findDispute(Dispute dispute) { + return findDispute(dispute.getTradeId(), dispute.getTraderId()); + } + + protected Optional findDispute(DisputeResult disputeResult) { + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId()); + } + + private Optional findDispute(ChatMessage message) { + return findDispute(message.getTradeId(), message.getTraderId()); + } + + private Optional findDispute(String tradeId, int traderId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream() + .filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId) + .findAny(); + } + + public Optional findDispute(String tradeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream() + .filter(e -> e.getTradeId().equals(tradeId)) + .findAny(); + } + + public Optional findTrade(Dispute dispute) { + Optional retVal = tradeManager.getTradeById(dispute.getTradeId()); + if (!retVal.isPresent()) { + retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst(); + } + return retVal; + } + + private void addMediationResultMessage(Dispute dispute) { + // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. + if (dispute.getMediatorsDisputeResult() != null) { + String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); + ChatMessage mediatorsDisputeResultMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + mediatorsDisputeResult, + p2PService.getAddress()); + mediatorsDisputeResultMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); + requestPersistence(); + } + } + + public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) { + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + dispute.getTraderId(), + senderIsTrader, + Res.get("support.info.disputeReOpened"), + p2PService.getAddress()); + chatMessage.setSystemMessage(false); + dispute.addAndPersistChatMessage(chatMessage); + this.sendChatMessage(chatMessage); + requestPersistence(); + } + + // If price was going down between take offer time and open dispute time the buyer has an incentive to + // not send the payment but to try to make a new trade with the better price. We risks to lose part of the + // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated + // proposal). But if gain is larger than this loss he has economically an incentive to default in the trade. + // We do all those calculations to give a hint to mediators to detect option trades. + protected void addPriceInfoMessage(Dispute dispute, int counter) { + if (!priceFeedService.hasPrices()) { + if (counter < 3) { + log.info("Price provider has still no data. This is expected at startup. We try again in 10 sec."); + UserThread.runAfter(() -> addPriceInfoMessage(dispute, counter + 1), 10); + } else { + log.warn("Price provider still has no data after 3 repeated requests and 30 seconds delay. We give up."); + } + return; + } + + Contract contract = dispute.getContract(); + OfferPayload offerPayload = contract.getOfferPayload(); + Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); + if (priceAtDisputeOpening == null) { + log.info("Price provider did not provide a price for {}. " + + "This is expected if this currency is not supported by the price providers.", + offerPayload.getCurrencyCode()); + return; + } + + // The amount we would get if we do a new trade with current price + Coin potentialAmountAtDisputeOpening = priceAtDisputeOpening.getAmountByVolume(contract.getTradeVolume()); + Coin buyerSecurityDeposit = Coin.valueOf(offerPayload.getBuyerSecurityDeposit()); + Coin minRefundAtMediatedDispute = Restrictions.getMinRefundAtMediatedDispute(); + // minRefundAtMediatedDispute is always larger as buyerSecurityDeposit at mediated payout, we ignore refund agent case here as there it can be 0. + Coin maxLossSecDeposit = buyerSecurityDeposit.subtract(minRefundAtMediatedDispute); + Coin tradeAmount = contract.getTradeAmount(); + Coin potentialGain = potentialAmountAtDisputeOpening.subtract(tradeAmount).subtract(maxLossSecDeposit); + String optionTradeDetails; + // We don't translate those strings (yet) as it is only displayed to mediators/arbitrators. + String headline; + if (potentialGain.isPositive()) { + headline = "This might be a potential option trade!"; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible gain from an option trade is: " + potentialGain.toFriendlyString(); + } else { + headline = "It does not appear to be an option trade."; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible loss from an option trade is: " + potentialGain.multiply(-1).toFriendlyString(); + } + + String percentagePriceDetails = offerPayload.isUseMarketBasedPrice() ? + " (market based price was used: " + offerPayload.getMarketPriceMargin() * 100 + "%)" : + " (fix price was used)"; + + String priceInfoText = "System message: " + headline + + "\n\nTrade price: " + contract.getTradePrice().toFriendlyString() + percentagePriceDetails + + "\nTrade amount: " + tradeAmount.toFriendlyString() + + "\nPrice at dispute opening: " + priceAtDisputeOpening.toFriendlyString() + + optionTradeDetails; + + // We use the existing msg to copy over the users data + ChatMessage priceInfoMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + priceInfoText, + p2PService.getAddress()); + priceInfoMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(priceInfoMessage); + requestPersistence(); + } + + @Nullable + private Price getPrice(String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double marketPriceAsDouble = marketPrice.getPrice(); + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e.toString()); + return null; + } + } else { + return null; + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java b/core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java new file mode 100644 index 0000000000..39eeebd65f --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.support.dispute; + +public class DisputeMessageDeliveryFailedException extends Exception { + public DisputeMessageDeliveryFailedException() { + super(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java new file mode 100644 index 0000000000..d046a16c98 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -0,0 +1,259 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.core.support.messages.ChatMessage; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.Date; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode +@Getter +@Slf4j +public final class DisputeResult implements NetworkPayload { + + public enum Winner { + BUYER, + SELLER + } + + public enum Reason { + OTHER, + BUG, + USABILITY, + SCAM, // Not used anymore + PROTOCOL_VIOLATION, // Not used anymore + NO_REPLY, // Not used anymore + BANK_PROBLEMS, + OPTION_TRADE, + SELLER_NOT_RESPONDING, + WRONG_SENDER_ACCOUNT, + TRADE_ALREADY_SETTLED, + PEER_WAS_LATE + } + + private final String tradeId; + private final int traderId; + @Setter + @Nullable + private Winner winner; + private int reasonOrdinal = Reason.OTHER.ordinal(); + private final BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty(); + private final BooleanProperty idVerificationProperty = new SimpleBooleanProperty(); + private final BooleanProperty screenCastProperty = new SimpleBooleanProperty(); + private final StringProperty summaryNotesProperty = new SimpleStringProperty(""); + @Setter + @Nullable + private ChatMessage chatMessage; + @Setter + @Nullable + private byte[] arbitratorSignature; + private long buyerPayoutAmount; + private long sellerPayoutAmount; + @Setter + @Nullable + private byte[] arbitratorPubKey; + private long closeDate; + @Setter + private boolean isLoserPublisher; + + public DisputeResult(String tradeId, int traderId) { + this.tradeId = tradeId; + this.traderId = traderId; + } + + public DisputeResult(String tradeId, + int traderId, + @Nullable Winner winner, + int reasonOrdinal, + boolean tamperProofEvidence, + boolean idVerification, + boolean screenCast, + String summaryNotes, + @Nullable ChatMessage chatMessage, + @Nullable byte[] arbitratorSignature, + long buyerPayoutAmount, + long sellerPayoutAmount, + @Nullable byte[] arbitratorPubKey, + long closeDate, + boolean isLoserPublisher) { + this.tradeId = tradeId; + this.traderId = traderId; + this.winner = winner; + this.reasonOrdinal = reasonOrdinal; + this.tamperProofEvidenceProperty.set(tamperProofEvidence); + this.idVerificationProperty.set(idVerification); + this.screenCastProperty.set(screenCast); + this.summaryNotesProperty.set(summaryNotes); + this.chatMessage = chatMessage; + this.arbitratorSignature = arbitratorSignature; + this.buyerPayoutAmount = buyerPayoutAmount; + this.sellerPayoutAmount = sellerPayoutAmount; + this.arbitratorPubKey = arbitratorPubKey; + this.closeDate = closeDate; + this.isLoserPublisher = isLoserPublisher; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static DisputeResult fromProto(protobuf.DisputeResult proto) { + return new DisputeResult(proto.getTradeId(), + proto.getTraderId(), + ProtoUtil.enumFromProto(DisputeResult.Winner.class, proto.getWinner().name()), + proto.getReasonOrdinal(), + proto.getTamperProofEvidence(), + proto.getIdVerification(), + proto.getScreenCast(), + proto.getSummaryNotes(), + proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), + proto.getArbitratorSignature().toByteArray(), + proto.getBuyerPayoutAmount(), + proto.getSellerPayoutAmount(), + proto.getArbitratorPubKey().toByteArray(), + proto.getCloseDate(), + proto.getIsLoserPublisher()); + } + + @Override + public protobuf.DisputeResult toProtoMessage() { + final protobuf.DisputeResult.Builder builder = protobuf.DisputeResult.newBuilder() + .setTradeId(tradeId) + .setTraderId(traderId) + .setReasonOrdinal(reasonOrdinal) + .setTamperProofEvidence(tamperProofEvidenceProperty.get()) + .setIdVerification(idVerificationProperty.get()) + .setScreenCast(screenCastProperty.get()) + .setSummaryNotes(summaryNotesProperty.get()) + .setBuyerPayoutAmount(buyerPayoutAmount) + .setSellerPayoutAmount(sellerPayoutAmount) + .setCloseDate(closeDate) + .setIsLoserPublisher(isLoserPublisher); + + Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); + Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); + Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); + Optional.ofNullable(chatMessage).ifPresent(chatMessage -> + builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); + + return builder.build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public BooleanProperty tamperProofEvidenceProperty() { + return tamperProofEvidenceProperty; + } + + public BooleanProperty idVerificationProperty() { + return idVerificationProperty; + } + + public BooleanProperty screenCastProperty() { + return screenCastProperty; + } + + public void setReason(Reason reason) { + this.reasonOrdinal = reason.ordinal(); + } + + public Reason getReason() { + if (reasonOrdinal < Reason.values().length) + return Reason.values()[reasonOrdinal]; + else + return Reason.OTHER; + } + + public void setSummaryNotes(String summaryNotes) { + this.summaryNotesProperty.set(summaryNotes); + } + + public StringProperty summaryNotesProperty() { + return summaryNotesProperty; + } + + public void setBuyerPayoutAmount(Coin buyerPayoutAmount) { + this.buyerPayoutAmount = buyerPayoutAmount.value; + } + + public Coin getBuyerPayoutAmount() { + return Coin.valueOf(buyerPayoutAmount); + } + + public void setSellerPayoutAmount(Coin sellerPayoutAmount) { + this.sellerPayoutAmount = sellerPayoutAmount.value; + } + + public Coin getSellerPayoutAmount() { + return Coin.valueOf(sellerPayoutAmount); + } + + public void setCloseDate(Date closeDate) { + this.closeDate = closeDate.getTime(); + } + + public Date getCloseDate() { + return new Date(closeDate); + } + + @Override + public String toString() { + return "DisputeResult{" + + "\n tradeId='" + tradeId + '\'' + + ",\n traderId=" + traderId + + ",\n winner=" + winner + + ",\n reasonOrdinal=" + reasonOrdinal + + ",\n tamperProofEvidenceProperty=" + tamperProofEvidenceProperty + + ",\n idVerificationProperty=" + idVerificationProperty + + ",\n screenCastProperty=" + screenCastProperty + + ",\n summaryNotesProperty=" + summaryNotesProperty + + ",\n chatMessage=" + chatMessage + + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + + ",\n buyerPayoutAmount=" + buyerPayoutAmount + + ",\n sellerPayoutAmount=" + sellerPayoutAmount + + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + + ",\n closeDate=" + closeDate + + ",\n isLoserPublisher=" + isLoserPublisher + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeSession.java b/core/src/main/java/bisq/core/support/dispute/DisputeSession.java new file mode 100644 index 0000000000..d381a0eaaf --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeSession.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.support.dispute; + +import bisq.core.support.SupportSession; +import bisq.core.support.messages.ChatMessage; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class DisputeSession extends SupportSession { + @Nullable + private Dispute dispute; + private final boolean isTrader; + + public DisputeSession(@Nullable Dispute dispute, boolean isTrader) { + super(); + this.dispute = dispute; + this.isTrader = isTrader; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dependent on selected dispute + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isClient() { + return isTrader; + } + + @Override + public String getTradeId() { + return dispute != null ? dispute.getTradeId() : ""; + } + + @Override + public int getClientId() { + // Get pubKeyRing of trader. Arbitrator is considered server for the chat session + try { + return dispute.getTraderPubKeyRing().hashCode(); + } catch (NullPointerException e) { + log.warn("Unable to get traderPubKeyRing from Dispute - {}", e.toString()); + } + return 0; + } + + @Override + public ObservableList getObservableChatMessageList() { + return dispute != null ? dispute.getChatMessages() : FXCollections.observableArrayList(); + } + + @Override + public boolean chatIsOpen() { + return dispute != null && !dispute.isClosed(); + } + + @Override + public boolean isDisputeAgent() { + return !isClient(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java new file mode 100644 index 0000000000..aa583d04de --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.core.support.dispute.agent; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; + +import java.security.PublicKey; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode +@Slf4j +@Getter +public abstract class DisputeAgent implements ProtectedStoragePayload, ExpirablePayload { + public static final long TTL = TimeUnit.DAYS.toMillis(10); + + protected final NodeAddress nodeAddress; + protected final PubKeyRing pubKeyRing; + protected final List languageCodes; + protected final long registrationDate; + protected final byte[] registrationPubKey; + protected final String registrationSignature; + @Nullable + protected final String emailAddress; + @Nullable + protected final String info; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + protected Map extraDataMap; + + public DisputeAgent(NodeAddress nodeAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + this.nodeAddress = nodeAddress; + this.pubKeyRing = pubKeyRing; + this.languageCodes = languageCodes; + this.registrationDate = registrationDate; + this.registrationPubKey = registrationPubKey; + this.registrationSignature = registrationSignature; + this.emailAddress = emailAddress; + this.info = info; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return pubKeyRing.getSignaturePubKey(); + } + + + @Override + public String toString() { + return "DisputeAgent{" + + "\n nodeAddress=" + nodeAddress + + ",\n pubKeyRing=" + pubKeyRing + + ",\n languageCodes=" + languageCodes + + ",\n registrationDate=" + registrationDate + + ",\n registrationPubKey=" + Utilities.bytesAsHexString(registrationPubKey) + + ",\n registrationSignature='" + registrationSignature + '\'' + + ",\n emailAddress='" + emailAddress + '\'' + + ",\n info='" + info + '\'' + + ",\n extraDataMap=" + extraDataMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java new file mode 100644 index 0000000000..c2d80bcde1 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.support.dispute.agent; + +import bisq.core.locale.Res; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class DisputeAgentLookupMap { + + // See also: https://bisq.wiki/Finding_your_mediator + @Nullable + public static String getKeyBaseUserName(String fullAddress) { + switch (fullAddress) { + case "sjlho4zwp3gecspf.onion:9999": + return "leo816"; + case "wizbisqzd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999": + return "wiz"; + case "apbp7ubuyezav4hy.onion:9999": + return "bisq_knight"; + case "a56olqlmmpxrn5q34itq5g5tb5d3fg7vxekpbceq7xqvfl3cieocgsyd.onion:9999": + return "huey735"; + case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999": + return "refundagent2"; + default: + log.warn("No user name for dispute agent with address {} found.", fullAddress); + return Res.get("shared.na"); + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java new file mode 100644 index 0000000000..73a73110d4 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java @@ -0,0 +1,341 @@ +/* + * 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 bisq.core.support.dispute.agent; + +import bisq.core.filter.FilterManager; +import bisq.core.user.User; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; + +import java.security.PublicKey; +import java.security.SignatureException; + +import java.math.BigInteger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static org.bitcoinj.core.Utils.HEX; + +@Slf4j +public abstract class DisputeAgentManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + protected static final long REPUBLISH_MILLIS = DisputeAgent.TTL / 2; + protected static final long RETRY_REPUBLISH_SEC = 5; + protected static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60; + + protected final List publicKeys; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + protected final KeyRing keyRing; + protected final DisputeAgentService disputeAgentService; + protected final User user; + protected final FilterManager filterManager; + protected final ObservableMap observableMap = FXCollections.observableHashMap(); + protected List persistedAcceptedDisputeAgents; + protected Timer republishTimer, retryRepublishTimer; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeAgentManager(KeyRing keyRing, + DisputeAgentService disputeAgentService, + User user, + FilterManager filterManager, + boolean useDevPrivilegeKeys) { + this.keyRing = keyRing; + this.disputeAgentService = disputeAgentService; + this.user = user; + this.filterManager = filterManager; + publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : getPubKeyList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract List getPubKeyList(); + + protected abstract boolean isExpectedInstance(ProtectedStorageEntry data); + + protected abstract void addAcceptedDisputeAgentToUser(T disputeAgent); + + protected abstract T getRegisteredDisputeAgentFromUser(); + + protected abstract void clearAcceptedDisputeAgentsAtUser(); + + protected abstract List getAcceptedDisputeAgentsFromUser(); + + protected abstract void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data); + + protected abstract void setRegisteredDisputeAgentAtUser(T disputeAgent); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + disputeAgentService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> { + if (isExpectedInstance(protectedStorageEntry)) { + updateMap(); + } + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.forEach(protectedStorageEntry -> { + if (isExpectedInstance(protectedStorageEntry)) { + updateMap(); + removeAcceptedDisputeAgentFromUser(protectedStorageEntry); + } + }); + } + }); + + persistedAcceptedDisputeAgents = new ArrayList<>(getAcceptedDisputeAgentsFromUser()); + clearAcceptedDisputeAgentsAtUser(); + + if (getRegisteredDisputeAgentFromUser() != null) { + P2PService p2PService = disputeAgentService.getP2PService(); + if (p2PService.isBootstrapped()) + startRepublishDisputeAgent(); + else + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + startRepublishDisputeAgent(); + } + }); + } + + filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateMap()); + + updateMap(); + } + + public void shutDown() { + stopRepublishTimer(); + stopRetryRepublishTimer(); + } + + protected void startRepublishDisputeAgent() { + if (republishTimer == null) { + republishTimer = UserThread.runPeriodically(this::republish, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS); + UserThread.runAfter(this::republish, REPEATED_REPUBLISH_AT_STARTUP_SEC); + republish(); + } + } + + public void updateMap() { + Map map = disputeAgentService.getDisputeAgents(); + observableMap.clear(); + Map filtered = map.values().stream() + .filter(e -> { + String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey()); + boolean isInPublicKeyInList = isPublicKeyInList(pubKeyAsHex); + if (!isInPublicKeyInList) { + if (DevEnv.DEV_PRIVILEGE_PUB_KEY.equals(pubKeyAsHex)) + log.info("We got the DEV_PRIVILEGE_PUB_KEY in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", + Utilities.bytesAsHexString(e.getRegistrationPubKey()), + e.getNodeAddress().getFullAddress()); + else + log.warn("We got an disputeAgent which is not in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", + Utilities.bytesAsHexString(e.getRegistrationPubKey()), + e.getNodeAddress().getFullAddress()); + } + final boolean isSigValid = verifySignature(e.getPubKeyRing().getSignaturePubKey(), + e.getRegistrationPubKey(), + e.getRegistrationSignature()); + if (!isSigValid) + log.warn("Sig check for disputeAgent failed. DisputeAgent={}", e.toString()); + + return isInPublicKeyInList && isSigValid; + }) + .collect(Collectors.toMap(DisputeAgent::getNodeAddress, Function.identity())); + + observableMap.putAll(filtered); + observableMap.values().forEach(this::addAcceptedDisputeAgentToUser); + } + + + public void addDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + setRegisteredDisputeAgentAtUser(disputeAgent); + observableMap.put(disputeAgent.getNodeAddress(), disputeAgent); + disputeAgentService.addDisputeAgent(disputeAgent, + () -> { + log.info("DisputeAgent successfully saved in P2P network"); + resultHandler.handleResult(); + + if (observableMap.size() > 0) + UserThread.runAfter(this::updateMap, 100, TimeUnit.MILLISECONDS); + }, + errorMessageHandler); + } + + + public void removeDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); + if (registeredDisputeAgent != null) { + setRegisteredDisputeAgentAtUser(null); + observableMap.remove(registeredDisputeAgent.getNodeAddress()); + disputeAgentService.removeDisputeAgent(registeredDisputeAgent, + () -> { + log.debug("DisputeAgent successfully removed from P2P network"); + resultHandler.handleResult(); + }, + errorMessageHandler); + } + } + + public ObservableMap getObservableMap() { + return observableMap; + } + + // A protected key is handed over to selected disputeAgents for registration. + // An invited disputeAgent will sign at registration his storageSignaturePubKey with that protected key and attach the signature and pubKey to his data. + // Other users will check the signature with the list of public keys hardcoded in the app. + public String signStorageSignaturePubKey(ECKey key) { + String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded()); + return key.signMessage(keyToSignAsHex); + } + + @Nullable + public ECKey getRegistrationKey(String privKeyBigIntString) { + try { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString))); + } catch (Throwable t) { + return null; + } + } + + public boolean isPublicKeyInList(String pubKeyAsHex) { + return publicKeys.contains(pubKeyAsHex); + } + + public boolean isAgentAvailableForLanguage(String languageCode) { + return observableMap.values().stream().anyMatch(agent -> + agent.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode))); + } + + public List getDisputeAgentLanguages(List nodeAddresses) { + return observableMap.values().stream() + .filter(disputeAgent -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(disputeAgent.getNodeAddress()))) + .flatMap(disputeAgent -> disputeAgent.getLanguageCodes().stream()) + .distinct() + .collect(Collectors.toList()); + } + + public Optional getDisputeAgentByNodeAddress(NodeAddress nodeAddress) { + return observableMap.containsKey(nodeAddress) ? + Optional.of(observableMap.get(nodeAddress)) : + Optional.empty(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void republish() { + T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); + if (registeredDisputeAgent != null) { + addDisputeAgent(registeredDisputeAgent, + this::updateMap, + errorMessage -> { + if (retryRepublishTimer == null) + retryRepublishTimer = UserThread.runPeriodically(() -> { + stopRetryRepublishTimer(); + republish(); + }, RETRY_REPUBLISH_SEC); + } + ); + } + } + + protected boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) { + String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded()); + try { + ECKey key = ECKey.fromPublicOnly(registrationPubKey); + key.verifyMessage(keyToSignAsHex, signature); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } + + + protected void stopRetryRepublishTimer() { + if (retryRepublishTimer != null) { + retryRepublishTimer.stop(); + retryRepublishTimer = null; + } + } + + protected void stopRepublishTimer() { + if (republishTimer != null) { + republishTimer.stop(); + republishTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java new file mode 100644 index 0000000000..335a6f7d53 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.core.support.dispute.agent; + +import bisq.core.filter.FilterManager; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; + +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +/** + * Used to store disputeAgents profile and load map of disputeAgents + */ +@Slf4j +public abstract class DisputeAgentService { + protected final P2PService p2PService; + protected final FilterManager filterManager; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeAgentService(P2PService p2PService, FilterManager filterManager) { + this.p2PService = p2PService; + this.filterManager = filterManager; + } + + public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { + p2PService.addHashSetChangedListener(hashMapChangedListener); + } + + public void addDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + log.debug("addDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); + if (!Config.baseCurrencyNetwork().isMainnet() || + !Utilities.encodeToHex(disputeAgent.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) { + boolean result = p2PService.addProtectedStorageEntry(disputeAgent); + if (result) { + log.trace("Add disputeAgent to network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode()); + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Add disputeAgent failed"); + } + } else { + log.error("Attempt to publish dev disputeAgent on mainnet."); + errorMessageHandler.handleErrorMessage("Add disputeAgent failed. Attempt to publish dev disputeAgent on mainnet."); + } + } + + public void removeDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + log.debug("removeDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); + if (p2PService.removeData(disputeAgent)) { + log.trace("Remove disputeAgent from network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode()); + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Remove disputeAgent failed"); + } + } + + public P2PService getP2PService() { + return p2PService; + } + + public Map getDisputeAgents() { + final List bannedDisputeAgents; + if (filterManager.getFilter() != null) { + bannedDisputeAgents = getDisputeAgentsFromFilter(); + } else { + bannedDisputeAgents = null; + } + + if (bannedDisputeAgents != null && !bannedDisputeAgents.isEmpty()) { + log.warn("bannedDisputeAgents=" + bannedDisputeAgents); + } + + Set disputeAgentSet = getDisputeAgentSet(bannedDisputeAgents); + + Map map = new HashMap<>(); + for (T disputeAgent : disputeAgentSet) { + NodeAddress disputeAgentNodeAddress = disputeAgent.getNodeAddress(); + if (!map.containsKey(disputeAgentNodeAddress)) + map.put(disputeAgentNodeAddress, disputeAgent); + else + log.warn("disputeAgentAddress already exists in disputeAgent map. Seems a disputeAgent object is already registered with the same address."); + } + return map; + } + + protected abstract Set getDisputeAgentSet(List bannedDisputeAgents); + + protected abstract List getDisputeAgentsFromFilter(); +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java new file mode 100644 index 0000000000..3bcbf50aa9 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java @@ -0,0 +1,271 @@ +/* + * 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 bisq.core.support.dispute.agent; + +import bisq.core.locale.Res; +import bisq.core.payment.payload.PayloadWithHolderName; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.trade.Contract; +import bisq.core.user.DontShowAgainLookup; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javafx.collections.ListChangeListener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Detects traders who had disputes where they used different account holder names. Only payment methods where a + * real name is required are used for the check. + * Strings are not translated here as it is only visible to dispute agents + */ +@Slf4j +public class MultipleHolderNameDetection { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onSuspiciousDisputeDetected(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final String ACK_KEY = "Ack-"; + + private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) { + return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes())); + } + + private static String getSigPubKeyHashAsHex(Dispute dispute) { + return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing()); + } + + private static boolean isBuyer(Dispute dispute) { + String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute); + String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing()); + return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex); + } + + private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) { + return (PayloadWithHolderName) getPaymentAccountPayload(dispute); + } + + public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerPaymentAccountPayload() : + dispute.getContract().getSellerPaymentAccountPayload(); + } + + public static String getAddress(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerNodeAddress().getHostName() : + dispute.getContract().getSellerNodeAddress().getHostName(); + } + + public static String getAckKey(Dispute dispute) { + return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId(); + } + + private static String getIsBuyerSubString(boolean isBuyer) { + return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final DisputeManager> disputeManager; + + // Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but + // once we support multiple onion addresses that would not work anymore. + @Getter + private final Map> suspiciousDisputesByTraderMap = new HashMap<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public MultipleHolderNameDetection(DisputeManager> disputeManager) { + this.disputeManager = disputeManager; + + disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + detectMultipleHolderNames(); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void detectMultipleHolderNames() { + String previous = suspiciousDisputesByTraderMap.toString(); + getAllDisputesByTraderMap().forEach((key, value) -> { + Set userNames = value.stream() + .map(dispute -> getPayloadWithHolderName(dispute).getHolderName()) + .collect(Collectors.toSet()); + if (userNames.size() > 1) { + // As we compare previous results we need to make sorting deterministic + value.sort(Comparator.comparing(Dispute::getId)); + suspiciousDisputesByTraderMap.put(key, value); + } + }); + String updated = suspiciousDisputesByTraderMap.toString(); + if (!previous.equals(updated)) { + listeners.forEach(Listener::onSuspiciousDisputeDetected); + } + } + + public boolean hasSuspiciousDisputesDetected() { + return !suspiciousDisputesByTraderMap.isEmpty(); + } + + // Returns all disputes of a trader who used multiple names + public List getDisputesForTrader(Dispute dispute) { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) { + return suspiciousDisputesByTraderMap.get(traderPubKeyHash); + } + return new ArrayList<>(); + } + + // Get a report of traders who used multiple names with all their disputes listed + public String getReportForAllDisputes() { + return getReport(suspiciousDisputesByTraderMap.values()); + } + + // Get a report for a trader who used multiple names with all their disputes listed + public String getReportForDisputeOfTrader(List disputes) { + Collection> values = new ArrayList<>(); + values.add(disputes); + return getReport(values); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map> getAllDisputesByTraderMap() { + Map> allDisputesByTraderMap = new HashMap<>(); + disputeManager.getDisputesAsObservableList().stream() + .filter(dispute -> { + Contract contract = dispute.getContract(); + PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ? + contract.getBuyerPaymentAccountPayload() : + contract.getSellerPaymentAccountPayload(); + return paymentAccountPayload instanceof PayloadWithHolderName; + }) + .forEach(dispute -> { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); + List disputes = allDisputesByTraderMap.get(traderPubKeyHash); + disputes.add(dispute); + }); + return allDisputesByTraderMap; + } + + // Get a text report for a trader who used multiple names and list all the his disputes + private String getReport(Collection> collectionOfDisputesOfTrader) { + return collectionOfDisputesOfTrader.stream() + .map(disputes -> { + Set addresses = new HashSet<>(); + Set isBuyerHashSet = new HashSet<>(); + Set names = new HashSet<>(); + String disputesReport = disputes.stream() + .map(dispute -> { + addresses.add(getAddress(dispute)); + String ackKey = getAckKey(dispute); + String ackSubString = " "; + if (!DontShowAgainLookup.showAgain(ackKey)) { + ackSubString = "[ACK] "; + } + String holderName = getPayloadWithHolderName(dispute).getHolderName(); + names.add(holderName); + boolean isBuyer = isBuyer(dispute); + isBuyerHashSet.add(isBuyer); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + DisputeResult disputeResult = dispute.disputeResultProperty().get(); + String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet"; + return ackSubString + + "Trade ID: '" + dispute.getShortTradeId() + + "'\n Account holder name: '" + holderName + + "'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) + + isBuyerSubString + + "'\n Summary: '" + summaryNotes; + }) + .collect(Collectors.joining("\n")); + + String addressSubString = addresses.size() > 1 ? + "used multiple addresses " + addresses + " with" : + "with address " + new ArrayList<>(addresses).get(0) + " used"; + + String roleSubString = "Trader "; + if (isBuyerHashSet.size() == 1) { + boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + disputesReport = disputesReport.replace(isBuyerSubString, ""); + roleSubString = isBuyer ? "Buyer " : "Seller "; + } + + + String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport; + return new Tuple2<>(roleSubString, traderReport); + }) + .sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role) + .map(e -> e.second) + .collect(Collectors.joining("\n\n")); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java new file mode 100644 index 0000000000..aa6bcc2372 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.support.dispute.arbitration; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; + +import com.google.protobuf.Message; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +@ToString +/* + * Holds a List of arbitration dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class ArbitrationDisputeList extends DisputeList { + + ArbitrationDisputeList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected ArbitrationDisputeList(Collection collection) { + super(collection); + } + + @Override + public Message toProtoMessage() { + + forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.ARBITRATION), "Support type has to be ARBITRATION")); + + return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } + + public static ArbitrationDisputeList fromProto(protobuf.ArbitrationDisputeList proto, + CoreProtoResolver coreProtoResolver) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .filter(e -> e.getSupportType().equals(SupportType.ARBITRATION)) + .collect(Collectors.toList()); + + return new ArbitrationDisputeList(list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java new file mode 100644 index 0000000000..d9b57bb595 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.support.dispute.arbitration; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class ArbitrationDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitrationDisputeListService(PersistenceManager persistenceManager) { + super(persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected ArbitrationDisputeList getConcreteDisputeList() { + return new ArbitrationDisputeList(); + } + + @Override + protected String getFileName() { + return "DisputeList"; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java new file mode 100644 index 0000000000..a2c53a29f5 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -0,0 +1,430 @@ +/* + * 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 bisq.core.support.dispute.arbitration; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Contract; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class ArbitrationManager extends DisputeManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitrationManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + DaoFacade daoFacade, + KeyRing keyRing, + ArbitrationDisputeListService arbitrationDisputeListService, + Config config, + PriceFeedService priceFeedService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, daoFacade, keyRing, arbitrationDisputeListService, config, priceFeedService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.ARBITRATION; + } + + @Override + public void onSupportMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else if (message instanceof PeerPublishedDisputePayoutTxMessage) { + onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Nullable + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return null; + } + + @Override + protected Trade.DisputeState getDisputeStateStartedByPeer() { + return Trade.DisputeState.DISPUTE_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.ARBITRATION_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED)); + } + + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.arbitrator").toLowerCase(); + String link = "https://docs.bisq.network/trading-rules.html#legacy-arbitration"; + return Res.get("support.initialInfo", role, role, link); + } + + @Override + protected String getDisputeIntroForPeer(String disputeInfo) { + return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION); + } + + @Override + protected String getDisputeIntroForDisputeCreator(String disputeInfo) { + return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); + } + + @Override + protected void addPriceInfoMessage(Dispute dispute, int counter) { + // Arbitrator is not used anymore. + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + if (Arrays.equals(disputeResult.getArbitratorPubKey(), + btcWalletService.getArbitratorAddressEntry().getPubKey())) { + log.error("Arbitrator received disputeResultMessage. That must never happen."); + return; + } + + String tradeId = disputeResult.getTradeId(); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(); + + if (dispute.disputeResultProperty().get() != null) { + log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " + + "again because the first close did not succeed. TradeId = " + tradeId); + } + + dispute.setDisputeResult(disputeResult); + Optional tradeOptional = tradeManager.getTradeById(tradeId); + String errorMessage = null; + boolean success = false; + try { + // We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals + // There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb) + // The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives + // more BTC as he has deposited + Contract contract = dispute.getContract(); + + boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); + DisputeResult.Winner publisher = disputeResult.getWinner(); + + // Sometimes the user who receives the trade amount is never online, so we might want to + // let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer. + // Default isLoserPublisher is set to false + if (disputeResult.isLoserPublisher()) { + // we invert the logic + if (publisher == DisputeResult.Winner.BUYER) + publisher = DisputeResult.Winner.SELLER; + else if (publisher == DisputeResult.Winner.SELLER) + publisher = DisputeResult.Winner.BUYER; + } + + if ((isBuyer && publisher == DisputeResult.Winner.BUYER) + || (!isBuyer && publisher == DisputeResult.Winner.SELLER)) { + + Transaction payoutTx = null; + if (tradeOptional.isPresent()) { + payoutTx = tradeOptional.get().getPayoutTx(); + } else { + Optional tradableOptional = closedTradableManager.getTradableById(tradeId); + if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) { + payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); + } + } + + if (payoutTx == null) { + if (dispute.getDepositTxSerialized() != null) { + byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); + DeterministicKey multiSigKeyPair = btcWalletService.getMultiSigKeyPair(tradeId, multiSigPubKey); + Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx( + dispute.getDepositTxSerialized(), + disputeResult.getArbitratorSignature(), + disputeResult.getBuyerPayoutAmount(), + disputeResult.getSellerPayoutAmount(), + contract.getBuyerPayoutAddressString(), + contract.getSellerPayoutAddressString(), + multiSigKeyPair, + contract.getBuyerMultiSigPubKey(), + contract.getSellerMultiSigPubKey(), + disputeResult.getArbitratorPubKey() + ); + Transaction committedDisputedPayoutTx = WalletService.maybeAddSelfTxToWallet(signedDisputedPayoutTx, btcWalletService.getWallet()); + tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + // after successful publish we send peer the tx + dispute.setDisputePayoutTxId(transaction.getTxId().toString()); + sendPeerPublishedPayoutTxMessage(transaction, dispute, contract); + updateTradeOrOpenOfferManager(tradeId); + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error(exception.getMessage()); + } + }, 15); + + success = true; + } else { + errorMessage = "DepositTx is null. TradeId = " + tradeId; + log.warn(errorMessage); + success = false; + } + } else { + log.warn("We already got a payout tx. That might be the case if the other peer did not get the " + + "payout tx and opened a dispute. TradeId = " + tradeId); + dispute.setDisputePayoutTxId(payoutTx.getTxId().toString()); + sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract); + + success = true; + } + } else { + log.trace("We don't publish the tx as we are not the winning party."); + // Clean up tangling trades + if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) { + updateTradeOrOpenOfferManager(tradeId); + } + + success = true; + } + } catch (TransactionVerificationException e) { + errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); + log.error(errorMessage, e); + success = false; + + // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used + // we get a TransactionVerificationException. No reason to keep that dispute open... + updateTradeOrOpenOfferManager(tradeId); + + throw new RuntimeException(errorMessage); + } catch (AddressFormatException | WalletException | SignatureDecodeException e) { + errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); + log.error(errorMessage, e); + success = false; + throw new RuntimeException(errorMessage); + } finally { + // We use the chatMessage as we only persist those not the disputeResultMessage. + // If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage. + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage); + } + + requestPersistence(); + } + + // Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer + private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { + String uid = peerPublishedDisputePayoutTxMessage.getUid(); + String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId(); + Optional disputeOptional = findOwnDispute(tradeId); + if (!disputeOptional.isPresent()) { + log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 3 sec. to be sure the close msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + Contract contract = dispute.getContract(); + boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); + PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + + cleanupRetryMap(uid); + + Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet()); + + dispute.setDisputePayoutTxId(committedDisputePayoutTx.getTxId().toString()); + BtcWalletService.printTx("Disputed payoutTx received from peer", committedDisputePayoutTx); + + // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute + sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send messages + /////////////////////////////////////////////////////////////////////////////////////////// + + // winner (or buyer in case of 50/50) sends tx to other peer + private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) { + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress(); + log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress); + PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(transaction.bitcoinSerialize(), + dispute.getTradeId(), + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + } + } + ); + } + + private void updateTradeOrOpenOfferManager(String tradeId) { + // set state after payout as we call swapTradeEntryToAvailableEntry + if (tradeManager.getTradeById(tradeId).isPresent()) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED); + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java new file mode 100644 index 0000000000..1dc5d7a830 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.support.dispute.arbitration; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class ArbitrationSession extends DisputeSession { + + public ArbitrationSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java b/core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java new file mode 100644 index 0000000000..1ae4cc1646 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.support.dispute.arbitration; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.payment.payload.PaymentAccountPayload; + +import org.bitcoinj.core.Coin; + +import java.security.PublicKey; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +// TODO consider to move to signed witness domain +@Getter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class TraderDataItem { + private final PaymentAccountPayload paymentAccountPayload; + @EqualsAndHashCode.Include + private final AccountAgeWitness accountAgeWitness; + private final Coin tradeAmount; + private final PublicKey peersPubKey; + + public TraderDataItem(PaymentAccountPayload paymentAccountPayload, + AccountAgeWitness accountAgeWitness, + Coin tradeAmount, + PublicKey peersPubKey) { + this.paymentAccountPayload = paymentAccountPayload; + this.accountAgeWitness = accountAgeWitness; + this.tradeAmount = tradeAmount; + this.peersPubKey = peersPubKey; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java new file mode 100644 index 0000000000..92d8c3e44c --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.support.dispute.agent.DisputeAgent; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Getter +public final class Arbitrator extends DisputeAgent { + private final byte[] btcPubKey; + private final String btcAddress; + + public Arbitrator(NodeAddress nodeAddress, + byte[] btcPubKey, + String btcAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + + this.btcPubKey = btcPubKey; + this.btcAddress = btcAddress; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder() + .setNodeAddress(nodeAddress.toProtoMessage()) + .setBtcPubKey(ByteString.copyFrom(btcPubKey)) + .setBtcAddress(btcAddress) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .addAllLanguageCodes(languageCodes) + .setRegistrationDate(registrationDate) + .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) + .setRegistrationSignature(registrationSignature); + Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); + Optional.ofNullable(info).ifPresent(builder::setInfo); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return protobuf.StoragePayload.newBuilder().setArbitrator(builder).build(); + } + + public static Arbitrator fromProto(protobuf.Arbitrator proto) { + return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()), + proto.getBtcPubKey().toByteArray(), + proto.getBtcAddress(), + PubKeyRing.fromProto(proto.getPubKeyRing()), + new ArrayList<>(proto.getLanguageCodesList()), + proto.getRegistrationDate(), + proto.getRegistrationPubKey().toByteArray(), + proto.getRegistrationSignature(), + ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), + ProtoUtil.stringOrNullFromProto(proto.getInfo()), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String toString() { + return "Arbitrator{" + + "\n btcPubKey=" + Utilities.bytesAsHexString(btcPubKey) + + ",\n btcAddress='" + btcAddress + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java new file mode 100644 index 0000000000..e858d68dd9 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.inject.Named; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class ArbitratorManager extends DisputeAgentManager { + + @Inject + public ArbitratorManager(KeyRing keyRing, + ArbitratorService arbitratorService, + User user, + FilterManager filterManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, arbitratorService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054", + "031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd", + "02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c", + "025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2", + "038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55", + "0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe", + "024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95", + "02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd", + "02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81", + "03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d", + "028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e", + "02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b", + "03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169", + "0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72", + "023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5"); + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof Arbitrator; + } + + @Override + protected void addAcceptedDisputeAgentToUser(Arbitrator disputeAgent) { + user.addAcceptedArbitrator(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedArbitrator((Arbitrator) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedArbitrators(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedArbitrators(); + } + + @Override + protected Arbitrator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredArbitrator(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(Arbitrator disputeAgent) { + user.setRegisteredArbitrator(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java new file mode 100644 index 0000000000..34f19790ca --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +public class ArbitratorService extends DisputeAgentService { + @Inject + public ArbitratorService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator) + .map(data -> (Arbitrator) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : new ArrayList<>(); + } + + public Map getArbitrators() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java new file mode 100644 index 0000000000..b88ed9e601 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java @@ -0,0 +1,27 @@ +/* + * 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 bisq.core.support.dispute.arbitration.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.dispute.messages.DisputeMessage; + +abstract class ArbitrationMessage extends DisputeMessage { + ArbitrationMessage(int messageVersion, String uid, SupportType supportType) { + super(messageVersion, uid, supportType); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java new file mode 100644 index 0000000000..17d9009044 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.support.dispute.arbitration.messages; + +import bisq.core.support.SupportType; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = true) +public final class PeerPublishedDisputePayoutTxMessage extends ArbitrationMessage { + private final byte[] transaction; + private final String tradeId; + private final NodeAddress senderNodeAddress; + + public PeerPublishedDisputePayoutTxMessage(byte[] transaction, + String tradeId, + NodeAddress senderNodeAddress, + String uid, + SupportType supportType) { + this(transaction, + tradeId, + senderNodeAddress, + uid, + Version.getP2PMessageVersion(), + supportType); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PeerPublishedDisputePayoutTxMessage(byte[] transaction, + String tradeId, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); + this.transaction = transaction; + this.tradeId = tradeId; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setPeerPublishedDisputePayoutTxMessage(protobuf.PeerPublishedDisputePayoutTxMessage.newBuilder() + .setTransaction(ByteString.copyFrom(transaction)) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid) + .setType(SupportType.toProtoMessage(supportType))) + .build(); + } + + public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto, + int messageVersion) { + return new PeerPublishedDisputePayoutTxMessage(proto.getTransaction().toByteArray(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion, + SupportType.fromProto(proto.getType())); + } + + @Override + public String getTradeId() { + return tradeId; + } + + @Override + public String toString() { + return "PeerPublishedDisputePayoutTxMessage{" + + "\n transaction=" + Utilities.bytesAsHexString(transaction) + + ",\n tradeId='" + tradeId + '\'' + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n PeerPublishedDisputePayoutTxMessage.uid='" + uid + '\'' + + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java new file mode 100644 index 0000000000..00b7b4df82 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.support.dispute.mediation; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; + +import com.google.protobuf.Message; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ToString +/* + * Holds a List of mediation dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class MediationDisputeList extends DisputeList { + + MediationDisputeList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected MediationDisputeList(Collection collection) { + super(collection); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } + + public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, + CoreProtoResolver coreProtoResolver) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .filter(e -> e.getSupportType().equals(SupportType.MEDIATION)) + .collect(Collectors.toList()); + return new MediationDisputeList(list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java new file mode 100644 index 0000000000..70cbd5bf78 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.support.dispute.mediation; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class MediationDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MediationDisputeListService(PersistenceManager persistenceManager) { + super(persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected MediationDisputeList getConcreteDisputeList() { + return new MediationDisputeList(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java new file mode 100644 index 0000000000..776e2191ed --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -0,0 +1,272 @@ +/* + * 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 bisq.core.support.dispute.mediation; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.protocol.DisputeProtocol; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class MediationManager extends DisputeManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MediationManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + DaoFacade daoFacade, + KeyRing keyRing, + MediationDisputeListService mediationDisputeListService, + Config config, + PriceFeedService priceFeedService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, daoFacade, keyRing, mediationDisputeListService, config, priceFeedService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.MEDIATION; + } + + @Override + public void onSupportMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Override + protected Trade.DisputeState getDisputeStateStartedByPeer() { + return Trade.DisputeState.MEDIATION_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.MEDIATION_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> { + tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) + .ifPresent(trade -> { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + }); + }); + } + + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.mediator").toLowerCase(); + String link = "https://docs.bisq.network/trading-rules.html#mediation"; + return Res.get("support.initialInfo", role, role, link); + } + + @Override + protected String getDisputeIntroForPeer(String disputeInfo) { + return Res.get("support.peerOpenedDisputeForMediation", disputeInfo, Version.VERSION); + } + + @Override + protected String getDisputeIntroForDisputeCreator(String disputeInfo) { + return Res.get("support.youOpenedDisputeForMediation", disputeInfo, Version.VERSION); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + String tradeId = disputeResult.getTradeId(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(); + + dispute.setDisputeResult(disputeResult); + + Optional tradeOptional = tradeManager.getTradeById(tradeId); + if (tradeOptional.isPresent()) { + Trade trade = tradeOptional.get(); + if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || + trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) { + trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmount().value); + trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmount().value); + + trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); + + tradeManager.requestPersistence(); + } + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); + + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return dispute.getContract().getMediatorNodeAddress(); + } + + public void onAcceptMediationResult(Trade trade, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + String tradeId = trade.getId(); + Optional optionalDispute = findDispute(tradeId); + checkArgument(optionalDispute.isPresent(), "dispute must be present"); + DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); + Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); + Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + ProcessModel processModel = trade.getProcessModel(); + processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.value); + processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.value); + DisputeProtocol tradeProtocol = (DisputeProtocol) tradeManager.getTradeProtocol(trade); + + trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_ACCEPTED); + tradeManager.requestPersistence(); + + // If we have not got yet the peers signature we sign and send to the peer our signature. + // Otherwise we sign and complete with the peers signature the payout tx. + if (processModel.getTradingPeer().getMediatedPayoutTxSignature() == null) { + tradeProtocol.onAcceptMediationResult(() -> { + if (trade.getPayoutTx() != null) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + } + resultHandler.handleResult(); + }, errorMessageHandler); + } else { + tradeProtocol.onFinalizeMediationResultPayout(() -> { + if (trade.getPayoutTx() != null) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + } + resultHandler.handleResult(); + }, errorMessageHandler); + } + } + + public void rejectMediationResult(Trade trade) { + trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED); + tradeManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java new file mode 100644 index 0000000000..0d9f0c93b2 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.core.support.dispute.mediation; + +import bisq.common.proto.ProtoUtil; + +public enum MediationResultState { + UNDEFINED_MEDIATION_RESULT, + MEDIATION_RESULT_ACCEPTED(), + MEDIATION_RESULT_REJECTED, + SIG_MSG_SENT, + SIG_MSG_ARRIVED, + SIG_MSG_IN_MAILBOX, + SIG_MSG_SEND_FAILED, + RECEIVED_SIG_MSG, + PAYOUT_TX_PUBLISHED, + PAYOUT_TX_PUBLISHED_MSG_SENT, + PAYOUT_TX_PUBLISHED_MSG_ARRIVED, + PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX, + PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED, + RECEIVED_PAYOUT_TX_PUBLISHED_MSG, + PAYOUT_TX_SEEN_IN_NETWORK; + + public static MediationResultState fromProto(protobuf.MediationResultState mediationResultState) { + return ProtoUtil.enumFromProto(MediationResultState.class, mediationResultState.name()); + } + + public static protobuf.MediationResultState toProtoMessage(MediationResultState mediationResultState) { + return protobuf.MediationResultState.valueOf(mediationResultState.name()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java new file mode 100644 index 0000000000..84fba15e6d --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.support.dispute.mediation; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class MediationSession extends DisputeSession { + + public MediationSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java new file mode 100644 index 0000000000..80610ebc23 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.support.dispute.mediation.mediator; + +import bisq.core.support.dispute.agent.DisputeAgent; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.ByteString; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public final class Mediator extends DisputeAgent { + public Mediator(NodeAddress nodeAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + final protobuf.Mediator.Builder builder = protobuf.Mediator.newBuilder() + .setNodeAddress(nodeAddress.toProtoMessage()) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .addAllLanguageCodes(languageCodes) + .setRegistrationDate(registrationDate) + .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) + .setRegistrationSignature(registrationSignature); + Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); + Optional.ofNullable(info).ifPresent(builder::setInfo); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return protobuf.StoragePayload.newBuilder().setMediator(builder).build(); + } + + public static Mediator fromProto(protobuf.Mediator proto) { + return new Mediator(NodeAddress.fromProto(proto.getNodeAddress()), + PubKeyRing.fromProto(proto.getPubKeyRing()), + new ArrayList<>(proto.getLanguageCodesList()), + proto.getRegistrationDate(), + proto.getRegistrationPubKey().toByteArray(), + proto.getRegistrationSignature(), + ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), + ProtoUtil.stringOrNullFromProto(proto.getInfo()), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + public String toString() { + return "Mediator{} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java new file mode 100644 index 0000000000..0e1bcc5c9c --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java @@ -0,0 +1,101 @@ +/* + * 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 bisq.core.support.dispute.mediation.mediator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import javax.inject.Singleton; +import javax.inject.Named; + +import javax.inject.Inject; + +import java.util.List; + +@Singleton +public class MediatorManager extends DisputeAgentManager { + + @Inject + public MediatorManager(KeyRing keyRing, + MediatorService mediatorService, + User user, + FilterManager filterManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, mediatorService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", + "023736953a5a6638db71d7f78edc38cea0e42143c3b184ee67f331dafdc2c59efa", + "03d82260038253f7367012a4fc0c52dac74cfc67ac9cfbc3c3ad8fca746d8e5fc6", + "02dac85f726121ef333d425bc8e13173b5b365a6444176306e6a0a9e76ae1073bd", + "0342a5b37c8f843c3302e930d0197cdd8948a6f76747c05e138a6671a6a4caf739", + "027afa67c920867a70dfad77db6c6f74051f5af8bf56a1ad479f0bc4005df92325", + "03505f44f1893b64a457f8883afdd60774d7f4def6f82bb6f60be83a4b5b85cf82", + "0277d2d505d28ad67a03b001ef66f0eaaf1184fa87ebeaa937703cec7073cb2e8f", + "027cb3e9a56a438714e2144e2f75db7293ad967f12d5c29b17623efbd35ddbceb0", + "03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", + "03756937d33d028eea274a3154775b2bffd076ffcc4a23fe0f9080f8b7fa0dab5b", + "03d8359823a91736cb7aecfaf756872daf258084133c9dd25b96ab3643707c38ca", + "03589ed6ded1a1aa92d6ad38bead13e4ad8ba24c60ca6ed8a8efc6e154e3f60add", + "0356965753f77a9c0e33ca7cc47fd43ce7f99b60334308ad3c11eed3665de79a78", + "031112eb033ebacb635754a2b7163c68270c9171c40f271e70e37b22a2590d3c18"); + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof Mediator; + } + + @Override + protected void addAcceptedDisputeAgentToUser(Mediator disputeAgent) { + user.addAcceptedMediator(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedMediator((Mediator) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedMediators(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedMediators(); + } + + @Override + protected Mediator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredMediator(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(Mediator disputeAgent) { + user.setRegisteredMediator(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java new file mode 100644 index 0000000000..8ff78d2d7e --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java @@ -0,0 +1,66 @@ +/* + * 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 bisq.core.support.dispute.mediation.mediator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@Singleton +public class MediatorService extends DisputeAgentService { + + @Inject + public MediatorService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof Mediator) + .map(data -> (Mediator) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getMediators() : new ArrayList<>(); + } + + public Map getMediators() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java new file mode 100644 index 0000000000..f74fabc38b --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.core.support.dispute.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.messages.SupportMessage; + +import java.util.concurrent.TimeUnit; + +public abstract class DisputeMessage extends SupportMessage { + public static final long TTL = TimeUnit.DAYS.toMillis(15); + + public DisputeMessage(int messageVersion, String uid, SupportType supportType) { + super(messageVersion, uid, supportType); + } + + @Override + public long getTTL() { + return TTL; + } + +} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java new file mode 100644 index 0000000000..50f78529eb --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.support.dispute.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.dispute.DisputeResult; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import static com.google.common.base.Preconditions.checkArgument; + +@Value +@EqualsAndHashCode(callSuper = true) +public final class DisputeResultMessage extends DisputeMessage { + private final DisputeResult disputeResult; + private final NodeAddress senderNodeAddress; + + public DisputeResultMessage(DisputeResult disputeResult, + NodeAddress senderNodeAddress, + String uid, + SupportType supportType) { + this(disputeResult, + senderNodeAddress, + uid, + Version.getP2PMessageVersion(), + supportType); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DisputeResultMessage(DisputeResult disputeResult, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); + this.disputeResult = disputeResult; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder() + .setDisputeResult(disputeResult.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid) + .setType(SupportType.toProtoMessage(supportType))) + .build(); + } + + public static DisputeResultMessage fromProto(protobuf.DisputeResultMessage proto, int messageVersion) { + checkArgument(proto.hasDisputeResult(), "DisputeResult must be set"); + return new DisputeResultMessage(DisputeResult.fromProto(proto.getDisputeResult()), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion, + SupportType.fromProto(proto.getType())); + } + + @Override + public String getTradeId() { + return disputeResult.getTradeId(); + } + + @Override + public String toString() { + return "DisputeResultMessage{" + + "\n disputeResult=" + disputeResult + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n DisputeResultMessage.uid='" + uid + '\'' + + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java new file mode 100644 index 0000000000..f60cda8056 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.support.dispute.messages; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class OpenNewDisputeMessage extends DisputeMessage { + private final Dispute dispute; + private final NodeAddress senderNodeAddress; + + public OpenNewDisputeMessage(Dispute dispute, + NodeAddress senderNodeAddress, + String uid, + SupportType supportType) { + this(dispute, + senderNodeAddress, + uid, + Version.getP2PMessageVersion(), + supportType); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private OpenNewDisputeMessage(Dispute dispute, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); + this.dispute = dispute; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder() + .setUid(uid) + .setDispute(dispute.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setType(SupportType.toProtoMessage(supportType))) + .build(); + } + + public static OpenNewDisputeMessage fromProto(protobuf.OpenNewDisputeMessage proto, + CoreProtoResolver coreProtoResolver, + int messageVersion) { + return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion, + SupportType.fromProto(proto.getType())); + } + + @Override + public String getTradeId() { + return dispute.getTradeId(); + } + + @Override + public String toString() { + return "OpenNewDisputeMessage{" + + "\n dispute=" + dispute + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n OpenNewDisputeMessage.uid='" + uid + '\'' + + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java new file mode 100644 index 0000000000..f2dc136acc --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java @@ -0,0 +1,97 @@ +/* + * 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 bisq.core.support.dispute.messages; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = true) +public final class PeerOpenedDisputeMessage extends DisputeMessage { + private final Dispute dispute; + private final NodeAddress senderNodeAddress; + + public PeerOpenedDisputeMessage(Dispute dispute, + NodeAddress senderNodeAddress, + String uid, + SupportType supportType) { + this(dispute, + senderNodeAddress, + uid, + Version.getP2PMessageVersion(), + supportType); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PeerOpenedDisputeMessage(Dispute dispute, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); + this.dispute = dispute; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setPeerOpenedDisputeMessage(protobuf.PeerOpenedDisputeMessage.newBuilder() + .setUid(uid) + .setDispute(dispute.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setType(SupportType.toProtoMessage(supportType))) + .build(); + } + + public static PeerOpenedDisputeMessage fromProto(protobuf.PeerOpenedDisputeMessage proto, CoreProtoResolver coreProtoResolver, int messageVersion) { + return new PeerOpenedDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion, + SupportType.fromProto(proto.getType())); + } + + @Override + public String getTradeId() { + return dispute.getTradeId(); + } + + @Override + public String toString() { + return "PeerOpenedDisputeMessage{" + + "\n dispute=" + dispute + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n PeerOpenedDisputeMessage.uid='" + uid + '\'' + + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java new file mode 100644 index 0000000000..1194ac9c0d --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java @@ -0,0 +1,77 @@ +/* + * 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 bisq.core.support.dispute.refund; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; + +import com.google.protobuf.Message; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +@ToString +/* + * Holds a List of refund dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class RefundDisputeList extends DisputeList { + + RefundDisputeList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected RefundDisputeList(Collection collection) { + super(collection); + } + + @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(); + } + + public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, + CoreProtoResolver coreProtoResolver) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .filter(e -> e.getSupportType().equals(SupportType.REFUND)) + .collect(Collectors.toList()); + return new RefundDisputeList(list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java new file mode 100644 index 0000000000..1709fa2f79 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.support.dispute.refund; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class RefundDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RefundDisputeListService(PersistenceManager persistenceManager) { + super(persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected RefundDisputeList getConcreteDisputeList() { + return new RefundDisputeList(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java new file mode 100644 index 0000000000..deb1537b62 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -0,0 +1,237 @@ +/* + * 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 bisq.core.support.dispute.refund; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class RefundManager extends DisputeManager { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RefundManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + DaoFacade daoFacade, + KeyRing keyRing, + RefundDisputeListService refundDisputeListService, + Config config, + PriceFeedService priceFeedService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.REFUND; + } + + @Override + public void onSupportMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Override + protected Trade.DisputeState getDisputeStateStartedByPeer() { + return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.REFUND_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED)); + } + + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.refundAgent").toLowerCase(); + String link = "https://docs.bisq.network/trading-rules.html#arbitration"; + return Res.get("support.initialInfo", role, role, link); + } + + @Override + protected String getDisputeIntroForPeer(String disputeInfo) { + return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION); + } + + @Override + protected String getDisputeIntroForDisputeCreator(String disputeInfo) { + return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); + } + + @Override + protected void addPriceInfoMessage(Dispute dispute, int counter) { + // At refund agent we do not add the option trade price check as the time for dispute opening is not correct. + // In case of an option trade the mediator adds to the result summary message automatically the system message + // with the option trade detection info so the refund agent can see that as well. + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + String tradeId = disputeResult.getTradeId(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(); + + if (dispute.disputeResultProperty().get() != null) { + log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + + "again because the first close did not succeed. TradeId = " + tradeId); + } + + dispute.setDisputeResult(disputeResult); + + Optional tradeOptional = tradeManager.getTradeById(tradeId); + if (tradeOptional.isPresent()) { + Trade trade = tradeOptional.get(); + if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED || + trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { + trade.setDisputeState(Trade.DisputeState.REFUND_REQUEST_CLOSED); + tradeManager.requestPersistence(); + } + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); + + // set state after payout as we call swapTradeEntryToAvailableEntry + if (tradeManager.getTradeById(tradeId).isPresent()) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return dispute.getContract().getRefundAgentNodeAddress(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java new file mode 100644 index 0000000000..1664b733bc --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.support.dispute.refund; + +import bisq.common.proto.ProtoUtil; + +// todo +public enum RefundResultState { + UNDEFINED_REFUND_RESULT; + + public static RefundResultState fromProto(protobuf.RefundResultState refundResultState) { + return ProtoUtil.enumFromProto(RefundResultState.class, refundResultState.name()); + } + + public static protobuf.RefundResultState toProtoMessage(RefundResultState refundResultState) { + return protobuf.RefundResultState.valueOf(refundResultState.name()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java new file mode 100644 index 0000000000..b5e9d7e5cc --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.core.support.dispute.refund; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class RefundSession extends DisputeSession { + + public RefundSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java new file mode 100644 index 0000000000..370a4800a0 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java @@ -0,0 +1,116 @@ +/* + * 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 bisq.core.support.dispute.refund.refundagent; + +import bisq.core.support.dispute.agent.DisputeAgent; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.ByteString; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Getter +public final class RefundAgent extends DisputeAgent implements CapabilityRequiringPayload { + + public RefundAgent(NodeAddress nodeAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + protobuf.RefundAgent.Builder builder = protobuf.RefundAgent.newBuilder() + .setNodeAddress(nodeAddress.toProtoMessage()) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .addAllLanguageCodes(languageCodes) + .setRegistrationDate(registrationDate) + .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) + .setRegistrationSignature(registrationSignature); + Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); + Optional.ofNullable(info).ifPresent(builder::setInfo); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return protobuf.StoragePayload.newBuilder().setRefundAgent(builder).build(); + } + + public static RefundAgent fromProto(protobuf.RefundAgent proto) { + return new RefundAgent(NodeAddress.fromProto(proto.getNodeAddress()), + PubKeyRing.fromProto(proto.getPubKeyRing()), + new ArrayList<>(proto.getLanguageCodesList()), + proto.getRegistrationDate(), + proto.getRegistrationPubKey().toByteArray(), + proto.getRegistrationSignature(), + ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), + ProtoUtil.stringOrNullFromProto(proto.getInfo()), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + public String toString() { + return "RefundAgent{} " + super.toString(); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.REFUND_AGENT); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java new file mode 100644 index 0000000000..3aa929c033 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.support.dispute.refund.refundagent; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.inject.Named; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class RefundAgentManager extends DisputeAgentManager { + + @Inject + public RefundAgentManager(KeyRing keyRing, + RefundAgentService refundAgentService, + User user, + FilterManager filterManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, refundAgentService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("02a25798e256b800d7ea71c31098ac9a47cb20892176afdfeb051f5ded382d44af", + "0360455d3cffe00ef73cc1284c84eedacc8c5c3374c43f4aac8ffb95f5130b9ef5", + "03b0513afbb531bc4551b379eba027feddd33c92b5990fd477b0fa6eff90a5b7db", + "03533fd75fda29c351298e50b8ea696656dcb8ce4e263d10618c6901a50450bf0e", + "028124436482aa4c61a4bc4097d60c80b09f4285413be3b023a37a0164cbd5d818", + "0384fcf883116d8e9469720ed7808cc4141f6dc6a5ed23d76dd48f2f5f255590d7", + "029bd318ecee4e212ff06a4396770d600d72e9e0c6532142a428bdb401491e9721", + "02e375b4b24d0a858953f7f94666667554d41f78000b9c8a301294223688b29011", + "0232c088ae7c070de89d2b6c8d485b34bf0e3b2a964a2c6622f39ca501260c23f7", + "033e047f74f2aa1ce41e8c85731f97ab83d448d65dc8518ab3df4474a5d53a3d19", + "02f52a8cf373c8cbddb318e523b7f111168bf753fdfb6f8aa81f88c950ede3a5ce", + "039784029922c54bcd0f0e7f14530f586053a5f4e596e86b3474cd7404657088ae", + "037969f9d5ab2cc609104c6e61323df55428f8f108c11aab7c7b5f953081d39304", + "031bd37475b8c5615ac46d6816e791c59d806d72a0bc6739ae94e5fe4545c7f8a6", + "021bb92c636feacf5b082313eb071a63dfcd26501a48b3cd248e35438e5afb7daf"); + + + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof RefundAgent; + } + + @Override + protected void addAcceptedDisputeAgentToUser(RefundAgent disputeAgent) { + user.addAcceptedRefundAgent(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedRefundAgent((RefundAgent) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedRefundAgents(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedRefundAgents(); + } + + @Override + protected RefundAgent getRegisteredDisputeAgentFromUser() { + return user.getRegisteredRefundAgent(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(RefundAgent disputeAgent) { + user.setRegisteredRefundAgent(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java new file mode 100644 index 0000000000..ab67223e98 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.support.dispute.refund.refundagent; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +public class RefundAgentService extends DisputeAgentService { + @Inject + public RefundAgentService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof RefundAgent) + .map(data -> (RefundAgent) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getRefundAgents() : new ArrayList<>(); + } + + public Map getRefundAgents() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/support/messages/ChatMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java new file mode 100644 index 0000000000..8171caeec4 --- /dev/null +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -0,0 +1,378 @@ +/* + * 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 bisq.core.support.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Attachment; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import java.lang.ref.WeakReference; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +/* Message for direct communication between two nodes. Originally built for trader to + * arbitrator communication as no other direct communication was allowed. Arbitrator is + * considered as the server and trader as the client in arbitration chats + * + * For trader to trader communication the maker is considered to be the server + * and the taker is considered as the client. + * */ +@EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway +@Getter +@Slf4j +public final class ChatMessage extends SupportMessage { + public static final long TTL = TimeUnit.DAYS.toMillis(7); + + public interface Listener { + void onMessageStateChanged(); + } + + private final String tradeId; + private final int traderId; + // This is only used for the server client relationship + // If senderIsTrader == true then the sender is the client + private final boolean senderIsTrader; + private final String message; + private final ArrayList attachments = new ArrayList<>(); + private final NodeAddress senderNodeAddress; + private final long date; + @Setter + private boolean isSystemMessage; + + // Added in v1.1.6. for trader chat to store if message was shown in popup + @Setter + private boolean wasDisplayed; + + //todo move to base class + private final BooleanProperty arrivedProperty; + private final BooleanProperty storedInMailboxProperty; + private final BooleanProperty acknowledgedProperty; + private final StringProperty sendMessageErrorProperty; + private final StringProperty ackErrorProperty; + + transient private WeakReference listener; + + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress) { + this(supportType, + tradeId, + traderId, + senderIsTrader, + message, + null, + senderNodeAddress, + new Date().getTime(), + false, + false, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + false, + null, + null, + false); + } + + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress, + ArrayList attachments) { + this(supportType, + tradeId, + traderId, + senderIsTrader, + message, + attachments, + senderNodeAddress, + new Date().getTime(), + false, + false, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + false, + null, + null, + false); + } + + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress, + long date) { + this(supportType, + tradeId, + traderId, + senderIsTrader, + message, + null, + senderNodeAddress, + date, + false, + false, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + false, + null, + null, + false); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + @Nullable List attachments, + NodeAddress senderNodeAddress, + long date, + boolean arrived, + boolean storedInMailbox, + String uid, + int messageVersion, + boolean acknowledged, + @Nullable String sendMessageError, + @Nullable String ackError, + boolean wasDisplayed) { + super(messageVersion, uid, supportType); + this.tradeId = tradeId; + this.traderId = traderId; + this.senderIsTrader = senderIsTrader; + this.message = message; + this.wasDisplayed = wasDisplayed; + Optional.ofNullable(attachments).ifPresent(e -> addAllAttachments(attachments)); + this.senderNodeAddress = senderNodeAddress; + this.date = date; + arrivedProperty = new SimpleBooleanProperty(arrived); + storedInMailboxProperty = new SimpleBooleanProperty(storedInMailbox); + acknowledgedProperty = new SimpleBooleanProperty(acknowledged); + sendMessageErrorProperty = new SimpleStringProperty(sendMessageError); + ackErrorProperty = new SimpleStringProperty(ackError); + notifyChangeListener(); + } + + // We cannot rename protobuf definition because it would break backward compatibility + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder() + .setType(SupportType.toProtoMessage(supportType)) + .setTradeId(tradeId) + .setTraderId(traderId) + .setSenderIsTrader(senderIsTrader) + .setMessage(message) + .addAllAttachments(attachments.stream().map(Attachment::toProtoMessage).collect(Collectors.toList())) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDate(date) + .setArrived(arrivedProperty.get()) + .setStoredInMailbox(storedInMailboxProperty.get()) + .setIsSystemMessage(isSystemMessage) + .setUid(uid) + .setAcknowledged(acknowledgedProperty.get()) + .setWasDisplayed(wasDisplayed); + Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); + Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); + return getNetworkEnvelopeBuilder() + .setChatMessage(builder) + .build(); + } + + // The protobuf definition ChatMessage cannot be changed as it would break backward compatibility. + public static ChatMessage fromProto(protobuf.ChatMessage proto, + int messageVersion) { + // If we get a msg from an old client type will be ordinal 0 which is the dispute entry and as we only added + // the trade case it is the desired behaviour. + final ChatMessage chatMessage = new ChatMessage( + SupportType.fromProto(proto.getType()), + proto.getTradeId(), + proto.getTraderId(), + proto.getSenderIsTrader(), + proto.getMessage(), + new ArrayList<>(proto.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDate(), + proto.getArrived(), + proto.getStoredInMailbox(), + proto.getUid(), + messageVersion, + proto.getAcknowledged(), + proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), + proto.getAckError().isEmpty() ? null : proto.getAckError(), + proto.getWasDisplayed()); + chatMessage.setSystemMessage(proto.getIsSystemMessage()); + return chatMessage; + } + + public static ChatMessage fromPayloadProto(protobuf.ChatMessage proto) { + // We have the case that an envelope got wrapped into a payload. + // We don't check the message version here as it was checked in the carrier envelope already (in connection class) + // Payloads don't have a message version and are also used for persistence + // We set the value to -1 to indicate it is set but irrelevant + return fromProto(proto, -1); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addAllAttachments(List attachments) { + this.attachments.addAll(attachments); + } + + public void setArrived(@SuppressWarnings("SameParameterValue") boolean arrived) { + this.arrivedProperty.set(arrived); + notifyChangeListener(); + } + + public ReadOnlyBooleanProperty arrivedProperty() { + return arrivedProperty; + } + + + public void setStoredInMailbox(@SuppressWarnings("SameParameterValue") boolean storedInMailbox) { + this.storedInMailboxProperty.set(storedInMailbox); + notifyChangeListener(); + } + + public ReadOnlyBooleanProperty storedInMailboxProperty() { + return storedInMailboxProperty; + } + + public void setAcknowledged(boolean acknowledged) { + this.acknowledgedProperty.set(acknowledged); + notifyChangeListener(); + } + + public ReadOnlyBooleanProperty acknowledgedProperty() { + return acknowledgedProperty; + } + + public void setSendMessageError(String sendMessageError) { + this.sendMessageErrorProperty.set(sendMessageError); + notifyChangeListener(); + } + + public ReadOnlyStringProperty sendMessageErrorProperty() { + return sendMessageErrorProperty; + } + + public void setAckError(String ackError) { + this.ackErrorProperty.set(ackError); + notifyChangeListener(); + } + + public ReadOnlyStringProperty ackErrorProperty() { + return ackErrorProperty; + } + + @Override + public String getTradeId() { + return tradeId; + } + + public String getShortId() { + return Utilities.getShortId(tradeId); + } + + public void addWeakMessageStateListener(Listener listener) { + this.listener = new WeakReference<>(listener); + } + + public boolean isResultMessage(Dispute dispute) { + DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); + if (disputeResult == null) { + return false; + } + + ChatMessage resultChatMessage = disputeResult.getChatMessage(); + return resultChatMessage != null && resultChatMessage.getUid().equals(uid); + } + + @Override + public long getTTL() { + return TTL; + } + + private void notifyChangeListener() { + if (listener != null) { + Listener listener = this.listener.get(); + if (listener != null) { + listener.onMessageStateChanged(); + } + } + } + + @Override + public String toString() { + return "ChatMessage{" + + "\n tradeId='" + tradeId + '\'' + + ",\n traderId=" + traderId + + ",\n senderIsTrader=" + senderIsTrader + + ",\n message='" + message + '\'' + + ",\n attachments=" + attachments + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n date=" + date + + ",\n isSystemMessage=" + isSystemMessage + + ",\n wasDisplayed=" + wasDisplayed + + ",\n arrivedProperty=" + arrivedProperty + + ",\n storedInMailboxProperty=" + storedInMailboxProperty + + ",\n acknowledgedProperty=" + acknowledgedProperty + + ",\n sendMessageErrorProperty=" + sendMessageErrorProperty + + ",\n ackErrorProperty=" + ackErrorProperty + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/messages/SupportMessage.java b/core/src/main/java/bisq/core/support/messages/SupportMessage.java new file mode 100644 index 0000000000..6d54bf353b --- /dev/null +++ b/core/src/main/java/bisq/core/support/messages/SupportMessage.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.support.messages; + +import bisq.core.support.SupportType; + +import bisq.network.p2p.UidMessage; +import bisq.network.p2p.mailbox.MailboxMessage; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class SupportMessage extends NetworkEnvelope implements MailboxMessage, UidMessage { + protected final String uid; + + // Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is ARBITRATION. + protected final SupportType supportType; + + public SupportMessage(int messageVersion, String uid, SupportType supportType) { + super(messageVersion); + this.uid = uid; + this.supportType = supportType; + } + + public abstract String getTradeId(); + + @Override + public String toString() { + return "DisputeMessage{" + + "\n uid='" + uid + '\'' + + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java new file mode 100644 index 0000000000..e69a18c3f6 --- /dev/null +++ b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.support.traderchat; + +import bisq.core.support.SupportSession; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Trade; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class TradeChatSession extends SupportSession { + + @Nullable + private Trade trade; + + public TradeChatSession(@Nullable Trade trade, + boolean isClient) { + super(isClient); + this.trade = trade; + } + + @Override + public String getTradeId() { + return trade != null ? trade.getId() : ""; + } + + @Override + public int getClientId() { + // TODO remove that client-server concept for trade chat + // Get pubKeyRing of taker. Maker is considered server for chat sessions + try { + return trade.getContract().getTakerPubKeyRing().hashCode(); + } catch (NullPointerException e) { + log.warn("Unable to get takerPubKeyRing from Trade Contract - {}", e.toString()); + } + return 0; + } + + @Override + public ObservableList getObservableChatMessageList() { + return trade != null ? trade.getChatMessages() : FXCollections.observableArrayList(); + } + + @Override + public boolean chatIsOpen() { + return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; + } + + @Override + public boolean isDisputeAgent() { + return false; + } +} diff --git a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java new file mode 100644 index 0000000000..a2dc887ad9 --- /dev/null +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -0,0 +1,175 @@ +/* + * 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 bisq.core.support.traderchat; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.support.SupportManager; +import bisq.core.support.SupportType; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.PubKeyRing; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class TraderChatManager extends SupportManager { + private final TradeManager tradeManager; + private final PubKeyRing pubKeyRing; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TraderChatManager(P2PService p2PService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + PubKeyRing pubKeyRing) { + super(p2PService, walletsSetup); + this.tradeManager = tradeManager; + this.pubKeyRing = pubKeyRing; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.TRADE; + } + + @Override + public void requestPersistence() { + tradeManager.requestPersistence(); + } + + @Override + public NodeAddress getPeerNodeAddress(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + if (trade.getContract() != null) { + return trade.getContract().getPeersNodeAddress(pubKeyRing); + } else { + return null; + } + }).orElse(null); + } + + @Override + public PubKeyRing getPeerPubKeyRing(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + if (trade.getContract() != null) { + return trade.getContract().getPeersPubKeyRing(pubKeyRing); + } else { + return null; + } + }).orElse(null); + } + + @Override + public List getAllChatMessages() { + return tradeManager.getObservableList().stream() + .flatMap(trade -> trade.getChatMessages().stream()) + .collect(Collectors.toList()); + } + + @Override + public boolean channelOpen(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).isPresent(); + } + + @Override + public void addAndPersistChatMessage(ChatMessage message) { + tradeManager.getTradeById(message.getTradeId()).ifPresent(trade -> { + ObservableList chatMessages = trade.getChatMessages(); + if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { + if (chatMessages.isEmpty()) { + addSystemMsg(trade); + } + trade.addAndPersistChatMessage(message); + tradeManager.requestPersistence(); + } else { + log.warn("Trade got a chatMessage that we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + }); + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.TRADE_CHAT_MESSAGE; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + super.onAllServicesInitialized(); + tryApplyMessages(); + } + + public void onSupportMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + public void addSystemMsg(Trade trade) { + // We need to use the trade date as otherwise our system msg would not be displayed first as the list is sorted + // by date. + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + trade.getId(), + 0, + false, + Res.get("tradeChat.rules"), + new NodeAddress("null:0000"), + trade.getDate().getTime()); + chatMessage.setSystemMessage(true); + trade.getChatMessages().add(chatMessage); + + requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java new file mode 100644 index 0000000000..3a0005f824 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.proto.ProtoUtil; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + public BuyerAsMakerTrade(Offer offer, + Coin txFee, + Coin takeOfferFee, + boolean isCurrencyForTakerFeeBtc, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + txFee, + takeOfferFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Tradable toProtoMessage() { + return protobuf.Tradable.newBuilder() + .setBuyerAsMakerTrade(protobuf.BuyerAsMakerTrade.newBuilder() + .setTrade((protobuf.Trade) super.toProtoMessage())) + .build(); + } + + public static Tradable fromProto(protobuf.BuyerAsMakerTrade buyerAsMakerTradeProto, + BtcWalletService btcWalletService, + CoreProtoResolver coreProtoResolver) { + protobuf.Trade proto = buyerAsMakerTradeProto.getTrade(); + ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); + String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); + if (uid == null) { + uid = UUID.randomUUID().toString(); + } + BuyerAsMakerTrade trade = new BuyerAsMakerTrade( + Offer.fromProto(proto.getOffer()), + Coin.valueOf(proto.getTxFeeAsLong()), + Coin.valueOf(proto.getTakerFeeAsLong()), + proto.getIsCurrencyForTakerFeeBtc(), + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + btcWalletService, + processModel, + uid); + + trade.setTradeAmountAsLong(proto.getTradeAmountAsLong()); + trade.setTradePrice(proto.getTradePrice()); + trade.setTradingPeerNodeAddress(proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null); + + return fromProto(trade, + proto, + coreProtoResolver); + } +} diff --git a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java new file mode 100644 index 0000000000..2ccf0fb3cb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.proto.ProtoUtil; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + public BuyerAsTakerTrade(Offer offer, + Coin tradeAmount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + NodeAddress tradingPeerNodeAddress, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Tradable toProtoMessage() { + return protobuf.Tradable.newBuilder() + .setBuyerAsTakerTrade(protobuf.BuyerAsTakerTrade.newBuilder() + .setTrade((protobuf.Trade) super.toProtoMessage())) + .build(); + } + + public static Tradable fromProto(protobuf.BuyerAsTakerTrade buyerAsTakerTradeProto, + BtcWalletService btcWalletService, + CoreProtoResolver coreProtoResolver) { + protobuf.Trade proto = buyerAsTakerTradeProto.getTrade(); + ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); + String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); + if (uid == null) { + uid = UUID.randomUUID().toString(); + } + return fromProto(new BuyerAsTakerTrade( + Offer.fromProto(proto.getOffer()), + Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getTxFeeAsLong()), + Coin.valueOf(proto.getTakerFeeAsLong()), + proto.getIsCurrencyForTakerFeeBtc(), + proto.getTradePrice(), + proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + btcWalletService, + processModel, + uid), + proto, + coreProtoResolver); + } +} diff --git a/core/src/main/java/bisq/core/trade/BuyerTrade.java b/core/src/main/java/bisq/core/trade/BuyerTrade.java new file mode 100644 index 0000000000..82f38cf9c1 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/BuyerTrade.java @@ -0,0 +1,96 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class BuyerTrade extends Trade { + BuyerTrade(Offer offer, + Coin tradeAmount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + NodeAddress tradingPeerNodeAddress, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + BuyerTrade(Offer offer, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + @Override + public Coin getPayoutAmount() { + checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null"); + return checkNotNull(getOffer()).getBuyerSecurityDeposit().add(getTradeAmount()); + } + + @Override + public boolean confirmPermitted() { + return !getDisputeState().isArbitrated(); + } +} diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java new file mode 100644 index 0000000000..81602d226d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -0,0 +1,320 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.OfferPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.util.VolumeUtil; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; + +import org.apache.commons.lang3.StringUtils; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +@Value +public final class Contract implements NetworkPayload { + private final OfferPayload offerPayload; + private final long tradeAmount; + private final long tradePrice; + private final String takerFeeTxID; + private final NodeAddress buyerNodeAddress; + private final NodeAddress sellerNodeAddress; + private final NodeAddress mediatorNodeAddress; + private final boolean isBuyerMakerAndSellerTaker; + private final String makerAccountId; + private final String takerAccountId; + private final PaymentAccountPayload makerPaymentAccountPayload; + private final PaymentAccountPayload takerPaymentAccountPayload; + @JsonExclude + private final PubKeyRing makerPubKeyRing; + @JsonExclude + private final PubKeyRing takerPubKeyRing; + private final String makerPayoutAddressString; + private final String takerPayoutAddressString; + @JsonExclude + private final byte[] makerMultiSigPubKey; + @JsonExclude + private final byte[] takerMultiSigPubKey; + + // Added in v1.2.0 + private long lockTime; + private final NodeAddress refundAgentNodeAddress; + + public Contract(OfferPayload offerPayload, + long tradeAmount, + long tradePrice, + String takerFeeTxID, + NodeAddress buyerNodeAddress, + NodeAddress sellerNodeAddress, + NodeAddress mediatorNodeAddress, + boolean isBuyerMakerAndSellerTaker, + String makerAccountId, + String takerAccountId, + PaymentAccountPayload makerPaymentAccountPayload, + PaymentAccountPayload takerPaymentAccountPayload, + PubKeyRing makerPubKeyRing, + PubKeyRing takerPubKeyRing, + String makerPayoutAddressString, + String takerPayoutAddressString, + byte[] makerMultiSigPubKey, + byte[] takerMultiSigPubKey, + long lockTime, + NodeAddress refundAgentNodeAddress) { + this.offerPayload = offerPayload; + this.tradeAmount = tradeAmount; + this.tradePrice = tradePrice; + this.takerFeeTxID = takerFeeTxID; + this.buyerNodeAddress = buyerNodeAddress; + this.sellerNodeAddress = sellerNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker; + this.makerAccountId = makerAccountId; + this.takerAccountId = takerAccountId; + this.makerPaymentAccountPayload = makerPaymentAccountPayload; + this.takerPaymentAccountPayload = takerPaymentAccountPayload; + this.makerPubKeyRing = makerPubKeyRing; + this.takerPubKeyRing = takerPubKeyRing; + this.makerPayoutAddressString = makerPayoutAddressString; + this.takerPayoutAddressString = takerPayoutAddressString; + this.makerMultiSigPubKey = makerMultiSigPubKey; + this.takerMultiSigPubKey = takerMultiSigPubKey; + this.lockTime = lockTime; + this.refundAgentNodeAddress = refundAgentNodeAddress; + + String makerPaymentMethodId = makerPaymentAccountPayload.getPaymentMethodId(); + String takerPaymentMethodId = takerPaymentAccountPayload.getPaymentMethodId(); + // For SEPA offers we accept also SEPA_INSTANT takers + // Otherwise both ids need to be the same + boolean result = (makerPaymentMethodId.equals(PaymentMethod.SEPA_ID) && takerPaymentMethodId.equals(PaymentMethod.SEPA_INSTANT_ID)) || + makerPaymentMethodId.equals(takerPaymentMethodId); + checkArgument(result, "payment methods of maker and taker must be the same.\n" + + "makerPaymentMethodId=" + makerPaymentMethodId + "\n" + + "takerPaymentMethodId=" + takerPaymentMethodId); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { + return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), + proto.getTradeAmount(), + proto.getTradePrice(), + proto.getTakerFeeTxId(), + NodeAddress.fromProto(proto.getBuyerNodeAddress()), + NodeAddress.fromProto(proto.getSellerNodeAddress()), + NodeAddress.fromProto(proto.getMediatorNodeAddress()), + proto.getIsBuyerMakerAndSellerTaker(), + proto.getMakerAccountId(), + proto.getTakerAccountId(), + coreProtoResolver.fromProto(proto.getMakerPaymentAccountPayload()), + coreProtoResolver.fromProto(proto.getTakerPaymentAccountPayload()), + PubKeyRing.fromProto(proto.getMakerPubKeyRing()), + PubKeyRing.fromProto(proto.getTakerPubKeyRing()), + proto.getMakerPayoutAddressString(), + proto.getTakerPayoutAddressString(), + proto.getMakerMultiSigPubKey().toByteArray(), + proto.getTakerMultiSigPubKey().toByteArray(), + proto.getLockTime(), + NodeAddress.fromProto(proto.getRefundAgentNodeAddress())); + } + + @Override + public protobuf.Contract toProtoMessage() { + return protobuf.Contract.newBuilder() + .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) + .setTradeAmount(tradeAmount) + .setTradePrice(tradePrice) + .setTakerFeeTxId(takerFeeTxID) + .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) + .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) + .setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage()) + .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) + .setMakerAccountId(makerAccountId) + .setTakerAccountId(takerAccountId) + .setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage()) + .setTakerPaymentAccountPayload((protobuf.PaymentAccountPayload) takerPaymentAccountPayload.toProtoMessage()) + .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setMakerMultiSigPubKey(ByteString.copyFrom(makerMultiSigPubKey)) + .setTakerMultiSigPubKey(ByteString.copyFrom(takerMultiSigPubKey)) + .setLockTime(lockTime) + .setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage()) + .build(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getBuyerPayoutAddressString() { + return isBuyerMakerAndSellerTaker ? makerPayoutAddressString : takerPayoutAddressString; + } + + public String getSellerPayoutAddressString() { + return isBuyerMakerAndSellerTaker ? takerPayoutAddressString : makerPayoutAddressString; + } + + public PubKeyRing getBuyerPubKeyRing() { + return isBuyerMakerAndSellerTaker ? makerPubKeyRing : takerPubKeyRing; + } + + public PubKeyRing getSellerPubKeyRing() { + return isBuyerMakerAndSellerTaker ? takerPubKeyRing : makerPubKeyRing; + } + + public byte[] getBuyerMultiSigPubKey() { + return isBuyerMakerAndSellerTaker ? makerMultiSigPubKey : takerMultiSigPubKey; + } + + public byte[] getSellerMultiSigPubKey() { + return isBuyerMakerAndSellerTaker ? takerMultiSigPubKey : makerMultiSigPubKey; + } + + public PaymentAccountPayload getBuyerPaymentAccountPayload() { + return isBuyerMakerAndSellerTaker ? makerPaymentAccountPayload : takerPaymentAccountPayload; + } + + public PaymentAccountPayload getSellerPaymentAccountPayload() { + return isBuyerMakerAndSellerTaker ? takerPaymentAccountPayload : makerPaymentAccountPayload; + } + + public String getPaymentMethodId() { + return makerPaymentAccountPayload.getPaymentMethodId(); + } + + public Coin getTradeAmount() { + return Coin.valueOf(tradeAmount); + } + + public Volume getTradeVolume() { + Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); + + if (getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(getOfferPayload().getCurrencyCode())) + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); + + return volumeByAmount; + } + + public Price getTradePrice() { + return Price.valueOf(offerPayload.getCurrencyCode(), tradePrice); + } + + public NodeAddress getMyNodeAddress(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getBuyerPubKeyRing())) + return buyerNodeAddress; + else + return sellerNodeAddress; + } + + public NodeAddress getPeersNodeAddress(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getSellerPubKeyRing())) + return buyerNodeAddress; + else + return sellerNodeAddress; + } + + public PubKeyRing getPeersPubKeyRing(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getSellerPubKeyRing())) + return getBuyerPubKeyRing(); + else + return getSellerPubKeyRing(); + } + + public boolean isMyRoleBuyer(PubKeyRing myPubKeyRing) { + return getBuyerPubKeyRing().equals(myPubKeyRing); + } + + public boolean isMyRoleMaker(PubKeyRing myPubKeyRing) { + return isBuyerMakerAndSellerTaker() == isMyRoleBuyer(myPubKeyRing); + } + + public void printDiff(@Nullable String peersContractAsJson) { + String json = Utilities.objectToJson(this); + String diff = StringUtils.difference(json, peersContractAsJson); + if (!diff.isEmpty()) { + log.warn("Diff of both contracts: \n" + diff); + log.warn("\n\n------------------------------------------------------------\n" + + "Contract as json\n" + + json + + "\n------------------------------------------------------------\n"); + + log.warn("\n\n------------------------------------------------------------\n" + + "Peers contract as json\n" + + peersContractAsJson + + "\n------------------------------------------------------------\n"); + } else { + log.debug("Both contracts are the same"); + } + } + + @Override + public String toString() { + return "Contract{" + + "\n offerPayload=" + offerPayload + + ",\n tradeAmount=" + tradeAmount + + ",\n tradePrice=" + tradePrice + + ",\n takerFeeTxID='" + takerFeeTxID + '\'' + + ",\n buyerNodeAddress=" + buyerNodeAddress + + ",\n sellerNodeAddress=" + sellerNodeAddress + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n isBuyerMakerAndSellerTaker=" + isBuyerMakerAndSellerTaker + + ",\n makerAccountId='" + makerAccountId + '\'' + + ",\n takerAccountId='" + takerAccountId + '\'' + + ",\n makerPaymentAccountPayload=" + makerPaymentAccountPayload + + ",\n takerPaymentAccountPayload=" + takerPaymentAccountPayload + + ",\n makerPubKeyRing=" + makerPubKeyRing + + ",\n takerPubKeyRing=" + takerPubKeyRing + + ",\n makerPayoutAddressString='" + makerPayoutAddressString + '\'' + + ",\n takerPayoutAddressString='" + takerPayoutAddressString + '\'' + + ",\n makerMultiSigPubKey=" + Utilities.bytesAsHexString(makerMultiSigPubKey) + + ",\n takerMultiSigPubKey=" + Utilities.bytesAsHexString(takerMultiSigPubKey) + + ",\n buyerMultiSigPubKey=" + Utilities.bytesAsHexString(getBuyerMultiSigPubKey()) + + ",\n sellerMultiSigPubKey=" + Utilities.bytesAsHexString(getSellerMultiSigPubKey()) + + ",\n lockTime=" + lockTime + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java new file mode 100644 index 0000000000..3d2785c01e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java @@ -0,0 +1,64 @@ +/* + * 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 bisq.core.trade; + +import bisq.common.config.Config; +import bisq.common.file.JsonFileManager; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.stream.Collectors; + +public class DumpDelayedPayoutTx { + private final boolean dumpDelayedPayoutTxs; + private final JsonFileManager jsonFileManager; + + @Inject + DumpDelayedPayoutTx(@Named(Config.STORAGE_DIR) File storageDir, + @Named(Config.DUMP_DELAYED_PAYOUT_TXS) boolean dumpDelayedPayoutTxs) { + this.dumpDelayedPayoutTxs = dumpDelayedPayoutTxs; + jsonFileManager = new JsonFileManager(storageDir); + } + + static class DelayedPayoutHash { + final String tradeId; + final String delayedPayoutTx; + + DelayedPayoutHash(String tradeId, String delayedPayoutTx) { + this.tradeId = tradeId; + this.delayedPayoutTx = delayedPayoutTx; + } + } + + public void maybeDumpDelayedPayoutTxs(TradableList tradableList, String fileName) { + if (!dumpDelayedPayoutTxs) + return; + + var delayedPayoutHashes = tradableList.stream() + .filter(tradable -> tradable instanceof Trade) + .map(trade -> new DelayedPayoutHash(trade.getId(), + Utilities.bytesAsHexString(((Trade) trade).getDelayedPayoutTxBytes()))) + .collect(Collectors.toList()); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(delayedPayoutHashes), fileName); + } + +} diff --git a/core/src/main/java/bisq/core/trade/MakerTrade.java b/core/src/main/java/bisq/core/trade/MakerTrade.java new file mode 100644 index 0000000000..2e0a41182d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/MakerTrade.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.core.trade; + +public interface MakerTrade { +} diff --git a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java new file mode 100644 index 0000000000..ccbf66a0f6 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java @@ -0,0 +1,108 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.proto.ProtoUtil; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + public SellerAsMakerTrade(Offer offer, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Tradable toProtoMessage() { + return protobuf.Tradable.newBuilder() + .setSellerAsMakerTrade(protobuf.SellerAsMakerTrade.newBuilder() + .setTrade((protobuf.Trade) super.toProtoMessage())) + .build(); + } + + public static Tradable fromProto(protobuf.SellerAsMakerTrade sellerAsMakerTradeProto, + BtcWalletService btcWalletService, + CoreProtoResolver coreProtoResolver) { + protobuf.Trade proto = sellerAsMakerTradeProto.getTrade(); + ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); + String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); + if (uid == null) { + uid = UUID.randomUUID().toString(); + } + SellerAsMakerTrade trade = new SellerAsMakerTrade( + Offer.fromProto(proto.getOffer()), + Coin.valueOf(proto.getTxFeeAsLong()), + Coin.valueOf(proto.getTakerFeeAsLong()), + proto.getIsCurrencyForTakerFeeBtc(), + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + btcWalletService, + processModel, + uid); + + trade.setTradeAmountAsLong(proto.getTradeAmountAsLong()); + trade.setTradePrice(proto.getTradePrice()); + trade.setTradingPeerNodeAddress(proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null); + + return fromProto(trade, + proto, + coreProtoResolver); + } +} diff --git a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java new file mode 100644 index 0000000000..11fb6c281d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.proto.ProtoUtil; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + public SellerAsTakerTrade(Offer offer, + Coin tradeAmount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + NodeAddress tradingPeerNodeAddress, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.Tradable toProtoMessage() { + return protobuf.Tradable.newBuilder() + .setSellerAsTakerTrade(protobuf.SellerAsTakerTrade.newBuilder() + .setTrade((protobuf.Trade) super.toProtoMessage())) + .build(); + } + + public static Tradable fromProto(protobuf.SellerAsTakerTrade sellerAsTakerTradeProto, + BtcWalletService btcWalletService, + CoreProtoResolver coreProtoResolver) { + protobuf.Trade proto = sellerAsTakerTradeProto.getTrade(); + ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); + String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); + if (uid == null) { + uid = UUID.randomUUID().toString(); + } + return fromProto(new SellerAsTakerTrade( + Offer.fromProto(proto.getOffer()), + Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getTxFeeAsLong()), + Coin.valueOf(proto.getTakerFeeAsLong()), + proto.getIsCurrencyForTakerFeeBtc(), + proto.getTradePrice(), + proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + btcWalletService, + processModel, + uid), + proto, + coreProtoResolver); + } +} diff --git a/core/src/main/java/bisq/core/trade/SellerTrade.java b/core/src/main/java/bisq/core/trade/SellerTrade.java new file mode 100644 index 0000000000..a87c18ddee --- /dev/null +++ b/core/src/main/java/bisq/core/trade/SellerTrade.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.offer.Offer; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.network.p2p.NodeAddress; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class SellerTrade extends Trade { + SellerTrade(Offer offer, + Coin tradeAmount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + NodeAddress tradingPeerNodeAddress, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + SellerTrade(Offer offer, + Coin txFee, + Coin takeOfferFee, + boolean isCurrencyForTakerFeeBtc, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + super(offer, + txFee, + takeOfferFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + } + + @Override + public Coin getPayoutAmount() { + return checkNotNull(getOffer()).getSellerSecurityDeposit(); + } + + @Override + public boolean confirmPermitted() { + // For altcoin there is no reason to delay BTC release as no chargeback risk + if (CurrencyUtil.isCryptoCurrency(getOffer().getCurrencyCode())) { + return true; + } + + switch (getDisputeState()) { + case NO_DISPUTE: + return true; + + case DISPUTE_REQUESTED: + case DISPUTE_STARTED_BY_PEER: + case DISPUTE_CLOSED: + case MEDIATION_REQUESTED: + case MEDIATION_STARTED_BY_PEER: + return false; + + case MEDIATION_CLOSED: + return !mediationResultAppliedPenaltyToSeller(); + + case REFUND_REQUESTED: + case REFUND_REQUEST_STARTED_BY_PEER: + case REFUND_REQUEST_CLOSED: + default: + return false; + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/TakerTrade.java b/core/src/main/java/bisq/core/trade/TakerTrade.java new file mode 100644 index 0000000000..a6d82ac840 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TakerTrade.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.core.trade; + +public interface TakerTrade { +} diff --git a/core/src/main/java/bisq/core/trade/Tradable.java b/core/src/main/java/bisq/core/trade/Tradable.java new file mode 100644 index 0000000000..e9b3f33193 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/Tradable.java @@ -0,0 +1,34 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.offer.Offer; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.util.Date; + +public interface Tradable extends PersistablePayload { + Offer getOffer(); + + Date getDate(); + + String getId(); + + String getShortId(); +} diff --git a/core/src/main/java/bisq/core/trade/TradableList.java b/core/src/main/java/bisq/core/trade/TradableList.java new file mode 100644 index 0000000000..c3a668708a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradableList.java @@ -0,0 +1,96 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.OpenOffer; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.ProtobufferRuntimeException; +import bisq.common.proto.persistable.PersistableListAsObservable; + +import com.google.protobuf.Message; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class TradableList extends PersistableListAsObservable { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TradableList() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected TradableList(Collection collection) { + super(collection); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setTradableList(protobuf.TradableList.newBuilder() + .addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class))) + .build(); + } + + public static TradableList fromProto(protobuf.TradableList proto, + CoreProtoResolver coreProtoResolver, + BtcWalletService btcWalletService) { + List list = proto.getTradableList().stream() + .map(tradable -> { + switch (tradable.getMessageCase()) { + case OPEN_OFFER: + return OpenOffer.fromProto(tradable.getOpenOffer()); + case BUYER_AS_MAKER_TRADE: + return BuyerAsMakerTrade.fromProto(tradable.getBuyerAsMakerTrade(), btcWalletService, coreProtoResolver); + case BUYER_AS_TAKER_TRADE: + return BuyerAsTakerTrade.fromProto(tradable.getBuyerAsTakerTrade(), btcWalletService, coreProtoResolver); + case SELLER_AS_MAKER_TRADE: + return SellerAsMakerTrade.fromProto(tradable.getSellerAsMakerTrade(), btcWalletService, coreProtoResolver); + case SELLER_AS_TAKER_TRADE: + return SellerAsTakerTrade.fromProto(tradable.getSellerAsTakerTrade(), btcWalletService, coreProtoResolver); + default: + log.error("Unknown messageCase. tradable.getMessageCase() = " + tradable.getMessageCase()); + throw new ProtobufferRuntimeException("Unknown messageCase. tradable.getMessageCase() = " + + tradable.getMessageCase()); + } + }) + .collect(Collectors.toList()); + + return new TradableList<>(list); + } + + @Override + public String toString() { + return "TradableList{" + + ",\n list=" + getList() + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java new file mode 100644 index 0000000000..de5c1949bb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -0,0 +1,1173 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.support.dispute.refund.RefundResultState; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.protocol.ProcessModel; +import bisq.core.trade.protocol.ProcessModelServiceProvider; +import bisq.core.trade.txproof.AssetTxProofResult; +import bisq.core.util.VolumeUtil; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.taskrunner.Model; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.Date; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Holds all data which are relevant to the trade, but not those which are only needed in the trade process as shared data between tasks. Those data are + * stored in the task model. + */ +@Slf4j +public abstract class Trade implements Tradable, Model { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Enums + /////////////////////////////////////////////////////////////////////////////////////////// + + public enum State { + // #################### Phase PREPARATION + // When trade protocol starts no funds are on stake + PREPARATION(Phase.INIT), + + // At first part maker/taker have different roles + // taker perspective + // #################### Phase TAKER_FEE_PUBLISHED + TAKER_PUBLISHED_TAKER_FEE_TX(Phase.TAKER_FEE_PUBLISHED), + + // PUBLISH_DEPOSIT_TX_REQUEST + // maker perspective + MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), + MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), + MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), //not a mailbox msg, not used... + MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), + + // taker perspective + TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), // Not used anymore + + + // #################### Phase DEPOSIT_PUBLISHED + // We changes order in trade protocol of publishing deposit tx and sending it to the peer. + // Now we send it first to the peer and only if that succeeds we publish it to avoid likelihood of + // failed trades. We do not want to change the order of the enum though so we keep it here as it was originally. + SELLER_PUBLISHED_DEPOSIT_TX(Phase.DEPOSIT_PUBLISHED), + + + // DEPOSIT_TX_PUBLISHED_MSG + // seller perspective + SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + + // buyer perspective + BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + + // Alternatively the buyer could have seen the deposit tx earlier before he received the DEPOSIT_TX_PUBLISHED_MSG + BUYER_SAW_DEPOSIT_TX_IN_NETWORK(Phase.DEPOSIT_PUBLISHED), + + + // #################### Phase DEPOSIT_CONFIRMED + DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN(Phase.DEPOSIT_CONFIRMED), + + + // #################### Phase FIAT_SENT + BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED(Phase.FIAT_SENT), + BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), + BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), + BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), + BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), + + SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), + + // #################### Phase FIAT_RECEIVED + // note that this state can also be triggered by auto confirmation feature + SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT(Phase.FIAT_RECEIVED), + + // #################### Phase PAYOUT_PUBLISHED + SELLER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED), + + SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), + SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), + SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), + SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), + + BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), + // Alternatively the maker could have seen the payout tx earlier before he received the PAYOUT_TX_PUBLISHED_MSG + BUYER_SAW_PAYOUT_TX_IN_NETWORK(Phase.PAYOUT_PUBLISHED), + + + // #################### Phase WITHDRAWN + WITHDRAW_COMPLETED(Phase.WITHDRAWN); + + @NotNull + public Phase getPhase() { + return phase; + } + + @NotNull + private final Phase phase; + + State(@NotNull Phase phase) { + this.phase = phase; + } + + public static Trade.State fromProto(protobuf.Trade.State state) { + return ProtoUtil.enumFromProto(Trade.State.class, state.name()); + } + + public static protobuf.Trade.State toProtoMessage(Trade.State state) { + return protobuf.Trade.State.valueOf(state.name()); + } + + + // We allow a state change only if the phase is the next phase or if we do not change the phase by the + // state change (e.g. detail change inside the same phase) + public boolean isValidTransitionTo(State newState) { + Phase newPhase = newState.getPhase(); + Phase currentPhase = this.getPhase(); + return currentPhase.isValidTransitionTo(newPhase) || newPhase.equals(currentPhase); + } + } + + public enum Phase { + INIT, + TAKER_FEE_PUBLISHED, + DEPOSIT_PUBLISHED, + DEPOSIT_CONFIRMED, + FIAT_SENT, + FIAT_RECEIVED, + PAYOUT_PUBLISHED, + WITHDRAWN; + + public static Trade.Phase fromProto(protobuf.Trade.Phase phase) { + return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name()); + } + + public static protobuf.Trade.Phase toProtoMessage(Trade.Phase phase) { + return protobuf.Trade.Phase.valueOf(phase.name()); + } + + // We allow a phase change only if the phase a future phase (we cannot limit it to next phase as we have cases where + // we skip a phase as it is only relevant to one role -> states and phases need a redesign ;-( ) + public boolean isValidTransitionTo(Phase newPhase) { + // this is current phase + return newPhase.ordinal() > this.ordinal(); + } + } + + public enum DisputeState { + NO_DISPUTE, + // arbitration + DISPUTE_REQUESTED, + DISPUTE_STARTED_BY_PEER, + DISPUTE_CLOSED, + + // mediation + MEDIATION_REQUESTED, + MEDIATION_STARTED_BY_PEER, + MEDIATION_CLOSED, + + // refund + REFUND_REQUESTED, + REFUND_REQUEST_STARTED_BY_PEER, + REFUND_REQUEST_CLOSED; + + public static Trade.DisputeState fromProto(protobuf.Trade.DisputeState disputeState) { + return ProtoUtil.enumFromProto(Trade.DisputeState.class, disputeState.name()); + } + + public static protobuf.Trade.DisputeState toProtoMessage(Trade.DisputeState disputeState) { + return protobuf.Trade.DisputeState.valueOf(disputeState.name()); + } + + public boolean isNotDisputed() { + return this == Trade.DisputeState.NO_DISPUTE; + } + + public boolean isMediated() { + return this == Trade.DisputeState.MEDIATION_REQUESTED || + this == Trade.DisputeState.MEDIATION_STARTED_BY_PEER || + this == Trade.DisputeState.MEDIATION_CLOSED; + } + + public boolean isArbitrated() { + return this == Trade.DisputeState.DISPUTE_REQUESTED || + this == Trade.DisputeState.DISPUTE_STARTED_BY_PEER || + this == Trade.DisputeState.DISPUTE_CLOSED || + this == Trade.DisputeState.REFUND_REQUESTED || + this == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER || + this == Trade.DisputeState.REFUND_REQUEST_CLOSED; + } + } + + public enum TradePeriodState { + FIRST_HALF, + SECOND_HALF, + TRADE_PERIOD_OVER; + + public static Trade.TradePeriodState fromProto(protobuf.Trade.TradePeriodState tradePeriodState) { + return ProtoUtil.enumFromProto(Trade.TradePeriodState.class, tradePeriodState.name()); + } + + public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodState tradePeriodState) { + return protobuf.Trade.TradePeriodState.valueOf(tradePeriodState.name()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Fields + /////////////////////////////////////////////////////////////////////////////////////////// + + // Persistable + // Immutable + @Getter + private final ProcessModel processModel; + @Getter + private final Offer offer; + @Getter + private final boolean isCurrencyForTakerFeeBtc; + @Getter + private final long txFeeAsLong; + @Getter + private final long takerFeeAsLong; + + // Added in 1.5.1 + @Getter + private final String uid; + + @Setter + private long takeOfferDate; + + // Mutable + @Nullable + @Getter + @Setter + private String takerFeeTxId; + @Nullable + @Getter + @Setter + private String depositTxId; + @Nullable + @Getter + @Setter + private String payoutTxId; + @Getter + @Setter + private long tradeAmountAsLong; + @Setter + private long tradePrice; + @Nullable + @Getter + private NodeAddress tradingPeerNodeAddress; + @Getter + private State state = State.PREPARATION; + @Getter + private DisputeState disputeState = DisputeState.NO_DISPUTE; + @Getter + private TradePeriodState tradePeriodState = TradePeriodState.FIRST_HALF; + @Nullable + @Getter + @Setter + private Contract contract; + @Nullable + @Getter + @Setter + private String contractAsJson; + @Nullable + @Getter + @Setter + private byte[] contractHash; + @Nullable + @Getter + @Setter + private String takerContractSignature; + @Nullable + @Getter + @Setter + private String makerContractSignature; + @Nullable + @Getter + @Setter + private NodeAddress arbitratorNodeAddress; + @Nullable + @Setter + private byte[] arbitratorBtcPubKey; + @Nullable + @Getter + @Setter + private PubKeyRing arbitratorPubKeyRing; + @Nullable + @Getter + @Setter + private NodeAddress mediatorNodeAddress; + @Nullable + @Getter + @Setter + private PubKeyRing mediatorPubKeyRing; + @Nullable + @Getter + @Setter + private String takerPaymentAccountId; + @Nullable + private String errorMessage; + @Getter + @Setter + @Nullable + private String counterCurrencyTxId; + @Getter + private final ObservableList chatMessages = FXCollections.observableArrayList(); + + // Transient + // Immutable + @Getter + transient final private Coin txFee; + @Getter + transient final private Coin takerFee; + @Getter + transient final private BtcWalletService btcWalletService; + + transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); + transient final private ObjectProperty statePhaseProperty = new SimpleObjectProperty<>(state.phase); + transient final private ObjectProperty disputeStateProperty = new SimpleObjectProperty<>(disputeState); + transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(tradePeriodState); + transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); + + // Mutable + @Nullable + transient private Transaction depositTx; + @Getter + transient private boolean isInitialized; + + // Added in v1.2.0 + @Nullable + transient private Transaction delayedPayoutTx; + + @Nullable + transient private Transaction payoutTx; + @Nullable + transient private Coin tradeAmount; + + transient private ObjectProperty tradeAmountProperty; + transient private ObjectProperty tradeVolumeProperty; + + // Added in v1.1.6 + @Getter + @Nullable + private MediationResultState mediationResultState = MediationResultState.UNDEFINED_MEDIATION_RESULT; + transient final private ObjectProperty mediationResultStateProperty = new SimpleObjectProperty<>(mediationResultState); + + // Added in v1.2.0 + @Getter + @Setter + private long lockTime; + @Nullable + @Getter + @Setter + private byte[] delayedPayoutTxBytes; + @Nullable + @Getter + @Setter + private NodeAddress refundAgentNodeAddress; + @Nullable + @Getter + @Setter + private PubKeyRing refundAgentPubKeyRing; + @Getter + @Nullable + private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT; + transient final private ObjectProperty refundResultStateProperty = new SimpleObjectProperty<>(refundResultState); + + // Added at v1.3.8 + // We use that for the XMR txKey but want to keep it generic to be flexible for other payment methods or assets. + @Getter + @Setter + private String counterCurrencyExtraData; + + // Added at v1.3.8 + // Generic tx proof result. We persist name if AssetTxProofResult enum. Other fields in the enum are not persisted + // as they are not very relevant as historical data (e.g. number of confirmations) + @Nullable + @Getter + private AssetTxProofResult assetTxProofResult; + // ObjectProperty with AssetTxProofResult does not notify changeListeners. Probably because AssetTxProofResult is + // an enum and enum does not support EqualsAndHashCode. Alternatively we could add a addListener and removeListener + // method and a listener interface, but the IntegerProperty seems to be less boilerplate. + @Getter + transient final private IntegerProperty assetTxProofResultUpdateProperty = new SimpleIntegerProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + // maker + protected Trade(Offer offer, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + this.offer = offer; + this.txFee = txFee; + this.takerFee = takerFee; + this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; + this.arbitratorNodeAddress = arbitratorNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; + this.btcWalletService = btcWalletService; + this.processModel = processModel; + this.uid = uid; + + txFeeAsLong = txFee.value; + takerFeeAsLong = takerFee.value; + takeOfferDate = new Date().getTime(); + } + + + // taker + @SuppressWarnings("NullableProblems") + protected Trade(Offer offer, + Coin tradeAmount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + NodeAddress tradingPeerNodeAddress, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, + BtcWalletService btcWalletService, + ProcessModel processModel, + String uid) { + + this(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + btcWalletService, + processModel, + uid); + this.tradePrice = tradePrice; + this.tradingPeerNodeAddress = tradingPeerNodeAddress; + + setTradeAmount(tradeAmount); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() + .setOffer(offer.toProtoMessage()) + .setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc) + .setTxFeeAsLong(txFeeAsLong) + .setTakerFeeAsLong(takerFeeAsLong) + .setTakeOfferDate(takeOfferDate) + .setProcessModel(processModel.toProtoMessage()) + .setTradeAmountAsLong(tradeAmountAsLong) + .setTradePrice(tradePrice) + .setState(Trade.State.toProtoMessage(state)) + .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) + .setTradePeriodState(Trade.TradePeriodState.toProtoMessage(tradePeriodState)) + .addAllChatMessage(chatMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) + .collect(Collectors.toList())) + .setLockTime(lockTime) + .setUid(uid); + + Optional.ofNullable(takerFeeTxId).ifPresent(builder::setTakerFeeTxId); + Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); + Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); + Optional.ofNullable(tradingPeerNodeAddress).ifPresent(e -> builder.setTradingPeerNodeAddress(tradingPeerNodeAddress.toProtoMessage())); + Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage())); + Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); + Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash))); + Optional.ofNullable(takerContractSignature).ifPresent(builder::setTakerContractSignature); + Optional.ofNullable(makerContractSignature).ifPresent(builder::setMakerContractSignature); + Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); + Optional.ofNullable(mediatorNodeAddress).ifPresent(e -> builder.setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage())); + Optional.ofNullable(refundAgentNodeAddress).ifPresent(e -> builder.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())); + Optional.ofNullable(arbitratorBtcPubKey).ifPresent(e -> builder.setArbitratorBtcPubKey(ByteString.copyFrom(arbitratorBtcPubKey))); + Optional.ofNullable(takerPaymentAccountId).ifPresent(builder::setTakerPaymentAccountId); + Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); + Optional.ofNullable(arbitratorPubKeyRing).ifPresent(e -> builder.setArbitratorPubKeyRing(arbitratorPubKeyRing.toProtoMessage())); + Optional.ofNullable(mediatorPubKeyRing).ifPresent(e -> builder.setMediatorPubKeyRing(mediatorPubKeyRing.toProtoMessage())); + Optional.ofNullable(refundAgentPubKeyRing).ifPresent(e -> builder.setRefundAgentPubKeyRing(refundAgentPubKeyRing.toProtoMessage())); + Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); + Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); + Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); + Optional.ofNullable(delayedPayoutTxBytes).ifPresent(e -> builder.setDelayedPayoutTxBytes(ByteString.copyFrom(delayedPayoutTxBytes))); + Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name())); + + return builder.build(); + } + + public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { + trade.setTakeOfferDate(proto.getTakeOfferDate()); + trade.setState(State.fromProto(proto.getState())); + trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); + trade.setTradePeriodState(TradePeriodState.fromProto(proto.getTradePeriodState())); + trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakerFeeTxId())); + trade.setDepositTxId(ProtoUtil.stringOrNullFromProto(proto.getDepositTxId())); + trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); + trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); + trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); + trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); + trade.setTakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature())); + trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature())); + trade.setArbitratorNodeAddress(proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); + trade.setMediatorNodeAddress(proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); + trade.setRefundAgentNodeAddress(proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null); + trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorBtcPubKey())); + trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(proto.getTakerPaymentAccountId())); + trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); + trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); + trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); + trade.setRefundAgentPubKeyRing(proto.hasRefundAgentPubKeyRing() ? PubKeyRing.fromProto(proto.getRefundAgentPubKeyRing()) : null); + trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); + trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); + trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState())); + trade.setDelayedPayoutTxBytes(ProtoUtil.byteArrayOrNullFromProto(proto.getDelayedPayoutTxBytes())); + trade.setLockTime(proto.getLockTime()); + trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData())); + + AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult()); + // We do not want to show the user the last pending state when he starts up the app again, so we clear it. + if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) { + persistedAssetTxProofResult = null; + } + trade.setAssetTxProofResult(persistedAssetTxProofResult); + + trade.chatMessages.addAll(proto.getChatMessageList().stream() + .map(ChatMessage::fromPayloadProto) + .collect(Collectors.toList())); + + return trade; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void initialize(ProcessModelServiceProvider serviceProvider) { + serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(arbitratorNodeAddress).ifPresent(arbitrator -> { + arbitratorBtcPubKey = arbitrator.getBtcPubKey(); + arbitratorPubKeyRing = arbitrator.getPubKeyRing(); + }); + + serviceProvider.getMediatorManager().getDisputeAgentByNodeAddress(mediatorNodeAddress) + .ifPresent(mediator -> mediatorPubKeyRing = mediator.getPubKeyRing()); + + serviceProvider.getRefundAgentManager().getDisputeAgentByNodeAddress(refundAgentNodeAddress) + .ifPresent(refundAgent -> refundAgentPubKeyRing = refundAgent.getPubKeyRing()); + + isInitialized = true; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet. + void updateDepositTxFromWallet() { + if (getDepositTx() != null) + applyDepositTx(processModel.getTradeWalletService().getWalletTx(getDepositTx().getTxId())); + } + + public void applyDepositTx(Transaction tx) { + this.depositTx = tx; + depositTxId = depositTx.getTxId().toString(); + setupConfidenceListener(); + } + + @Nullable + public Transaction getDepositTx() { + if (depositTx == null) { + depositTx = depositTxId != null ? btcWalletService.getTransaction(depositTxId) : null; + } + return depositTx; + } + + public void applyDelayedPayoutTx(Transaction delayedPayoutTx) { + this.delayedPayoutTx = delayedPayoutTx; + this.delayedPayoutTxBytes = delayedPayoutTx.bitcoinSerialize(); + } + + public void applyDelayedPayoutTxBytes(byte[] delayedPayoutTxBytes) { + this.delayedPayoutTxBytes = delayedPayoutTxBytes; + } + + @Nullable + public Transaction getDelayedPayoutTx() { + return getDelayedPayoutTx(processModel.getBtcWalletService()); + } + + // If called from a not initialized trade (or a closed or failed trade) + // we need to pass the btcWalletService + @Nullable + public Transaction getDelayedPayoutTx(BtcWalletService btcWalletService) { + if (delayedPayoutTx == null) { + if (btcWalletService == null) { + log.warn("btcWalletService is null. You might call that method before the tradeManager has " + + "initialized all trades"); + return null; + } + + if (delayedPayoutTxBytes == null) { + log.warn("delayedPayoutTxBytes are null"); + return null; + } + + delayedPayoutTx = btcWalletService.getTxFromSerializedTx(delayedPayoutTxBytes); + } + return delayedPayoutTx; + } + + public void addAndPersistChatMessage(ChatMessage chatMessage) { + if (!chatMessages.contains(chatMessage)) { + chatMessages.add(chatMessage); + } else { + log.error("Trade ChatMessage already exists"); + } + } + + public boolean mediationResultAppliedPenaltyToSeller() { + // If mediated payout is same or more then normal payout we enable otherwise a penalty was applied + // by mediators and we keep the confirm disabled to avoid that the seller can complete the trade + // without the penalty. + long payoutAmountFromMediation = processModel.getSellerPayoutAmountFromMediation(); + long normalPayoutAmount = offer.getSellerSecurityDeposit().value; + return payoutAmountFromMediation < normalPayoutAmount; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Model implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + public abstract Coin getPayoutAmount(); + + public abstract boolean confirmPermitted(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setStateIfValidTransitionTo(State newState) { + if (state.isValidTransitionTo(newState)) { + setState(newState); + } else { + log.warn("State change is not getting applied because it would cause an invalid transition. " + + "Trade state={}, intended state={}", state, newState); + } + } + + public void setState(State state) { + if (isInitialized) { + // We don't want to log at startup the setState calls from all persisted trades + log.info("Set new state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), state); + } + if (state.getPhase().ordinal() < this.state.getPhase().ordinal()) { + String message = "We got a state change to a previous phase.\n" + + "Old state is: " + this.state + ". New state is: " + state; + log.warn(message); + } + + this.state = state; + stateProperty.set(state); + statePhaseProperty.set(state.getPhase()); + } + + public void setDisputeState(DisputeState disputeState) { + this.disputeState = disputeState; + disputeStateProperty.set(disputeState); + } + + public void setMediationResultState(MediationResultState mediationResultState) { + this.mediationResultState = mediationResultState; + mediationResultStateProperty.set(mediationResultState); + } + + public void setRefundResultState(RefundResultState refundResultState) { + this.refundResultState = refundResultState; + refundResultStateProperty.set(refundResultState); + } + + public void setTradePeriodState(TradePeriodState tradePeriodState) { + this.tradePeriodState = tradePeriodState; + tradePeriodStateProperty.set(tradePeriodState); + } + + public void setTradingPeerNodeAddress(NodeAddress tradingPeerNodeAddress) { + if (tradingPeerNodeAddress == null) + log.error("tradingPeerAddress=null"); + else + this.tradingPeerNodeAddress = tradingPeerNodeAddress; + } + + public void setTradeAmount(Coin tradeAmount) { + this.tradeAmount = tradeAmount; + tradeAmountAsLong = tradeAmount.value; + getTradeAmountProperty().set(tradeAmount); + getTradeVolumeProperty().set(getTradeVolume()); + } + + public void setPayoutTx(Transaction payoutTx) { + this.payoutTx = payoutTx; + payoutTxId = payoutTx.getTxId().toString(); + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + errorMessageProperty.set(errorMessage); + } + + public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) { + this.assetTxProofResult = assetTxProofResult; + assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + public Date getTakeOfferDate() { + return new Date(takeOfferDate); + } + + public Phase getPhase() { + return state.getPhase(); + } + + @Nullable + public Volume getTradeVolume() { + try { + if (getTradeAmount() != null && getTradePrice() != null) { + Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); + if (offer != null) { + if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); + } + return volumeByAmount; + } else { + return null; + } + } catch (Throwable ignore) { + return null; + } + } + + public Date getHalfTradePeriodDate() { + return new Date(getTradeStartTime() + getMaxTradePeriod() / 2); + } + + public Date getMaxTradePeriodDate() { + return new Date(getTradeStartTime() + getMaxTradePeriod()); + } + + private long getMaxTradePeriod() { + return getOffer().getPaymentMethod().getMaxTradePeriod(); + } + + private long getTradeStartTime() { + long now = System.currentTimeMillis(); + long startTime; + Transaction depositTx = getDepositTx(); + if (depositTx != null && getTakeOfferDate() != null) { + if (depositTx.getConfidence().getDepthInBlocks() > 0) { + final long tradeTime = getTakeOfferDate().getTime(); + // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() + long blockTime = depositTx.getIncludedInBestChainAt() != null + ? depositTx.getIncludedInBestChainAt().getTime() + : depositTx.getUpdateTime().getTime(); + // If block date is in future (Date in Bitcoin 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) + startTime = now; + else + startTime = Math.max(blockTime, 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)); + } else { + log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. txId={}", + depositTx.getTxId().toString()); + startTime = now; + } + } else { + startTime = now; + } + return startTime; + } + + public boolean hasFailed() { + return errorMessageProperty().get() != null; + } + + public boolean isInPreparation() { + return getState().getPhase().ordinal() == Phase.INIT.ordinal(); + } + + public boolean isTakerFeePublished() { + return getState().getPhase().ordinal() >= Phase.TAKER_FEE_PUBLISHED.ordinal(); + } + + public boolean isDepositPublished() { + return getState().getPhase().ordinal() >= Phase.DEPOSIT_PUBLISHED.ordinal(); + } + + public boolean isFundsLockedIn() { + // If no deposit tx was published we have no funds locked in + if (!isDepositPublished()) { + return false; + } + + // If we have the payout tx published (non disputed case) we have no funds locked in. Here we might have more + // complex cases where users open a mediation but continue the trade to finalize it without mediated payout. + // The trade state handles that but does not handle mediated payouts or refund agents payouts. + if (isPayoutPublished()) { + return false; + } + + // Legacy arbitration is not handled anymore as not used anymore. + + // In mediation case we check for the mediationResultState. As there are multiple sub-states we use ordinal. + if (disputeState == DisputeState.MEDIATION_CLOSED) { + if (mediationResultState != null && + mediationResultState.ordinal() >= MediationResultState.PAYOUT_TX_PUBLISHED.ordinal()) { + return false; + } + } + + // In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as + // locked in funds. + return disputeState != DisputeState.REFUND_REQUESTED && + disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER && + disputeState != DisputeState.REFUND_REQUEST_CLOSED; + } + + public boolean isDepositConfirmed() { + return getState().getPhase().ordinal() >= Phase.DEPOSIT_CONFIRMED.ordinal(); + } + + public boolean isFiatSent() { + return getState().getPhase().ordinal() >= Phase.FIAT_SENT.ordinal(); + } + + public boolean isFiatReceived() { + return getState().getPhase().ordinal() >= Phase.FIAT_RECEIVED.ordinal(); + } + + public boolean isPayoutPublished() { + return getState().getPhase().ordinal() >= Phase.PAYOUT_PUBLISHED.ordinal() || isWithdrawn(); + } + + public boolean isWithdrawn() { + return getState().getPhase().ordinal() == Phase.WITHDRAWN.ordinal(); + } + + public ReadOnlyObjectProperty stateProperty() { + return stateProperty; + } + + public ReadOnlyObjectProperty statePhaseProperty() { + return statePhaseProperty; + } + + public ReadOnlyObjectProperty disputeStateProperty() { + return disputeStateProperty; + } + + public ReadOnlyObjectProperty mediationResultStateProperty() { + return mediationResultStateProperty; + } + + public ReadOnlyObjectProperty refundResultStateProperty() { + return refundResultStateProperty; + } + + public ReadOnlyObjectProperty tradePeriodStateProperty() { + return tradePeriodStateProperty; + } + + public ReadOnlyObjectProperty tradeAmountProperty() { + return tradeAmountProperty; + } + + public ReadOnlyObjectProperty tradeVolumeProperty() { + return tradeVolumeProperty; + } + + public ReadOnlyStringProperty errorMessageProperty() { + return errorMessageProperty; + } + + @Override + public Date getDate() { + return getTakeOfferDate(); + } + + @Override + public String getId() { + return offer.getId(); + } + + @Override + public String getShortId() { + return offer.getShortId(); + } + + public Price getTradePrice() { + return Price.valueOf(offer.getCurrencyCode(), tradePrice); + } + + @Nullable + public Coin getTradeAmount() { + if (tradeAmount == null) + tradeAmount = Coin.valueOf(tradeAmountAsLong); + return tradeAmount; + } + + @Nullable + public Transaction getPayoutTx() { + if (payoutTx == null) + payoutTx = payoutTxId != null ? btcWalletService.getTransaction(payoutTxId) : null; + return payoutTx; + } + + public boolean hasErrorMessage() { + return getErrorMessage() != null && !getErrorMessage().isEmpty(); + } + + @Nullable + public String getErrorMessage() { + return errorMessageProperty.get(); + } + + public boolean isTxChainInvalid() { + return offer.getOfferFeePaymentTxId() == null || + getTakerFeeTxId() == null || + getDepositTxId() == null || + getDepositTx() == null || + getDelayedPayoutTxBytes() == null; + } + + public byte[] getArbitratorBtcPubKey() { + // In case we are already in a trade the arbitrator can have been revoked and we still can complete the trade + // Only new trades cannot start without any arbitrator + if (arbitratorBtcPubKey == null) { + Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); + checkNotNull(arbitrator, "arbitrator must not be null"); + arbitratorBtcPubKey = arbitrator.getBtcPubKey(); + } + + checkNotNull(arbitratorBtcPubKey, "ArbitratorPubKey must not be null"); + return arbitratorBtcPubKey; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // lazy initialization + private ObjectProperty getTradeAmountProperty() { + if (tradeAmountProperty == null) + tradeAmountProperty = getTradeAmount() != null ? new SimpleObjectProperty<>(getTradeAmount()) : new SimpleObjectProperty<>(); + + return tradeAmountProperty; + } + + // lazy initialization + private ObjectProperty getTradeVolumeProperty() { + if (tradeVolumeProperty == null) + tradeVolumeProperty = getTradeVolume() != null ? new SimpleObjectProperty<>(getTradeVolume()) : new SimpleObjectProperty<>(); + return tradeVolumeProperty; + } + + private void setupConfidenceListener() { + if (getDepositTx() != null) { + TransactionConfidence transactionConfidence = getDepositTx().getConfidence(); + if (transactionConfidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + setConfirmedState(); + } else { + ListenableFuture future = transactionConfidence.getDepthFuture(1); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(TransactionConfidence result) { + setConfirmedState(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + t.printStackTrace(); + log.error(t.getMessage()); + throw new RuntimeException(t); + } + }, MoreExecutors.directExecutor()); + } + } else { + log.error("depositTx == null. That must not happen."); + } + } + + private void setConfirmedState() { + // we only apply the state if we are not already further in the process + if (!isDepositConfirmed()) { + // As setState is called here from the trade itself we cannot trigger a requestPersistence call. + // But as we get setupConfidenceListener called at startup anyway there is no issue if it would not be + // persisted in case the shutdown routine did not persist the trade. + setState(State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN); + } + } + + @Override + public String toString() { + return "Trade{" + + "\n offer=" + offer + + ",\n isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + + ",\n txFeeAsLong=" + txFeeAsLong + + ",\n takerFeeAsLong=" + takerFeeAsLong + + ",\n takeOfferDate=" + takeOfferDate + + ",\n processModel=" + processModel + + ",\n takerFeeTxId='" + takerFeeTxId + '\'' + + ",\n depositTxId='" + depositTxId + '\'' + + ",\n payoutTxId='" + payoutTxId + '\'' + + ",\n tradeAmountAsLong=" + tradeAmountAsLong + + ",\n tradePrice=" + tradePrice + + ",\n tradingPeerNodeAddress=" + tradingPeerNodeAddress + + ",\n state=" + state + + ",\n disputeState=" + disputeState + + ",\n tradePeriodState=" + tradePeriodState + + ",\n contract=" + contract + + ",\n contractAsJson='" + contractAsJson + '\'' + + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + + ",\n takerContractSignature='" + takerContractSignature + '\'' + + ",\n makerContractSignature='" + makerContractSignature + '\'' + + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + + ",\n arbitratorBtcPubKey=" + Utilities.bytesAsHexString(arbitratorBtcPubKey) + + ",\n arbitratorPubKeyRing=" + arbitratorPubKeyRing + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n mediatorPubKeyRing=" + mediatorPubKeyRing + + ",\n takerPaymentAccountId='" + takerPaymentAccountId + '\'' + + ",\n errorMessage='" + errorMessage + '\'' + + ",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' + + ",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' + + ",\n assetTxProofResult='" + assetTxProofResult + '\'' + + ",\n chatMessages=" + chatMessages + + ",\n txFee=" + txFee + + ",\n takerFee=" + takerFee + + ",\n btcWalletService=" + btcWalletService + + ",\n stateProperty=" + stateProperty + + ",\n statePhaseProperty=" + statePhaseProperty + + ",\n disputeStateProperty=" + disputeStateProperty + + ",\n tradePeriodStateProperty=" + tradePeriodStateProperty + + ",\n errorMessageProperty=" + errorMessageProperty + + ",\n depositTx=" + depositTx + + ",\n delayedPayoutTx=" + delayedPayoutTx + + ",\n payoutTx=" + payoutTx + + ",\n tradeAmount=" + tradeAmount + + ",\n tradeAmountProperty=" + tradeAmountProperty + + ",\n tradeVolumeProperty=" + tradeVolumeProperty + + ",\n mediationResultState=" + mediationResultState + + ",\n mediationResultStateProperty=" + mediationResultStateProperty + + ",\n lockTime=" + lockTime + + ",\n delayedPayoutTxBytes=" + Utilities.bytesAsHexString(delayedPayoutTxBytes) + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n refundAgentPubKeyRing=" + refundAgentPubKeyRing + + ",\n refundResultState=" + refundResultState + + ",\n refundResultStateProperty=" + refundResultStateProperty + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeDataValidation.java b/core/src/main/java/bisq/core/trade/TradeDataValidation.java new file mode 100644 index 0000000000..013dcdb42a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeDataValidation.java @@ -0,0 +1,446 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.offer.Offer; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.util.validation.RegexValidatorFactory; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.config.Config; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.core.TransactionOutput; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TradeDataValidation { + + public static void validateDonationAddress(String addressAsString, DaoFacade daoFacade) + throws AddressException { + validateDonationAddress(null, addressAsString, daoFacade); + } + + public static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress, Config config) + throws NodeAddressException { + if (!config.useLocalhostForP2P && !RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) { + String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " + + dispute.getShortTradeId() + " is not a valid address"; + log.error(msg); + throw new NodeAddressException(dispute, msg); + } + } + + public static void validateDonationAddress(@Nullable Dispute dispute, String addressAsString, DaoFacade daoFacade) + throws AddressException { + + if (addressAsString == null) { + log.debug("address is null at validateDonationAddress. This is expected in case of an not updated trader."); + return; + } + + Set allPastParamValues = daoFacade.getAllDonationAddresses(); + if (!allPastParamValues.contains(addressAsString)) { + String errorMsg = "Donation address is not a valid DAO donation address." + + "\nAddress used in the dispute: " + addressAsString + + "\nAll DAO param donation addresses:" + allPastParamValues; + log.error(errorMsg); + throw new AddressException(dispute, errorMsg); + } + } + + public static void testIfAnyDisputeTriedReplay(List disputeList, + Consumer exceptionHandler) { + var tuple = getTestReplayHashMaps(disputeList); + Map> disputesPerTradeId = tuple.first; + Map> disputesPerDelayedPayoutTxId = tuple.second; + Map> disputesPerDepositTxId = tuple.third; + + disputeList.forEach(disputeToTest -> { + try { + testIfDisputeTriesReplay(disputeToTest, + disputesPerTradeId, + disputesPerDelayedPayoutTxId, + disputesPerDepositTxId); + + } catch (DisputeReplayException e) { + exceptionHandler.accept(e); + } + }); + } + + + public static void testIfDisputeTriesReplay(Dispute dispute, + List disputeList) throws DisputeReplayException { + var tuple = TradeDataValidation.getTestReplayHashMaps(disputeList); + Map> disputesPerTradeId = tuple.first; + Map> disputesPerDelayedPayoutTxId = tuple.second; + Map> disputesPerDepositTxId = tuple.third; + + testIfDisputeTriesReplay(dispute, + disputesPerTradeId, + disputesPerDelayedPayoutTxId, + disputesPerDepositTxId); + } + + + private static Tuple3>, Map>, Map>> getTestReplayHashMaps( + List disputeList) { + Map> disputesPerTradeId = new HashMap<>(); + Map> disputesPerDelayedPayoutTxId = new HashMap<>(); + Map> disputesPerDepositTxId = new HashMap<>(); + disputeList.forEach(dispute -> { + String uid = dispute.getUid(); + + String tradeId = dispute.getTradeId(); + disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>()); + Set set = disputesPerTradeId.get(tradeId); + set.add(uid); + + String delayedPayoutTxId = dispute.getDelayedPayoutTxId(); + if (delayedPayoutTxId != null) { + disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>()); + set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId); + set.add(uid); + } + + String depositTxId = dispute.getDepositTxId(); + if (depositTxId != null) { + disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>()); + set = disputesPerDepositTxId.get(depositTxId); + set.add(uid); + } + }); + + return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId); + } + + private static void testIfDisputeTriesReplay(Dispute disputeToTest, + Map> disputesPerTradeId, + Map> disputesPerDelayedPayoutTxId, + Map> disputesPerDepositTxId) + throws DisputeReplayException { + + try { + String disputeToTestTradeId = disputeToTest.getTradeId(); + String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId(); + String disputeToTestDepositTxId = disputeToTest.getDepositTxId(); + String disputeToTestUid = disputeToTest.getUid(); + + // For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do. + // So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the + // delayed payout tx also in mediation cases and that if check can be removed. + if (disputeToTest.getSupportType() == SupportType.REFUND) { + checkNotNull(disputeToTestDelayedPayoutTxId, + "Delayed payout transaction ID is null. " + + "Trade ID: " + disputeToTestTradeId); + } + checkNotNull(disputeToTestDepositTxId, + "depositTxId must not be null. Trade ID: " + disputeToTestTradeId); + checkNotNull(disputeToTestUid, + "agentsUid must not be null. Trade ID: " + disputeToTestTradeId); + + Set disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId); + checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2, + "We found more then 2 disputes with the same trade ID. " + + "Trade ID: " + disputeToTestTradeId); + if (!disputesPerDelayedPayoutTxId.isEmpty()) { + Set disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId); + checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2, + "We found more then 2 disputes with the same delayedPayoutTxId. " + + "Trade ID: " + disputeToTestTradeId); + } + if (!disputesPerDepositTxId.isEmpty()) { + Set disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId); + checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2, + "We found more then 2 disputes with the same depositTxId. " + + "Trade ID: " + disputeToTestTradeId); + } + } catch (IllegalArgumentException e) { + throw new DisputeReplayException(disputeToTest, e.getMessage()); + } catch (NullPointerException e) { + log.error("NullPointerException at testIfDisputeTriesReplay: " + + "disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " + + "disputesPerDepositTxId={}", + disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId); + throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString()); + } + } + + public static void validateDelayedPayoutTx(Trade trade, + Transaction delayedPayoutTx, + DaoFacade daoFacade, + BtcWalletService btcWalletService) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validateDelayedPayoutTx(trade, + delayedPayoutTx, + null, + daoFacade, + btcWalletService, + null); + } + + public static void validateDelayedPayoutTx(Trade trade, + Transaction delayedPayoutTx, + @Nullable Dispute dispute, + DaoFacade daoFacade, + BtcWalletService btcWalletService) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validateDelayedPayoutTx(trade, + delayedPayoutTx, + dispute, + daoFacade, + btcWalletService, + null); + } + + public static void validateDelayedPayoutTx(Trade trade, + Transaction delayedPayoutTx, + DaoFacade daoFacade, + BtcWalletService btcWalletService, + @Nullable Consumer addressConsumer) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validateDelayedPayoutTx(trade, + delayedPayoutTx, + null, + daoFacade, + btcWalletService, + addressConsumer); + } + + public static void validateDelayedPayoutTx(Trade trade, + Transaction delayedPayoutTx, + @Nullable Dispute dispute, + DaoFacade daoFacade, + BtcWalletService btcWalletService, + @Nullable Consumer addressConsumer) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + String errorMsg; + if (delayedPayoutTx == null) { + errorMsg = "DelayedPayoutTx must not be null"; + log.error(errorMsg); + throw new MissingTxException("DelayedPayoutTx must not be null"); + } + + // Validate tx structure + if (delayedPayoutTx.getInputs().size() != 1) { + errorMsg = "Number of delayedPayoutTx inputs must be 1"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidTxException(errorMsg); + } + + if (delayedPayoutTx.getOutputs().size() != 1) { + errorMsg = "Number of delayedPayoutTx outputs must be 1"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidTxException(errorMsg); + } + + // connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet + // yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol. + + // Validate lock time + if (delayedPayoutTx.getLockTime() != trade.getLockTime()) { + errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime()"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidLockTimeException(errorMsg); + } + + // Validate seq num + if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) { + errorMsg = "Sequence number must be 0xFFFFFFFE"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidLockTimeException(errorMsg); + } + + // Check amount + TransactionOutput output = delayedPayoutTx.getOutput(0); + Offer offer = checkNotNull(trade.getOffer()); + Coin msOutputAmount = offer.getBuyerSecurityDeposit() + .add(offer.getSellerSecurityDeposit()) + .add(checkNotNull(trade.getTradeAmount())); + + if (!output.getValue().equals(msOutputAmount)) { + errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidAmountException(errorMsg); + } + + NetworkParameters params = btcWalletService.getParams(); + Address address = output.getScriptPubKey().getToAddress(params); + if (address == null) { + errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new AddressException(dispute, errorMsg); + } + + String addressAsString = address.toString(); + if (addressConsumer != null) { + addressConsumer.accept(addressAsString); + } + + validateDonationAddress(addressAsString, daoFacade); + + if (dispute != null) { + // Verify that address in the dispute matches the one in the trade. + String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx(); + // Old clients don't have it set yet. Can be removed after a forced update + if (donationAddressOfDelayedPayoutTx != null) { + checkArgument(addressAsString.equals(donationAddressOfDelayedPayoutTx), + "donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx"); + } + } + } + + public static void validatePayoutTxInput(Transaction depositTx, + Transaction delayedPayoutTx) + throws InvalidInputException { + TransactionInput input = delayedPayoutTx.getInput(0); + checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null"); + // input.getConnectedOutput() is null as the tx is not committed at that point + + TransactionOutPoint outpoint = input.getOutpoint(); + if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) { + throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" + + "Delayed payout tx=" + delayedPayoutTx + "\n" + + "Deposit tx=" + depositTx); + } + } + + public static void validateDepositInputs(Trade trade) throws InvalidTxException { + // assumption: deposit tx always has 2 inputs, the maker and taker + if (trade == null || trade.getDepositTx() == null || trade.getDepositTx().getInputs().size() != 2) { + throw new InvalidTxException("Deposit transaction is null or has unexpected input count"); + } + Transaction depositTx = trade.getDepositTx(); + String txIdInput0 = depositTx.getInput(0).getOutpoint().getHash().toString(); + String txIdInput1 = depositTx.getInput(1).getOutpoint().getHash().toString(); + String contractMakerTxId = trade.getContract().getOfferPayload().getOfferFeePaymentTxId(); + String contractTakerTxId = trade.getContract().getTakerFeeTxID(); + boolean makerFirstMatch = contractMakerTxId.equalsIgnoreCase(txIdInput0) && contractTakerTxId.equalsIgnoreCase(txIdInput1); + boolean takerFirstMatch = contractMakerTxId.equalsIgnoreCase(txIdInput1) && contractTakerTxId.equalsIgnoreCase(txIdInput0); + if (!makerFirstMatch && !takerFirstMatch) { + String errMsg = "Maker/Taker txId in contract does not match deposit tx input"; + log.error(errMsg + + "\nContract Maker tx=" + contractMakerTxId + " Contract Taker tx=" + contractTakerTxId + + "\nDeposit Input0=" + txIdInput0 + " Deposit Input1=" + txIdInput1); + throw new InvalidTxException(errMsg); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Exceptions + /////////////////////////////////////////////////////////////////////////////////////////// + + public static class ValidationException extends Exception { + @Nullable + @Getter + private final Dispute dispute; + + ValidationException(String msg) { + this(null, msg); + } + + ValidationException(@Nullable Dispute dispute, String msg) { + super(msg); + this.dispute = dispute; + } + } + + public static class AddressException extends ValidationException { + AddressException(@Nullable Dispute dispute, String msg) { + super(dispute, msg); + } + } + + public static class MissingTxException extends ValidationException { + MissingTxException(String msg) { + super(msg); + } + } + + public static class InvalidTxException extends ValidationException { + InvalidTxException(String msg) { + super(msg); + } + } + + public static class InvalidAmountException extends ValidationException { + InvalidAmountException(String msg) { + super(msg); + } + } + + public static class InvalidLockTimeException extends ValidationException { + InvalidLockTimeException(String msg) { + super(msg); + } + } + + public static class InvalidInputException extends ValidationException { + InvalidInputException(String msg) { + super(msg); + } + } + + public static class DisputeReplayException extends ValidationException { + DisputeReplayException(Dispute dispute, String msg) { + super(dispute, msg); + } + } + + public static class NodeAddressException extends ValidationException { + NodeAddressException(Dispute dispute, String msg) { + super(dispute, msg); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java new file mode 100644 index 0000000000..6707fde8ea --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -0,0 +1,733 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.exceptions.AddressEntryException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.availability.OfferAvailabilityModel; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; +import bisq.core.trade.handlers.TradeResultHandler; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.protocol.MakerProtocol; +import bisq.core.trade.protocol.ProcessModel; +import bisq.core.trade.protocol.ProcessModelServiceProvider; +import bisq.core.trade.protocol.TakerProtocol; +import bisq.core.trade.protocol.TradeProtocol; +import bisq.core.trade.protocol.TradeProtocolFactory; +import bisq.core.trade.statistics.ReferralIdService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; +import bisq.core.util.Validator; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.DecryptedDirectMessageListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.network.TorNetworkNode; + +import bisq.common.ClockWatcher; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.FaultHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.proto.persistable.PersistedDataHost; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.util.concurrent.FutureCallback; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleLongProperty; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; +import lombok.Setter; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener { + private static final Logger log = LoggerFactory.getLogger(TradeManager.class); + + private final User user; + @Getter + private final KeyRing keyRing; + private final BtcWalletService btcWalletService; + private final BsqWalletService bsqWalletService; + private final OpenOfferManager openOfferManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; + private final P2PService p2PService; + private final PriceFeedService priceFeedService; + private final TradeStatisticsManager tradeStatisticsManager; + private final TradeUtil tradeUtil; + @Getter + private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; + private final ProcessModelServiceProvider processModelServiceProvider; + private final ClockWatcher clockWatcher; + + private final Map tradeProtocolByTradeId = new HashMap<>(); + private final PersistenceManager> persistenceManager; + private final TradableList tradableList = new TradableList<>(); + @Getter + private final BooleanProperty persistedTradesInitialized = new SimpleBooleanProperty(); + @Setter + @Nullable + private ErrorMessageHandler takeOfferRequestErrorMessageHandler; + @Getter + private final LongProperty numPendingTrades = new SimpleLongProperty(); + private final ReferralIdService referralIdService; + private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + @Getter + private final boolean allowFaultyDelayedTxs; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradeManager(User user, + KeyRing keyRing, + BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + OpenOfferManager openOfferManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, + P2PService p2PService, + PriceFeedService priceFeedService, + TradeStatisticsManager tradeStatisticsManager, + TradeUtil tradeUtil, + ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, + ProcessModelServiceProvider processModelServiceProvider, + ClockWatcher clockWatcher, + PersistenceManager> persistenceManager, + ReferralIdService referralIdService, + DumpDelayedPayoutTx dumpDelayedPayoutTx, + @Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) { + this.user = user; + this.keyRing = keyRing; + this.btcWalletService = btcWalletService; + this.bsqWalletService = bsqWalletService; + this.openOfferManager = openOfferManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; + this.p2PService = p2PService; + this.priceFeedService = priceFeedService; + this.tradeStatisticsManager = tradeStatisticsManager; + this.tradeUtil = tradeUtil; + this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; + this.processModelServiceProvider = processModelServiceProvider; + this.clockWatcher = clockWatcher; + this.referralIdService = referralIdService; + this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; + this.allowFaultyDelayedTxs = allowFaultyDelayedTxs; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(tradableList, "PendingTrades", PersistenceManager.Source.PRIVATE); + + p2PService.addDecryptedDirectMessageListener(this); + + failedTradesManager.setUnFailTradeCallback(this::unFailTrade); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @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)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(tradableList, "delayed_payout_txs_pending"); + completeHandler.run(); + }, + completeHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DecryptedDirectMessageListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress peer) { + NetworkEnvelope networkEnvelope = message.getNetworkEnvelope(); + if (networkEnvelope instanceof InputsForDepositTxRequest) { + handleTakeOfferRequest(peer, (InputsForDepositTxRequest) networkEnvelope); + } + } + + // The maker received a TakeOfferRequest + private void handleTakeOfferRequest(NodeAddress peer, InputsForDepositTxRequest inputsForDepositTxRequest) { + log.info("Received inputsForDepositTxRequest from {} with tradeId {} and uid {}", + peer, inputsForDepositTxRequest.getTradeId(), inputsForDepositTxRequest.getUid()); + + try { + Validator.nonEmptyStringOf(inputsForDepositTxRequest.getTradeId()); + } catch (Throwable t) { + log.warn("Invalid inputsForDepositTxRequest " + inputsForDepositTxRequest.toString()); + return; + } + + Optional openOfferOptional = openOfferManager.getOpenOfferById(inputsForDepositTxRequest.getTradeId()); + if (!openOfferOptional.isPresent()) { + return; + } + + OpenOffer openOffer = openOfferOptional.get(); + if (openOffer.getState() != OpenOffer.State.AVAILABLE) { + return; + } + + Offer offer = openOffer.getOffer(); + openOfferManager.reserveOpenOffer(openOffer); + Trade trade; + if (offer.isBuyOffer()) { + trade = new BuyerAsMakerTrade(offer, + Coin.valueOf(inputsForDepositTxRequest.getTxFee()), + Coin.valueOf(inputsForDepositTxRequest.getTakerFee()), + inputsForDepositTxRequest.isCurrencyForTakerFeeBtc(), + openOffer.getArbitratorNodeAddress(), + openOffer.getMediatorNodeAddress(), + openOffer.getRefundAgentNodeAddress(), + btcWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString()); + } else { + trade = new SellerAsMakerTrade(offer, + Coin.valueOf(inputsForDepositTxRequest.getTxFee()), + Coin.valueOf(inputsForDepositTxRequest.getTakerFee()), + inputsForDepositTxRequest.isCurrencyForTakerFeeBtc(), + openOffer.getArbitratorNodeAddress(), + openOffer.getMediatorNodeAddress(), + openOffer.getRefundAgentNodeAddress(), + btcWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString()); + } + TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); + TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol); + if (prev != null) { + log.error("We had already an entry with uid {}", trade.getUid()); + } + + tradableList.add(trade); + initTradeAndProtocol(trade, tradeProtocol); + + ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(inputsForDepositTxRequest, peer, errorMessage -> { + if (takeOfferRequestErrorMessageHandler != null) + takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); + }); + + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + if (p2PService.isBootstrapped()) { + initPersistedTrades(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + initPersistedTrades(); + } + }); + } + + getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); + onTradesChanged(); + + btcWalletService.getAddressEntriesForAvailableBalanceStream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> { + log.warn("Swapping pending OFFER_FUNDING entries at startup. offerId={}", addressEntry.getOfferId()); + btcWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING); + }); + } + + public TradeProtocol getTradeProtocol(Trade trade) { + String uid = trade.getUid(); + if (tradeProtocolByTradeId.containsKey(uid)) { + return tradeProtocolByTradeId.get(uid); + } else { + TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); + TradeProtocol prev = tradeProtocolByTradeId.put(uid, tradeProtocol); + if (prev != null) { + log.error("We had already an entry with uid {}", trade.getUid()); + } + + return tradeProtocol; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Init pending trade + /////////////////////////////////////////////////////////////////////////////////////////// + + private void initPersistedTrades() { + tradableList.forEach(this::initPersistedTrade); + persistedTradesInitialized.set(true); + + // We do not include failed trades as they should not be counted anyway in the trade statistics + Set allTrades = new HashSet<>(closedTradableManager.getClosedTrades()); + allTrades.addAll(tradableList.getList()); + String referralId = referralIdService.getOptionalReferralId().orElse(null); + boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; + tradeStatisticsManager.maybeRepublishTradeStatistics(allTrades, referralId, isTorNetworkNode); + } + + private void initPersistedTrade(Trade trade) { + initTradeAndProtocol(trade, getTradeProtocol(trade)); + trade.updateDepositTxFromWallet(); + requestPersistence(); + } + + private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) { + tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer()); + trade.initialize(processModelServiceProvider); + requestPersistence(); + } + + public void requestPersistence() { + persistenceManager.requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Take offer + /////////////////////////////////////////////////////////////////////////////////////////// + + public void checkOfferAvailability(Offer offer, + boolean isTakerApiUser, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + if (btcWalletService.isUnconfirmedTransactionsLimitHit() || + bsqWalletService.isUnconfirmedTransactionsLimitHit()) { + String errorMessage = Res.get("shared.unconfirmedTransactionsLimitReached"); + errorMessageHandler.handleErrorMessage(errorMessage); + log.warn(errorMessage); + return; + } + + offer.checkOfferAvailability(getOfferAvailabilityModel(offer, isTakerApiUser), resultHandler, errorMessageHandler); + } + + // First we check if offer is still available then we create the trade with the protocol + public void onTakeOffer(Coin amount, + Coin txFee, + Coin takerFee, + boolean isCurrencyForTakerFeeBtc, + long tradePrice, + Coin fundsNeededForTrade, + Offer offer, + String paymentAccountId, + boolean useSavingsWallet, + boolean isTakerApiUser, + TradeResultHandler tradeResultHandler, + ErrorMessageHandler errorMessageHandler) { + + checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); + + OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser); + offer.checkOfferAvailability(model, + () -> { + if (offer.getState() == Offer.State.AVAILABLE) { + Trade trade; + if (offer.isBuyOffer()) { + trade = new SellerAsTakerTrade(offer, + amount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + model.getPeerNodeAddress(), + model.getSelectedArbitrator(), + model.getSelectedMediator(), + model.getSelectedRefundAgent(), + btcWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString()); + } else { + trade = new BuyerAsTakerTrade(offer, + amount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + model.getPeerNodeAddress(), + model.getSelectedArbitrator(), + model.getSelectedMediator(), + model.getSelectedRefundAgent(), + btcWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString()); + } + trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); + trade.getProcessModel().setFundsNeededForTradeAsLong(fundsNeededForTrade.value); + trade.setTakerPaymentAccountId(paymentAccountId); + + TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); + TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol); + if (prev != null) { + log.error("We had already an entry with uid {}", trade.getUid()); + } + tradableList.add(trade); + + initTradeAndProtocol(trade, tradeProtocol); + + ((TakerProtocol) tradeProtocol).onTakeOffer(); + tradeResultHandler.handleResult(trade); + requestPersistence(); + } + }, + errorMessageHandler); + + requestPersistence(); + } + + private ProcessModel getNewProcessModel(Offer offer) { + return new ProcessModel(checkNotNull(offer).getId(), + processModelServiceProvider.getUser().getAccountId(), + processModelServiceProvider.getKeyRing().getPubKeyRing()); + } + + private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer, boolean isTakerApiUser) { + return new OfferAvailabilityModel( + offer, + keyRing.getPubKeyRing(), + p2PService, + user, + mediatorManager, + tradeStatisticsManager, + isTakerApiUser); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Complete trade + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onWithdrawRequest(String toAddress, + Coin amount, + Coin fee, + KeyParameter aesKey, + Trade trade, + @Nullable String memo, + ResultHandler resultHandler, + FaultHandler faultHandler) { + String fromAddress = btcWalletService.getOrCreateAddressEntry(trade.getId(), + AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + FutureCallback callback = new FutureCallback<>() { + @Override + public void onSuccess(@javax.annotation.Nullable Transaction transaction) { + if (transaction != null) { + log.debug("onWithdraw onSuccess tx ID:" + transaction.getTxId().toString()); + onTradeCompleted(trade); + trade.setState(Trade.State.WITHDRAW_COMPLETED); + getTradeProtocol(trade).onWithdrawCompleted(); + requestPersistence(); + resultHandler.handleResult(); + } + } + + @Override + public void onFailure(@NotNull Throwable t) { + t.printStackTrace(); + log.error(t.getMessage()); + faultHandler.handleFault("An exception occurred at requestWithdraw (onFailure).", t); + } + }; + try { + btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, + AddressEntry.Context.TRADE_PAYOUT, memo, callback); + } catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) { + e.printStackTrace(); + log.error(e.getMessage()); + faultHandler.handleFault("An exception occurred at requestWithdraw.", e); + } + } + + // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades + public void onTradeCompleted(Trade trade) { + removeTrade(trade); + closedTradableManager.add(trade); + + // TODO The address entry should have been removed already. Check and if its the case remove that. + btcWalletService.resetAddressEntriesForPendingTrade(trade.getId()); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dispute + /////////////////////////////////////////////////////////////////////////////////////////// + + public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) { + Optional tradeOptional = getTradeById(tradeId); + if (tradeOptional.isPresent()) { + Trade trade = tradeOptional.get(); + trade.setDisputeState(disputeState); + onTradeCompleted(trade); + btcWalletService.swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT); + requestPersistence(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trade period state + /////////////////////////////////////////////////////////////////////////////////////////// + + public void applyTradePeriodState() { + updateTradePeriodState(); + clockWatcher.addListener(new ClockWatcher.Listener() { + @Override + public void onSecondTick() { + } + + @Override + public void onMinuteTick() { + updateTradePeriodState(); + } + }); + } + + private void updateTradePeriodState() { + getObservableList().forEach(trade -> { + if (!trade.isPayoutPublished()) { + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); + if (maxTradePeriodDate != null && halfTradePeriodDate != null) { + Date now = new Date(); + if (now.after(maxTradePeriodDate)) { + trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + requestPersistence(); + } else if (now.after(halfTradePeriodDate)) { + trade.setTradePeriodState(Trade.TradePeriodState.SECOND_HALF); + requestPersistence(); + } + } + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Failed trade handling + /////////////////////////////////////////////////////////////////////////////////////////// + + // If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published) + // we move the trade to failedTradesManager + public void onMoveInvalidTradeToFailedTrades(Trade trade) { + removeTrade(trade); + failedTradesManager.add(trade); + } + + public void addFailedTradeToPendingTrades(Trade trade) { + if (!trade.isInitialized()) { + initPersistedTrade(trade); + } + addTrade(trade); + } + + public Stream getTradesStreamWithFundsLockedIn() { + return getObservableList().stream().filter(Trade::isFundsLockedIn); + } + + public Set getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { + AtomicReference tradeTxException = new AtomicReference<>(); + Set tradesIdSet = getTradesStreamWithFundsLockedIn() + .filter(Trade::hasFailed) + .map(Trade::getId) + .collect(Collectors.toSet()); + tradesIdSet.addAll(failedTradesManager.getTradesStreamWithFundsLockedIn() + .filter(trade -> trade.getDepositTx() != null) + .map(trade -> { + log.warn("We found a failed trade with locked up funds. " + + "That should never happen. trade ID=" + trade.getId()); + return trade.getId(); + }) + .collect(Collectors.toSet())); + tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn() + .map(trade -> { + Transaction depositTx = trade.getDepositTx(); + if (depositTx != null) { + TransactionConfidence confidence = btcWalletService.getConfidenceForTxId(depositTx.getTxId().toString()); + if (confidence != null && confidence.getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING) { + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); + } else { + log.warn("We found a closed trade with locked up funds. " + + "That should never happen. trade ID=" + trade.getId()); + } + } else { + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); + } + return trade.getId(); + }) + .collect(Collectors.toSet())); + + if (tradeTxException.get() != null) + throw tradeTxException.get(); + + return tradesIdSet; + } + + // If trade still has funds locked up it might come back from failed trades + // Aborts unfailing if the address entries needed are not available + private boolean unFailTrade(Trade trade) { + if (!recoverAddresses(trade)) { + log.warn("Failed to recover address during unFail trade"); + return false; + } + + initPersistedTrade(trade); + + if (!tradableList.contains(trade)) { + tradableList.add(trade); + } + return true; + } + + // The trade is added to pending trades if the associated address entries are AVAILABLE and + // the relevant entries are changed, otherwise it's not added and no address entries are changed + private boolean recoverAddresses(Trade trade) { + // Find addresses associated with this trade. + var entries = tradeUtil.getAvailableAddresses(trade); + if (entries == null) + return false; + + btcWalletService.recoverAddressEntry(trade.getId(), entries.first, + AddressEntry.Context.MULTI_SIG); + btcWalletService.recoverAddressEntry(trade.getId(), entries.second, + AddressEntry.Context.TRADE_PAYOUT); + return true; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters, Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObservableList getObservableList() { + return tradableList.getObservableList(); + } + + public BooleanProperty persistedTradesInitializedProperty() { + return persistedTradesInitialized; + } + + public boolean isMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); + } + + public boolean wasOfferAlreadyUsedInTrade(String offerId) { + return getTradeById(offerId).isPresent() || + failedTradesManager.getTradeById(offerId).isPresent() || + closedTradableManager.getTradableById(offerId).isPresent(); + } + + public boolean isBuyer(Offer offer) { + // If I am the maker, we use the OfferPayload.Direction, otherwise the mirrored direction + if (isMyOffer(offer)) + return offer.isBuyOffer(); + else + return offer.getDirection() == OfferPayload.Direction.SELL; + } + + public Optional getTradeById(String tradeId) { + return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); + } + + private void removeTrade(Trade trade) { + if (tradableList.remove(trade)) { + requestPersistence(); + } + } + + private void addTrade(Trade trade) { + if (tradableList.add(trade)) { + requestPersistence(); + } + } + + // TODO Remove once tradableList is refactored to a final field + // (part of the persistence refactor PR) + private void onTradesChanged() { + this.numPendingTrades.set(getObservableList().size()); + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeModule.java b/core/src/main/java/bisq/core/trade/TradeModule.java new file mode 100644 index 0000000000..751bd5a0eb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeModule.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.sign.SignedWitnessStorageService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.account.witness.AccountAgeWitnessStorageService; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; +import bisq.core.trade.statistics.ReferralIdService; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; + +import static bisq.common.config.Config.ALLOW_FAULTY_DELAYED_TXS; +import static bisq.common.config.Config.DUMP_DELAYED_PAYOUT_TXS; +import static bisq.common.config.Config.DUMP_STATISTICS; +import static com.google.inject.name.Names.named; + +public class TradeModule extends AppModule { + + public TradeModule(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(TradeManager.class).in(Singleton.class); + bind(ClosedTradableManager.class).in(Singleton.class); + bind(FailedTradesManager.class).in(Singleton.class); + bind(AccountAgeWitnessService.class).in(Singleton.class); + bind(AccountAgeWitnessStorageService.class).in(Singleton.class); + bind(SignedWitnessService.class).in(Singleton.class); + bind(SignedWitnessStorageService.class).in(Singleton.class); + bind(ReferralIdService.class).in(Singleton.class); + + bindConstant().annotatedWith(named(DUMP_STATISTICS)).to(config.dumpStatistics); + bindConstant().annotatedWith(named(DUMP_DELAYED_PAYOUT_TXS)).to(config.dumpDelayedPayoutTxs); + bindConstant().annotatedWith(named(ALLOW_FAULTY_DELAYED_TXS)).to(config.allowFaultyDelayedTxs); + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeTxException.java b/core/src/main/java/bisq/core/trade/TradeTxException.java new file mode 100644 index 0000000000..cbc7966067 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeTxException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.trade; + +public class TradeTxException extends Exception { + public TradeTxException(String message) { + super(message); + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeUtil.java b/core/src/main/java/bisq/core/trade/TradeUtil.java new file mode 100644 index 0000000000..a026f6ab98 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeUtil.java @@ -0,0 +1,227 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; + +import bisq.common.crypto.KeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Date; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.core.locale.CurrencyUtil.getCurrencyPair; +import static bisq.core.locale.CurrencyUtil.isFiatCurrency; +import static bisq.core.util.FormattingUtils.formatDurationAsWords; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +/** + * This class contains trade utility methods. + */ +@Slf4j +@Singleton +public class TradeUtil { + + private final BtcWalletService btcWalletService; + private final KeyRing keyRing; + + @Inject + public TradeUtil(BtcWalletService btcWalletService, KeyRing keyRing) { + this.btcWalletService = btcWalletService; + this.keyRing = keyRing; + } + + /** + * Returns if and only if both are AVAILABLE, + * otherwise null. + * @param trade the trade being queried for MULTI_SIG, TRADE_PAYOUT addresses + * @return Tuple2 tuple containing MULTI_SIG, TRADE_PAYOUT addresses for trade + */ + public Tuple2 getAvailableAddresses(Trade trade) { + var addresses = getTradeAddresses(trade); + if (addresses == null) + return null; + + if (btcWalletService.getAvailableAddressEntries().stream() + .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first))) + return null; + + if (btcWalletService.getAvailableAddressEntries().stream() + .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second))) + return null; + + return new Tuple2<>(addresses.first, addresses.second); + } + + /** + * Returns addresses as strings if they're known by the + * wallet. + * @param trade the trade being queried for MULTI_SIG, TRADE_PAYOUT addresses + * @return Tuple2 tuple containing MULTI_SIG, TRADE_PAYOUT addresses for trade + */ + public Tuple2 getTradeAddresses(Trade trade) { + var contract = trade.getContract(); + if (contract == null) + return null; + + // Get multisig address + var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing()); + var multiSigPubKey = isMyRoleBuyer + ? contract.getBuyerMultiSigPubKey() + : contract.getSellerMultiSigPubKey(); + if (multiSigPubKey == null) + return null; + + var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey); + var multiSigAddress = btcWalletService.getAddressEntryListAsImmutableList().stream() + .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString)) + .findAny() + .orElse(null); + if (multiSigAddress == null) + return null; + + // Get payout address + var payoutAddress = isMyRoleBuyer + ? contract.getBuyerPayoutAddressString() + : contract.getSellerPayoutAddressString(); + var payoutAddressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream() + .filter(e -> Objects.equals(e.getAddressString(), payoutAddress)) + .findAny() + .orElse(null); + if (payoutAddressEntry == null) + return null; + + return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress); + } + + public long getRemainingTradeDuration(Trade trade) { + return trade.getMaxTradePeriodDate() != null + ? trade.getMaxTradePeriodDate().getTime() - new Date().getTime() + : getMaxTradePeriod(trade); + } + + public long getMaxTradePeriod(Trade trade) { + return trade.getOffer() != null + ? trade.getOffer().getPaymentMethod().getMaxTradePeriod() + : 0; + } + + public double getRemainingTradeDurationAsPercentage(Trade trade) { + long maxPeriod = getMaxTradePeriod(trade); + long remaining = getRemainingTradeDuration(trade); + if (maxPeriod != 0) { + return 1 - (double) remaining / (double) maxPeriod; + } else + return 0; + } + + public String getRemainingTradeDurationAsWords(Trade trade) { + return formatDurationAsWords(Math.max(0, getRemainingTradeDuration(trade))); + } + + @Nullable + public Date getHalfTradePeriodDate(Trade trade) { + return trade != null ? trade.getHalfTradePeriodDate() : null; + } + + public Date getDateForOpenDispute(Trade trade) { + return new Date(new Date().getTime() + getRemainingTradeDuration(trade)); + } + + public String getMarketDescription(Trade trade) { + if (trade == null) + return ""; + + checkNotNull(trade.getOffer()); + checkNotNull(trade.getOffer().getCurrencyCode()); + return getCurrencyPair(trade.getOffer().getCurrencyCode()); + } + + public String getPaymentMethodNameWithCountryCode(Trade trade) { + if (trade == null) + return ""; + + Offer offer = trade.getOffer(); + checkNotNull(offer); + checkNotNull(offer.getPaymentMethod()); + return offer.getPaymentMethodNameWithCountryCode(); + } + + /** + * Returns a string describing a trader's role for a given trade. + * @param trade Trade + * @return String describing a trader's role for a given trade + */ + public String getRole(Trade trade) { + Contract contract = trade.getContract(); + if (contract == null) + throw new IllegalStateException(format("could not get role because no contract was found for trade '%s'", + trade.getShortId())); + + Offer offer = trade.getOffer(); + if (offer == null) + throw new IllegalStateException(format("could not get role because no offer was found for trade '%s'", + trade.getShortId())); + + return getRole(contract.isBuyerMakerAndSellerTaker(), + offer.isMyOffer(keyRing), + offer.getCurrencyCode()); + } + + /** + * Returns a string describing a trader's role. + * + * @param isBuyerMakerAndSellerTaker boolean + * @param isMaker boolean + * @param currencyCode String + * @return String describing a trader's role + */ + public String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { + if (isFiatCurrency(currencyCode)) { + String baseCurrencyCode = Res.getBaseCurrencyCode(); + if (isBuyerMakerAndSellerTaker) + return isMaker + ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.buyer")) + : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.seller")); + else + return isMaker + ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.seller")) + : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.buyer")); + } else { + if (isBuyerMakerAndSellerTaker) + return isMaker + ? Res.get("formatter.asMaker", currencyCode, Res.get("shared.seller")) + : Res.get("formatter.asTaker", currencyCode, Res.get("shared.buyer")); + else + return isMaker + ? Res.get("formatter.asMaker", currencyCode, Res.get("shared.buyer")) + : Res.get("formatter.asTaker", currencyCode, Res.get("shared.seller")); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java b/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java new file mode 100644 index 0000000000..5e3e40810b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java @@ -0,0 +1,135 @@ +/* + * 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 bisq.core.trade.closed; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.mailbox.MailboxMessage; +import bisq.network.p2p.mailbox.MailboxMessageService; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +//TODO with the redesign of mailbox messages that is not required anymore. We leave it for now as we want to minimize +// changes for the 1.5.0 release but we should clean up afterwards... + +/** + * Util for removing pending mailbox messages in case the trade has been closed by the seller after confirming receipt + * and a AckMessage as mailbox message will be sent by the buyer once they go online. In that case the seller's trade + * is closed already and the TradeProtocol is not executing the message processing, thus the mailbox message would not + * be removed. To ensure that in such cases (as well other potential cases in failure scenarios) the mailbox message + * gets removed from the network we use that util. + * + * This class must not be injected as a singleton! + */ +@Slf4j +public class CleanupMailboxMessages { + private final P2PService p2PService; + private final MailboxMessageService mailboxMessageService; + + @Inject + public CleanupMailboxMessages(P2PService p2PService, MailboxMessageService mailboxMessageService) { + this.p2PService = p2PService; + this.mailboxMessageService = mailboxMessageService; + } + + 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 onUpdatedDataReceived() { + cleanupMailboxMessages(trades); + } + }); + } + } catch (Throwable t) { + log.error("Cleanup mailbox messages failed. {}", t.toString()); + } + } + + private void cleanupMailboxMessages(List trades) { + mailboxMessageService.getMyDecryptedMailboxMessages() + .forEach(message -> handleDecryptedMessageWithPubKey(message, trades)); + } + + private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, + List trades) { + trades.stream() + .filter(trade -> isMessageForTrade(decryptedMessageWithPubKey, trade)) + .filter(trade -> isPubKeyValid(decryptedMessageWithPubKey, trade)) + .filter(trade -> decryptedMessageWithPubKey.getNetworkEnvelope() instanceof MailboxMessage) + .forEach(trade -> removeEntryFromMailbox((MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(), trade)); + } + + private boolean isMessageForTrade(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof TradeMessage) { + return isMyMessage((TradeMessage) networkEnvelope, trade); + } else if (networkEnvelope instanceof AckMessage) { + return isMyMessage((AckMessage) networkEnvelope, trade); + } + // Instance must be TradeMessage or AckMessage. + return false; + } + + private void removeEntryFromMailbox(MailboxMessage mailboxMessage, Trade trade) { + log.info("We found a pending mailbox message ({}) for trade {}. " + + "As the trade is closed we remove the mailbox message.", + mailboxMessage.getClass().getSimpleName(), trade.getId()); + mailboxMessageService.removeMailboxMsg(mailboxMessage); + } + + private boolean isMyMessage(TradeMessage message, Trade trade) { + return message.getTradeId().equals(trade.getId()); + } + + private boolean isMyMessage(AckMessage ackMessage, Trade trade) { + return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && + ackMessage.getSourceId().equals(trade.getId()); + } + + private boolean isPubKeyValid(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { + // We can only validate the peers pubKey if we have it already. If we are the taker we get it from the offer + // Otherwise it depends on the state of the trade protocol if we have received the peers pubKeyRing already. + PubKeyRing peersPubKeyRing = trade.getProcessModel().getTradingPeer().getPubKeyRing(); + boolean isValid = true; + if (peersPubKeyRing != null && + !decryptedMessageWithPubKey.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) { + isValid = false; + log.warn("SignaturePubKey in decryptedMessageWithPubKey does not match the SignaturePubKey we have set for our trading peer."); + } + return isValid; + } +} diff --git a/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java new file mode 100644 index 0000000000..01c36beb4d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.core.trade.closed; + +import bisq.core.offer.Offer; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.DumpDelayedPayoutTx; +import bisq.core.trade.Tradable; +import bisq.core.trade.TradableList; +import bisq.core.trade.Trade; + +import bisq.common.crypto.KeyRing; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import com.google.inject.Inject; + +import com.google.common.collect.ImmutableList; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ClosedTradableManager implements PersistedDataHost { + private final PersistenceManager> persistenceManager; + private final TradableList closedTradables = new TradableList<>(); + private final KeyRing keyRing; + private final PriceFeedService priceFeedService; + private final CleanupMailboxMessages cleanupMailboxMessages; + private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + + @Inject + public ClosedTradableManager(KeyRing keyRing, + PriceFeedService priceFeedService, + PersistenceManager> persistenceManager, + CleanupMailboxMessages cleanupMailboxMessages, + DumpDelayedPayoutTx dumpDelayedPayoutTx) { + this.keyRing = keyRing; + this.priceFeedService = priceFeedService; + this.cleanupMailboxMessages = cleanupMailboxMessages; + this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(closedTradables, "ClosedTrades", PersistenceManager.Source.PRIVATE); + } + + @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)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(closedTradables, "delayed_payout_txs_closed"); + completeHandler.run(); + }, + completeHandler); + } + + public void onAllServicesInitialized() { + cleanupMailboxMessages.handleTrades(getClosedTrades()); + } + + public void add(Tradable tradable) { + if (closedTradables.add(tradable)) { + requestPersistence(); + } + } + + public void remove(Tradable tradable) { + if (closedTradables.remove(tradable)) { + requestPersistence(); + } + } + + public boolean wasMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); + } + + public ObservableList getObservableList() { + return closedTradables.getObservableList(); + } + + public List getClosedTrades() { + return ImmutableList.copyOf(getObservableList().stream() + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .collect(Collectors.toList())); + } + + public Optional getTradableById(String id) { + return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); + } + + public Stream getTradesStreamWithFundsLockedIn() { + return getClosedTrades().stream() + .filter(Trade::isFundsLockedIn); + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java new file mode 100644 index 0000000000..84746a2128 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java @@ -0,0 +1,158 @@ +/* + * 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 bisq.core.trade.failed; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.DumpDelayedPayoutTx; +import bisq.core.trade.TradableList; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeUtil; +import bisq.core.trade.closed.CleanupMailboxMessages; + +import bisq.common.crypto.KeyRing; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import com.google.inject.Inject; + +import javafx.collections.ObservableList; + +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Setter; + +public class FailedTradesManager implements PersistedDataHost { + private static final Logger log = LoggerFactory.getLogger(FailedTradesManager.class); + private final TradableList failedTrades = new TradableList<>(); + private final KeyRing keyRing; + private final PriceFeedService priceFeedService; + private final BtcWalletService btcWalletService; + private final CleanupMailboxMessages cleanupMailboxMessages; + private final PersistenceManager> persistenceManager; + private final TradeUtil tradeUtil; + private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + @Setter + private Function unFailTradeCallback; + + @Inject + public FailedTradesManager(KeyRing keyRing, + PriceFeedService priceFeedService, + BtcWalletService btcWalletService, + PersistenceManager> persistenceManager, + TradeUtil tradeUtil, + CleanupMailboxMessages cleanupMailboxMessages, + DumpDelayedPayoutTx dumpDelayedPayoutTx) { + this.keyRing = keyRing; + this.priceFeedService = priceFeedService; + this.btcWalletService = btcWalletService; + this.cleanupMailboxMessages = cleanupMailboxMessages; + this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; + this.persistenceManager = persistenceManager; + this.tradeUtil = tradeUtil; + + this.persistenceManager.initialize(failedTrades, "FailedTrades", PersistenceManager.Source.PRIVATE); + } + + @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)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(failedTrades, "delayed_payout_txs_failed"); + completeHandler.run(); + }, + completeHandler); + } + + public void onAllServicesInitialized() { + cleanupMailboxMessages.handleTrades(failedTrades.getList()); + } + + public void add(Trade trade) { + if (failedTrades.add(trade)) { + requestPersistence(); + } + } + + public void removeTrade(Trade trade) { + if (failedTrades.remove(trade)) { + requestPersistence(); + } + } + + public boolean wasMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); + } + + public ObservableList getObservableList() { + return failedTrades.getObservableList(); + } + + public Optional getTradeById(String id) { + return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst(); + } + + public Stream getTradesStreamWithFundsLockedIn() { + return failedTrades.stream() + .filter(Trade::isFundsLockedIn); + } + + public void unFailTrade(Trade trade) { + if (unFailTradeCallback == null) + return; + + if (unFailTradeCallback.apply(trade)) { + log.info("Unfailing trade {}", trade.getId()); + if (failedTrades.remove(trade)) { + requestPersistence(); + } + } + } + + public String checkUnFail(Trade trade) { + var addresses = tradeUtil.getTradeAddresses(trade); + if (addresses == null) { + return "Addresses not found"; + } + StringBuilder blockingTrades = new StringBuilder(); + for (var entry : btcWalletService.getAddressEntryListAsImmutableList()) { + if (entry.getContext() == AddressEntry.Context.AVAILABLE) + continue; + if (entry.getAddressString() != null && + (entry.getAddressString().equals(addresses.first) || + entry.getAddressString().equals(addresses.second))) { + blockingTrades.append(entry.getOfferId()).append(", "); + } + } + return blockingTrades.toString(); + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/handlers/TradeResultHandler.java b/core/src/main/java/bisq/core/trade/handlers/TradeResultHandler.java new file mode 100644 index 0000000000..aab6dc4e46 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/handlers/TradeResultHandler.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.trade.handlers; + +import bisq.core.trade.Trade; + +public interface TradeResultHandler { + void handleResult(Trade trade); +} diff --git a/core/src/main/java/bisq/core/trade/handlers/TransactionResultHandler.java b/core/src/main/java/bisq/core/trade/handlers/TransactionResultHandler.java new file mode 100644 index 0000000000..ca3531226b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/handlers/TransactionResultHandler.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.trade.handlers; + +import org.bitcoinj.core.Transaction; + +public interface TransactionResultHandler { + void handleResult(Transaction transaction); +} diff --git a/core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java new file mode 100644 index 0000000000..ea56069cfc --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java @@ -0,0 +1,126 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class CounterCurrencyTransferStartedMessage extends TradeMailboxMessage { + private final String buyerPayoutAddress; + private final NodeAddress senderNodeAddress; + private final byte[] buyerSignature; + @Nullable + private final String counterCurrencyTxId; + + // Added after v1.3.7 + // We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets. + @Nullable + private String counterCurrencyExtraData; + + public CounterCurrencyTransferStartedMessage(String tradeId, + String buyerPayoutAddress, + NodeAddress senderNodeAddress, + byte[] buyerSignature, + @Nullable String counterCurrencyTxId, + @Nullable String counterCurrencyExtraData, + String uid) { + this(tradeId, + buyerPayoutAddress, + senderNodeAddress, + buyerSignature, + counterCurrencyTxId, + counterCurrencyExtraData, + uid, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CounterCurrencyTransferStartedMessage(String tradeId, + String buyerPayoutAddress, + NodeAddress senderNodeAddress, + byte[] buyerSignature, + @Nullable String counterCurrencyTxId, + @Nullable String counterCurrencyExtraData, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.buyerPayoutAddress = buyerPayoutAddress; + this.senderNodeAddress = senderNodeAddress; + this.buyerSignature = buyerSignature; + this.counterCurrencyTxId = counterCurrencyTxId; + this.counterCurrencyExtraData = counterCurrencyExtraData; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.CounterCurrencyTransferStartedMessage.Builder builder = protobuf.CounterCurrencyTransferStartedMessage.newBuilder(); + builder.setTradeId(tradeId) + .setBuyerPayoutAddress(buyerPayoutAddress) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setBuyerSignature(ByteString.copyFrom(buyerSignature)) + .setUid(uid); + + Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); + Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + + return getNetworkEnvelopeBuilder().setCounterCurrencyTransferStartedMessage(builder).build(); + } + + public static CounterCurrencyTransferStartedMessage fromProto(protobuf.CounterCurrencyTransferStartedMessage proto, + int messageVersion) { + return new CounterCurrencyTransferStartedMessage(proto.getTradeId(), + proto.getBuyerPayoutAddress(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getBuyerSignature().toByteArray(), + ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()), + ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()), + proto.getUid(), + messageVersion); + } + + + @Override + public String toString() { + return "CounterCurrencyTransferStartedMessage{" + + "\n buyerPayoutAddress='" + buyerPayoutAddress + '\'' + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n counterCurrencyTxId=" + counterCurrencyTxId + + ",\n counterCurrencyExtraData=" + counterCurrencyExtraData + + ",\n uid='" + uid + '\'' + + ",\n buyerSignature=" + Utilities.bytesAsHexString(buyerSignature) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java new file mode 100644 index 0000000000..81afad3caf --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class DelayedPayoutTxSignatureRequest extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] delayedPayoutTx; + private final byte[] delayedPayoutTxSellerSignature; + + public DelayedPayoutTxSignatureRequest(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTx, + byte[] delayedPayoutTxSellerSignature) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + delayedPayoutTx, + delayedPayoutTxSellerSignature); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DelayedPayoutTxSignatureRequest(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTx, + byte[] delayedPayoutTxSellerSignature) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.delayedPayoutTx = delayedPayoutTx; + this.delayedPayoutTxSellerSignature = delayedPayoutTxSellerSignature; + } + + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDelayedPayoutTxSignatureRequest(protobuf.DelayedPayoutTxSignatureRequest.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx)) + .setDelayedPayoutTxSellerSignature(ByteString.copyFrom(delayedPayoutTxSellerSignature))) + .build(); + } + + public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto, + int messageVersion) { + return new DelayedPayoutTxSignatureRequest(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDelayedPayoutTx().toByteArray(), + proto.getDelayedPayoutTxSellerSignature().toByteArray()); + } + + @Override + public String toString() { + return "DelayedPayoutTxSignatureRequest{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) + + ",\n delayedPayoutTxSellerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSellerSignature) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java new file mode 100644 index 0000000000..ad3767a3ab --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class DelayedPayoutTxSignatureResponse extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] delayedPayoutTxBuyerSignature; + private final byte[] depositTx; + + public DelayedPayoutTxSignatureResponse(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTxBuyerSignature, + byte[] depositTx) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + delayedPayoutTxBuyerSignature, + depositTx); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DelayedPayoutTxSignatureResponse(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTxBuyerSignature, + byte[] depositTx) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.delayedPayoutTxBuyerSignature = delayedPayoutTxBuyerSignature; + this.depositTx = depositTx; + } + + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDelayedPayoutTxSignatureResponse(protobuf.DelayedPayoutTxSignatureResponse.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDelayedPayoutTxBuyerSignature(ByteString.copyFrom(delayedPayoutTxBuyerSignature)) + .setDepositTx(ByteString.copyFrom(depositTx)) + ) + .build(); + } + + public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto, + int messageVersion) { + return new DelayedPayoutTxSignatureResponse(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDelayedPayoutTxBuyerSignature().toByteArray(), + proto.getDepositTx().toByteArray()); + } + + @Override + public String toString() { + return "DelayedPayoutTxSignatureResponse{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n delayedPayoutTxBuyerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxBuyerSignature) + + ",\n depositTx=" + Utilities.bytesAsHexString(depositTx) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..caa255d43c --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +// It is the last message in the take offer phase. We use MailboxMessage instead of DirectMessage to add more tolerance +// in case of network issues and as the message does not trigger further protocol execution. +@EqualsAndHashCode(callSuper = true) +@Value +public final class DepositTxAndDelayedPayoutTxMessage extends TradeMailboxMessage { + private final NodeAddress senderNodeAddress; + private final byte[] depositTx; + private final byte[] delayedPayoutTx; + + public DepositTxAndDelayedPayoutTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx, + byte[] delayedPayoutTx) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + depositTx, + delayedPayoutTx); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DepositTxAndDelayedPayoutTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx, + byte[] delayedPayoutTx) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.depositTx = depositTx; + this.delayedPayoutTx = delayedPayoutTx; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDepositTxAndDelayedPayoutTxMessage(protobuf.DepositTxAndDelayedPayoutTxMessage.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDepositTx(ByteString.copyFrom(depositTx)) + .setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx))) + .build(); + } + + public static DepositTxAndDelayedPayoutTxMessage fromProto(protobuf.DepositTxAndDelayedPayoutTxMessage proto, + int messageVersion) { + return new DepositTxAndDelayedPayoutTxMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDepositTx().toByteArray(), + proto.getDelayedPayoutTx().toByteArray()); + } + + @Override + public String toString() { + return "DepositTxAndDelayedPayoutTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n depositTx=" + Utilities.bytesAsHexString(depositTx) + + ",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java b/core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java new file mode 100644 index 0000000000..631f1a9ca2 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +// It is the last message in the take offer phase. We use MailboxMessage instead of DirectMessage to add more tolerance +// in case of network issues and as the message does not trigger further protocol execution. +@EqualsAndHashCode(callSuper = true) +@Value +public final class DepositTxMessage extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] depositTxWithoutWitnesses; + + public DepositTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTxWithoutWitnesses) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + depositTxWithoutWitnesses); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DepositTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTxWithoutWitnesses) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.depositTxWithoutWitnesses = depositTxWithoutWitnesses; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDepositTxMessage(protobuf.DepositTxMessage.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDepositTxWithoutWitnesses(ByteString.copyFrom(depositTxWithoutWitnesses))) + .build(); + } + + public static DepositTxMessage fromProto(protobuf.DepositTxMessage proto, int messageVersion) { + return new DepositTxMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDepositTxWithoutWitnesses().toByteArray()); + } + + @Override + public String toString() { + return "DepositTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n depositTxWithoutWitnesses=" + Utilities.bytesAsHexString(depositTxWithoutWitnesses) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java new file mode 100644 index 0000000000..a67a879e3d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java @@ -0,0 +1,235 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.proto.CoreProtoResolver; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class InputsForDepositTxRequest extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final long tradeAmount; + private final long tradePrice; + private final long txFee; + private final long takerFee; + private final boolean isCurrencyForTakerFeeBtc; + private final List rawTransactionInputs; + private final long changeOutputValue; + @Nullable + private final String changeOutputAddress; + private final byte[] takerMultiSigPubKey; + private final String takerPayoutAddressString; + private final PubKeyRing takerPubKeyRing; + private final PaymentAccountPayload takerPaymentAccountPayload; + private final String takerAccountId; + private final String takerFeeTxId; + private final List acceptedArbitratorNodeAddresses; + private final List acceptedMediatorNodeAddresses; + private final List acceptedRefundAgentNodeAddresses; + @Nullable + private final NodeAddress arbitratorNodeAddress; + private final NodeAddress mediatorNodeAddress; + private final NodeAddress refundAgentNodeAddress; + + private final byte[] accountAgeWitnessSignatureOfOfferId; + private final long currentDate; + + public InputsForDepositTxRequest(String tradeId, + NodeAddress senderNodeAddress, + long tradeAmount, + long tradePrice, + long txFee, + long takerFee, + boolean isCurrencyForTakerFeeBtc, + List rawTransactionInputs, + long changeOutputValue, + @Nullable String changeOutputAddress, + byte[] takerMultiSigPubKey, + String takerPayoutAddressString, + PubKeyRing takerPubKeyRing, + PaymentAccountPayload takerPaymentAccountPayload, + String takerAccountId, + String takerFeeTxId, + List acceptedArbitratorNodeAddresses, + List acceptedMediatorNodeAddresses, + List acceptedRefundAgentNodeAddresses, + @Nullable NodeAddress arbitratorNodeAddress, + NodeAddress mediatorNodeAddress, + NodeAddress refundAgentNodeAddress, + String uid, + int messageVersion, + byte[] accountAgeWitnessSignatureOfOfferId, + long currentDate) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.tradeAmount = tradeAmount; + this.tradePrice = tradePrice; + this.txFee = txFee; + this.takerFee = takerFee; + this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; + this.rawTransactionInputs = rawTransactionInputs; + this.changeOutputValue = changeOutputValue; + this.changeOutputAddress = changeOutputAddress; + this.takerMultiSigPubKey = takerMultiSigPubKey; + this.takerPayoutAddressString = takerPayoutAddressString; + this.takerPubKeyRing = takerPubKeyRing; + this.takerPaymentAccountPayload = takerPaymentAccountPayload; + this.takerAccountId = takerAccountId; + this.takerFeeTxId = takerFeeTxId; + this.acceptedArbitratorNodeAddresses = acceptedArbitratorNodeAddresses; + this.acceptedMediatorNodeAddresses = acceptedMediatorNodeAddresses; + this.acceptedRefundAgentNodeAddresses = acceptedRefundAgentNodeAddresses; + this.arbitratorNodeAddress = arbitratorNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; + this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId; + this.currentDate = currentDate; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.InputsForDepositTxRequest.Builder builder = protobuf.InputsForDepositTxRequest.newBuilder() + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setTradeAmount(tradeAmount) + .setTradePrice(tradePrice) + .setTxFee(txFee) + .setTakerFee(takerFee) + .setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc) + .addAllRawTransactionInputs(rawTransactionInputs.stream() + .map(RawTransactionInput::toProtoMessage).collect(Collectors.toList())) + .setChangeOutputValue(changeOutputValue) + .setTakerMultiSigPubKey(ByteString.copyFrom(takerMultiSigPubKey)) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setTakerPaymentAccountPayload((protobuf.PaymentAccountPayload) takerPaymentAccountPayload.toProtoMessage()) + .setTakerAccountId(takerAccountId) + .setTakerFeeTxId(takerFeeTxId) + .addAllAcceptedArbitratorNodeAddresses(acceptedArbitratorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) + .addAllAcceptedMediatorNodeAddresses(acceptedMediatorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) + .addAllAcceptedRefundAgentNodeAddresses(acceptedRefundAgentNodeAddresses.stream() + .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) + .setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage()) + .setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage()) + .setUid(uid) + .setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(accountAgeWitnessSignatureOfOfferId)) + .setCurrentDate(currentDate); + + Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); + Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); + return getNetworkEnvelopeBuilder().setInputsForDepositTxRequest(builder).build(); + } + + public static InputsForDepositTxRequest fromProto(protobuf.InputsForDepositTxRequest proto, + CoreProtoResolver coreProtoResolver, + int messageVersion) { + List rawTransactionInputs = proto.getRawTransactionInputsList().stream() + .map(rawTransactionInput -> new RawTransactionInput(rawTransactionInput.getIndex(), + rawTransactionInput.getParentTransaction().toByteArray(), rawTransactionInput.getValue())) + .collect(Collectors.toList()); + List acceptedArbitratorNodeAddresses = proto.getAcceptedArbitratorNodeAddressesList().stream() + .map(NodeAddress::fromProto).collect(Collectors.toList()); + List acceptedMediatorNodeAddresses = proto.getAcceptedMediatorNodeAddressesList().stream() + .map(NodeAddress::fromProto).collect(Collectors.toList()); + List acceptedRefundAgentNodeAddresses = proto.getAcceptedRefundAgentNodeAddressesList().stream() + .map(NodeAddress::fromProto).collect(Collectors.toList()); + + return new InputsForDepositTxRequest(proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getTradeAmount(), + proto.getTradePrice(), + proto.getTxFee(), + proto.getTakerFee(), + proto.getIsCurrencyForTakerFeeBtc(), + rawTransactionInputs, + proto.getChangeOutputValue(), + ProtoUtil.stringOrNullFromProto(proto.getChangeOutputAddress()), + proto.getTakerMultiSigPubKey().toByteArray(), + proto.getTakerPayoutAddressString(), + PubKeyRing.fromProto(proto.getTakerPubKeyRing()), + coreProtoResolver.fromProto(proto.getTakerPaymentAccountPayload()), + proto.getTakerAccountId(), + proto.getTakerFeeTxId(), + acceptedArbitratorNodeAddresses, + acceptedMediatorNodeAddresses, + acceptedRefundAgentNodeAddresses, + NodeAddress.fromProto(proto.getArbitratorNodeAddress()), + NodeAddress.fromProto(proto.getMediatorNodeAddress()), + NodeAddress.fromProto(proto.getRefundAgentNodeAddress()), + proto.getUid(), + messageVersion, + ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()), + proto.getCurrentDate()); + } + + @Override + public String toString() { + return "InputsForDepositTxRequest{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n tradeAmount=" + tradeAmount + + ",\n tradePrice=" + tradePrice + + ",\n txFee=" + txFee + + ",\n takerFee=" + takerFee + + ",\n isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + + ",\n rawTransactionInputs=" + rawTransactionInputs + + ",\n changeOutputValue=" + changeOutputValue + + ",\n changeOutputAddress='" + changeOutputAddress + '\'' + + ",\n takerMultiSigPubKey=" + Utilities.bytesAsHexString(takerMultiSigPubKey) + + ",\n takerPayoutAddressString='" + takerPayoutAddressString + '\'' + + ",\n takerPubKeyRing=" + takerPubKeyRing + + ",\n takerPaymentAccountPayload=" + takerPaymentAccountPayload + + ",\n takerAccountId='" + takerAccountId + '\'' + + ",\n takerFeeTxId='" + takerFeeTxId + '\'' + + ",\n acceptedArbitratorNodeAddresses=" + acceptedArbitratorNodeAddresses + + ",\n acceptedMediatorNodeAddresses=" + acceptedMediatorNodeAddresses + + ",\n acceptedRefundAgentNodeAddresses=" + acceptedRefundAgentNodeAddresses + + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n accountAgeWitnessSignatureOfOfferId=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfOfferId) + + ",\n currentDate=" + currentDate + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java new file mode 100644 index 0000000000..994be36e5b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java @@ -0,0 +1,193 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.proto.CoreProtoResolver; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class InputsForDepositTxResponse extends TradeMessage implements DirectMessage { + private final PaymentAccountPayload makerPaymentAccountPayload; + private final String makerAccountId; + private final byte[] makerMultiSigPubKey; + private final String makerContractAsJson; + private final String makerContractSignature; + private final String makerPayoutAddressString; + private final byte[] preparedDepositTx; + private final List makerInputs; + private final NodeAddress senderNodeAddress; + + // added in v 0.6. can be null if we trade with an older peer + @Nullable + private final byte[] accountAgeWitnessSignatureOfPreparedDepositTx; + private final long currentDate; + private final long lockTime; + + public InputsForDepositTxResponse(String tradeId, + PaymentAccountPayload makerPaymentAccountPayload, + String makerAccountId, + byte[] makerMultiSigPubKey, + String makerContractAsJson, + String makerContractSignature, + String makerPayoutAddressString, + byte[] preparedDepositTx, + List makerInputs, + NodeAddress senderNodeAddress, + String uid, + @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, + long currentDate, + long lockTime) { + this(tradeId, + makerPaymentAccountPayload, + makerAccountId, + makerMultiSigPubKey, + makerContractAsJson, + makerContractSignature, + makerPayoutAddressString, + preparedDepositTx, + makerInputs, + senderNodeAddress, + uid, + Version.getP2PMessageVersion(), + accountAgeWitnessSignatureOfPreparedDepositTx, + currentDate, + lockTime); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private InputsForDepositTxResponse(String tradeId, + PaymentAccountPayload makerPaymentAccountPayload, + String makerAccountId, + byte[] makerMultiSigPubKey, + String makerContractAsJson, + String makerContractSignature, + String makerPayoutAddressString, + byte[] preparedDepositTx, + List makerInputs, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, + long currentDate, + long lockTime) { + super(messageVersion, tradeId, uid); + this.makerPaymentAccountPayload = makerPaymentAccountPayload; + this.makerAccountId = makerAccountId; + this.makerMultiSigPubKey = makerMultiSigPubKey; + this.makerContractAsJson = makerContractAsJson; + this.makerContractSignature = makerContractSignature; + this.makerPayoutAddressString = makerPayoutAddressString; + this.preparedDepositTx = preparedDepositTx; + this.makerInputs = makerInputs; + this.senderNodeAddress = senderNodeAddress; + this.accountAgeWitnessSignatureOfPreparedDepositTx = accountAgeWitnessSignatureOfPreparedDepositTx; + this.currentDate = currentDate; + this.lockTime = lockTime; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.InputsForDepositTxResponse.Builder builder = protobuf.InputsForDepositTxResponse.newBuilder() + .setTradeId(tradeId) + .setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage()) + .setMakerAccountId(makerAccountId) + .setMakerMultiSigPubKey(ByteString.copyFrom(makerMultiSigPubKey)) + .setMakerContractAsJson(makerContractAsJson) + .setMakerContractSignature(makerContractSignature) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setPreparedDepositTx(ByteString.copyFrom(preparedDepositTx)) + .addAllMakerInputs(makerInputs.stream().map(RawTransactionInput::toProtoMessage).collect(Collectors.toList())) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid) + .setLockTime(lockTime); + + Optional.ofNullable(accountAgeWitnessSignatureOfPreparedDepositTx).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfPreparedDepositTx(ByteString.copyFrom(e))); + builder.setCurrentDate(currentDate); + + return getNetworkEnvelopeBuilder() + .setInputsForDepositTxResponse(builder) + .build(); + } + + public static InputsForDepositTxResponse fromProto(protobuf.InputsForDepositTxResponse proto, CoreProtoResolver coreProtoResolver, int messageVersion) { + List makerInputs = proto.getMakerInputsList().stream() + .map(RawTransactionInput::fromProto) + .collect(Collectors.toList()); + + return new InputsForDepositTxResponse(proto.getTradeId(), + coreProtoResolver.fromProto(proto.getMakerPaymentAccountPayload()), + proto.getMakerAccountId(), + proto.getMakerMultiSigPubKey().toByteArray(), + proto.getMakerContractAsJson(), + proto.getMakerContractSignature(), + proto.getMakerPayoutAddressString(), + proto.getPreparedDepositTx().toByteArray(), + makerInputs, + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion, + ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfPreparedDepositTx()), + proto.getCurrentDate(), + proto.getLockTime()); + } + + + @Override + public String toString() { + return "InputsForDepositTxResponse{" + + "\n makerPaymentAccountPayload=" + makerPaymentAccountPayload + + ",\n makerAccountId='" + makerAccountId + '\'' + + ",\n makerMultiSigPubKey=" + Utilities.bytesAsHexString(makerMultiSigPubKey) + + ",\n makerContractAsJson='" + makerContractAsJson + '\'' + + ",\n makerContractSignature='" + makerContractSignature + '\'' + + ",\n makerPayoutAddressString='" + makerPayoutAddressString + '\'' + + ",\n preparedDepositTx=" + Utilities.bytesAsHexString(preparedDepositTx) + + ",\n makerInputs=" + makerInputs + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n uid='" + uid + '\'' + + ",\n accountAgeWitnessSignatureOfPreparedDepositTx=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfPreparedDepositTx) + + ",\n currentDate=" + new Date(currentDate) + + ",\n lockTime=" + lockTime + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..444b6af804 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class MediatedPayoutTxPublishedMessage extends TradeMailboxMessage { + private final byte[] payoutTx; + private final NodeAddress senderNodeAddress; + + public MediatedPayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + String uid) { + this(tradeId, + payoutTx, + senderNodeAddress, + uid, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MediatedPayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.payoutTx = payoutTx; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setMediatedPayoutTxPublishedMessage(protobuf.MediatedPayoutTxPublishedMessage.newBuilder() + .setTradeId(tradeId) + .setPayoutTx(ByteString.copyFrom(payoutTx)) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.MediatedPayoutTxPublishedMessage proto, int messageVersion) { + return new MediatedPayoutTxPublishedMessage(proto.getTradeId(), + proto.getPayoutTx().toByteArray(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion); + } + + @Override + public String toString() { + return "MediatedPayoutTxPublishedMessage{" + + "\n payoutTx=" + Utilities.bytesAsHexString(payoutTx) + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n uid='" + uid + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java new file mode 100644 index 0000000000..bc7cc84571 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +public class MediatedPayoutTxSignatureMessage extends TradeMailboxMessage { + private final byte[] txSignature; + private final NodeAddress senderNodeAddress; + + public MediatedPayoutTxSignatureMessage(byte[] txSignature, + String tradeId, + NodeAddress senderNodeAddress, + String uid) { + this(txSignature, + tradeId, + senderNodeAddress, + uid, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MediatedPayoutTxSignatureMessage(byte[] txSignature, + String tradeId, + NodeAddress senderNodeAddress, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.txSignature = txSignature; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setMediatedPayoutTxSignatureMessage(protobuf.MediatedPayoutTxSignatureMessage.newBuilder() + .setTxSignature(ByteString.copyFrom(txSignature)) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid)) + .build(); + } + + public static MediatedPayoutTxSignatureMessage fromProto(protobuf.MediatedPayoutTxSignatureMessage proto, + int messageVersion) { + return new MediatedPayoutTxSignatureMessage(proto.getTxSignature().toByteArray(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion); + } + + @Override + public String getTradeId() { + return tradeId; + } + + + @Override + public String toString() { + return "MediatedPayoutSignatureMessage{" + + "\n txSignature=" + Utilities.bytesAsHexString(txSignature) + + ",\n tradeId='" + tradeId + '\'' + + ",\n senderNodeAddress=" + senderNodeAddress + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java new file mode 100644 index 0000000000..86ed851ba8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.core.account.sign.SignedWitness; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import java.util.Optional; +import java.util.UUID; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Value +public final class PayoutTxPublishedMessage extends TradeMailboxMessage { + private final byte[] payoutTx; + private final NodeAddress senderNodeAddress; + + // Added in v1.4.0 + @Nullable + private final SignedWitness signedWitness; + + public PayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + @Nullable SignedWitness signedWitness) { + this(tradeId, + payoutTx, + senderNodeAddress, + signedWitness, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + @Nullable SignedWitness signedWitness, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.payoutTx = payoutTx; + this.senderNodeAddress = senderNodeAddress; + this.signedWitness = signedWitness; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder() + .setTradeId(tradeId) + .setPayoutTx(ByteString.copyFrom(payoutTx)) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid); + Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); + return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build(); + } + + public static NetworkEnvelope fromProto(protobuf.PayoutTxPublishedMessage proto, int messageVersion) { + // There is no method to check for a nullable non-primitive data type object but we know that all fields + // are empty/null, so we check for the signature to see if we got a valid signedWitness. + protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness(); + SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ? + SignedWitness.fromProto(protoSignedWitness) : + null; + return new PayoutTxPublishedMessage(proto.getTradeId(), + proto.getPayoutTx().toByteArray(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + signedWitness, + proto.getUid(), + messageVersion); + } + + @Override + public String toString() { + return "PayoutTxPublishedMessage{" + + "\n payoutTx=" + Utilities.bytesAsHexString(payoutTx) + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n signedWitness=" + signedWitness + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..d93a32737b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class PeerPublishedDelayedPayoutTxMessage extends TradeMailboxMessage { + private final NodeAddress senderNodeAddress; + + public PeerPublishedDelayedPayoutTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PeerPublishedDelayedPayoutTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.PeerPublishedDelayedPayoutTxMessage.Builder builder = protobuf.PeerPublishedDelayedPayoutTxMessage.newBuilder(); + builder.setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()); + return getNetworkEnvelopeBuilder().setPeerPublishedDelayedPayoutTxMessage(builder).build(); + } + + public static PeerPublishedDelayedPayoutTxMessage fromProto(protobuf.PeerPublishedDelayedPayoutTxMessage proto, int messageVersion) { + return new PeerPublishedDelayedPayoutTxMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress())); + } + + @Override + public String toString() { + return "PeerPublishedDelayedPayoutTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java b/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java new file mode 100644 index 0000000000..c6abcd67ee --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.NodeAddress; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +/** + * Not used anymore since v1.4.0 + * We do the re-sending of the payment sent message via the BuyerSendCounterCurrencyTransferStartedMessage task on the + * buyer side, so seller do not need to do anything interactively. + */ +@Deprecated +@SuppressWarnings("ALL") +@EqualsAndHashCode(callSuper = true) +@Value +public class RefreshTradeStateRequest extends TradeMailboxMessage { + private final NodeAddress senderNodeAddress; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RefreshTradeStateRequest(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.RefreshTradeStateRequest.Builder builder = protobuf.RefreshTradeStateRequest.newBuilder(); + builder.setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()); + return getNetworkEnvelopeBuilder().setRefreshTradeStateRequest(builder).build(); + } + + public static RefreshTradeStateRequest fromProto(protobuf.RefreshTradeStateRequest proto, int messageVersion) { + return new RefreshTradeStateRequest(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress())); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/TradeMailboxMessage.java b/core/src/main/java/bisq/core/trade/messages/TradeMailboxMessage.java new file mode 100644 index 0000000000..705c0b12ed --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/TradeMailboxMessage.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.mailbox.MailboxMessage; + +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@ToString +public abstract class TradeMailboxMessage extends TradeMessage implements MailboxMessage { + public static final long TTL = TimeUnit.DAYS.toMillis(15); + + protected TradeMailboxMessage(int messageVersion, String tradeId, String uid) { + super(messageVersion, tradeId, uid); + } + + @Override + public long getTTL() { + return TTL; + } + +} diff --git a/core/src/main/java/bisq/core/trade/messages/TradeMessage.java b/core/src/main/java/bisq/core/trade/messages/TradeMessage.java new file mode 100644 index 0000000000..e90cbb0265 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/TradeMessage.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.network.p2p.UidMessage; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@Getter +@ToString +public abstract class TradeMessage extends NetworkEnvelope implements UidMessage { + protected final String tradeId; + protected final String uid; + + protected TradeMessage(int messageVersion, String tradeId, String uid) { + super(messageVersion); + this.tradeId = tradeId; + this.uid = uid; + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java b/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java new file mode 100644 index 0000000000..c2bbde53b3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java @@ -0,0 +1,92 @@ +/* + * 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 bisq.core.trade.messages; + +import bisq.core.account.sign.SignedWitness; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +/** + * Not used anymore since v1.4.0 + */ +@Deprecated +@SuppressWarnings("ALL") +@EqualsAndHashCode(callSuper = true) +@Value +public class TraderSignedWitnessMessage extends TradeMailboxMessage { + private final NodeAddress senderNodeAddress; + private final SignedWitness signedWitness; + + public TraderSignedWitnessMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + SignedWitness signedWitness) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + signedWitness); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TraderSignedWitnessMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + SignedWitness signedWitness) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.signedWitness = signedWitness; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.TraderSignedWitnessMessage.Builder builder = protobuf.TraderSignedWitnessMessage.newBuilder(); + builder.setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setSignedWitness(signedWitness.toProtoSignedWitness()); + return getNetworkEnvelopeBuilder().setTraderSignedWitnessMessage(builder).build(); + } + + public static TraderSignedWitnessMessage fromProto(protobuf.TraderSignedWitnessMessage proto, int messageVersion) { + return new TraderSignedWitnessMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + SignedWitness.fromProto(proto.getSignedWitness())); + } + + @Override + public String toString() { + return "TraderSignedWitnessMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n signedWitness=" + signedWitness + + "\n} " + super.toString(); + } + +} diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java new file mode 100644 index 0000000000..c9280d0e20 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -0,0 +1,146 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.BuyerAsMakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesPreparedDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerCreatesAndSignsDepositTx; +import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerSendsInputsForDepositTxResponse; +import bisq.core.trade.protocol.tasks.maker.MakerCreateAndSignContract; +import bisq.core.trade.protocol.tasks.maker.MakerProcessesInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.maker.MakerRemovesOpenOffer; +import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime; +import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BuyerAsMakerProtocol(BuyerAsMakerTrade trade) { + super(trade); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handle take offer request + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress peer, + ErrorMessageHandler errorMessageHandler) { + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + MakerProcessesInputsForDepositTxRequest.class, + ApplyFilter.class, + VerifyPeersAccountAgeWitness.class, + getVerifyPeersFeePaymentClass(), + MakerSetsLockTime.class, + MakerCreateAndSignContract.class, + BuyerAsMakerCreatesAndSignsDepositTx.class, + BuyerSetupDepositTxListener.class, + BuyerAsMakerSendsInputsForDepositTxResponse.class). + using(new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess(message), + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(message, errorMessage); + })) + .withTimeout(60)) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming messages Take offer process + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(DelayedPayoutTxSignatureRequest message, NodeAddress peer) { + expect(phase(Trade.Phase.TAKER_FEE_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks( + MakerRemovesOpenOffer.class, + BuyerProcessDelayedPayoutTxSignatureRequest.class, + BuyerVerifiesPreparedDelayedPayoutTx.class, + BuyerSignsDelayedPayoutTx.class, + BuyerFinalizesDelayedPayoutTx.class, + BuyerSendsDelayedPayoutTxSignatureResponse.class) + .withTimeout(60)) + .executeTasks(); + } + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(DepositTxAndDelayedPayoutTxMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which events we expect + @Override + public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + super.onPaymentStarted(resultHandler, errorMessageHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message Payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + @Override + protected Class getVerifyPeersFeePaymentClass() { + return MakerVerifyTakerFeePayment.class; + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java new file mode 100644 index 0000000000..48631776af --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -0,0 +1,175 @@ +/* + * 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 bisq.core.trade.protocol; + + +import bisq.core.offer.Offer; +import bisq.core.trade.BuyerAsTakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesPreparedDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerCreatesDepositTxInputs; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSendsDepositTxMessage; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignsDepositTx; +import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; +import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; +import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; +import bisq.core.trade.protocol.tasks.taker.TakerSendInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; +import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) { + super(trade); + + Offer offer = checkNotNull(trade.getOffer()); + processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Take offer + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onTakeOffer() { + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER)) + .setup(tasks( + ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + CreateTakerFeeTx.class, + BuyerAsTakerCreatesDepositTxInputs.class, + TakerSendInputsForDepositTxRequest.class) + .withTimeout(60)) + .run(() -> { + processModel.setTempTradingPeerNodeAddress(trade.getTradingPeerNodeAddress()); + processModel.getTradeManager().requestPersistence(); + }) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming messages Take offer process + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handle(InputsForDepositTxResponse message, NodeAddress peer) { + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks(TakerProcessesInputsForDepositTxResponse.class, + ApplyFilter.class, + VerifyPeersAccountAgeWitness.class, + TakerVerifyAndSignContract.class, + TakerPublishFeeTx.class, + BuyerAsTakerSignsDepositTx.class, + BuyerSetupDepositTxListener.class, + BuyerAsTakerSendsDepositTxMessage.class) + .withTimeout(60)) + .executeTasks(); + } + + protected void handle(DelayedPayoutTxSignatureRequest message, NodeAddress peer) { + expect(phase(Trade.Phase.TAKER_FEE_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks( + BuyerProcessDelayedPayoutTxSignatureRequest.class, + BuyerVerifiesPreparedDelayedPayoutTx.class, + BuyerSignsDelayedPayoutTx.class, + BuyerFinalizesDelayedPayoutTx.class, + BuyerSendsDelayedPayoutTxSignatureResponse.class) + .withTimeout(60)) + .executeTasks(); + } + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(DepositTxAndDelayedPayoutTxMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which events we expect + @Override + public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + super.onPaymentStarted(resultHandler, errorMessageHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message Payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + super.onTradeMessage(message, peer); + + if (message instanceof InputsForDepositTxResponse) { + handle((InputsForDepositTxResponse) message, peer); + } + } + + @Override + protected Class getVerifyPeersFeePaymentClass() { + return TakerVerifyMakerFeePayment.class; + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java new file mode 100644 index 0000000000..1fee12e22b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -0,0 +1,192 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.BuyerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesFinalDelayedPayoutTx; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class BuyerProtocol extends DisputeProtocol { + enum BuyerEvent implements FluentProtocol.Event { + STARTUP, + PAYMENT_SENT + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BuyerProtocol(BuyerTrade trade) { + super(trade); + } + + @Override + protected void onInitialized() { + super.onInitialized(); + // We get called the constructor with any possible state and phase. As we don't want to log an error for such + // cases we use the alternative 'given' method instead of 'expect'. + given(phase(Trade.Phase.TAKER_FEE_PUBLISHED) + .with(BuyerEvent.STARTUP)) + .setup(tasks(BuyerSetupDepositTxListener.class)) + .executeTasks(); + + given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED) + .with(BuyerEvent.STARTUP)) + .setup(tasks(BuyerSetupPayoutTxListener.class)) + .executeTasks(); + + given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED) + .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG, + Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG) + .with(BuyerEvent.STARTUP)) + .setup(tasks(BuyerSendCounterCurrencyTransferStartedMessage.class)) + .executeTasks(); + } + + @Override + public void onMailboxMessage(TradeMessage message, NodeAddress peer) { + super.onMailboxMessage(message, peer); + + if (message instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) message, peer); + } else if (message instanceof PayoutTxPublishedMessage) { + handle((PayoutTxPublishedMessage) message, peer); + } + } + + protected abstract void handle(DelayedPayoutTxSignatureRequest message, NodeAddress peer); + + // The DepositTxAndDelayedPayoutTxMessage is a mailbox message as earlier we use only the deposit tx which can + // be also with from the network once published. + // Now we send the delayed payout tx as well and with that this message is mandatory for continuing the protocol. + // We do not support mailbox message handling during the take offer process as it is expected that both peers + // are online. + // For backward compatibility and extra resilience we still keep DepositTxAndDelayedPayoutTxMessage as a + // mailbox message but the stored in mailbox case is not expected and the seller would try to send the message again + // in the hope to reach the buyer directly. + protected void handle(DepositTxAndDelayedPayoutTxMessage message, NodeAddress peer) { + expect(anyPhase(Trade.Phase.TAKER_FEE_PUBLISHED, Trade.Phase.DEPOSIT_PUBLISHED) + .with(message) + .from(peer) + .preCondition(trade.getDepositTx() == null || trade.getDelayedPayoutTx() == null, + () -> { + log.warn("We with a DepositTxAndDelayedPayoutTxMessage but we have already processed the deposit and " + + "delayed 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."); + stopTimeout(); + sendAckMessage(message, true, null); + removeMailboxMessageAfterProcessing(message); + })) + .setup(tasks(BuyerProcessDepositTxAndDelayedPayoutTxMessage.class, + BuyerVerifiesFinalDelayedPayoutTx.class) + .using(new TradeTaskRunner(trade, + () -> { + stopTimeout(); + handleTaskRunnerSuccess(message); + }, + errorMessage -> handleTaskRunnerFault(message, errorMessage)))) + .run(() -> processModel.witnessDebugLog(trade)) + .executeTasks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + BuyerEvent event = BuyerEvent.PAYMENT_SENT; + expect(phase(Trade.Phase.DEPOSIT_CONFIRMED) + .with(event) + .preCondition(trade.confirmPermitted())) + .setup(tasks(ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + BuyerSignPayoutTx.class, + BuyerSetupPayoutTxListener.class, + BuyerSendCounterCurrencyTransferStartedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + (errorMessage) -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .run(() -> { + trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED); + processModel.getTradeManager().requestPersistence(); + }) + .executeTasks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message Payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { + expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks(BuyerProcessPayoutTxPublishedMessage.class)) + .executeTasks(); + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + super.onTradeMessage(message, peer); + + log.info("Received {} from {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); + + if (message instanceof DelayedPayoutTxSignatureRequest) { + handle((DelayedPayoutTxSignatureRequest) message, peer); + } else if (message instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) message, peer); + } else if (message instanceof PayoutTxPublishedMessage) { + handle((PayoutTxPublishedMessage) message, peer); + } + } + + abstract protected Class getVerifyPeersFeePaymentClass(); +} diff --git a/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java new file mode 100644 index 0000000000..23807c96f5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java @@ -0,0 +1,208 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.ProcessPeerPublishedDelayedPayoutTxMessage; +import bisq.core.trade.protocol.tasks.arbitration.PublishedDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.arbitration.SendPeerPublishedDelayedPayoutTxMessage; +import bisq.core.trade.protocol.tasks.mediation.BroadcastMediatedPayoutTx; +import bisq.core.trade.protocol.tasks.mediation.FinalizeMediatedPayoutTx; +import bisq.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutSignatureMessage; +import bisq.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.mediation.SendMediatedPayoutSignatureMessage; +import bisq.core.trade.protocol.tasks.mediation.SendMediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.mediation.SetupMediatedPayoutTxListener; +import bisq.core.trade.protocol.tasks.mediation.SignMediatedPayoutTx; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DisputeProtocol extends TradeProtocol { + + enum DisputeEvent implements FluentProtocol.Event { + MEDIATION_RESULT_ACCEPTED, + MEDIATION_RESULT_REJECTED, + ARBITRATION_REQUESTED + } + + public DisputeProtocol(Trade trade) { + super(trade); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction: Trader accepts mediation result + /////////////////////////////////////////////////////////////////////////////////////////// + + // Trader has not yet received the peer's signature but has clicked the accept button. + public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(event) + .preCondition(trade.getProcessModel().getTradingPeer().getMediatedPayoutTxSignature() == null, + () -> errorMessageHandler.handleErrorMessage("We have received already the signature from the peer.")) + .preCondition(trade.getPayoutTx() == null, + () -> errorMessageHandler.handleErrorMessage("Payout tx is already published."))) + .setup(tasks(ApplyFilter.class, + SignMediatedPayoutTx.class, + SendMediatedPayoutSignatureMessage.class, + SetupMediatedPayoutTxListener.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .executeTasks(); + } + + // Trader has already received the peer's signature and has clicked the accept button as well. + public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(event) + .preCondition(trade.getPayoutTx() == null, + () -> errorMessageHandler.handleErrorMessage("Payout tx is already published."))) + .setup(tasks(ApplyFilter.class, + SignMediatedPayoutTx.class, + FinalizeMediatedPayoutTx.class, + BroadcastMediatedPayoutTx.class, + SendMediatedPayoutTxPublishedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Mediation: incoming message + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) { + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(message) + .from(peer)) + .setup(tasks(ProcessMediatedPayoutSignatureMessage.class)) + .executeTasks(); + } + + protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) { + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(message) + .from(peer)) + .setup(tasks(ProcessMediatedPayoutTxPublishedMessage.class)) + .executeTasks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onPublishDelayedPayoutTx(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + DisputeEvent event = DisputeEvent.ARBITRATION_REQUESTED; + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(event) + .preCondition(trade.getDelayedPayoutTx() != null)) + .setup(tasks(PublishedDelayedPayoutTx.class, + SendPeerPublishedDelayedPayoutTxMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Peer has published the delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handle(PeerPublishedDelayedPayoutTxMessage message, NodeAddress peer) { + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, + Trade.Phase.FIAT_SENT, + Trade.Phase.FIAT_RECEIVED) + .with(message) + .from(peer)) + .setup(tasks(ProcessPeerPublishedDelayedPayoutTxMessage.class)) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + if (message instanceof MediatedPayoutTxSignatureMessage) { + handle((MediatedPayoutTxSignatureMessage) message, peer); + } else if (message instanceof MediatedPayoutTxPublishedMessage) { + handle((MediatedPayoutTxPublishedMessage) message, peer); + } else if (message instanceof PeerPublishedDelayedPayoutTxMessage) { + handle((PeerPublishedDelayedPayoutTxMessage) message, peer); + } + } + + @Override + protected void onMailboxMessage(TradeMessage message, NodeAddress peer) { + super.onMailboxMessage(message, peer); + if (message instanceof MediatedPayoutTxSignatureMessage) { + handle((MediatedPayoutTxSignatureMessage) message, peer); + } else if (message instanceof MediatedPayoutTxPublishedMessage) { + handle((MediatedPayoutTxPublishedMessage) message, peer); + } else if (message instanceof PeerPublishedDelayedPayoutTxMessage) { + handle((PeerPublishedDelayedPayoutTxMessage) message, peer); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java new file mode 100644 index 0000000000..2d79f9ed7e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java @@ -0,0 +1,387 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.taskrunner.Task; + +import java.text.MessageFormat; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.core.util.Validator.isTradeIdValid; +import static com.google.common.base.Preconditions.checkArgument; + +// Main class. Contains the condition and setup, if condition is valid it will execute the +// taskRunner and the optional runnable. +public class FluentProtocol { + + + interface Event { + String name(); + } + + private final TradeProtocol tradeProtocol; + private Condition condition; + private Setup setup; + private Consumer resultHandler; + + public FluentProtocol(TradeProtocol tradeProtocol) { + this.tradeProtocol = tradeProtocol; + } + + protected FluentProtocol condition(Condition condition) { + this.condition = condition; + return this; + } + + protected FluentProtocol setup(Setup setup) { + this.setup = setup; + return this; + } + + + public FluentProtocol resultHandler(Consumer resultHandler) { + this.resultHandler = resultHandler; + return this; + } + + // Can be used before or after executeTasks + public FluentProtocol run(Runnable runnable) { + Condition.Result result = condition.getResult(); + if (result.isValid) { + runnable.run(); + } else if (resultHandler != null) { + resultHandler.accept(result); + } + return this; + } + + public FluentProtocol executeTasks() { + Condition.Result result = condition.getResult(); + if (!result.isValid) { + if (resultHandler != null) { + resultHandler.accept(result); + } + return this; + } + + if (setup.getTimeoutSec() > 0) { + tradeProtocol.startTimeout(setup.getTimeoutSec()); + } + + NodeAddress peer = condition.getPeer(); + if (peer != null) { + tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer); + tradeProtocol.processModel.getTradeManager().requestPersistence(); + } + + TradeMessage message = condition.getMessage(); + if (message != null) { + tradeProtocol.processModel.setTradeMessage(message); + tradeProtocol.processModel.getTradeManager().requestPersistence(); + } + + TradeTaskRunner taskRunner = setup.getTaskRunner(message, condition.getEvent()); + taskRunner.addTasks(setup.getTasks()); + taskRunner.run(); + return this; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Condition class + /////////////////////////////////////////////////////////////////////////////////////////// + + @Slf4j + public static class Condition { + enum Result { + VALID(true), + INVALID_PHASE, + INVALID_STATE, + INVALID_PRE_CONDITION, + INVALID_TRADE_ID; + + @Getter + private boolean isValid; + @Getter + private String info; + + Result() { + } + + Result(boolean isValid) { + this.isValid = isValid; + } + + public Result info(String info) { + this.info = info; + return this; + } + } + + private final Set expectedPhases = new HashSet<>(); + private final Set expectedStates = new HashSet<>(); + private final Set preConditions = new HashSet<>(); + private final Trade trade; + @Nullable + private Result result; + + @Nullable + @Getter + private TradeMessage message; + @Nullable + @Getter + private Event event; + @Nullable + @Getter + private NodeAddress peer; + @Nullable + private Runnable preConditionFailedHandler; + + + public Condition(Trade trade) { + this.trade = trade; + } + + public Condition phase(Trade.Phase expectedPhase) { + checkArgument(result == null); + this.expectedPhases.add(expectedPhase); + return this; + } + + public Condition anyPhase(Trade.Phase... expectedPhases) { + checkArgument(result == null); + this.expectedPhases.addAll(Set.of(expectedPhases)); + return this; + } + + public Condition state(Trade.State state) { + checkArgument(result == null); + this.expectedStates.add(state); + return this; + } + + public Condition anyState(Trade.State... states) { + checkArgument(result == null); + this.expectedStates.addAll(Set.of(states)); + return this; + } + + public Condition with(TradeMessage message) { + checkArgument(result == null); + this.message = message; + return this; + } + + public Condition with(Event event) { + checkArgument(result == null); + this.event = event; + return this; + } + + public Condition from(NodeAddress peer) { + checkArgument(result == null); + this.peer = peer; + return this; + } + + public Condition preCondition(boolean preCondition) { + checkArgument(result == null); + preConditions.add(preCondition); + return this; + } + + public Condition preCondition(boolean preCondition, Runnable conditionFailedHandler) { + checkArgument(result == null); + preCondition(preCondition); + + this.preConditionFailedHandler = conditionFailedHandler; + return this; + } + + public Result getResult() { + if (result == null) { + boolean isTradeIdValid = message == null || isTradeIdValid(trade.getId(), message); + if (!isTradeIdValid) { + String info = MessageFormat.format("TradeId does not match tradeId in message, TradeId={0}, tradeId in message={1}", + trade.getId(), message.getTradeId()); + result = Result.INVALID_TRADE_ID.info(info); + return result; + } + + + Result phaseValidationResult = getPhaseResult(); + if (!phaseValidationResult.isValid) { + result = phaseValidationResult; + return result; + } + + Result stateResult = getStateResult(); + if (!stateResult.isValid) { + result = stateResult; + return result; + } + + boolean allPreConditionsMet = preConditions.stream().allMatch(e -> e); + if (!allPreConditionsMet) { + String info = MessageFormat.format("PreConditions not met. preConditions={0}, this={1}, tradeId={2}", + preConditions, this, trade.getId()); + result = Result.INVALID_PRE_CONDITION.info(info); + + if (preConditionFailedHandler != null) { + preConditionFailedHandler.run(); + } + return result; + } + + result = Result.VALID; + } + return result; + } + + private Result getPhaseResult() { + if (expectedPhases.isEmpty()) { + return Result.VALID; + } + + boolean isPhaseValid = expectedPhases.stream().anyMatch(e -> e == trade.getPhase()); + String trigger = message != null ? + message.getClass().getSimpleName() : + event != null ? + event.name() + " event" : + ""; + if (isPhaseValid) { + String info = MessageFormat.format("We received a {0} at phase {1} and state {2}, tradeId={3}", + trigger, + trade.getPhase(), + trade.getState(), + trade.getId()); + log.info(info); + return Result.VALID.info(info); + } else { + String info = MessageFormat.format("We received a {0} but we are are not in the expected phase.\n" + + "This can be an expected case if we get a repeated CounterCurrencyTransferStartedMessage " + + "after we have already received one as the peer re-sends that message at each startup.\n" + + "Expected phases={1},\nTrade phase={2},\nTrade state= {3},\ntradeId={4}", + trigger, + expectedPhases, + trade.getPhase(), + trade.getState(), + trade.getId()); + return Result.INVALID_PHASE.info(info); + } + } + + private Result getStateResult() { + if (expectedStates.isEmpty()) { + return Result.VALID; + } + + boolean isStateValid = expectedStates.stream().anyMatch(e -> e == trade.getState()); + String trigger = message != null ? + message.getClass().getSimpleName() : + event != null ? + event.name() + " event" : + ""; + if (isStateValid) { + String info = MessageFormat.format("We received a {0} at state {1}, tradeId={2}", + trigger, + trade.getState(), + trade.getId()); + log.info(info); + return Result.VALID.info(info); + } else { + String info = MessageFormat.format("We received a {0} but we are are not in the expected state. " + + "Expected states={1}, Trade state= {2}, tradeId={3}", + trigger, + expectedStates, + trade.getState(), + trade.getId()); + return Result.INVALID_STATE.info(info); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setup class + /////////////////////////////////////////////////////////////////////////////////////////// + + @Slf4j + public static class Setup { + private final TradeProtocol tradeProtocol; + private final Trade trade; + @Getter + private Class>[] tasks; + @Getter + private int timeoutSec; + @Nullable + private TradeTaskRunner taskRunner; + + public Setup(TradeProtocol tradeProtocol, Trade trade) { + this.tradeProtocol = tradeProtocol; + this.trade = trade; + } + + @SafeVarargs + public final Setup tasks(Class>... tasks) { + this.tasks = tasks; + return this; + } + + public Setup withTimeout(int timeoutSec) { + this.timeoutSec = timeoutSec; + return this; + } + + public Setup using(TradeTaskRunner taskRunner) { + this.taskRunner = taskRunner; + return this; + } + + public TradeTaskRunner getTaskRunner(@Nullable TradeMessage message, @Nullable Event event) { + if (taskRunner == null) { + if (message != null) { + taskRunner = new TradeTaskRunner(trade, + () -> tradeProtocol.handleTaskRunnerSuccess(message), + errorMessage -> tradeProtocol.handleTaskRunnerFault(message, errorMessage)); + } else if (event != null) { + taskRunner = new TradeTaskRunner(trade, + () -> tradeProtocol.handleTaskRunnerSuccess(event), + errorMessage -> tradeProtocol.handleTaskRunnerFault(event, errorMessage)); + } else { + throw new IllegalStateException("addTasks must not be called without message or event " + + "set in case no taskRunner has been created yet"); + } + } + return taskRunner; + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java new file mode 100644 index 0000000000..349e677a70 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.core.trade.protocol; + + +import bisq.core.trade.messages.InputsForDepositTxRequest; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; + +public interface MakerProtocol { + void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler); +} diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java new file mode 100644 index 0000000000..ad0e10d671 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -0,0 +1,384 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.filter.FilterManager; +import bisq.core.network.MessageState; +import bisq.core.offer.Offer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.trade.MakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.statistics.ReferralIdService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.taskrunner.Model; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not +// persist them. + +/** + * This is the base model for the trade protocol. It is persisted with the trade (non transient fields). + * It uses the {@link ProcessModelServiceProvider} for access to domain services. + */ + +@Getter +@Slf4j +public class ProcessModel implements Model, PersistablePayload { + // Transient/Immutable (net set in constructor so they are not final, but at init) + transient private ProcessModelServiceProvider provider; + transient private TradeManager tradeManager; + transient private Offer offer; + + // Transient/Mutable + transient private Transaction takeOfferFeeTx; + @Setter + transient private TradeMessage tradeMessage; + + // Added in v1.2.0 + @Setter + @Nullable + transient private byte[] delayedPayoutTxSignature; + @Setter + @Nullable + transient private Transaction preparedDelayedPayoutTx; + + // Added in v1.4.0 + // MessageState of the last message sent from the seller to the buyer in the take offer process. + // It is used only in a task which would not be executed after restart, so no need to persist it. + @Setter + transient private ObjectProperty depositTxMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + @Getter + transient private Transaction depositTx; + + + // Persistable Immutable + private final TradingPeer tradingPeer; + private final String offerId; + private final String accountId; + private final PubKeyRing pubKeyRing; + + // Persistable Mutable + @Nullable + @Setter + private String takeOfferFeeTxId; + @Nullable + @Setter + private byte[] payoutTxSignature; + @Nullable + @Setter + private byte[] preparedDepositTx; + @Nullable + @Setter + private List rawTransactionInputs; + @Setter + private long changeOutputValue; + @Nullable + @Setter + private String changeOutputAddress; + @Setter + private boolean useSavingsWallet; + @Setter + private long fundsNeededForTradeAsLong; + @Nullable + @Setter + private byte[] myMultiSigPubKey; + // that is used to store temp. the peers address when we get an incoming message before the message is verified. + // After successful verified we copy that over to the trade.tradingPeerAddress + @Nullable + @Setter + private NodeAddress tempTradingPeerNodeAddress; + + // Added in v.1.1.6 + @Nullable + @Setter + private byte[] mediatedPayoutTxSignature; + @Setter + private long buyerPayoutAmountFromMediation; + @Setter + private long sellerPayoutAmountFromMediation; + + + // We want to indicate the user the state of the message delivery of the + // CounterCurrencyTransferStartedMessage. 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. + @Setter + private ObjectProperty paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + + public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { + this(offerId, accountId, pubKeyRing, new TradingPeer()); + } + + public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing, TradingPeer tradingPeer) { + this.offerId = offerId; + this.accountId = accountId; + this.pubKeyRing = pubKeyRing; + // If tradingPeer was null in persisted data from some error cases we set a new one to not cause nullPointers + this.tradingPeer = tradingPeer != null ? tradingPeer : new TradingPeer(); + } + + public void applyTransient(ProcessModelServiceProvider provider, + TradeManager tradeManager, + Offer offer) { + this.offer = offer; + this.provider = provider; + this.tradeManager = tradeManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.ProcessModel toProtoMessage() { + protobuf.ProcessModel.Builder builder = protobuf.ProcessModel.newBuilder() + .setTradingPeer((protobuf.TradingPeer) tradingPeer.toProtoMessage()) + .setOfferId(offerId) + .setAccountId(accountId) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .setChangeOutputValue(changeOutputValue) + .setUseSavingsWallet(useSavingsWallet) + .setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong) + .setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name()) + .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) + .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation); + + Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId); + Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature))); + Optional.ofNullable(preparedDepositTx).ifPresent(e -> builder.setPreparedDepositTx(ByteString.copyFrom(preparedDepositTx))); + Optional.ofNullable(rawTransactionInputs).ifPresent(e -> builder.addAllRawTransactionInputs( + ProtoUtil.collectionToProto(rawTransactionInputs, protobuf.RawTransactionInput.class))); + Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); + Optional.ofNullable(myMultiSigPubKey).ifPresent(e -> builder.setMyMultiSigPubKey(ByteString.copyFrom(myMultiSigPubKey))); + Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage())); + Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); + + return builder.build(); + } + + public static ProcessModel fromProto(protobuf.ProcessModel proto, CoreProtoResolver coreProtoResolver) { + TradingPeer tradingPeer = TradingPeer.fromProto(proto.getTradingPeer(), coreProtoResolver); + PubKeyRing pubKeyRing = PubKeyRing.fromProto(proto.getPubKeyRing()); + ProcessModel processModel = new ProcessModel(proto.getOfferId(), proto.getAccountId(), pubKeyRing, tradingPeer); + processModel.setChangeOutputValue(proto.getChangeOutputValue()); + processModel.setUseSavingsWallet(proto.getUseSavingsWallet()); + processModel.setFundsNeededForTradeAsLong(proto.getFundsNeededForTradeAsLong()); + processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); + processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); + + // nullable + processModel.setTakeOfferFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakeOfferFeeTxId())); + processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); + processModel.setPreparedDepositTx(ProtoUtil.byteArrayOrNullFromProto(proto.getPreparedDepositTx())); + List rawTransactionInputs = proto.getRawTransactionInputsList().isEmpty() ? + null : proto.getRawTransactionInputsList().stream() + .map(RawTransactionInput::fromProto).collect(Collectors.toList()); + processModel.setRawTransactionInputs(rawTransactionInputs); + processModel.setChangeOutputAddress(ProtoUtil.stringOrNullFromProto(proto.getChangeOutputAddress())); + processModel.setMyMultiSigPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getMyMultiSigPubKey())); + processModel.setTempTradingPeerNodeAddress(proto.hasTempTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTempTradingPeerNodeAddress()) : null); + processModel.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); + + String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState()); + MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString); + processModel.setPaymentStartedMessageState(paymentStartedMessageState); + + return processModel; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onComplete() { + } + + public void setTakeOfferFeeTx(Transaction takeOfferFeeTx) { + this.takeOfferFeeTx = takeOfferFeeTx; + takeOfferFeeTxId = takeOfferFeeTx.getTxId().toString(); + } + + @Nullable + public PaymentAccountPayload getPaymentAccountPayload(Trade trade) { + PaymentAccount paymentAccount; + if (trade instanceof MakerTrade) + paymentAccount = getUser().getPaymentAccount(offer.getMakerPaymentAccountId()); + else + paymentAccount = getUser().getPaymentAccount(trade.getTakerPaymentAccountId()); + return paymentAccount != null ? paymentAccount.getPaymentAccountPayload() : null; + } + + public Coin getFundsNeededForTrade() { + return Coin.valueOf(fundsNeededForTradeAsLong); + } + + public Transaction resolveTakeOfferFeeTx(Trade trade) { + if (takeOfferFeeTx == null) { + if (!trade.isCurrencyForTakerFeeBtc()) + takeOfferFeeTx = getBsqWalletService().getTransaction(takeOfferFeeTxId); + else + takeOfferFeeTx = getBtcWalletService().getTransaction(takeOfferFeeTxId); + } + return takeOfferFeeTx; + } + + public NodeAddress getMyNodeAddress() { + return getP2PService().getAddress(); + } + + void setPaymentStartedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setPaymentStartedMessageState(messageState); + } + + public void setPaymentStartedMessageState(MessageState paymentStartedMessageStateProperty) { + this.paymentStartedMessageStateProperty.set(paymentStartedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + void setDepositTxSentAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setDepositTxMessageState(messageState); + } + + public void setDepositTxMessageState(MessageState messageState) { + this.depositTxMessageStateProperty.set(messageState); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + void witnessDebugLog(Trade trade) { + getAccountAgeWitnessService().getAccountAgeWitnessUtils().witnessDebugLog(trade, null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegates + /////////////////////////////////////////////////////////////////////////////////////////// + + public BtcWalletService getBtcWalletService() { + return provider.getBtcWalletService(); + } + + public AccountAgeWitnessService getAccountAgeWitnessService() { + return provider.getAccountAgeWitnessService(); + } + + public P2PService getP2PService() { + return provider.getP2PService(); + } + + public BsqWalletService getBsqWalletService() { + return provider.getBsqWalletService(); + } + + public TradeWalletService getTradeWalletService() { + return provider.getTradeWalletService(); + } + + public User getUser() { + return provider.getUser(); + } + + public OpenOfferManager getOpenOfferManager() { + return provider.getOpenOfferManager(); + } + + public ReferralIdService getReferralIdService() { + return provider.getReferralIdService(); + } + + public FilterManager getFilterManager() { + return provider.getFilterManager(); + } + + public TradeStatisticsManager getTradeStatisticsManager() { + return provider.getTradeStatisticsManager(); + } + + public ArbitratorManager getArbitratorManager() { + return provider.getArbitratorManager(); + } + + public MediatorManager getMediatorManager() { + return provider.getMediatorManager(); + } + + public RefundAgentManager getRefundAgentManager() { + return provider.getRefundAgentManager(); + } + + public KeyRing getKeyRing() { + return provider.getKeyRing(); + } + + public DaoFacade getDaoFacade() { + return provider.getDaoFacade(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModelServiceProvider.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModelServiceProvider.java new file mode 100644 index 0000000000..37600eccaf --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModelServiceProvider.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.filter.FilterManager; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.trade.statistics.ReferralIdService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import javax.inject.Inject; + +import lombok.Getter; + +@Getter +public class ProcessModelServiceProvider { + private final OpenOfferManager openOfferManager; + private final P2PService p2PService; + private final BtcWalletService btcWalletService; + private final BsqWalletService bsqWalletService; + private final TradeWalletService tradeWalletService; + private final DaoFacade daoFacade; + private final ReferralIdService referralIdService; + private final User user; + private final FilterManager filterManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final TradeStatisticsManager tradeStatisticsManager; + private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final KeyRing keyRing; + + @Inject + public ProcessModelServiceProvider(OpenOfferManager openOfferManager, + P2PService p2PService, + BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + TradeWalletService tradeWalletService, + DaoFacade daoFacade, + ReferralIdService referralIdService, + User user, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService, + TradeStatisticsManager tradeStatisticsManager, + ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + KeyRing keyRing) { + + this.openOfferManager = openOfferManager; + this.p2PService = p2PService; + this.btcWalletService = btcWalletService; + this.bsqWalletService = bsqWalletService; + this.tradeWalletService = tradeWalletService; + this.daoFacade = daoFacade; + this.referralIdService = referralIdService; + this.user = user; + this.filterManager = filterManager; + this.accountAgeWitnessService = accountAgeWitnessService; + this.tradeStatisticsManager = tradeStatisticsManager; + this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.keyRing = keyRing; + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java new file mode 100644 index 0000000000..0cdaad17c0 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -0,0 +1,162 @@ +/* + * 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 bisq.core.trade.protocol; + + +import bisq.core.trade.SellerAsMakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.maker.MakerCreateAndSignContract; +import bisq.core.trade.protocol.tasks.maker.MakerProcessesInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.maker.MakerRemovesOpenOffer; +import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime; +import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; +import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerSendsInputsForDepositTxResponse; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public SellerAsMakerProtocol(SellerAsMakerTrade trade) { + super(trade); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handle take offer request + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress peer, + ErrorMessageHandler errorMessageHandler) { + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + MakerProcessesInputsForDepositTxRequest.class, + ApplyFilter.class, + VerifyPeersAccountAgeWitness.class, + getVerifyPeersFeePaymentClass(), + MakerSetsLockTime.class, + MakerCreateAndSignContract.class, + SellerAsMakerCreatesUnsignedDepositTx.class, + SellerAsMakerSendsInputsForDepositTxResponse.class) + .using(new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess(message), + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(message, errorMessage); + })) + .withTimeout(60)) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming messages Take offer process + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(DepositTxMessage message, NodeAddress peer) { + expect(phase(Trade.Phase.TAKER_FEE_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks( + MakerRemovesOpenOffer.class, + SellerAsMakerProcessDepositTxMessage.class, + SellerAsMakerFinalizesDepositTx.class, + SellerCreatesDelayedPayoutTx.class, + SellerSignsDelayedPayoutTx.class, + SellerSendDelayedPayoutTxSignatureRequest.class) + .withTimeout(60)) + .executeTasks(); + } + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(DelayedPayoutTxSignatureResponse message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message when buyer has clicked payment started button + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which events we expect + @Override + public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + super.onPaymentReceived(resultHandler, errorMessageHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Massage dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + super.onTradeMessage(message, peer); + + log.info("Received {} from {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); + + if (message instanceof DepositTxMessage) { + handle((DepositTxMessage) message, peer); + } + } + + @Override + protected Class getVerifyPeersFeePaymentClass() { + return MakerVerifyTakerFeePayment.class; + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java new file mode 100644 index 0000000000..7adf9026c5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -0,0 +1,157 @@ +/* + * 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 bisq.core.trade.protocol; + + +import bisq.core.offer.Offer; +import bisq.core.trade.SellerAsTakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs; +import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx; +import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; +import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; +import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; +import bisq.core.trade.protocol.tasks.taker.TakerSendInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; +import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtocol { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public SellerAsTakerProtocol(SellerAsTakerTrade trade) { + super(trade); + Offer offer = checkNotNull(trade.getOffer()); + processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction: Take offer + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onTakeOffer() { + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER) + .from(trade.getTradingPeerNodeAddress())) + .setup(tasks( + ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + CreateTakerFeeTx.class, + SellerAsTakerCreatesDepositTxInputs.class, + TakerSendInputsForDepositTxRequest.class) + .withTimeout(60)) + .executeTasks(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming messages Take offer process + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handle(InputsForDepositTxResponse message, NodeAddress peer) { + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + TakerProcessesInputsForDepositTxResponse.class, + ApplyFilter.class, + VerifyPeersAccountAgeWitness.class, + TakerVerifyAndSignContract.class, + TakerPublishFeeTx.class, + SellerAsTakerSignsDepositTx.class, + SellerCreatesDelayedPayoutTx.class, + SellerSignsDelayedPayoutTx.class, + SellerSendDelayedPayoutTxSignatureRequest.class) + .withTimeout(60)) + .executeTasks(); + } + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(DelayedPayoutTxSignatureResponse message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message when buyer has clicked payment started button + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which messages we expect + @Override + protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + super.handle(message, peer); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + // We keep the handler here in as well to make it more transparent which events we expect + @Override + public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + super.onPaymentReceived(resultHandler, errorMessageHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Massage dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + super.onTradeMessage(message, peer); + + log.info("Received {} from {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); + + if (message instanceof InputsForDepositTxResponse) { + handle((InputsForDepositTxResponse) message, peer); + } + } + + @Override + protected Class getVerifyPeersFeePaymentClass() { + return TakerVerifyMakerFeePayment.class; + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java new file mode 100644 index 0000000000..54d010eb7e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -0,0 +1,171 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.SellerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.seller.SellerBroadcastPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerFinalizesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerProcessDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.seller.SellerPublishesDepositTx; +import bisq.core.trade.protocol.tasks.seller.SellerPublishesTradeStatistics; +import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class SellerProtocol extends DisputeProtocol { + enum SellerEvent implements FluentProtocol.Event { + STARTUP, + PAYMENT_RECEIVED + } + + public SellerProtocol(SellerTrade trade) { + super(trade); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Mailbox + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { + super.onMailboxMessage(message, peerNodeAddress); + + if (message instanceof CounterCurrencyTransferStartedMessage) { + handle((CounterCurrencyTransferStartedMessage) message, peerNodeAddress); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming messages + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(DelayedPayoutTxSignatureResponse message, NodeAddress peer) { + expect(phase(Trade.Phase.TAKER_FEE_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks(SellerProcessDelayedPayoutTxSignatureResponse.class, + SellerFinalizesDelayedPayoutTx.class, + SellerSendsDepositTxAndDelayedPayoutTxMessage.class, + SellerPublishesDepositTx.class, + SellerPublishesTradeStatistics.class)) + .run(() -> { + // We stop timeout here and don't start a new one as the + // SellerSendsDepositTxAndDelayedPayoutTxMessage repeats the send the message and has it's own + // timeout if it never succeeds. + stopTimeout(); + + //TODO still needed? If so move to witness domain + processModel.witnessDebugLog(trade); + }) + .executeTasks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message when buyer has clicked payment started button + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + // We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case + // that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received + // a mailbox message with CounterCurrencyTransferStartedMessage. + // 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. + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED) + .with(message) + .from(peer) + .preCondition(trade.getPayoutTx() == null, + () -> { + log.warn("We received a CounterCurrencyTransferStartedMessage 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(message, true, null); + removeMailboxMessageAfterProcessing(message); + })) + .setup(tasks( + SellerProcessCounterCurrencyTransferStartedMessage.class, + ApplyFilter.class, + getVerifyPeersFeePaymentClass())) + .executeTasks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // User interaction + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + SellerEvent event = SellerEvent.PAYMENT_RECEIVED; + expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + .with(event) + .preCondition(trade.confirmPermitted())) + .setup(tasks( + ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + SellerSignAndFinalizePayoutTx.class, + SellerBroadcastPayoutTx.class, + SellerSendPayoutTxPublishedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + (errorMessage) -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .run(() -> { + trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT); + processModel.getTradeManager().requestPersistence(); + }) + .executeTasks(); + } + + + @Override + protected void onTradeMessage(TradeMessage message, NodeAddress peer) { + super.onTradeMessage(message, peer); + + log.info("Received {} from {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); + + if (message instanceof DelayedPayoutTxSignatureResponse) { + handle((DelayedPayoutTxSignatureResponse) message, peer); + } else if (message instanceof CounterCurrencyTransferStartedMessage) { + handle((CounterCurrencyTransferStartedMessage) message, peer); + } + } + + abstract protected Class getVerifyPeersFeePaymentClass(); + +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TakerProtocol.java new file mode 100644 index 0000000000..249d0ac73b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/TakerProtocol.java @@ -0,0 +1,26 @@ +/* + * 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 bisq.core.trade.protocol; + +public interface TakerProtocol { + void onTakeOffer(); + + enum TakerEvent implements FluentProtocol.Event { + TAKE_OFFER + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java new file mode 100644 index 0000000000..2135aecec8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -0,0 +1,403 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.DecryptedDirectMessageListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendMailboxMessageListener; +import bisq.network.p2p.mailbox.MailboxMessage; +import bisq.network.p2p.mailbox.MailboxMessageService; +import bisq.network.p2p.messaging.DecryptedMailboxListener; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.taskrunner.Task; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener { + + protected final ProcessModel processModel; + protected final Trade trade; + private Timer timeoutTimer; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TradeProtocol(Trade trade) { + this.trade = trade; + this.processModel = trade.getProcessModel(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) { + processModel.applyTransient(serviceProvider, tradeManager, offer); + onInitialized(); + } + + protected void onInitialized() { + if (!trade.isWithdrawn()) { + processModel.getP2PService().addDecryptedDirectMessageListener(this); + } + + MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); + // We delay a bit here as the trade gets updated from the wallet to update the trade + // state (deposit confirmed) and that happens after our method is called. + // TODO To fix that in a better way we would need to change the order of some routines + // from the TradeManager, but as we are close to a release I dont want to risk a bigger + // change and leave that for a later PR + UserThread.runAfter(() -> { + mailboxMessageService.addDecryptedMailboxListener(this); + handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); + }, 100, TimeUnit.MILLISECONDS); + } + + public void onWithdrawCompleted() { + cleanup(); + } + + protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { + log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DecryptedDirectMessageListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (!isMyMessage(networkEnvelope)) { + return; + } + + if (!isPubKeyValid(decryptedMessageWithPubKey)) { + return; + } + + if (networkEnvelope instanceof TradeMessage) { + onTradeMessage((TradeMessage) networkEnvelope, peer); + } else if (networkEnvelope instanceof AckMessage) { + onAckMessage((AckMessage) networkEnvelope, peer); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DecryptedMailboxListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { + handleMailboxCollection(Collections.singletonList(decryptedMessageWithPubKey)); + } + + private void handleMailboxCollection(Collection collection) { + collection.stream() + .filter(this::isPubKeyValid) + .map(DecryptedMessageWithPubKey::getNetworkEnvelope) + .filter(this::isMyMessage) + .filter(e -> e instanceof MailboxMessage) + .map(e -> (MailboxMessage) e) + .forEach(this::handleMailboxMessage); + } + + private void handleMailboxMessage(MailboxMessage mailboxMessage) { + if (mailboxMessage instanceof TradeMessage) { + TradeMessage tradeMessage = (TradeMessage) mailboxMessage; + // We only remove here if we have already completed the trade. + // Otherwise removal is done after successfully applied the task runner. + if (trade.isWithdrawn()) { + processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage); + log.info("Remove {} from the P2P network as trade is already completed.", + tradeMessage.getClass().getSimpleName()); + return; + } + onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); + } else if (mailboxMessage instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) mailboxMessage; + if (!trade.isWithdrawn()) { + // We only apply the msg if we have not already completed the trade + onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); + } + // In any case we remove the msg + processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage); + log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName()); + } + } + + public void removeMailboxMessageAfterProcessing(TradeMessage tradeMessage) { + if (tradeMessage instanceof MailboxMessage) { + processModel.getP2PService().getMailboxMessageService().removeMailboxMsg((MailboxMessage) tradeMessage); + log.info("Remove {} from the P2P network.", tradeMessage.getClass().getSimpleName()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void onTradeMessage(TradeMessage message, NodeAddress peer); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // FluentProtocol + /////////////////////////////////////////////////////////////////////////////////////////// + + // We log an error if condition is not met and call the protocol error handler + protected FluentProtocol expect(FluentProtocol.Condition condition) { + return new FluentProtocol(this) + .condition(condition) + .resultHandler(result -> { + if (!result.isValid()) { + log.warn(result.getInfo()); + handleTaskRunnerFault(null, + result.name(), + result.getInfo()); + } + }); + } + + // We execute only if condition is met but do not log an error. + protected FluentProtocol given(FluentProtocol.Condition condition) { + return new FluentProtocol(this) + .condition(condition); + } + + protected FluentProtocol.Condition phase(Trade.Phase expectedPhase) { + return new FluentProtocol.Condition(trade).phase(expectedPhase); + } + + protected FluentProtocol.Condition anyPhase(Trade.Phase... expectedPhases) { + return new FluentProtocol.Condition(trade).anyPhase(expectedPhases); + } + + @SafeVarargs + public final FluentProtocol.Setup tasks(Class>... tasks) { + return new FluentProtocol.Setup(this, trade).tasks(tasks); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ACK msg + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onAckMessage(AckMessage ackMessage, NodeAddress peer) { + // We handle the ack for CounterCurrencyTransferStartedMessage and DepositTxAndDelayedPayoutTxMessage + // as we support automatic re-send of the msg in case it was not ACKed after a certain time + if (ackMessage.getSourceMsgClassName().equals(CounterCurrencyTransferStartedMessage.class.getSimpleName())) { + processModel.setPaymentStartedAckMessage(ackMessage); + } else if (ackMessage.getSourceMsgClassName().equals(DepositTxAndDelayedPayoutTxMessage.class.getSimpleName())) { + processModel.setDepositTxSentAckMessage(ackMessage); + } + + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} from {} with tradeId {} and uid {}", + ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {} from {} with tradeId {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getErrorMessage()); + } + } + + protected void sendAckMessage(TradeMessage message, boolean result, @Nullable String errorMessage) { + PubKeyRing peersPubKeyRing = processModel.getTradingPeer().getPubKeyRing(); + if (peersPubKeyRing == null) { + log.error("We cannot send the ACK message as peersPubKeyRing is null"); + return; + } + + String tradeId = message.getTradeId(); + String sourceUid = message.getUid(); + AckMessage ackMessage = new AckMessage(processModel.getMyNodeAddress(), + AckMessageSourceType.TRADE_MESSAGE, + message.getClass().getSimpleName(), + sourceUid, + tradeId, + result, + errorMessage); + // If there was an error during offer verification, the tradingPeerNodeAddress of the trade might not be set yet. + // We can find the peer's node address in the processModel's tempTradingPeerNodeAddress in that case. + NodeAddress peer = trade.getTradingPeerNodeAddress() != null ? + trade.getTradingPeerNodeAddress() : + processModel.getTempTradingPeerNodeAddress(); + log.info("Send AckMessage for {} to peer {}. tradeId={}, sourceUid={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( + peer, + peersPubKeyRing, + ackMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for {} arrived at peer {}. tradeId={}, sourceUid={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + } + + @Override + public void onStoredInMailbox() { + log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, sourceUid={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for {} failed. Peer {}. tradeId={}, sourceUid={}, errorMessage={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid, errorMessage); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeout + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void startTimeout(long timeoutSec) { + stopTimeout(); + + timeoutTimer = UserThread.runAfter(() -> { + log.error("Timeout reached. TradeID={}, state={}, timeoutSec={}", + trade.getId(), trade.stateProperty().get(), timeoutSec); + trade.setErrorMessage("Timeout reached. Protocol did not complete in " + timeoutSec + " sec."); + + processModel.getTradeManager().requestPersistence(); + cleanup(); + }, timeoutSec); + } + + protected void stopTimeout() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Task runner + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handleTaskRunnerSuccess(TradeMessage message) { + handleTaskRunnerSuccess(message, message.getClass().getSimpleName()); + } + + protected void handleTaskRunnerSuccess(FluentProtocol.Event event) { + handleTaskRunnerSuccess(null, event.name()); + } + + protected void handleTaskRunnerFault(TradeMessage message, String errorMessage) { + handleTaskRunnerFault(message, message.getClass().getSimpleName(), errorMessage); + } + + protected void handleTaskRunnerFault(FluentProtocol.Event event, String errorMessage) { + handleTaskRunnerFault(null, event.name(), errorMessage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isPubKeyValid(DecryptedMessageWithPubKey message) { + // We can only validate the peers pubKey if we have it already. If we are the taker we get it from the offer + // Otherwise it depends on the state of the trade protocol if we have received the peers pubKeyRing already. + PubKeyRing peersPubKeyRing = processModel.getTradingPeer().getPubKeyRing(); + boolean isValid = true; + if (peersPubKeyRing != null && + !message.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) { + isValid = false; + log.error("SignaturePubKey in message does not match the SignaturePubKey we have set for our trading peer."); + } + return isValid; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handleTaskRunnerSuccess(@Nullable TradeMessage message, String source) { + log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId()); + if (message != null) { + sendAckMessage(message, true, null); + + // Once a taskRunner is completed we remove the mailbox message. To not remove it directly at the task + // adds some resilience in case of minor errors, so after a restart the mailbox message can be applied + // again. + removeMailboxMessageAfterProcessing(message); + } + } + + void handleTaskRunnerFault(@Nullable TradeMessage message, String source, String errorMessage) { + log.error("Task runner failed with error {}. Triggered from {}", errorMessage, source); + + if (message != null) { + sendAckMessage(message, false, errorMessage); + } + cleanup(); + } + + private boolean isMyMessage(NetworkEnvelope message) { + if (message instanceof TradeMessage) { + TradeMessage tradeMessage = (TradeMessage) message; + return tradeMessage.getTradeId().equals(trade.getId()); + } else if (message instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) message; + return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && + ackMessage.getSourceId().equals(trade.getId()); + } else { + return false; + } + } + + private void cleanup() { + stopTimeout(); + // We do not remove the decryptedDirectMessageListener as in case of not critical failures we want allow to receive + // follow-up messages still + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocolFactory.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocolFactory.java new file mode 100644 index 0000000000..fe521e9918 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocolFactory.java @@ -0,0 +1,40 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.BuyerAsMakerTrade; +import bisq.core.trade.BuyerAsTakerTrade; +import bisq.core.trade.SellerAsMakerTrade; +import bisq.core.trade.SellerAsTakerTrade; +import bisq.core.trade.Trade; + +public class TradeProtocolFactory { + public static TradeProtocol getNewTradeProtocol(Trade trade) { + if (trade instanceof BuyerAsMakerTrade) { + return new BuyerAsMakerProtocol((BuyerAsMakerTrade) trade); + } else if (trade instanceof BuyerAsTakerTrade) { + return new BuyerAsTakerProtocol((BuyerAsTakerTrade) trade); + } else if (trade instanceof SellerAsMakerTrade) { + return new SellerAsMakerProtocol((SellerAsMakerTrade) trade); + } else if (trade instanceof SellerAsTakerTrade) { + return new SellerAsTakerProtocol((SellerAsTakerTrade) trade); + } else { + throw new IllegalStateException("Trade not of expected type. Trade=" + trade); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeTaskRunner.java b/core/src/main/java/bisq/core/trade/protocol/TradeTaskRunner.java new file mode 100644 index 0000000000..fef4ff490d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/TradeTaskRunner.java @@ -0,0 +1,32 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.trade.Trade; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.taskrunner.TaskRunner; + +public class TradeTaskRunner extends TaskRunner { + + public TradeTaskRunner(Trade sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + //noinspection unchecked + super(sharedModel, (Class) sharedModel.getClass().getSuperclass().getSuperclass(), resultHandler, errorMessageHandler); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java new file mode 100644 index 0000000000..5da74be208 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -0,0 +1,145 @@ +/* + * 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 bisq.core.trade.protocol; + +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not +// persist them. +//todo clean up older fields as well to make most transient +@Slf4j +@Getter +@Setter +public final class TradingPeer implements PersistablePayload { + // Transient/Mutable + // Added in v1.2.0 + @Setter + @Nullable + transient private byte[] delayedPayoutTxSignature; + @Setter + @Nullable + transient private byte[] preparedDepositTx; + + // Persistable mutable + @Nullable + private String accountId; + @Nullable + private PaymentAccountPayload paymentAccountPayload; + @Nullable + private String payoutAddressString; + @Nullable + private String contractAsJson; + @Nullable + private String contractSignature; + @Nullable + private byte[] signature; + @Nullable + private PubKeyRing pubKeyRing; + @Nullable + private byte[] multiSigPubKey; + @Nullable + private List rawTransactionInputs; + private long changeOutputValue; + @Nullable + private String changeOutputAddress; + + // added in v 0.6 + @Nullable + private byte[] accountAgeWitnessNonce; + @Nullable + private byte[] accountAgeWitnessSignature; + private long currentDate; + + // Added in v.1.1.6 + @Nullable + private byte[] mediatedPayoutTxSignature; + + + public TradingPeer() { + } + + @Override + public Message toProtoMessage() { + final protobuf.TradingPeer.Builder builder = protobuf.TradingPeer.newBuilder() + .setChangeOutputValue(changeOutputValue); + Optional.ofNullable(accountId).ifPresent(builder::setAccountId); + Optional.ofNullable(paymentAccountPayload).ifPresent(e -> builder.setPaymentAccountPayload((protobuf.PaymentAccountPayload) e.toProtoMessage())); + Optional.ofNullable(payoutAddressString).ifPresent(builder::setPayoutAddressString); + Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); + Optional.ofNullable(contractSignature).ifPresent(builder::setContractSignature); + Optional.ofNullable(signature).ifPresent(e -> builder.setSignature(ByteString.copyFrom(e))); + Optional.ofNullable(pubKeyRing).ifPresent(e -> builder.setPubKeyRing(e.toProtoMessage())); + Optional.ofNullable(multiSigPubKey).ifPresent(e -> builder.setMultiSigPubKey(ByteString.copyFrom(e))); + Optional.ofNullable(rawTransactionInputs).ifPresent(e -> builder.addAllRawTransactionInputs( + ProtoUtil.collectionToProto(e, protobuf.RawTransactionInput.class))); + Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); + Optional.ofNullable(accountAgeWitnessNonce).ifPresent(e -> builder.setAccountAgeWitnessNonce(ByteString.copyFrom(e))); + Optional.ofNullable(accountAgeWitnessSignature).ifPresent(e -> builder.setAccountAgeWitnessSignature(ByteString.copyFrom(e))); + Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); + builder.setCurrentDate(currentDate); + return builder.build(); + } + + public static TradingPeer fromProto(protobuf.TradingPeer proto, CoreProtoResolver coreProtoResolver) { + if (proto.getDefaultInstanceForType().equals(proto)) { + return null; + } else { + TradingPeer tradingPeer = new TradingPeer(); + tradingPeer.setChangeOutputValue(proto.getChangeOutputValue()); + tradingPeer.setAccountId(ProtoUtil.stringOrNullFromProto(proto.getAccountId())); + tradingPeer.setPaymentAccountPayload(proto.hasPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getPaymentAccountPayload()) : null); + tradingPeer.setPayoutAddressString(ProtoUtil.stringOrNullFromProto(proto.getPayoutAddressString())); + tradingPeer.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); + tradingPeer.setContractSignature(ProtoUtil.stringOrNullFromProto(proto.getContractSignature())); + tradingPeer.setSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSignature())); + tradingPeer.setPubKeyRing(proto.hasPubKeyRing() ? PubKeyRing.fromProto(proto.getPubKeyRing()) : null); + tradingPeer.setMultiSigPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getMultiSigPubKey())); + List rawTransactionInputs = proto.getRawTransactionInputsList().isEmpty() ? + null : + proto.getRawTransactionInputsList().stream() + .map(RawTransactionInput::fromProto) + .collect(Collectors.toList()); + tradingPeer.setRawTransactionInputs(rawTransactionInputs); + tradingPeer.setChangeOutputAddress(ProtoUtil.stringOrNullFromProto(proto.getChangeOutputAddress())); + tradingPeer.setAccountAgeWitnessNonce(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessNonce())); + tradingPeer.setAccountAgeWitnessSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignature())); + tradingPeer.setCurrentDate(proto.getCurrentDate()); + tradingPeer.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); + return tradingPeer; + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java new file mode 100644 index 0000000000..f3a2d34159 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java @@ -0,0 +1,76 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.filter.FilterManager; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ApplyFilter extends TradeTask { + public ApplyFilter(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + NodeAddress nodeAddress = checkNotNull(processModel.getTempTradingPeerNodeAddress()); + @Nullable + PaymentAccountPayload paymentAccountPayload = processModel.getTradingPeer().getPaymentAccountPayload(); + + FilterManager filterManager = processModel.getFilterManager(); + if (filterManager.isNodeAddressBanned(nodeAddress)) { + failed("Other trader is banned by their node address.\n" + + "tradingPeerNodeAddress=" + nodeAddress); + } else if (filterManager.isOfferIdBanned(trade.getId())) { + failed("Offer ID is banned.\n" + + "Offer ID=" + trade.getId()); + } else if (trade.getOffer() != null && filterManager.isCurrencyBanned(trade.getOffer().getCurrencyCode())) { + failed("Currency is banned.\n" + + "Currency code=" + trade.getOffer().getCurrencyCode()); + } else if (filterManager.isPaymentMethodBanned(checkNotNull(trade.getOffer()).getPaymentMethod())) { + failed("Payment method is banned.\n" + + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); + } else if (paymentAccountPayload != null && filterManager.arePeersPaymentAccountDataBanned(paymentAccountPayload)) { + failed("Other trader is banned by their trading account data.\n" + + "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails()); + } else if (filterManager.requireUpdateToNewVersionForTrading()) { + failed("Your version of Bisq is not compatible for trading anymore. " + + "Please update to the latest Bisq version at https://bisq.network/downloads."); + } else { + complete(); + } + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java new file mode 100644 index 0000000000..df3b8df2c6 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.trade.Trade; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class BroadcastPayoutTx extends TradeTask { + public BroadcastPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected abstract void setState(); + + @Override + protected void run() { + try { + runInterceptHook(); + Transaction payoutTx = trade.getPayoutTx(); + checkNotNull(payoutTx, "payoutTx must not be null"); + + TransactionConfidence.ConfidenceType confidenceType = payoutTx.getConfidence().getConfidenceType(); + log.debug("payoutTx confidenceType:" + confidenceType); + if (confidenceType.equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidenceType.equals(TransactionConfidence.ConfidenceType.PENDING)) { + log.debug("payoutTx was already published. confidenceType:" + confidenceType); + setState(); + complete(); + } else { + processModel.getTradeWalletService().broadcastTx(payoutTx, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + if (!completed) { + log.debug("BroadcastTx succeeded. Transaction:" + transaction); + setState(); + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + log.error("BroadcastTx failed. Error:" + exception.getMessage()); + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..aab0bb5a3b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessPeerPublishedDelayedPayoutTxMessage extends TradeTask { + public ProcessPeerPublishedDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + PeerPublishedDelayedPayoutTxMessage message = (PeerPublishedDelayedPayoutTxMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + // We add the tx to our wallet. + Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); + WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, processModel.getBtcWalletService().getWallet()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java new file mode 100644 index 0000000000..b4eb80cf3d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java @@ -0,0 +1,101 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class SendMailboxMessageTask extends TradeTask { + public SendMailboxMessageTask(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected abstract TradeMailboxMessage getTradeMailboxMessage(String id); + + protected abstract void setStateSent(); + + protected abstract void setStateArrived(); + + protected abstract void setStateStoredInMailbox(); + + protected abstract void setStateFault(); + + @Override + protected void run() { + try { + runInterceptHook(); + String id = processModel.getOfferId(); + TradeMailboxMessage message = getTradeMailboxMessage(id); + setStateSent(); + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + setStateArrived(); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + SendMailboxMessageTask.this.onStoredInMailbox(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + SendMailboxMessageTask.this.onFault(errorMessage, message); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } + + protected void onStoredInMailbox() { + setStateStoredInMailbox(); + complete(); + } + + protected void onFault(String errorMessage, TradeMessage message) { + setStateFault(); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(errorMessage); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java new file mode 100644 index 0000000000..5d925ae41c --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.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 bisq.core.trade.protocol.tasks; + +import bisq.core.btc.listeners.AddressConfidenceListener; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; + +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class SetupPayoutTxListener extends TradeTask { + // Use instance fields to not get eaten up by the GC + private Subscription tradeStateSubscription; + private AddressConfidenceListener confidenceListener; + + public SetupPayoutTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + + protected abstract void setState(); + + @Override + protected void run() { + try { + runInterceptHook(); + if (!trade.isPayoutPublished()) { + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + Address address = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).getAddress(); + + TransactionConfidence confidence = walletService.getConfidenceForAddress(address); + if (isInNetwork(confidence)) { + applyConfidence(confidence); + } else { + confidenceListener = new AddressConfidenceListener(address) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + if (isInNetwork(confidence)) + applyConfidence(confidence); + } + }; + walletService.addAddressConfidenceListener(confidenceListener); + + tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { + if (trade.isPayoutPublished()) { + processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); + + // hack to remove tradeStateSubscription at callback + UserThread.execute(this::unSubscribe); + } + }); + } + } + + // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService + complete(); + } catch (Throwable t) { + failed(t); + } + } + + private void applyConfidence(TransactionConfidence confidence) { + if (trade.getPayoutTx() == null) { + Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); + trade.setPayoutTx(walletTx); + processModel.getTradeManager().requestPersistence(); + BtcWalletService.printTx("payoutTx received from network", walletTx); + setState(); + } else { + log.info("We had the payout tx already set. tradeId={}, state={}", trade.getId(), trade.getState()); + } + + processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); + + // need delay as it can be called inside the handler before the listener and tradeStateSubscription are actually set. + UserThread.execute(this::unSubscribe); + } + + private boolean isInNetwork(TransactionConfidence confidence) { + return confidence != null && + (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)); + } + + private void unSubscribe() { + if (tradeStateSubscription != null) + tradeStateSubscription.unsubscribe(); + + if (confidenceListener != null) + processModel.getBtcWalletService().removeAddressConfidenceListener(confidenceListener); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/TradeTask.java b/core/src/main/java/bisq/core/trade/protocol/tasks/TradeTask.java new file mode 100644 index 0000000000..332b571f74 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/TradeTask.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.ProcessModel; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class TradeTask extends Task { + protected final ProcessModel processModel; + protected final Trade trade; + + protected TradeTask(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + + this.trade = trade; + processModel = trade.getProcessModel(); + } + + @Override + protected void complete() { + processModel.getTradeManager().requestPersistence(); + + super.complete(); + } + + @Override + protected void failed() { + trade.setErrorMessage(errorMessage); + processModel.getTradeManager().requestPersistence(); + + super.failed(); + } + + @Override + protected void failed(String message) { + appendToErrorMessage(message); + trade.setErrorMessage(errorMessage); + processModel.getTradeManager().requestPersistence(); + + super.failed(); + } + + @Override + protected void failed(Throwable t) { + t.printStackTrace(); + appendExceptionToErrorMessage(t); + trade.setErrorMessage(errorMessage); + processModel.getTradeManager().requestPersistence(); + + super.failed(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java b/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java new file mode 100644 index 0000000000..68ea2d9eeb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.trade.protocol.tasks; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import java.util.Date; +import java.util.concurrent.atomic.AtomicReference; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class VerifyPeersAccountAgeWitness extends TradeTask { + + public VerifyPeersAccountAgeWitness(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Offer offer = checkNotNull(trade.getOffer()); + if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) { + complete(); + return; + } + + AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + PaymentAccountPayload peersPaymentAccountPayload = checkNotNull(tradingPeer.getPaymentAccountPayload(), + "Peers peersPaymentAccountPayload must not be null"); + PubKeyRing peersPubKeyRing = checkNotNull(tradingPeer.getPubKeyRing(), "peersPubKeyRing must not be null"); + byte[] nonce = checkNotNull(tradingPeer.getAccountAgeWitnessNonce()); + byte[] signature = checkNotNull(tradingPeer.getAccountAgeWitnessSignature()); + AtomicReference errorMsg = new AtomicReference<>(); + long currentDateAsLong = tradingPeer.getCurrentDate(); + // In case the peer has an older version we get 0, so we use our time instead + Date peersCurrentDate = currentDateAsLong > 0 ? new Date(currentDateAsLong) : new Date(); + boolean isValid = accountAgeWitnessService.verifyAccountAgeWitness(trade, + peersPaymentAccountPayload, + peersCurrentDate, + peersPubKeyRing, + nonce, + signature, + errorMsg::set); + if (isValid) { + complete(); + } else { + failed(errorMsg.get()); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/PublishedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/PublishedDelayedPayoutTx.java new file mode 100644 index 0000000000..b35e1f53a6 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/PublishedDelayedPayoutTx.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.core.trade.protocol.tasks.arbitration; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PublishedDelayedPayoutTx extends TradeTask { + public PublishedDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + + // We have spent the funds from the deposit tx with the delayedPayoutTx + btcWalletService.resetCoinLockedInMultiSigAddressEntry(trade.getId()); + // We might receive funds on AddressEntry.Context.TRADE_PAYOUT so we don't swap that + + Transaction committedDelayedPayoutTx = WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, btcWalletService.getWallet()); + + processModel.getTradeWalletService().broadcastTx(committedDelayedPayoutTx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("publishDelayedPayoutTx onSuccess " + transaction); + complete(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error("publishDelayedPayoutTx onFailure", exception); + failed(exception.toString()); + } + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/SendPeerPublishedDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/SendPeerPublishedDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..cdc995c612 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/arbitration/SendPeerPublishedDelayedPayoutTxMessage.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.trade.protocol.tasks.arbitration; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SendPeerPublishedDelayedPayoutTxMessage extends SendMailboxMessageTask { + + public SendPeerPublishedDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String id) { + return new PeerPublishedDelayedPayoutTxMessage(UUID.randomUUID().toString(), + trade.getId(), + trade.getTradingPeerNodeAddress()); + } + + @Override + protected void setStateSent() { + } + + @Override + protected void setStateArrived() { + } + + @Override + protected void setStateStoredInMailbox() { + } + + @Override + protected void setStateFault() { + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerFinalizesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerFinalizesDelayedPayoutTx.java new file mode 100644 index 0000000000..30824a14d7 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerFinalizesDelayedPayoutTx.java @@ -0,0 +1,61 @@ +package bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerFinalizesDelayedPayoutTx extends TradeTask { + public BuyerFinalizesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + Transaction preparedDepositTx = btcWalletService.getTxFromSerializedTx(processModel.getPreparedDepositTx()); + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + + byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(buyerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + + byte[] buyerSignature = processModel.getDelayedPayoutTxSignature(); + byte[] sellerSignature = processModel.getTradingPeer().getDelayedPayoutTxSignature(); + + Transaction signedDelayedPayoutTx = processModel.getTradeWalletService().finalizeUnconnectedDelayedPayoutTx( + preparedDelayedPayoutTx, + buyerMultiSigPubKey, + sellerMultiSigPubKey, + buyerSignature, + sellerSignature, + preparedDepositTx.getOutput(0).getValue()); + + trade.applyDelayedPayoutTxBytes(signedDelayedPayoutTx.bitcoinSerialize()); + log.info("DelayedPayoutTxBytes = {}", Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes())); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java new file mode 100644 index 0000000000..19cdb957f7 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerProcessDelayedPayoutTxSignatureRequest extends TradeTask { + public BuyerProcessDelayedPayoutTxSignatureRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + DelayedPayoutTxSignatureRequest request = (DelayedPayoutTxSignatureRequest) processModel.getTradeMessage(); + checkNotNull(request); + Validator.checkTradeId(processModel.getOfferId(), request); + byte[] delayedPayoutTxAsBytes = checkNotNull(request.getDelayedPayoutTx()); + Transaction preparedDelayedPayoutTx = processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxAsBytes); + processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx); + processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(request.getDelayedPayoutTxSellerSignature())); + + // When we receive that message the taker has published the taker fee, so we apply it to the trade. + // The takerFeeTx was sent in the first message. It should be part of DelayedPayoutTxSignatureRequest + // but that cannot be changed due backward compatibility issues. It is a left over from the old trade protocol. + trade.setTakerFeeTxId(processModel.getTakeOfferFeeTxId()); + + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..f57a6a7892 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java @@ -0,0 +1,89 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.wallet.Wallet; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerProcessDepositTxAndDelayedPayoutTxMessage extends TradeTask { + public BuyerProcessDepositTxAndDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + var message = checkNotNull((DepositTxAndDelayedPayoutTxMessage) processModel.getTradeMessage()); + checkNotNull(message); + Validator.checkTradeId(processModel.getOfferId(), message); + + // To access tx confidence we need to add that tx into our wallet. + byte[] depositTxBytes = checkNotNull(message.getDepositTx()); + Transaction depositTx = processModel.getBtcWalletService().getTxFromSerializedTx(depositTxBytes); + // update with full tx + Wallet wallet = processModel.getBtcWalletService().getWallet(); + Transaction committedDepositTx = WalletService.maybeAddSelfTxToWallet(depositTx, wallet); + trade.applyDepositTx(committedDepositTx); + BtcWalletService.printTx("depositTx received from peer", committedDepositTx); + + // To access tx confidence we need to add that tx into our wallet. + byte[] delayedPayoutTxBytes = checkNotNull(message.getDelayedPayoutTx()); + checkArgument(Arrays.equals(delayedPayoutTxBytes, trade.getDelayedPayoutTxBytes()), + "mismatch between delayedPayoutTx received from peer and our one." + + "\n Expected: " + Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()) + + "\n Received: " + Utilities.bytesAsHexString(delayedPayoutTxBytes)); + trade.applyDelayedPayoutTxBytes(delayedPayoutTxBytes); + + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + // If we got already the confirmation we don't want to apply an earlier state + if (trade.getState().ordinal() < Trade.State.BUYER_SAW_DEPOSIT_TX_IN_NETWORK.ordinal()) { + trade.setState(Trade.State.BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG); + } + + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), + AddressEntry.Context.RESERVED_FOR_TRADE); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..5b546201af --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java @@ -0,0 +1,82 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerProcessPayoutTxPublishedMessage extends TradeTask { + public BuyerProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + PayoutTxPublishedMessage message = (PayoutTxPublishedMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + checkArgument(message.getPayoutTx() != null); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + if (trade.getPayoutTx() == null) { + Transaction committedPayoutTx = WalletService.maybeAddNetworkTxToWallet(message.getPayoutTx(), processModel.getBtcWalletService().getWallet()); + trade.setPayoutTx(committedPayoutTx); + BtcWalletService.printTx("payoutTx received from peer", committedPayoutTx); + + trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG); + processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); + } else { + log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); + } + + SignedWitness signedWitness = message.getSignedWitness(); + if (signedWitness != null) { + // 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); + } + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java new file mode 100644 index 0000000000..b594d646ce --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java @@ -0,0 +1,198 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.network.MessageState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import javafx.beans.value.ChangeListener; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +/** + * We send the seller the BuyerSendCounterCurrencyTransferStartedMessage. + * We wait to receive a ACK message back and resend the message + * in case that does not happen in 10 minutes or if the message was stored in mailbox or failed. We keep repeating that + * with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached. + * If never successful we give up and complete. It might be a valid case that the peer was not online for an extended + * time but we can be very sure that our message was stored as mailbox message in the network and one the peer goes + * online he will process it. + */ +@Slf4j +public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxMessageTask { + private static final int MAX_RESEND_ATTEMPTS = 10; + private int delayInMin = 15; + private int resendCounter = 0; + private CounterCurrencyTransferStartedMessage message; + private ChangeListener listener; + private Timer timer; + + public BuyerSendCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { + if (message == null) { + AddressEntry payoutAddressEntry = processModel.getBtcWalletService().getOrCreateAddressEntry(tradeId, + AddressEntry.Context.TRADE_PAYOUT); + + // 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 + // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox + // 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 = tradeId + processModel.getMyNodeAddress().getFullAddress(); + message = new CounterCurrencyTransferStartedMessage( + tradeId, + payoutAddressEntry.getAddressString(), + processModel.getMyNodeAddress(), + processModel.getPayoutTxSignature(), + trade.getCounterCurrencyTxId(), + trade.getCounterCurrencyExtraData(), + deterministicId + ); + } + return message; + } + + @Override + protected void setStateSent() { + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG.ordinal()) { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG); + } + + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + // the message has arrived but we're ultimately waiting for an AckMessage response + if (!trade.isPayoutPublished()) { + tryToSendAgainLater(); + } + } + + // We override the default behaviour for onStoredInMailbox and do not call complete + @Override + protected void onStoredInMailbox() { + setStateStoredInMailbox(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG); + if (!trade.isPayoutPublished()) { + tryToSendAgainLater(); + } + processModel.getTradeManager().requestPersistence(); + } + + // We override the default behaviour for onFault and do not call appendToErrorMessage and failed + @Override + protected void onFault(String errorMessage, TradeMessage message) { + setStateFault(); + } + + @Override + protected void setStateFault() { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG); + if (!trade.isPayoutPublished()) { + tryToSendAgainLater(); + } + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } finally { + cleanup(); + } + } + + // complete() is called from base class SendMailboxMessageTask=>onArrived() + // We override the default behaviour for complete and keep this task open until receipt of the AckMessage + @Override + protected void complete() { + onMessageStateChange(processModel.getPaymentStartedMessageStateProperty().get()); // check for AckMessage + } + + private void cleanup() { + if (timer != null) { + timer.stop(); + } + if (listener != null) { + processModel.getPaymentStartedMessageStateProperty().removeListener(listener); + } + } + + private void tryToSendAgainLater() { + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + log.warn("We never received an ACK message when sending the CounterCurrencyTransferStartedMessage to the peer. " + + "We stop now and complete the protocol task."); + complete(); + return; + } + + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + if (timer != null) { + timer.stop(); + } + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + + if (resendCounter == 0) { + // We want to register listener only once + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + processModel.getPaymentStartedMessageStateProperty().addListener(listener); + onMessageStateChange(processModel.getPaymentStartedMessageStateProperty().get()); + } + + delayInMin = delayInMin * 2; + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + // Once we receive an ACK from our msg we know the peer has received the msg and we stop. + if (newValue == MessageState.ACKNOWLEDGED) { + // We treat a ACK like BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG); + + processModel.getTradeManager().requestPersistence(); + + cleanup(); + super.complete(); // received AckMessage, complete this task + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java new file mode 100644 index 0000000000..5b9702798b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerSendsDelayedPayoutTxSignatureResponse extends TradeTask { + public BuyerSendsDelayedPayoutTxSignatureResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature()); + byte[] depositTxBytes = processModel.getDepositTx() != null + ? processModel.getDepositTx().bitcoinSerialize() // set in BuyerAsTakerSignsDepositTx task + : processModel.getPreparedDepositTx(); // set in BuyerAsMakerCreatesAndSignsDepositTx task + + DelayedPayoutTxSignatureResponse message = new DelayedPayoutTxSignatureResponse(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + delayedPayoutTxSignature, + depositTxBytes); + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(errorMessage); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java new file mode 100644 index 0000000000..9c6bfa2b74 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java @@ -0,0 +1,197 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.listeners.AddressConfidenceListener; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class BuyerSetupDepositTxListener extends TradeTask { + // Use instance fields to not get eaten up by the GC + private Subscription tradeStateSubscription; + private AddressConfidenceListener confidenceListener; + + public BuyerSetupDepositTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + if (trade.getDepositTx() == null && processModel.getPreparedDepositTx() != null) { + BtcWalletService walletService = processModel.getBtcWalletService(); + NetworkParameters params = walletService.getParams(); + Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx()); + checkArgument(!preparedDepositTx.getOutputs().isEmpty(), "preparedDepositTx.getOutputs() must not be empty"); + Address depositTxAddress = preparedDepositTx.getOutput(0).getScriptPubKey().getToAddress(params); + + // For buyer as maker takerFeeTxId is null + @Nullable String takerFeeTxId = trade.getTakerFeeTxId(); + String makerFeeTxId = trade.getOffer().getOfferFeePaymentTxId(); + TransactionConfidence confidence = walletService.getConfidenceForAddress(depositTxAddress); + if (isConfTxDepositTx(confidence, params, depositTxAddress, takerFeeTxId, makerFeeTxId) && + isVisibleInNetwork(confidence)) { + applyConfidence(confidence); + } else { + confidenceListener = new AddressConfidenceListener(depositTxAddress) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + if (isConfTxDepositTx(confidence, params, depositTxAddress, + takerFeeTxId, makerFeeTxId) && isVisibleInNetwork(confidence)) { + applyConfidence(confidence); + } + } + }; + walletService.addAddressConfidenceListener(confidenceListener); + + tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { + if (trade.isDepositPublished()) { + swapReservedForTradeEntry(); + + // hack to remove tradeStateSubscription at callback + UserThread.execute(this::unSubscribeAndRemoveListener); + } + }); + } + } + + // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService + complete(); + } catch (Throwable t) { + failed(t); + } + } + + // We check if the txIds of the inputs matches our maker fee tx and taker fee tx and if the depositTxAddress we + // use for the confidence lookup is use as an output address. + // This prevents that past txs which have the our depositTxAddress as input or output (deposit or payout txs) could + // be interpreted as our deposit tx. This happened because if a bug which caused re-use of the Multisig address + // entries and if both traders use the same key for multiple trades the depositTxAddress would be the same. + // We fix that bug as well but we also need to avoid that past already used addresses might be taken again + // (the Multisig flag got reverted to available in the address entry). + private boolean isConfTxDepositTx(@Nullable TransactionConfidence confidence, + NetworkParameters params, + Address depositTxAddress, + @Nullable String takerFeeTxId, + String makerFeeTxId) { + if (confidence == null) { + return false; + } + + Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); + long numInputMatches = walletTx.getInputs().stream() + .map(TransactionInput::getOutpoint) + .filter(Objects::nonNull) + .map(TransactionOutPoint::getHash) + .map(Sha256Hash::toString) + .filter(txId -> txId.equals(takerFeeTxId) || txId.equals(makerFeeTxId)) + .count(); + if (takerFeeTxId == null && numInputMatches != 1) { + log.warn("We got a transactionConfidenceTx which does not match our inputs. " + + "takerFeeTxId is null (valid if role is buyer as maker) and numInputMatches " + + "is not 1 as expected (for makerFeeTxId). " + + "numInputMatches={}, transactionConfidenceTx={}", + numInputMatches, walletTx); + return false; + } else if (takerFeeTxId != null && numInputMatches != 2) { + log.warn("We got a transactionConfidenceTx which does not match our inputs. " + + "numInputMatches is not 2 as expected (for makerFeeTxId and takerFeeTxId). " + + "numInputMatches={}, transactionConfidenceTx={}", + numInputMatches, walletTx); + return false; + } + + boolean isOutputMatching = walletTx.getOutputs().stream() + .map(transactionOutput -> transactionOutput.getScriptPubKey().getToAddress(params)) + .anyMatch(address -> address.equals(depositTxAddress)); + if (!isOutputMatching) { + log.warn("We got a transactionConfidenceTx which does not has the depositTxAddress " + + "as output (but as input). depositTxAddress={}, transactionConfidenceTx={}", + depositTxAddress, walletTx); + } + return isOutputMatching; + } + + private void applyConfidence(TransactionConfidence confidence) { + if (trade.getDepositTx() == null) { + Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); + trade.applyDepositTx(walletTx); + BtcWalletService.printTx("depositTx received from network", walletTx); + + // We don't want to trigger the tradeStateSubscription when setting the state, so we unsubscribe before + unSubscribeAndRemoveListener(); + trade.setState(Trade.State.BUYER_SAW_DEPOSIT_TX_IN_NETWORK); + + processModel.getTradeManager().requestPersistence(); + } else { + unSubscribeAndRemoveListener(); + } + + swapReservedForTradeEntry(); + } + + private boolean isVisibleInNetwork(TransactionConfidence confidence) { + return confidence != null && + (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)); + } + + private void swapReservedForTradeEntry() { + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), + AddressEntry.Context.RESERVED_FOR_TRADE); + } + + private void unSubscribeAndRemoveListener() { + if (tradeStateSubscription != null) { + tradeStateSubscription.unsubscribe(); + tradeStateSubscription = null; + } + + if (confidenceListener != null) { + processModel.getBtcWalletService().removeAddressConfidenceListener(confidenceListener); + confidenceListener = null; + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java new file mode 100644 index 0000000000..c58ce41ef1 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java @@ -0,0 +1,51 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerSetupPayoutTxListener extends SetupPayoutTxListener { + public BuyerSetupPayoutTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + + } catch (Throwable t) { + failed(t); + } + } + + @Override + protected void setState() { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_PAYOUT_TX_IN_NETWORK); + + processModel.getTradeManager().requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java new file mode 100644 index 0000000000..70dd1b8074 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java @@ -0,0 +1,92 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.crypto.DeterministicKey; + +import com.google.common.base.Preconditions; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerSignPayoutTx extends TradeTask { + + public BuyerSignPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Preconditions.checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + Coin buyerPayoutAmount = offer.getBuyerSecurityDeposit().add(trade.getTradeAmount()); + Coin sellerPayoutAmount = offer.getSellerSecurityDeposit(); + + String buyerPayoutAddressString = walletService.getOrCreateAddressEntry(id, + AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + final String sellerPayoutAddressString = processModel.getTradingPeer().getPayoutAddressString(); + + DeterministicKey buyerMultiSigKeyPair = walletService.getMultiSigKeyPair(id, processModel.getMyMultiSigPubKey()); + + byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(buyerMultiSigPubKey, + walletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + + byte[] payoutTxSignature = processModel.getTradeWalletService().buyerSignsPayoutTx( + trade.getDepositTx(), + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + buyerMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + processModel.setPayoutTxSignature(payoutTxSignature); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java new file mode 100644 index 0000000000..cab7fc896b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerSignsDelayedPayoutTx extends TradeTask { + public BuyerSignsDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + NetworkParameters params = btcWalletService.getParams(); + Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx()); + + String id = processModel.getOffer().getId(); + + byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + DeterministicKey myMultiSigKeyPair = btcWalletService.getMultiSigKeyPair(id, buyerMultiSigPubKey); + + checkArgument(Arrays.equals(buyerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx( + preparedDelayedPayoutTx, + preparedDepositTx, + myMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java new file mode 100644 index 0000000000..b2a5eaf1b3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { + public BuyerVerifiesFinalDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null"); + // Check again tx + TradeDataValidation.validateDelayedPayoutTx(trade, + delayedPayoutTx, + processModel.getDaoFacade(), + processModel.getBtcWalletService()); + + // Now as we know the deposit tx we can also verify the input + Transaction depositTx = trade.getDepositTx(); + checkNotNull(depositTx, "trade.getDepositTx() must not be null"); + TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx); + + complete(); + } catch (TradeDataValidation.ValidationException e) { + failed(e.getMessage()); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java new file mode 100644 index 0000000000..5f259b5574 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { + public BuyerVerifiesPreparedDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx(); + TradeDataValidation.validateDelayedPayoutTx(trade, + preparedDelayedPayoutTx, + processModel.getDaoFacade(), + processModel.getBtcWalletService()); + + // If the deposit tx is non-malleable, we already know its final ID, so should check that now + // before sending any further data to the seller, to provide extra protection for the buyer. + if (isDepositTxNonMalleable()) { + var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx( + processModel.getPreparedDepositTx()); + TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx)); + } else { + log.info("Deposit tx is malleable, so we skip preparedDelayedPayoutTx input validation."); + } + + complete(); + } catch (TradeDataValidation.ValidationException e) { + failed(e.getMessage()); + } catch (Throwable t) { + failed(t); + } + } + + private boolean isDepositTxNonMalleable() { + var buyerInputs = checkNotNull(processModel.getRawTransactionInputs()); + var sellerInputs = checkNotNull(processModel.getTradingPeer().getRawTransactionInputs()); + + return buyerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH) && + sellerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java new file mode 100644 index 0000000000..5932f735d0 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java @@ -0,0 +1,109 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer_as_maker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.PreparedDepositTxAndMakerInputs; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask { + public BuyerAsMakerCreatesAndSignsDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + Offer offer = checkNotNull(trade.getOffer()); + + Coin makerInputAmount = offer.getBuyerSecurityDeposit(); + Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); + checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); + AddressEntry makerMultiSigAddressEntry = addressEntryOptional.get(); + processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(makerMultiSigAddressEntry, makerInputAmount.value); + walletService.saveAddressEntryList(); + + Coin msOutputAmount = makerInputAmount + .add(trade.getTxFee()) + .add(offer.getSellerSecurityDeposit()) + .add(tradeAmount); + + List takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH), + "all takerRawTransactionInputs must be P2WH"); + long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); + @Nullable String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); + Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); + byte[] buyerPubKey = processModel.getMyMultiSigPubKey(); + byte[] sellerPubKey = checkNotNull(tradingPeer.getMultiSigPubKey()); + checkArgument(Arrays.equals(buyerPubKey, + makerMultiSigAddressEntry.getPubKey()), + "buyerPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().buyerAsMakerCreatesAndSignsDepositTx( + trade.getContractHash(), + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + + processModel.setPreparedDepositTx(result.depositTransaction); + processModel.setRawTransactionInputs(result.rawMakerInputs); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java new file mode 100644 index 0000000000..85de45cf24 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInputsForDepositTxResponse { + public BuyerAsMakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected byte[] getPreparedDepositTx() { + Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx()); + // Remove witnesses from preparedDepositTx, so that the seller can still compute the final + // tx id, but cannot publish it before providing the buyer with a signed delayed payout tx. + return preparedDepositTx.bitcoinSerialize(false); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java new file mode 100644 index 0000000000..0277ca7b76 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java @@ -0,0 +1,63 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer_as_taker; + +import bisq.core.btc.model.InputsAndChangeOutput; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerAsTakerCreatesDepositTxInputs extends TradeTask { + + public BuyerAsTakerCreatesDepositTxInputs(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Coin txFee = trade.getTxFee(); + Coin takerInputAmount = checkNotNull(trade.getOffer()).getBuyerSecurityDeposit() + .add(txFee) + .add(txFee); // 2 times the fee as we need it for payout tx as well + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositTxInputs( + processModel.getTakeOfferFeeTx(), + takerInputAmount, + txFee); + processModel.setRawTransactionInputs(result.rawTransactionInputs); + processModel.setChangeOutputValue(result.changeOutputValue); + processModel.setChangeOutputAddress(result.changeOutputAddress); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java new file mode 100644 index 0000000000..ab03ece48a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer_as_taker; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerAsTakerSendsDepositTxMessage extends TradeTask { + public BuyerAsTakerSendsDepositTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + if (processModel.getDepositTx() != null) { + // Remove witnesses from the sent depositTx, so that the seller can still compute the final + // tx id, but cannot publish it before providing the buyer with a signed delayed payout tx. + DepositTxMessage message = new DepositTxMessage(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + processModel.getDepositTx().bitcoinSerialize(false)); + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(); + } + } + ); + } else { + log.error("processModel.getDepositTx() = {}", processModel.getDepositTx()); + failed("DepositTx is null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java new file mode 100644 index 0000000000..63883ae4c9 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.trade.protocol.tasks.buyer_as_taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.crypto.Hash; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerAsTakerSignsDepositTx extends TradeTask { + + public BuyerAsTakerSignsDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + /* log.debug("\n\n------------------------------------------------------------\n" + + "Contract as json\n" + + trade.getContractAsJson() + + "\n------------------------------------------------------------\n");*/ + + + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); + trade.setContractHash(contractHash); + List buyerInputs = checkNotNull(processModel.getRawTransactionInputs(), "buyerInputs must not be null"); + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); + checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); + AddressEntry buyerMultiSigAddressEntry = addressEntryOptional.get(); + Coin buyerInput = Coin.valueOf(buyerInputs.stream().mapToLong(input -> input.value).sum()); + + Coin multiSigValue = buyerInput.subtract(trade.getTxFee().multiply(2)); + processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(buyerMultiSigAddressEntry, multiSigValue.value); + walletService.saveAddressEntryList(); + + Offer offer = trade.getOffer(); + Coin msOutputAmount = offer.getBuyerSecurityDeposit().add(offer.getSellerSecurityDeposit()).add(trade.getTxFee()) + .add(checkNotNull(trade.getTradeAmount())); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(buyerMultiSigPubKey, buyerMultiSigAddressEntry.getPubKey()), + "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + List sellerInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + byte[] sellerMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx( + false, + contractHash, + processModel.getPreparedDepositTx(), + msOutputAmount, + buyerInputs, + sellerInputs, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + processModel.setDepositTx(depositTx); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java new file mode 100644 index 0000000000..4c9d05c4c3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java @@ -0,0 +1,105 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.BuyerAsMakerTrade; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.Sig; +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MakerCreateAndSignContract extends TradeTask { + public MakerCreateAndSignContract(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + String takerFeeTxId = checkNotNull(processModel.getTakeOfferFeeTxId()); + + TradingPeer taker = processModel.getTradingPeer(); + boolean isBuyerMakerAndSellerTaker = trade instanceof BuyerAsMakerTrade; + NodeAddress buyerNodeAddress = isBuyerMakerAndSellerTaker ? + processModel.getMyNodeAddress() : processModel.getTempTradingPeerNodeAddress(); + NodeAddress sellerNodeAddress = isBuyerMakerAndSellerTaker ? + processModel.getTempTradingPeerNodeAddress() : processModel.getMyNodeAddress(); + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + AddressEntry makerAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG); + byte[] makerMultiSigPubKey = makerAddressEntry.getPubKey(); + + AddressEntry takerAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); + Contract contract = new Contract( + processModel.getOffer().getOfferPayload(), + checkNotNull(trade.getTradeAmount()).value, + trade.getTradePrice().getValue(), + takerFeeTxId, + buyerNodeAddress, + sellerNodeAddress, + trade.getMediatorNodeAddress(), + isBuyerMakerAndSellerTaker, + processModel.getAccountId(), + checkNotNull(taker.getAccountId()), + checkNotNull(processModel.getPaymentAccountPayload(trade)), + checkNotNull(taker.getPaymentAccountPayload()), + processModel.getPubKeyRing(), + checkNotNull(taker.getPubKeyRing()), + takerAddressEntry.getAddressString(), + checkNotNull(taker.getPayoutAddressString()), + makerMultiSigPubKey, + checkNotNull(taker.getMultiSigPubKey()), + trade.getLockTime(), + trade.getRefundAgentNodeAddress() + ); + String contractAsJson = Utilities.objectToJson(contract); + String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); + + trade.setContract(contract); + trade.setContractAsJson(contractAsJson); + trade.setMakerContractSignature(signature); + + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); + trade.setContractHash(contractHash); + + processModel.setMyMultiSigPubKey(makerMultiSigPubKey); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java new file mode 100644 index 0000000000..d048ba2ef4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java @@ -0,0 +1,117 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.exceptions.TradePriceOutOfToleranceException; +import bisq.core.offer.Offer; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import com.google.common.base.Charsets; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.util.Validator.checkTradeId; +import static bisq.core.util.Validator.nonEmptyStringOf; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MakerProcessesInputsForDepositTxRequest extends TradeTask { + public MakerProcessesInputsForDepositTxRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + InputsForDepositTxRequest request = (InputsForDepositTxRequest) processModel.getTradeMessage(); + checkNotNull(request); + checkTradeId(processModel.getOfferId(), request); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + tradingPeer.setPaymentAccountPayload(checkNotNull(request.getTakerPaymentAccountPayload())); + tradingPeer.setRawTransactionInputs(checkNotNull(request.getRawTransactionInputs())); + checkArgument(request.getRawTransactionInputs().size() > 0); + + tradingPeer.setChangeOutputValue(request.getChangeOutputValue()); + tradingPeer.setChangeOutputAddress(request.getChangeOutputAddress()); + + tradingPeer.setMultiSigPubKey(checkNotNull(request.getTakerMultiSigPubKey())); + tradingPeer.setPayoutAddressString(nonEmptyStringOf(request.getTakerPayoutAddressString())); + tradingPeer.setPubKeyRing(checkNotNull(request.getTakerPubKeyRing())); + + tradingPeer.setAccountId(nonEmptyStringOf(request.getTakerAccountId())); + + // We set the taker fee only in the processModel yet not in the trade as the tx was only created but not + // published yet. Once it was published we move it to trade. The takerFeeTx should be sent in a later + // message but that cannot be changed due backward compatibility issues. It is a left over from the + // old trade protocol. + processModel.setTakeOfferFeeTxId(nonEmptyStringOf(request.getTakerFeeTxId())); + + // Taker has to sign offerId (he cannot manipulate that - so we avoid to have a challenge protocol for + // passing the nonce we want to get signed) + tradingPeer.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); + tradingPeer.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId()); + tradingPeer.setCurrentDate(request.getCurrentDate()); + + User user = checkNotNull(processModel.getUser(), "User must not be null"); + + NodeAddress mediatorNodeAddress = checkNotNull(request.getMediatorNodeAddress(), + "InputsForDepositTxRequest.getMediatorNodeAddress() must not be null"); + trade.setMediatorNodeAddress(mediatorNodeAddress); + Mediator mediator = checkNotNull(user.getAcceptedMediatorByAddress(mediatorNodeAddress), + "user.getAcceptedMediatorByAddress(mediatorNodeAddress) must not be null"); + trade.setMediatorPubKeyRing(checkNotNull(mediator.getPubKeyRing(), + "mediator.getPubKeyRing() must not be null")); + + Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); + try { + long takersTradePrice = request.getTradePrice(); + offer.checkTradePriceTolerance(takersTradePrice); + trade.setTradePrice(takersTradePrice); + } catch (TradePriceOutOfToleranceException e) { + failed(e.getMessage()); + } catch (Throwable e2) { + failed(e2); + } + + checkArgument(request.getTradeAmount() > 0); + trade.setTradeAmount(Coin.valueOf(request.getTradeAmount())); + + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerRemovesOpenOffer.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerRemovesOpenOffer.java new file mode 100644 index 0000000000..6fe8c58769 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerRemovesOpenOffer.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MakerRemovesOpenOffer extends TradeTask { + public MakerRemovesOpenOffer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(trade.getOffer())); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java new file mode 100644 index 0000000000..34380f5bcf --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java @@ -0,0 +1,125 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.crypto.Sig; +import bisq.common.taskrunner.TaskRunner; + +import java.security.PrivateKey; + +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class MakerSendsInputsForDepositTxResponse extends TradeTask { + public MakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected abstract byte[] getPreparedDepositTx(); + + @Override + protected void run() { + try { + runInterceptHook(); + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + Optional optionalMultiSigAddressEntry = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); + checkArgument(optionalMultiSigAddressEntry.isPresent(), "addressEntry must be set here."); + AddressEntry makerPayoutAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); + byte[] makerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(makerMultiSigPubKey, + optionalMultiSigAddressEntry.get().getPubKey()), + "makerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + byte[] preparedDepositTx = getPreparedDepositTx(); + + // Maker has to use preparedDepositTx as nonce. + // He cannot manipulate the preparedDepositTx - so we avoid to have a challenge protocol for passing the + // nonce we want to get signed. + // This is used for verifying the peers account age witness + PrivateKey privateKey = processModel.getKeyRing().getSignatureKeyPair().getPrivate(); + byte[] signatureOfNonce = Sig.sign(privateKey, preparedDepositTx); + InputsForDepositTxResponse message = new InputsForDepositTxResponse( + processModel.getOfferId(), + checkNotNull(processModel.getPaymentAccountPayload(trade)), + processModel.getAccountId(), + makerMultiSigPubKey, + trade.getContractAsJson(), + trade.getMakerContractSignature(), + makerPayoutAddressEntry.getAddressString(), + preparedDepositTx, + processModel.getRawTransactionInputs(), + processModel.getMyNodeAddress(), + UUID.randomUUID().toString(), + signatureOfNonce, + new Date().getTime(), + trade.getLockTime()); + + trade.setState(Trade.State.MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST); + processModel.getTradeManager().requestPersistence(); + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + trade.setState(Trade.State.MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST); + processModel.getTradeManager().requestPersistence(); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + trade.setState(Trade.State.MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + processModel.getTradeManager().requestPersistence(); + failed(errorMessage); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java new file mode 100644 index 0000000000..fde4359bec --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.btc.wallet.Restrictions; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.config.Config; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MakerSetsLockTime extends TradeTask { + public MakerSetsLockTime(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // 10 days for altcoins, 20 days for other payment methods + // For regtest dev environment we use 5 blocks + int delay = Config.baseCurrencyNetwork().isRegtest() ? + 5 : + Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset()); + + long lockTime = processModel.getBtcWalletService().getBestChainHeight() + delay; + log.info("lockTime={}, delay={}", lockTime, delay); + trade.setLockTime(lockTime); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java new file mode 100644 index 0000000000..0c8a5fa67b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.trade.protocol.tasks.maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MakerVerifyTakerFeePayment extends TradeTask { + + public MakerVerifyTakerFeePayment(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + //TODO missing impl. + // int numOfPeersSeenTx = processModel.getWalletService().getNumOfPeersSeenTx(processModel.getTakeOfferFeeTxId()); + /* if (numOfPeersSeenTx > 2) { + resultHandler.handleResult(); + }*/ + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java new file mode 100644 index 0000000000..7ebfa20ce5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.BroadcastPayoutTx; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BroadcastMediatedPayoutTx extends BroadcastPayoutTx { + public BroadcastMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } + } + + @Override + protected void setState() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED); + processModel.getTradeManager().requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java new file mode 100644 index 0000000000..875b78f7af --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java @@ -0,0 +1,121 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class FinalizeMediatedPayoutTx extends TradeTask { + + public FinalizeMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction depositTx = checkNotNull(trade.getDepositTx()); + String tradeId = trade.getId(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + BtcWalletService walletService = processModel.getBtcWalletService(); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + + + byte[] mySignature = checkNotNull(processModel.getMediatedPayoutTxSignature(), + "processModel.getTxSignatureFromMediation must not be null"); + byte[] peersSignature = checkNotNull(tradingPeer.getMediatedPayoutTxSignature(), + "tradingPeer.getTxSignatureFromMediation must not be null"); + + boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); + byte[] buyerSignature = isMyRoleBuyer ? mySignature : peersSignature; + byte[] sellerSignature = isMyRoleBuyer ? peersSignature : mySignature; + + Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); + Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); + Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); + checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), + "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + + "; sellerPayoutAmount=" + sellerPayoutAmount); + + String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String peersPayoutAddressString = tradingPeer.getPayoutAddressString(); + String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; + String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; + + byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); + byte[] peersMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; + byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; + + DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); + + checkArgument(Arrays.equals(myMultiSigPubKey, + walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), + "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); + + Transaction transaction = processModel.getTradeWalletService().finalizeMediatedPayoutTx( + depositTx, + buyerSignature, + sellerSignature, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + multiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey + ); + + trade.setPayoutTx(transaction); + + processModel.getTradeManager().requestPersistence(); + + walletService.resetCoinLockedInMultiSigAddressEntry(tradeId); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java new file mode 100644 index 0000000000..ba0aad7204 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessMediatedPayoutSignatureMessage extends TradeTask { + public ProcessMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + MediatedPayoutTxSignatureMessage message = (MediatedPayoutTxSignatureMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + processModel.getTradingPeer().setMediatedPayoutTxSignature(checkNotNull(message.getTxSignature())); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + trade.setMediationResultState(MediationResultState.RECEIVED_SIG_MSG); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..cb06ed645e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessMediatedPayoutTxPublishedMessage extends TradeTask { + public ProcessMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + MediatedPayoutTxPublishedMessage message = (MediatedPayoutTxPublishedMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + checkArgument(message.getPayoutTx() != null); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + if (trade.getPayoutTx() == null) { + Transaction committedMediatedPayoutTx = WalletService.maybeAddNetworkTxToWallet(message.getPayoutTx(), processModel.getBtcWalletService().getWallet()); + trade.setPayoutTx(committedMediatedPayoutTx); + log.info("MediatedPayoutTx received from peer. Txid: {}\nhex: {}", + committedMediatedPayoutTx.getTxId().toString(), Utils.HEX.encode(committedMediatedPayoutTx.bitcoinSerialize())); + + trade.setMediationResultState(MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG); + + if (trade.getPayoutTx() != null) { + // We need to delay that call as we might get executed at startup after mailbox messages are + // applied where we iterate over out pending trades. The closeDisputedTrade method would remove + // that trade from the list causing a ConcurrentModificationException. + // To avoid that we delay for one render frame. + UserThread.execute(() -> processModel.getTradeManager() + .closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED)); + } + + processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); + } else { + log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); + } + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java new file mode 100644 index 0000000000..51ff7267f9 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SendMediatedPayoutSignatureMessage extends TradeTask { + public SendMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + PubKeyRing pubKeyRing = processModel.getPubKeyRing(); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + PubKeyRing peersPubKeyRing = contract.getPeersPubKeyRing(pubKeyRing); + NodeAddress peersNodeAddress = contract.getPeersNodeAddress(pubKeyRing); + P2PService p2PService = processModel.getP2PService(); + MediatedPayoutTxSignatureMessage message = new MediatedPayoutTxSignatureMessage(processModel.getMediatedPayoutTxSignature(), + trade.getId(), + p2PService.getAddress(), + UUID.randomUUID().toString()); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_SENT); + processModel.getTradeManager().requestPersistence(); + p2PService.getMailboxMessageService().sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_ARRIVED); + processModel.getTradeManager().requestPersistence(); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_IN_MAILBOX); + processModel.getTradeManager().requestPersistence(); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + trade.setMediationResultState(MediationResultState.SIG_MSG_SEND_FAILED); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + processModel.getTradeManager().requestPersistence(); + failed(errorMessage); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..f03baace1b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java @@ -0,0 +1,94 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + +@Slf4j +public class SendMediatedPayoutTxPublishedMessage extends SendMailboxMessageTask { + public SendMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String id) { + Transaction payoutTx = checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); + return new MediatedPayoutTxPublishedMessage( + id, + payoutTx.bitcoinSerialize(), + processModel.getMyNodeAddress(), + UUID.randomUUID().toString() + ); + } + + @Override + protected void setStateSent() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SENT); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_ARRIVED); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateFault() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + if (trade.getPayoutTx() == null) { + log.error("PayoutTx is null"); + failed("PayoutTx is null"); + return; + } + + super.run(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java new file mode 100644 index 0000000000..dc0b60b314 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java @@ -0,0 +1,54 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SetupMediatedPayoutTxListener extends SetupPayoutTxListener { + public SetupMediatedPayoutTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + + } catch (Throwable t) { + failed(t); + } + } + + @Override + protected void setState() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK); + if (trade.getPayoutTx() != null) { + processModel.getTradeManager().closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED); + } + processModel.getTradeManager().requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java new file mode 100644 index 0000000000..6699d83200 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java @@ -0,0 +1,110 @@ +/* + * 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 bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SignMediatedPayoutTx extends TradeTask { + + public SignMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + if (processModel.getMediatedPayoutTxSignature() != null) { + log.warn("processModel.getTxSignatureFromMediation is already set"); + } + + String tradeId = trade.getId(); + BtcWalletService walletService = processModel.getBtcWalletService(); + Transaction depositTx = checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + + Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); + Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); + Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); + + checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), + "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + + "; sellerPayoutAmount=" + sellerPayoutAmount); + + boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); + + String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String peersPayoutAddressString = tradingPeer.getPayoutAddressString(); + String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; + String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; + + byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); + byte[] peersMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; + byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; + + DeterministicKey myMultiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); + + checkArgument(Arrays.equals(myMultiSigPubKey, + walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), + "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); + + byte[] mediatedPayoutTxSignature = processModel.getTradeWalletService().signMediatedPayoutTx( + depositTx, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + myMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + processModel.setMediatedPayoutTxSignature(mediatedPayoutTxSignature); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java new file mode 100644 index 0000000000..d0a0f6c20d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.BroadcastPayoutTx; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerBroadcastPayoutTx extends BroadcastPayoutTx { + public SellerBroadcastPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } + } + + @Override + protected void setState() { + trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); + processModel.getTradeManager().requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java new file mode 100644 index 0000000000..a466f11264 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.governance.param.Param; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerCreatesDelayedPayoutTx extends TradeTask { + + public SellerCreatesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS); + Coin minerFee = trade.getTxFee(); + TradeWalletService tradeWalletService = processModel.getTradeWalletService(); + Transaction depositTx = checkNotNull(processModel.getDepositTx()); + + long lockTime = trade.getLockTime(); + Transaction preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx, + donationAddressString, + minerFee, + lockTime); + TradeDataValidation.validateDelayedPayoutTx(trade, + preparedDelayedPayoutTx, + processModel.getDaoFacade(), + processModel.getBtcWalletService()); + + processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java new file mode 100644 index 0000000000..2683e52602 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerFinalizesDelayedPayoutTx extends TradeTask { + public SellerFinalizesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + byte[] buyerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(sellerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + byte[] buyerSignature = processModel.getTradingPeer().getDelayedPayoutTxSignature(); + byte[] sellerSignature = processModel.getDelayedPayoutTxSignature(); + + Transaction signedDelayedPayoutTx = processModel.getTradeWalletService().finalizeDelayedPayoutTx( + preparedDelayedPayoutTx, + buyerMultiSigPubKey, + sellerMultiSigPubKey, + buyerSignature, + sellerSignature); + + trade.applyDelayedPayoutTx(signedDelayedPayoutTx); + log.info("DelayedPayoutTxBytes = {}", Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes())); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java new file mode 100644 index 0000000000..9cbba9b670 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTask { + public SellerProcessCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + CounterCurrencyTransferStartedMessage message = (CounterCurrencyTransferStartedMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + processModel.getTradingPeer().setPayoutAddressString(Validator.nonEmptyStringOf(message.getBuyerPayoutAddress())); + processModel.getTradingPeer().setSignature(checkNotNull(message.getBuyerSignature())); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + String counterCurrencyTxId = message.getCounterCurrencyTxId(); + if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) { + trade.setCounterCurrencyTxId(counterCurrencyTxId); + } + + String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); + if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) { + trade.setCounterCurrencyExtraData(counterCurrencyExtraData); + } + + trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java new file mode 100644 index 0000000000..33d60965a4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.util.Validator.checkTradeId; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerProcessDelayedPayoutTxSignatureResponse extends TradeTask { + public SellerProcessDelayedPayoutTxSignatureResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + DelayedPayoutTxSignatureResponse response = (DelayedPayoutTxSignatureResponse) processModel.getTradeMessage(); + checkNotNull(response); + checkTradeId(processModel.getOfferId(), response); + + processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(response.getDelayedPayoutTxBuyerSignature())); + + processModel.getTradeWalletService().sellerAddsBuyerWitnessesToDepositTx( + processModel.getDepositTx(), + processModel.getBtcWalletService().getTxFromSerializedTx(response.getDepositTx()) + ); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java new file mode 100644 index 0000000000..1afae6dad3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerPublishesDepositTx extends TradeTask { + public SellerPublishesDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + final Transaction depositTx = processModel.getDepositTx(); + processModel.getTradeWalletService().broadcastTx(depositTx, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + if (!completed) { + // Now as we have published the deposit tx we set it in trade + trade.applyDepositTx(depositTx); + + trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX); + + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(processModel.getOffer().getId(), + AddressEntry.Context.RESERVED_FOR_TRADE); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesTradeStatistics.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesTradeStatistics.java new file mode 100644 index 0000000000..89b6ac5a14 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesTradeStatistics.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.statistics.TradeStatistics3; + +import bisq.network.p2p.network.TorNetworkNode; + +import bisq.common.app.Capability; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerPublishesTradeStatistics extends TradeTask { + public SellerPublishesTradeStatistics(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + checkNotNull(trade.getDepositTx()); + + processModel.getP2PService().findPeersCapabilities(trade.getTradingPeerNodeAddress()) + .filter(capabilities -> capabilities.containsAll(Capability.TRADE_STATISTICS_3)) + .ifPresentOrElse(capabilities -> { + // Our peer has updated, so as we are the seller we will publish the trade statistics. + // The peer as buyer does not publish anymore with v.1.4.0 (where Capability.TRADE_STATISTICS_3 was added) + + String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null); + boolean isTorNetworkNode = model.getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode; + TradeStatistics3 tradeStatistics = TradeStatistics3.from(trade, referralId, isTorNetworkNode); + if (tradeStatistics.isValid()) { + log.info("Publishing trade statistics"); + processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true); + } else { + log.warn("Trade statistics are invalid. We do not publish. {}", tradeStatistics); + } + + complete(); + }, + () -> { + log.info("Our peer does not has updated yet, so they will publish the trade statistics. " + + "To avoid duplicates we do not publish from our side."); + complete(); + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java new file mode 100644 index 0000000000..58cfe2da58 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerSendDelayedPayoutTxSignatureRequest extends TradeTask { + public SellerSendDelayedPayoutTxSignatureRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx(), + "processModel.getPreparedDelayedPayoutTx() must not be null"); + byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature(), + "processModel.getDelayedPayoutTxSignature() must not be null"); + DelayedPayoutTxSignatureRequest message = new DelayedPayoutTxSignatureRequest(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + preparedDelayedPayoutTx.bitcoinSerialize(), + delayedPayoutTxSignature); + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..be6990835d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java @@ -0,0 +1,111 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask { + SignedWitness signedWitness = null; + + public SellerSendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String id) { + Transaction payoutTx = checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); + + AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); + if (accountAgeWitnessService.isSignWitnessTrade(trade)) { + // Broadcast is done in accountAgeWitness domain. + accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); + } + + return new PayoutTxPublishedMessage( + id, + payoutTx.bitcoinSerialize(), + processModel.getMyNodeAddress(), + signedWitness + ); + } + + @Override + protected void setStateSent() { + trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG); + log.info("Sent PayoutTxPublishedMessage: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG); + log.info("PayoutTxPublishedMessage arrived: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG); + log.info("PayoutTxPublishedMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateFault() { + trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG); + log.error("PayoutTxPublishedMessage failed: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + if (trade.getPayoutTx() == null) { + log.error("PayoutTx is null"); + failed("PayoutTx is null"); + return; + } + + super.run(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java new file mode 100644 index 0000000000..2ee9d8c9d1 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java @@ -0,0 +1,191 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.network.MessageState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import javafx.beans.value.ChangeListener; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * We send the buyer the deposit and delayed payout tx. We wait to receive a ACK message back and resend the message + * in case that does not happen in 4 seconds or if the message was stored in mailbox or failed. We keep repeating that + * with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached. If never successful we fail and + * do not continue the protocol with publishing the deposit tx. That way we avoid that a deposit tx is published but the + * buyer does not have the delayed payout tx and would not be able to open arbitration. + */ +@Slf4j +public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends SendMailboxMessageTask { + private static final int MAX_RESEND_ATTEMPTS = 7; + private int delayInSec = 4; + private int resendCounter = 0; + private DepositTxAndDelayedPayoutTxMessage message; + private ChangeListener listener; + private Timer timer; + + public SellerSendsDepositTxAndDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { + if (message == null) { + // 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 + // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox + // 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 = tradeId + processModel.getMyNodeAddress().getFullAddress(); + message = new DepositTxAndDelayedPayoutTxMessage( + deterministicId, + processModel.getOfferId(), + processModel.getMyNodeAddress(), + checkNotNull(processModel.getDepositTx()).bitcoinSerialize(), + checkNotNull(trade.getDelayedPayoutTx()).bitcoinSerialize()); + } + return message; + } + + @Override + protected void setStateSent() { + trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG); + + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + trade.setStateIfValidTransitionTo(Trade.State.SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG); + + processModel.getTradeManager().requestPersistence(); + cleanup(); + // Complete is called in base class + } + + // We override the default behaviour for onStoredInMailbox and do not call complete + @Override + protected void onStoredInMailbox() { + setStateStoredInMailbox(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setStateIfValidTransitionTo(Trade.State.SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG); + + processModel.getTradeManager().requestPersistence(); + // The DepositTxAndDelayedPayoutTxMessage is a mailbox message as earlier we use only the deposit tx which can + // be also received from the network once published. + // Now we send the delayed payout tx as well and with that this message is mandatory for continuing the protocol. + // We do not support mailbox message handling during the take offer process as it is expected that both peers + // are online. + // For backward compatibility and extra resilience we still keep DepositTxAndDelayedPayoutTxMessage as a + // mailbox message but the stored in mailbox case is not expected and the seller would try to send the message again + // in the hope to reach the buyer directly. + if (!trade.isDepositConfirmed()) { + tryToSendAgainLater(); + } + } + + // We override the default behaviour for onFault and do not call appendToErrorMessage and failed + @Override + protected void onFault(String errorMessage, TradeMessage message) { + setStateFault(); + } + + @Override + protected void setStateFault() { + trade.setStateIfValidTransitionTo(Trade.State.SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG); + if (!trade.isDepositConfirmed()) { + tryToSendAgainLater(); + } + + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } finally { + cleanup(); + } + } + + private void cleanup() { + if (timer != null) { + timer.stop(); + } + if (listener != null) { + processModel.getPaymentStartedMessageStateProperty().removeListener(listener); + } + } + + private void tryToSendAgainLater() { + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + failed("We never received an ACK message when sending the msg to the peer. " + + "We fail here and do not publish the deposit tx."); + return; + } + + log.info("We send the message again to the peer after a delay of {} sec.", delayInSec); + if (timer != null) { + timer.stop(); + } + timer = UserThread.runAfter(this::run, delayInSec, TimeUnit.SECONDS); + + if (resendCounter == 0) { + // We want to register listener only once + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + processModel.getDepositTxMessageStateProperty().addListener(listener); + onMessageStateChange(processModel.getDepositTxMessageStateProperty().get()); + } + + delayInSec = delayInSec * 2; + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + // Once we receive an ACK from our msg we know the peer has received the msg and we stop. + if (newValue == MessageState.ACKNOWLEDGED) { + // We treat a ACK like SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG + trade.setStateIfValidTransitionTo(Trade.State.SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG); + + processModel.getTradeManager().requestPersistence(); + cleanup(); + complete(); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java new file mode 100644 index 0000000000..d6b0babc65 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java @@ -0,0 +1,112 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerSignAndFinalizePayoutTx extends TradeTask { + + public SellerSignAndFinalizePayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + + Offer offer = trade.getOffer(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + final byte[] buyerSignature = tradingPeer.getSignature(); + + Coin buyerPayoutAmount = checkNotNull(offer.getBuyerSecurityDeposit()).add(trade.getTradeAmount()); + Coin sellerPayoutAmount = offer.getSellerSecurityDeposit(); + + final String buyerPayoutAddressString = tradingPeer.getPayoutAddressString(); + String sellerPayoutAddressString = walletService.getOrCreateAddressEntry(id, + AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + + final byte[] buyerMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + + Optional multiSigAddressEntryOptional = walletService.getAddressEntry(id, + AddressEntry.Context.MULTI_SIG); + if (!multiSigAddressEntryOptional.isPresent() || !Arrays.equals(sellerMultiSigPubKey, + multiSigAddressEntryOptional.get().getPubKey())) { + // In some error edge cases it can be that the address entry is not marked (or was unmarked). + // We do not want to fail in that case and only report a warning. + // One case where that helped to avoid a failed payout attempt was when the taker had a power failure + // at the moment when the offer was taken. This caused first to not see step 1 in the trade process + // (all greyed out) but after the deposit tx was confirmed the trade process was on step 2 and + // everything looked ok. At the payout multiSigAddressEntryOptional was not present and payout + // could not be done. By changing the previous behaviour from fail if multiSigAddressEntryOptional + // is not present to only log a warning the payout worked. + log.warn("sellerMultiSigPubKey from AddressEntry does not match the one from the trade data. " + + "Trade id ={}, multiSigAddressEntryOptional={}", id, multiSigAddressEntryOptional); + } + + DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(id, sellerMultiSigPubKey); + + Transaction transaction = processModel.getTradeWalletService().sellerSignsAndFinalizesPayoutTx( + checkNotNull(trade.getDepositTx()), + buyerSignature, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + multiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey + ); + + trade.setPayoutTx(transaction); + + processModel.getTradeManager().requestPersistence(); + + walletService.resetCoinLockedInMultiSigAddressEntry(id); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java new file mode 100644 index 0000000000..adabac92eb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerSignsDelayedPayoutTx extends TradeTask { + public SellerSignsDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + NetworkParameters params = btcWalletService.getParams(); + Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx()); + + String id = processModel.getOffer().getId(); + + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + DeterministicKey myMultiSigKeyPair = btcWalletService.getMultiSigKeyPair(id, sellerMultiSigPubKey); + + checkArgument(Arrays.equals(sellerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] buyerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + + byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx( + preparedDelayedPayoutTx, + preparedDepositTx, + myMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + + processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java new file mode 100644 index 0000000000..b484324d3f --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java @@ -0,0 +1,116 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_maker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.PreparedDepositTxAndMakerInputs; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.crypto.Hash; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask { + public SellerAsMakerCreatesUnsignedDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + Offer offer = checkNotNull(trade.getOffer()); + + // params + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); + trade.setContractHash(contractHash); + log.debug("\n\n------------------------------------------------------------\n" + + "Contract as json\n" + + trade.getContractAsJson() + + "\n------------------------------------------------------------\n"); + + Coin makerInputAmount = offer.getSellerSecurityDeposit().add(trade.getTradeAmount()); + Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); + checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); + AddressEntry makerMultiSigAddressEntry = addressEntryOptional.get(); + processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(makerMultiSigAddressEntry, makerInputAmount.value); + + walletService.saveAddressEntryList(); + + Coin msOutputAmount = makerInputAmount + .add(trade.getTxFee()) + .add(offer.getBuyerSecurityDeposit()); + + List takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH), + "all takerRawTransactionInputs must be P2WH"); + long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); + String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); + Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); + byte[] buyerPubKey = tradingPeer.getMultiSigPubKey(); + byte[] sellerPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(sellerPubKey, + makerMultiSigAddressEntry.getPubKey()), + "sellerPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().sellerAsMakerCreatesDepositTx( + contractHash, + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + + processModel.setPreparedDepositTx(result.depositTransaction); + processModel.setRawTransactionInputs(result.rawMakerInputs); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java new file mode 100644 index 0000000000..3902668f82 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsMakerFinalizesDepositTx extends TradeTask { + public SellerAsMakerFinalizesDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + byte[] takersRawPreparedDepositTx = checkNotNull(processModel.getTradingPeer().getPreparedDepositTx()); + byte[] myRawPreparedDepositTx = checkNotNull(processModel.getPreparedDepositTx()); + Transaction takersDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(takersRawPreparedDepositTx); + Transaction myDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(myRawPreparedDepositTx); + int numTakersInputs = checkNotNull(processModel.getTradingPeer().getRawTransactionInputs()).size(); + processModel.getTradeWalletService().sellerAsMakerFinalizesDepositTx(myDepositTx, takersDepositTx, numTakersInputs); + + processModel.setDepositTx(myDepositTx); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java new file mode 100644 index 0000000000..0433347f84 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsMakerProcessDepositTxMessage extends TradeTask { + public SellerAsMakerProcessDepositTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + DepositTxMessage message = (DepositTxMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + processModel.getTradingPeer().setPreparedDepositTx(checkNotNull(message.getDepositTxWithoutWitnesses())); + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + // When we receive that message the taker has published the taker fee, so we apply it to the trade. + // The takerFeeTx was sent in the first message. It should be part of DelayedPayoutTxSignatureRequest + // but that cannot be changed due backward compatibility issues. It is a left over from the old trade protocol. + trade.setTakerFeeTxId(processModel.getTakeOfferFeeTxId()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java new file mode 100644 index 0000000000..e22d7b3dc5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.script.Script; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInputsForDepositTxResponse { + public SellerAsMakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected byte[] getPreparedDepositTx() { + Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx()); + preparedDepositTx.getInputs().forEach(input -> { + // Remove signature before sending to peer as we don't want to risk that buyer could publish deposit tx + // before we have received his signature for the delayed payout tx. + input.setScriptSig(new Script(new byte[]{})); + }); + + processModel.getTradeManager().requestPersistence(); + + // Make sure witnesses are removed as well before sending, to cover the segwit case. + return preparedDepositTx.bitcoinSerialize(false); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java new file mode 100644 index 0000000000..7aa4118e6e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_taker; + +import bisq.core.btc.model.InputsAndChangeOutput; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsTakerCreatesDepositTxInputs extends TradeTask { + public SellerAsTakerCreatesDepositTxInputs(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Coin tradeAmount = checkNotNull(trade.getTradeAmount()); + Offer offer = checkNotNull(trade.getOffer()); + Coin txFee = trade.getTxFee(); + Coin takerInputAmount = offer.getSellerSecurityDeposit() + .add(txFee) + .add(txFee) // We add 2 times the fee as one is for the payout tx + .add(tradeAmount); + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositTxInputs( + processModel.getTakeOfferFeeTx(), + takerInputAmount, + txFee); + + processModel.setRawTransactionInputs(result.rawTransactionInputs); + processModel.setChangeOutputValue(result.changeOutputValue); + processModel.setChangeOutputAddress(result.changeOutputAddress); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java new file mode 100644 index 0000000000..5e2cc1f3ee --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.trade.protocol.tasks.seller_as_taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsTakerSignsDepositTx extends TradeTask { + public SellerAsTakerSignsDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + List sellerInputs = checkNotNull(processModel.getRawTransactionInputs(), + "sellerInputs must not be null"); + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + Optional optionalMultiSigAddressEntry = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); + checkArgument(optionalMultiSigAddressEntry.isPresent(), "addressEntryOptional must be present"); + AddressEntry sellerMultiSigAddressEntry = optionalMultiSigAddressEntry.get(); + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(sellerMultiSigPubKey, + sellerMultiSigAddressEntry.getPubKey()), + "sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + Coin sellerInput = Coin.valueOf(sellerInputs.stream().mapToLong(input -> input.value).sum()); + + Coin totalFee = trade.getTxFee().multiply(2); // Fee for deposit and payout tx + Coin multiSigValue = sellerInput.subtract(totalFee); + processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(sellerMultiSigAddressEntry, multiSigValue.value); + walletService.saveAddressEntryList(); + + Offer offer = trade.getOffer(); + Coin msOutputAmount = offer.getBuyerSecurityDeposit().add(offer.getSellerSecurityDeposit()).add(trade.getTxFee()) + .add(checkNotNull(trade.getTradeAmount())); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + + Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx( + true, + trade.getContractHash(), + processModel.getPreparedDepositTx(), + msOutputAmount, + checkNotNull(tradingPeer.getRawTransactionInputs()), + sellerInputs, + tradingPeer.getMultiSigPubKey(), + sellerMultiSigPubKey); + + // We set the deposit tx to trade once we have it published + processModel.setDepositTx(depositTx); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + Contract contract = trade.getContract(); + if (contract != null) + contract.printDiff(processModel.getTradingPeer().getContractAsJson()); + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java new file mode 100644 index 0000000000..e646802008 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java @@ -0,0 +1,117 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.dao.exceptions.DaoDisabledException; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.FeeReceiverSelector; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CreateTakerFeeTx extends TradeTask { + + public CreateTakerFeeTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + // We enforce here to create a MULTI_SIG and TRADE_PAYOUT address entry to avoid that the change output would be used later + // for those address entries. Because we do not commit our fee tx yet the change address would + // appear as unused and therefore selected for the outputs for the MS tx. + // That would cause incorrect display of the balance as + // the change output would be considered as not available balance (part of the locked trade amount). + walletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG); + walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); + + AddressEntry fundingAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.OFFER_FUNDING); + AddressEntry reservedForTradeAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE); + AddressEntry changeAddressEntry = walletService.getFreshAddressEntry(); + Address fundingAddress = fundingAddressEntry.getAddress(); + Address reservedForTradeAddress = reservedForTradeAddressEntry.getAddress(); + Address changeAddress = changeAddressEntry.getAddress(); + TradeWalletService tradeWalletService = processModel.getTradeWalletService(); + Transaction transaction; + + String feeReceiver = FeeReceiverSelector.getAddress(processModel.getDaoFacade(), processModel.getFilterManager()); + + if (trade.isCurrencyForTakerFeeBtc()) { + transaction = tradeWalletService.createBtcTradingFeeTx( + fundingAddress, + reservedForTradeAddress, + changeAddress, + processModel.getFundsNeededForTrade(), + processModel.isUseSavingsWallet(), + trade.getTakerFee(), + trade.getTxFee(), + feeReceiver, + false, + null); + } else { + Transaction preparedBurnFeeTx = processModel.getBsqWalletService().getPreparedTradeFeeTx(trade.getTakerFee()); + Transaction txWithBsqFee = tradeWalletService.completeBsqTradingFeeTx(preparedBurnFeeTx, + fundingAddress, + reservedForTradeAddress, + changeAddress, + processModel.getFundsNeededForTrade(), + processModel.isUseSavingsWallet(), + trade.getTxFee()); + transaction = processModel.getBsqWalletService().signTx(txWithBsqFee); + WalletService.checkAllScriptSignaturesForTx(transaction); + } + + // We did not broadcast and commit the tx yet to avoid issues with lost trade fee in case the + // take offer attempt failed. + + // We do not set the takerFeeTxId yet to trade as it is not published. + processModel.setTakeOfferFeeTxId(transaction.getTxId().toString()); + + processModel.setTakeOfferFeeTx(transaction); + walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.OFFER_FUNDING); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + if (t instanceof DaoDisabledException) { + failed("You cannot pay the trade fee in BSQ at the moment because the DAO features have been " + + "disabled due technical problems. Please use the BTC fee option until the issues are resolved. " + + "For more information please visit the Bisq Forum."); + } else { + failed(t); + } + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java new file mode 100644 index 0000000000..11218d6fef --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.wallet.Restrictions; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.config.Config; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.util.Validator.checkTradeId; +import static bisq.core.util.Validator.nonEmptyStringOf; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TakerProcessesInputsForDepositTxResponse extends TradeTask { + public TakerProcessesInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + InputsForDepositTxResponse response = (InputsForDepositTxResponse) processModel.getTradeMessage(); + checkTradeId(processModel.getOfferId(), response); + checkNotNull(response); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + tradingPeer.setPaymentAccountPayload(checkNotNull(response.getMakerPaymentAccountPayload())); + tradingPeer.setAccountId(nonEmptyStringOf(response.getMakerAccountId())); + tradingPeer.setMultiSigPubKey(checkNotNull(response.getMakerMultiSigPubKey())); + tradingPeer.setContractAsJson(nonEmptyStringOf(response.getMakerContractAsJson())); + tradingPeer.setContractSignature(nonEmptyStringOf(response.getMakerContractSignature())); + tradingPeer.setPayoutAddressString(nonEmptyStringOf(response.getMakerPayoutAddressString())); + tradingPeer.setRawTransactionInputs(checkNotNull(response.getMakerInputs())); + byte[] preparedDepositTx = checkNotNull(response.getPreparedDepositTx()); + processModel.setPreparedDepositTx(preparedDepositTx); + long lockTime = response.getLockTime(); + if (Config.baseCurrencyNetwork().isMainnet()) { + int myLockTime = processModel.getBtcWalletService().getBestChainHeight() + + Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset()); + // We allow a tolerance of 3 blocks as BestChainHeight might be a bit different on maker and taker in case a new + // block was just found + checkArgument(Math.abs(lockTime - myLockTime) <= 3, + "Lock time of maker is more than 3 blocks different to the lockTime I " + + "calculated. Makers lockTime= " + lockTime + ", myLockTime=" + myLockTime); + } + trade.setLockTime(lockTime); + long delay = processModel.getBtcWalletService().getBestChainHeight() - lockTime; + log.info("lockTime={}, delay={}", lockTime, delay); + + // Maker has to sign preparedDepositTx. He cannot manipulate the preparedDepositTx - so we avoid to have a + // challenge protocol for passing the nonce we want to get signed. + tradingPeer.setAccountAgeWitnessNonce(preparedDepositTx); + tradingPeer.setAccountAgeWitnessSignature(checkNotNull(response.getAccountAgeWitnessSignatureOfPreparedDepositTx())); + + tradingPeer.setCurrentDate(response.getCurrentDate()); + + checkArgument(response.getMakerInputs().size() > 0); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java new file mode 100644 index 0000000000..6324f74e00 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class TakerPublishFeeTx extends TradeTask { + public TakerPublishFeeTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + TradeWalletService tradeWalletService = processModel.getTradeWalletService(); + Transaction takeOfferFeeTx = processModel.getTakeOfferFeeTx(); + + if (trade.isCurrencyForTakerFeeBtc()) { + // We committed to be sure the tx gets into the wallet even in the broadcast process it would be + // committed as well, but if user would close app before success handler returns the commit would not + // be done. + tradeWalletService.commitTx(takeOfferFeeTx); + + tradeWalletService.broadcastTx(takeOfferFeeTx, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + TakerPublishFeeTx.this.onSuccess(transaction); + } + + @Override + public void onFailure(TxBroadcastException exception) { + TakerPublishFeeTx.this.onFailure(exception); + } + }); + } else { + BsqWalletService bsqWalletService = processModel.getBsqWalletService(); + bsqWalletService.commitTx(takeOfferFeeTx, TxType.PAY_TRADE_FEE); + // We need to create another instance, otherwise the tx would trigger an invalid state exception + // if it gets committed 2 times + tradeWalletService.commitTx(tradeWalletService.getClonedTransaction(takeOfferFeeTx)); + + // We use a short timeout as there are issues with BSQ txs. See comment in TxBroadcaster + bsqWalletService.broadcastTx(takeOfferFeeTx, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(@Nullable Transaction transaction) { + TakerPublishFeeTx.this.onSuccess(transaction); + } + + @Override + public void onFailure(TxBroadcastException exception) { + TakerPublishFeeTx.this.onFailure(exception); + } + }, + 1); + } + } catch (Throwable t) { + failed(t); + } + } + + protected void onFailure(TxBroadcastException exception) { + if (!completed) { + log.error(exception.toString()); + exception.printStackTrace(); + trade.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + exception.getMessage()); + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + + protected void onSuccess(@org.jetbrains.annotations.Nullable Transaction transaction) { + if (!completed) { + if (transaction != null) { + trade.setTakerFeeTxId(transaction.getTxId().toString()); + trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); + + processModel.getTradeManager().requestPersistence(); + + complete(); + } + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java new file mode 100644 index 0000000000..89b201b4ad --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java @@ -0,0 +1,160 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.app.Version; +import bisq.common.crypto.Sig; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import com.google.common.base.Charsets; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TakerSendInputsForDepositTxRequest extends TradeTask { + public TakerSendInputsForDepositTxRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "TradeAmount must not be null"); + String takerFeeTxId = checkNotNull(processModel.getTakeOfferFeeTxId(), "TakeOfferFeeTxId must not be null"); + User user = checkNotNull(processModel.getUser(), "User must not be null"); + List acceptedArbitratorAddresses = user.getAcceptedArbitratorAddresses() == null ? + new ArrayList<>() : + user.getAcceptedArbitratorAddresses(); + List acceptedMediatorAddresses = user.getAcceptedMediatorAddresses(); + List acceptedRefundAgentAddresses = user.getAcceptedRefundAgentAddresses() == null ? + new ArrayList<>() : + user.getAcceptedRefundAgentAddresses(); + // We don't check for arbitrators as they should vanish soon + checkNotNull(acceptedMediatorAddresses, "acceptedMediatorAddresses must not be null"); + // We also don't check for refund agents yet as we don't want to restrict us too much. They are not mandatory. + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + Optional optionalMultiSigAddressEntry = walletService.getAddressEntry(id, + AddressEntry.Context.MULTI_SIG); + checkArgument(optionalMultiSigAddressEntry.isPresent(), + "MULTI_SIG addressEntry must have been already set here."); + AddressEntry multiSigAddressEntry = optionalMultiSigAddressEntry.get(); + byte[] takerMultiSigPubKey = multiSigAddressEntry.getPubKey(); + processModel.setMyMultiSigPubKey(takerMultiSigPubKey); + + Optional optionalPayoutAddressEntry = walletService.getAddressEntry(id, + AddressEntry.Context.TRADE_PAYOUT); + checkArgument(optionalPayoutAddressEntry.isPresent(), + "TRADE_PAYOUT multiSigAddressEntry must have been already set here."); + AddressEntry payoutAddressEntry = optionalPayoutAddressEntry.get(); + String takerPayoutAddressString = payoutAddressEntry.getAddressString(); + + String offerId = processModel.getOfferId(); + + // Taker has to use offerId as nonce (he cannot manipulate that - so we avoid to have a challenge + // protocol for passing the nonce we want to get signed) + // This is used for verifying the peers account age witness + PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade), + "processModel.getPaymentAccountPayload(trade) must not be null"); + byte[] signatureOfNonce = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), + offerId.getBytes(Charsets.UTF_8)); + + InputsForDepositTxRequest request = new InputsForDepositTxRequest( + offerId, + processModel.getMyNodeAddress(), + tradeAmount.value, + trade.getTradePrice().getValue(), + trade.getTxFee().getValue(), + trade.getTakerFee().getValue(), + trade.isCurrencyForTakerFeeBtc(), + processModel.getRawTransactionInputs(), + processModel.getChangeOutputValue(), + processModel.getChangeOutputAddress(), + takerMultiSigPubKey, + takerPayoutAddressString, + processModel.getPubKeyRing(), + paymentAccountPayload, + processModel.getAccountId(), + takerFeeTxId, + acceptedArbitratorAddresses, + acceptedMediatorAddresses, + acceptedRefundAgentAddresses, + trade.getArbitratorNodeAddress(), + trade.getMediatorNodeAddress(), + trade.getRefundAgentNodeAddress(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + signatureOfNonce, + new Date().getTime()); + log.info("Send {} with offerId {} and uid {} to peer {}", + request.getClass().getSimpleName(), request.getTradeId(), + request.getUid(), trade.getTradingPeerNodeAddress()); + + processModel.getTradeManager().requestPersistence(); + + processModel.getP2PService().sendEncryptedDirectMessage( + trade.getTradingPeerNodeAddress(), + processModel.getTradingPeer().getPubKeyRing(), + request, + new SendDirectMessageListener() { + public void onArrived() { + log.info("{} arrived at peer: offerId={}; uid={}", + request.getClass().getSimpleName(), request.getTradeId(), request.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", + request.getClass().getSimpleName(), request.getUid(), + trade.getTradingPeerNodeAddress(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java new file mode 100644 index 0000000000..afe14cba20 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java @@ -0,0 +1,132 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Contract; +import bisq.core.trade.SellerAsTakerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.Sig; +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TakerVerifyAndSignContract extends TradeTask { + public TakerVerifyAndSignContract(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + String takerFeeTxId = checkNotNull(processModel.getTakeOfferFeeTxId()); + TradingPeer maker = processModel.getTradingPeer(); + PaymentAccountPayload makerPaymentAccountPayload = checkNotNull(maker.getPaymentAccountPayload()); + PaymentAccountPayload takerPaymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade)); + + boolean isBuyerMakerAndSellerTaker = trade instanceof SellerAsTakerTrade; + NodeAddress buyerNodeAddress = isBuyerMakerAndSellerTaker ? + processModel.getTempTradingPeerNodeAddress() : + processModel.getMyNodeAddress(); + NodeAddress sellerNodeAddress = isBuyerMakerAndSellerTaker ? + processModel.getMyNodeAddress() : + processModel.getTempTradingPeerNodeAddress(); + + BtcWalletService walletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + AddressEntry takerPayoutAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); + String takerPayoutAddressString = takerPayoutAddressEntry.getAddressString(); + AddressEntry takerMultiSigAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG); + byte[] takerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(takerMultiSigPubKey, + takerMultiSigAddressEntry.getPubKey()), + "takerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + Coin tradeAmount = checkNotNull(trade.getTradeAmount()); + Contract contract = new Contract( + processModel.getOffer().getOfferPayload(), + tradeAmount.value, + trade.getTradePrice().getValue(), + takerFeeTxId, + buyerNodeAddress, + sellerNodeAddress, + trade.getMediatorNodeAddress(), + isBuyerMakerAndSellerTaker, + maker.getAccountId(), + processModel.getAccountId(), + makerPaymentAccountPayload, + takerPaymentAccountPayload, + maker.getPubKeyRing(), + processModel.getPubKeyRing(), + maker.getPayoutAddressString(), + takerPayoutAddressString, + maker.getMultiSigPubKey(), + takerMultiSigPubKey, + trade.getLockTime(), + trade.getRefundAgentNodeAddress() + ); + String contractAsJson = Utilities.objectToJson(contract); + + if (!contractAsJson.equals(processModel.getTradingPeer().getContractAsJson())) { + contract.printDiff(processModel.getTradingPeer().getContractAsJson()); + failed("Contracts are not matching"); + } + + String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); + trade.setContract(contract); + trade.setContractAsJson(contractAsJson); + + byte[] contractHash = Hash.getSha256Hash(checkNotNull(contractAsJson)); + trade.setContractHash(contractHash); + + trade.setTakerContractSignature(signature); + + processModel.getTradeManager().requestPersistence(); + try { + checkNotNull(maker.getPubKeyRing(), "maker.getPubKeyRing() must nto be null"); + Sig.verify(maker.getPubKeyRing().getSignaturePubKey(), + contractAsJson, + maker.getContractSignature()); + complete(); + } catch (Throwable t) { + failed("Contract signature verification failed. " + t.getMessage()); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java new file mode 100644 index 0000000000..7d8d208d4d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.trade.protocol.tasks.taker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TakerVerifyMakerFeePayment extends TradeTask { + public TakerVerifyMakerFeePayment(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + //TODO impl. missing + // int numOfPeersSeenTx = processModel.getWalletService().getNumOfPeersSeenTx(processModel.getTakeOfferFeeTxId().getHashAsString()); + /* if (numOfPeersSeenTx > 2) { + resultHandler.handleResult(); + }*/ + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/ReferralId.java b/core/src/main/java/bisq/core/trade/statistics/ReferralId.java new file mode 100644 index 0000000000..a4f8bec1b4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/ReferralId.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.trade.statistics; + +/** + * Those are random ids which can be assigned to a market maker or API provider who generates trade volume for Bisq. + * + * The assignment process is that a partner requests a referralId from the core developers and if accepted they get + * assigned an ID. With the ID we can quantify the generated trades from that partner from analysing the trade + * statistics. Compensation requests will be based on that data. + */ +public enum ReferralId { + REF_ID_342, + REF_ID_768, + REF_ID_196, + REF_ID_908, + REF_ID_023, + REF_ID_605, + REF_ID_896, + REF_ID_183 +} diff --git a/core/src/main/java/bisq/core/trade/statistics/ReferralIdService.java b/core/src/main/java/bisq/core/trade/statistics/ReferralIdService.java new file mode 100644 index 0000000000..14d7e6802e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/ReferralIdService.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.user.Preferences; + +import javax.inject.Inject; + +import java.util.Arrays; +import java.util.Optional; + +import javax.annotation.Nullable; + +public class ReferralIdService { + private final Preferences preferences; + private Optional optionalReferralId = Optional.empty(); + + @Inject + public ReferralIdService(Preferences preferences) { + this.preferences = preferences; + } + + public boolean verify(String referralId) { + return Arrays.stream(ReferralId.values()).anyMatch(e -> e.name().equals(referralId)); + } + + public Optional getOptionalReferralId() { + String referralId = preferences.getReferralId(); + if (referralId != null && !referralId.isEmpty() && verify(referralId)) + optionalReferralId = Optional.of(referralId); + else + optionalReferralId = Optional.empty(); + + return optionalReferralId; + } + + public void setReferralId(@Nullable String referralId) { + if (referralId == null || referralId.isEmpty() || verify(referralId)) { + optionalReferralId = Optional.ofNullable(referralId); + preferences.setReferralId(referralId); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java new file mode 100644 index 0000000000..864b08f967 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -0,0 +1,367 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.AltcoinExchangeRate; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.trade.Trade; +import bisq.core.util.VolumeUtil; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.crypto.Hash; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + +import com.google.common.base.Charsets; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Serialized size is about 180-210 byte. Nov 2017 we have 5500 objects + */ +@Deprecated +@Slf4j +@Value +public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, + CapabilityRequiringPayload, Comparable { + + public static TradeStatistics2 from(Trade trade, + @Nullable String referralId, + boolean isTorNetworkNode) { + Map extraDataMap = new HashMap<>(); + if (referralId != null) { + extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); + } + + NodeAddress mediatorNodeAddress = trade.getMediatorNodeAddress(); + if (mediatorNodeAddress != null) { + // The first 4 chars are sufficient to identify a mediator. + // For testing with regtest/localhost we use the full address as its localhost and would result in + // same values for multiple mediators. + String address = isTorNetworkNode ? + mediatorNodeAddress.getFullAddress().substring(0, 4) : + mediatorNodeAddress.getFullAddress(); + extraDataMap.put(TradeStatistics2.MEDIATOR_ADDRESS, address); + } + + Offer offer = trade.getOffer(); + checkNotNull(offer, "offer must not ne null"); + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not ne null"); + return new TradeStatistics2(offer.getOfferPayload(), + trade.getTradePrice(), + trade.getTradeAmount(), + trade.getDate(), + trade.getDepositTxId(), + extraDataMap); + } + + @SuppressWarnings("SpellCheckingInspection") + public static final String MEDIATOR_ADDRESS = "medAddr"; + @SuppressWarnings("SpellCheckingInspection") + public static final String REFUND_AGENT_ADDRESS = "refAddr"; + + private final OfferPayload.Direction direction; + private final String baseCurrency; + private final String counterCurrency; + private final String offerPaymentMethod; + private final long offerDate; + private final boolean offerUseMarketBasedPrice; + private final double offerMarketPriceMargin; + private final long offerAmount; + private final long offerMinAmount; + private final String offerId; + private final long tradePrice; + private final long tradeAmount; + // tradeDate is different for both peers so we ignore it for hash + @JsonExclude + private final long tradeDate; + @JsonExclude + @Nullable + private final String depositTxId; + + // Hash get set in constructor from json of all the other data fields (with hash = null). + @JsonExclude + private final byte[] hash; + // PB field signature_pub_key_bytes not used anymore from v0.6 on + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + @JsonExclude + private Map extraDataMap; + + public TradeStatistics2(OfferPayload offerPayload, + Price tradePrice, + Coin tradeAmount, + Date tradeDate, + String depositTxId, + Map extraDataMap) { + this(offerPayload.getDirection(), + offerPayload.getBaseCurrencyCode(), + offerPayload.getCounterCurrencyCode(), + offerPayload.getPaymentMethodId(), + offerPayload.getDate(), + offerPayload.isUseMarketBasedPrice(), + offerPayload.getMarketPriceMargin(), + offerPayload.getAmount(), + offerPayload.getMinAmount(), + offerPayload.getId(), + tradePrice.getValue(), + tradeAmount.value, + tradeDate.getTime(), + depositTxId, + null, + extraDataMap); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public TradeStatistics2(OfferPayload.Direction direction, + String baseCurrency, + String counterCurrency, + String offerPaymentMethod, + long offerDate, + boolean offerUseMarketBasedPrice, + double offerMarketPriceMargin, + long offerAmount, + long offerMinAmount, + String offerId, + long tradePrice, + long tradeAmount, + long tradeDate, + @Nullable String depositTxId, + @Nullable byte[] hash, + @Nullable Map extraDataMap) { + this.direction = direction; + this.baseCurrency = baseCurrency; + this.counterCurrency = counterCurrency; + this.offerPaymentMethod = offerPaymentMethod; + this.offerDate = offerDate; + this.offerUseMarketBasedPrice = offerUseMarketBasedPrice; + this.offerMarketPriceMargin = offerMarketPriceMargin; + this.offerAmount = offerAmount; + this.offerMinAmount = offerMinAmount; + this.offerId = offerId; + this.tradePrice = tradePrice; + this.tradeAmount = tradeAmount; + this.tradeDate = tradeDate; + this.depositTxId = depositTxId; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + + this.hash = hash == null ? createHash() : hash; + } + + public byte[] createHash() { + // We create hash from all fields excluding hash itself. We use json as simple data serialisation. + // TradeDate is different for both peers so we ignore it for hash. ExtraDataMap is ignored as well as at + // software updates we might have different entries which would cause a different hash. + return Hash.getSha256Ripemd160hash(Utilities.objectToJson(this).getBytes(Charsets.UTF_8)); + } + + private protobuf.TradeStatistics2.Builder getBuilder() { + final protobuf.TradeStatistics2.Builder builder = protobuf.TradeStatistics2.newBuilder() + .setDirection(OfferPayload.Direction.toProtoMessage(direction)) + .setBaseCurrency(baseCurrency) + .setCounterCurrency(counterCurrency) + .setPaymentMethodId(offerPaymentMethod) + .setOfferDate(offerDate) + .setOfferUseMarketBasedPrice(offerUseMarketBasedPrice) + .setOfferMarketPriceMargin(offerMarketPriceMargin) + .setOfferAmount(offerAmount) + .setOfferMinAmount(offerMinAmount) + .setOfferId(offerId) + .setTradePrice(tradePrice) + .setTradeAmount(tradeAmount) + .setTradeDate(tradeDate) + .setHash(ByteString.copyFrom(hash)); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); + return builder; + } + + public protobuf.TradeStatistics2 toProtoTradeStatistics2() { + return getBuilder().build(); + } + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + return protobuf.PersistableNetworkPayload.newBuilder().setTradeStatistics2(getBuilder()).build(); + } + + public static TradeStatistics2 fromProto(protobuf.TradeStatistics2 proto) { + return new TradeStatistics2( + OfferPayload.Direction.fromProto(proto.getDirection()), + proto.getBaseCurrency(), + proto.getCounterCurrency(), + proto.getPaymentMethodId(), + proto.getOfferDate(), + proto.getOfferUseMarketBasedPrice(), + proto.getOfferMarketPriceMargin(), + proto.getOfferAmount(), + proto.getOfferMinAmount(), + proto.getOfferId(), + proto.getTradePrice(), + proto.getTradeAmount(), + proto.getTradeDate(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxId()), + null, // We want to clean up the hashes with the changed hash method in v.1.2.0 so we don't use the value from the proto + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public boolean verifyHashSize() { + checkNotNull(hash, "hash must not be null"); + return hash.length == 20; + } + + // With v1.2.0 we changed the way how the hash is created. To not create too heavy load for seed nodes from + // requests from old nodes we use the TRADE_STATISTICS_HASH_UPDATE capability to send trade statistics only to new + // nodes. As trade statistics are only used for informational purpose it will not have any critical issue for the + // old nodes beside that they don't see the latest trades. We added TRADE_STATISTICS_HASH_UPDATE in v1.2.2 to fix a + // problem of not handling the hashes correctly. + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.TRADE_STATISTICS_HASH_UPDATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + + public Date getTradeDate() { + return new Date(tradeDate); + } + + public Price getTradePrice() { + return Price.valueOf(getCurrencyCode(), tradePrice); + } + + public String getCurrencyCode() { + return baseCurrency.equals("BTC") ? counterCurrency : baseCurrency; + } + + public Coin getTradeAmount() { + return Coin.valueOf(tradeAmount); + } + + public Volume getTradeVolume() { + if (getTradePrice().getMonetary() instanceof Altcoin) { + return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); + } else { + Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); + return VolumeUtil.getRoundedFiatVolume(volume); + } + } + + public boolean isValid() { + // Exclude a disputed BSQ trade where the price was off by a factor 10 due to a mistake by the maker. + // Since the trade wasn't executed it's better to filter it out to avoid it having an undue influence on the + // BSQ trade stats. + boolean excludedFailedTrade = offerId.equals("6E5KOI6O-3a06a037-6f03-4bfa-98c2-59f49f73466a-112"); + boolean depositTxIdValid = depositTxId == null || !depositTxId.isEmpty(); + return tradeAmount > 0 && tradePrice > 0 && !excludedFailedTrade && depositTxIdValid; + } + + // TODO: Can be removed as soon as everyone uses v1.2.6+ + @Override + public int compareTo(@NotNull TradeStatistics2 o) { + if (direction.equals(o.direction) && + baseCurrency.equals(o.baseCurrency) && + counterCurrency.equals(o.counterCurrency) && + offerPaymentMethod.equals(o.offerPaymentMethod) && + offerDate == o.offerDate && + offerUseMarketBasedPrice == o.offerUseMarketBasedPrice && + offerAmount == o.offerAmount && + offerMinAmount == o.offerMinAmount && + offerId.equals(o.offerId) && + tradePrice == o.tradePrice && + tradeAmount == o.tradeAmount) { + return 0; + } + + return -1; + } + + @Override + public String toString() { + return "TradeStatistics2{" + + "\n direction=" + direction + + ",\n baseCurrency='" + baseCurrency + '\'' + + ",\n counterCurrency='" + counterCurrency + '\'' + + ",\n offerPaymentMethod='" + offerPaymentMethod + '\'' + + ",\n offerDate=" + offerDate + + ",\n offerUseMarketBasedPrice=" + offerUseMarketBasedPrice + + ",\n offerMarketPriceMargin=" + offerMarketPriceMargin + + ",\n offerAmount=" + offerAmount + + ",\n offerMinAmount=" + offerMinAmount + + ",\n offerId='" + offerId + '\'' + + ",\n tradePrice=" + tradePrice + + ",\n tradeAmount=" + tradeAmount + + ",\n tradeDate=" + tradeDate + + ",\n depositTxId='" + depositTxId + '\'' + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n extraDataMap=" + extraDataMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java new file mode 100644 index 0000000000..29855fefe8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.MapStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TradeStatistics2StorageService extends MapStoreService { + private static final String FILE_NAME = "TradeStatistics2Store"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradeStatistics2StorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + public Map getMap() { + // As it is used for data request and response and we do not want to send any old trade stat data anymore. + return new HashMap<>(); + } + + // We overwrite that method to receive old trade stats from the network. As we deactivated getMap to not deliver + // hashes we needed to use the getMapOfAllData method to actually store the data. + // That's a bit of a hack but it's just for transition and can be removed after a few months anyway. + // Alternatively we could create a new interface to handle it differently on the other client classes but that + // seems to be not justified as it is needed only temporarily. + @Override + protected PersistableNetworkPayload putIfAbsent(P2PDataStorage.ByteArray hash, PersistableNetworkPayload payload) { + return getMapOfAllData().putIfAbsent(hash, payload); + } + + @Override + protected void readFromResources(String postFix, Runnable completeHandler) { + // We do not attempt to read from resources as that file is not provided anymore + readStore(persisted -> completeHandler.run()); + } + + public Map getMapOfAllData() { + return store.getMap(); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof TradeStatistics2; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected TradeStatistics2Store createStore() { + return new TradeStatistics2Store(); + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java new file mode 100644 index 0000000000..266c6a0d3b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class TradeStatistics2Store extends PersistableNetworkPayloadStore { + + TradeStatistics2Store() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TradeStatistics2Store(List list) { + super(list); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setTradeStatistics2Store(getBuilder()) + .build(); + } + + private protobuf.TradeStatistics2Store.Builder getBuilder() { + final List protoList = map.values().stream() + .map(payload -> (TradeStatistics2) payload) + .map(TradeStatistics2::toProtoTradeStatistics2) + .collect(Collectors.toList()); + return protobuf.TradeStatistics2Store.newBuilder().addAllItems(protoList); + } + + public static TradeStatistics2Store fromProto(protobuf.TradeStatistics2Store proto) { + List list = proto.getItemsList().stream() + .map(TradeStatistics2::fromProto).collect(Collectors.toList()); + return new TradeStatistics2Store(list); + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java new file mode 100644 index 0000000000..a4dd770c47 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -0,0 +1,447 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.AltcoinExchangeRate; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.trade.Trade; +import bisq.core.util.VolumeUtil; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; +import bisq.network.p2p.storage.payload.DateSortedTruncatablePayload; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.crypto.Hash; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.JsonExclude; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * This new trade statistics class uses only the bare minimum of data. + * Data size is about 50 bytes in average + */ +@Slf4j +public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, + CapabilityRequiringPayload, DateSortedTruncatablePayload { + + @JsonExclude + private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + public static TradeStatistics3 from(Trade trade, + @Nullable String referralId, + boolean isTorNetworkNode) { + Map extraDataMap = new HashMap<>(); + if (referralId != null) { + extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); + } + + NodeAddress mediatorNodeAddress = checkNotNull(trade.getMediatorNodeAddress()); + // The first 4 chars are sufficient to identify a mediator. + // For testing with regtest/localhost we use the full address as its localhost and would result in + // same values for multiple mediators. + String truncatedMediatorNodeAddress = isTorNetworkNode ? + mediatorNodeAddress.getFullAddress().substring(0, 4) : + mediatorNodeAddress.getFullAddress(); + + // RefundAgentNodeAddress can be null if converted from old version. + String truncatedRefundAgentNodeAddress = null; + NodeAddress refundAgentNodeAddress = trade.getRefundAgentNodeAddress(); + if (refundAgentNodeAddress != null) { + truncatedRefundAgentNodeAddress = isTorNetworkNode ? + refundAgentNodeAddress.getFullAddress().substring(0, 4) : + refundAgentNodeAddress.getFullAddress(); + } + + Offer offer = checkNotNull(trade.getOffer()); + return new TradeStatistics3(offer.getCurrencyCode(), + trade.getTradePrice().getValue(), + trade.getTradeAmountAsLong(), + offer.getPaymentMethod().getId(), + trade.getTakeOfferDate().getTime(), + truncatedMediatorNodeAddress, + truncatedRefundAgentNodeAddress, + extraDataMap); + } + + // This enum must not change the order as we use the ordinal for storage to reduce data size. + // The payment method string can be quite long and would consume 15% more space. + // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not + // recognized. + private enum PaymentMethodMapper { + OK_PAY, + CASH_APP, + VENMO, + AUSTRALIA_PAYID, // seems there is a dev trade + UPHOLD, + MONEY_BEAM, + POPMONEY, + REVOLUT, + PERFECT_MONEY, + SEPA, + SEPA_INSTANT, + FASTER_PAYMENTS, + NATIONAL_BANK, + JAPAN_BANK, + SAME_BANK, + SPECIFIC_BANKS, + SWISH, + ALI_PAY, + WECHAT_PAY, + CLEAR_X_CHANGE, + CHASE_QUICK_PAY, + INTERAC_E_TRANSFER, + US_POSTAL_MONEY_ORDER, + CASH_DEPOSIT, + MONEY_GRAM, + WESTERN_UNION, + HAL_CASH, + F2F, + BLOCK_CHAINS, + PROMPT_PAY, + ADVANCED_CASH, + BLOCK_CHAINS_INSTANT, + TRANSFERWISE, + AMAZON_GIFT_CARD, + CASH_BY_MAIL + } + + @Getter + private final String currency; + @Getter + private final long price; + @Getter + private final long amount; // BTC amount + private final String paymentMethod; + // As only seller is publishing it is the sellers trade date + private final long date; + + // Old converted trade stat objects might not have it set + @Nullable + @JsonExclude + @Getter + private String mediator; + @Nullable + @JsonExclude + @Getter + private String refundAgent; + + // todo should we add referrerId as well? get added to extra map atm but not used so far + + // Hash get set in constructor from json of all the other data fields (with hash = null). + @JsonExclude + private final byte[] hash; + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + @JsonExclude + @Getter + private final Map extraDataMap; + + // We cache the date object to avoid reconstructing a new Date at each getDate call. + @JsonExclude + private transient final Date dateObj; + + @JsonExclude + private transient Volume volume = null; // Fiat or altcoin volume + @JsonExclude + private transient LocalDateTime localDateTime; + + public TradeStatistics3(String currency, + long price, + long amount, + String paymentMethod, + long date, + String mediator, + String refundAgent, + @Nullable Map extraDataMap) { + this(currency, + price, + amount, + paymentMethod, + date, + mediator, + refundAgent, + extraDataMap, + null); + } + + // Used from conversion method where we use the hash of the TradeStatistics2 objects to avoid duplicate entries + public TradeStatistics3(String currency, + long price, + long amount, + String paymentMethod, + long date, + String mediator, + String refundAgent, + @Nullable byte[] hash) { + this(currency, + price, + amount, + paymentMethod, + date, + mediator, + refundAgent, + null, + hash); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + public TradeStatistics3(String currency, + long price, + long amount, + String paymentMethod, + long date, + @Nullable String mediator, + @Nullable String refundAgent, + @Nullable Map extraDataMap, + @Nullable byte[] hash) { + this.currency = currency; + this.price = price; + this.amount = amount; + String tempPaymentMethod; + try { + tempPaymentMethod = String.valueOf(PaymentMethodMapper.valueOf(paymentMethod).ordinal()); + } catch (Throwable t) { + tempPaymentMethod = paymentMethod; + } + this.paymentMethod = tempPaymentMethod; + this.date = date; + this.mediator = mediator; + this.refundAgent = refundAgent; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + + this.hash = hash == null ? createHash() : hash; + + dateObj = new Date(date); + } + + public byte[] createHash() { + // We create hash from all fields excluding hash itself. We use json as simple data serialisation. + // TradeDate is different for both peers so we ignore it for hash. ExtraDataMap is ignored as well as at + // software updates we might have different entries which would cause a different hash. + return Hash.getSha256Ripemd160hash(Utilities.objectToJson(this).getBytes(Charsets.UTF_8)); + } + + private protobuf.TradeStatistics3.Builder getBuilder() { + protobuf.TradeStatistics3.Builder builder = protobuf.TradeStatistics3.newBuilder() + .setCurrency(currency) + .setPrice(price) + .setAmount(amount) + .setPaymentMethod(paymentMethod) + .setDate(date) + .setHash(ByteString.copyFrom(hash)); + Optional.ofNullable(mediator).ifPresent(builder::setMediator); + Optional.ofNullable(refundAgent).ifPresent(builder::setRefundAgent); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return builder; + } + + public protobuf.TradeStatistics3 toProtoTradeStatistics3() { + return getBuilder().build(); + } + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + return protobuf.PersistableNetworkPayload.newBuilder().setTradeStatistics3(getBuilder()).build(); + } + + public static TradeStatistics3 fromProto(protobuf.TradeStatistics3 proto) { + return new TradeStatistics3( + proto.getCurrency(), + proto.getPrice(), + proto.getAmount(), + proto.getPaymentMethod(), + proto.getDate(), + ProtoUtil.stringOrNullFromProto(proto.getMediator()), + ProtoUtil.stringOrNullFromProto(proto.getRefundAgent()), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), + proto.getHash().toByteArray()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getHash() { + return hash; + } + + @Override + public boolean verifyHashSize() { + checkNotNull(hash, "hash must not be null"); + return hash.length == 20; + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.TRADE_STATISTICS_3); + } + + @Override + public Date getDate() { + return dateObj; + } + + public LocalDateTime getLocalDateTime() { + if (localDateTime == null) { + localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime(); + } + return localDateTime; + } + + public long getDateAsLong() { + return date; + } + + @Override + public int maxItems() { + return 3000; + } + + public void pruneOptionalData() { + mediator = null; + refundAgent = null; + } + + public String getPaymentMethod() { + try { + return PaymentMethodMapper.values()[Integer.parseInt(paymentMethod)].name(); + } catch (Throwable ignore) { + return paymentMethod; + } + } + + private transient Price priceObj; + + public Price getTradePrice() { + if (priceObj == null) { + priceObj = Price.valueOf(currency, price); + } + return priceObj; + } + + public Coin getTradeAmount() { + return Coin.valueOf(amount); + } + + public Volume getTradeVolume() { + if (volume == null) { + if (getTradePrice().getMonetary() instanceof Altcoin) { + volume = new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); + } else { + Volume exactVolume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); + volume = VolumeUtil.getRoundedFiatVolume(exactVolume); + } + } + return volume; + } + + public boolean isValid() { + return amount > 0 && + price > 0 && + date > 0 && + paymentMethod != null && + !paymentMethod.isEmpty() && + currency != null && + !currency.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TradeStatistics3)) return false; + + TradeStatistics3 that = (TradeStatistics3) o; + + if (price != that.price) return false; + if (amount != that.amount) return false; + if (date != that.date) return false; + if (currency != null ? !currency.equals(that.currency) : that.currency != null) return false; + if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) + return false; + return Arrays.equals(hash, that.hash); + } + + @Override + public int hashCode() { + int result = currency != null ? currency.hashCode() : 0; + result = 31 * result + (int) (price ^ (price >>> 32)); + result = 31 * result + (int) (amount ^ (amount >>> 32)); + result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0); + result = 31 * result + (int) (date ^ (date >>> 32)); + result = 31 * result + Arrays.hashCode(hash); + return result; + } + + @Override + public String toString() { + return "TradeStatistics3{" + + "\n currency='" + currency + '\'' + + ",\n price=" + price + + ",\n amount=" + amount + + ",\n paymentMethod='" + paymentMethod + '\'' + + ",\n date=" + date + + ",\n mediator='" + mediator + '\'' + + ",\n refundAgent='" + refundAgent + '\'' + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n extraDataMap=" + extraDataMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3StorageService.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3StorageService.java new file mode 100644 index 0000000000..1639841231 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3StorageService.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.HistoricalDataStoreService; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.File; + +import lombok.extern.slf4j.Slf4j; + +@Singleton +@Slf4j +public class TradeStatistics3StorageService extends HistoricalDataStoreService { + private static final String FILE_NAME = "TradeStatistics3Store"; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradeStatistics3StorageService(@Named(Config.STORAGE_DIR) File storageDir, + PersistenceManager persistenceManager) { + super(storageDir, persistenceManager); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getFileName() { + return FILE_NAME; + } + + @Override + protected void initializePersistenceManager() { + persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); + } + + @Override + public boolean canHandle(PersistableNetworkPayload payload) { + return payload instanceof TradeStatistics3; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected TradeStatistics3Store createStore() { + return new TradeStatistics3Store(); + } + + public void persistNow() { + persistenceManager.persistNow(() -> { + }); + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3Store.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3Store.java new file mode 100644 index 0000000000..53b7e3281c --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3Store.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.persistence.PersistableNetworkPayloadStore; + +import com.google.protobuf.Message; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and + * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer + * definition and provide a hashMap for the domain access. + */ +@Slf4j +public class TradeStatistics3Store extends PersistableNetworkPayloadStore { + + public TradeStatistics3Store() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TradeStatistics3Store(List list) { + list.forEach(item -> map.put(new P2PDataStorage.ByteArray(item.getHash()), item)); + } + + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder() + .setTradeStatistics3Store(getBuilder()) + .build(); + } + + private protobuf.TradeStatistics3Store.Builder getBuilder() { + List protoList = map.values().stream() + .map(payload -> (TradeStatistics3) payload) + .map(TradeStatistics3::toProtoTradeStatistics3) + .collect(Collectors.toList()); + return protobuf.TradeStatistics3Store.newBuilder().addAllItems(protoList); + } + + public static TradeStatistics3Store fromProto(protobuf.TradeStatistics3Store proto) { + List list = proto.getItemsList().stream() + .map(TradeStatistics3::fromProto).collect(Collectors.toList()); + return new TradeStatistics3Store(list); + } + + public boolean containsKey(P2PDataStorage.ByteArray hash) { + return map.containsKey(hash); + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java new file mode 100644 index 0000000000..f360dd5557 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java @@ -0,0 +1,179 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.offer.availability.DisputeAgentSelection; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.util.Utilities; + +import com.google.inject.Inject; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import lombok.extern.slf4j.Slf4j; + +@Singleton +@Slf4j +public class TradeStatisticsConverter { + + private ExecutorService executor; + + @Inject + public TradeStatisticsConverter(P2PService p2PService, + P2PDataStorage p2PDataStorage, + TradeStatistics2StorageService tradeStatistics2StorageService, + TradeStatistics3StorageService tradeStatistics3StorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + @Named(Config.STORAGE_DIR) File storageDir) { + File tradeStatistics2Store = new File(storageDir, "TradeStatistics2Store"); + appendOnlyDataStoreService.addService(tradeStatistics2StorageService); + + p2PService.addP2PServiceListener(new BootstrapListener() { + + @Override + public void onTorNodeReady() { + if (!tradeStatistics2Store.exists()) { + return; + } + executor = Utilities.getSingleThreadExecutor("TradeStatisticsConverter"); + executor.submit(() -> { + // We convert early once tor is initialized but still not ready to receive data + Map tempMap = new HashMap<>(); + convertToTradeStatistics3(tradeStatistics2StorageService.getMapOfAllData().values()) + .forEach(e -> tempMap.put(new P2PDataStorage.ByteArray(e.getHash()), e)); + + // We map to user thread to avoid potential threading issues + UserThread.execute(() -> { + tradeStatistics3StorageService.getMapOfLiveData().putAll(tempMap); + tradeStatistics3StorageService.persistNow(); + }); + + try { + log.info("We delete now the old trade statistics file as it was converted to the new format."); + FileUtil.deleteFileIfExists(tradeStatistics2Store); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.toString()); + } + }); + } + + @Override + public void onUpdatedDataReceived() { + } + }); + + // We listen to old TradeStatistics2 objects, convert and store them and rebroadcast. + p2PDataStorage.addAppendOnlyDataStoreListener(payload -> { + if (payload instanceof TradeStatistics2) { + TradeStatistics3 tradeStatistics3 = convertToTradeStatistics3((TradeStatistics2) payload); + // We add it to the p2PDataStorage, which handles to get the data stored in the maps and maybe + // re-broadcast as tradeStatistics3 object if not already received. + p2PDataStorage.addPersistableNetworkPayload(tradeStatistics3, null, true); + } + }); + } + + public void shutDown() { + if (executor != null) + executor.shutdown(); + } + + private static List convertToTradeStatistics3(Collection persistableNetworkPayloads) { + List list = new ArrayList<>(); + long ts = System.currentTimeMillis(); + + // We might have duplicate entries from both traders as the trade date was different from old clients. + // This should not be the case with converting old persisted data as we did filter those out but it is the case + // when we receive old trade stat objects from the network of 2 not updated traders. + // The hash was ignoring the trade date so we use that to get a unique list + Map mapWithoutDuplicates = new HashMap<>(); + persistableNetworkPayloads.stream() + .filter(e -> e instanceof TradeStatistics2) + .map(e -> (TradeStatistics2) e) + .filter(TradeStatistics2::isValid) + .forEach(e -> mapWithoutDuplicates.putIfAbsent(new P2PDataStorage.ByteArray(e.getHash()), e)); + + log.info("We convert the existing {} trade statistics objects to the new format.", mapWithoutDuplicates.size()); + + mapWithoutDuplicates.values().stream() + .map(TradeStatisticsConverter::convertToTradeStatistics3) + .filter(TradeStatistics3::isValid) + .forEach(list::add); + + int size = list.size(); + log.info("Conversion to {} new trade statistic objects has been completed after {} ms", + size, System.currentTimeMillis() - ts); + + // We prune mediator and refundAgent data from all objects but the last 100 as we only use the + // last 100 entries (DisputeAgentSelection.LOOK_BACK_RANGE). + list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); + if (size > DisputeAgentSelection.LOOK_BACK_RANGE) { + int start = size - DisputeAgentSelection.LOOK_BACK_RANGE; + for (int i = start; i < size; i++) { + TradeStatistics3 tradeStatistics3 = list.get(i); + tradeStatistics3.pruneOptionalData(); + } + } + return list; + } + + private static TradeStatistics3 convertToTradeStatistics3(TradeStatistics2 tradeStatistics2) { + Map extraDataMap = tradeStatistics2.getExtraDataMap(); + String mediator = extraDataMap != null ? extraDataMap.get(TradeStatistics2.MEDIATOR_ADDRESS) : null; + String refundAgent = extraDataMap != null ? extraDataMap.get(TradeStatistics2.REFUND_AGENT_ADDRESS) : null; + long time = tradeStatistics2.getTradeDate().getTime(); + // We need to avoid that we duplicate tradeStatistics2 objects in case both traders have not updated yet. + // Before v1.4.0 both traders published the trade statistics. If one trader has updated he will check + // the capabilities of the peer and if the peer has not updated he will leave publishing to the peer, so we + // do not have the problem of duplicated objects. + // Also at conversion of locally stored old trade statistics we need to avoid duplicated entries. + // To ensure we add only one object we will use the hash of the tradeStatistics2 object which is the same + // for both traders as it excluded the trade date which is different for both. + byte[] hash = tradeStatistics2.getHash(); + return new TradeStatistics3(tradeStatistics2.getCurrencyCode(), + tradeStatistics2.getTradePrice().getValue(), + tradeStatistics2.getTradeAmount().getValue(), + tradeStatistics2.getOfferPaymentMethod(), + time, + mediator, + refundAgent, + hash); + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java new file mode 100644 index 0000000000..34fde08f83 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@EqualsAndHashCode +@ToString +@Slf4j +public final class TradeStatisticsForJson { + public final String currency; + public final long tradePrice; + public final long tradeAmount; + public final long tradeDate; + public final String paymentMethod; + + // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) + public String currencyPair; + + public long primaryMarketTradePrice; + public long primaryMarketTradeAmount; + public long primaryMarketTradeVolume; + + public TradeStatisticsForJson(TradeStatistics3 tradeStatistics) { + this.currency = tradeStatistics.getCurrency(); + this.paymentMethod = tradeStatistics.getPaymentMethod(); + this.tradePrice = tradeStatistics.getPrice(); + this.tradeAmount = tradeStatistics.getAmount(); + this.tradeDate = tradeStatistics.getDateAsLong(); + + try { + Price tradePrice = getTradePrice(); + if (CurrencyUtil.isCryptoCurrency(currency)) { + currencyPair = currency + "/" + Res.getBaseCurrencyCode(); + primaryMarketTradePrice = tradePrice.getValue(); + primaryMarketTradeAmount = getTradeVolume() != null ? + getTradeVolume().getValue() : + 0; + primaryMarketTradeVolume = getTradeAmount().getValue(); + } else { + currencyPair = Res.getBaseCurrencyCode() + "/" + currency; + // we use precision 4 for fiat based price but on the markets api we use precision 8 so we scale up by 10000 + primaryMarketTradePrice = (long) MathUtils.scaleUpByPowerOf10(tradePrice.getValue(), 4); + primaryMarketTradeAmount = getTradeAmount().getValue(); + // we use precision 4 for fiat but on the markets api we use precision 8 so we scale up by 10000 + primaryMarketTradeVolume = getTradeVolume() != null ? + (long) MathUtils.scaleUpByPowerOf10(getTradeVolume().getValue(), 4) : + 0; + } + } catch (Throwable t) { + log.error(t.getMessage()); + t.printStackTrace(); + } + } + + public Price getTradePrice() { + return Price.valueOf(currency, tradePrice); + } + + public Coin getTradeAmount() { + return Coin.valueOf(tradeAmount); + } + + public Volume getTradeVolume() { + try { + return getTradePrice().getVolumeByAmount(getTradeAmount()); + } catch (Throwable t) { + return Volume.parse("0", currency); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java new file mode 100644 index 0000000000..044018c018 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -0,0 +1,213 @@ +/* + * 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 bisq.core.trade.statistics; + +import bisq.core.locale.CurrencyTuple; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.BuyerTrade; +import bisq.core.trade.Trade; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.config.Config; +import bisq.common.file.JsonFileManager; +import bisq.common.util.Utilities; + +import com.google.inject.Inject; + +import javax.inject.Named; +import javax.inject.Singleton; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; + +import java.time.Instant; + +import java.io.File; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Singleton +@Slf4j +public class TradeStatisticsManager { + private final P2PService p2PService; + private final PriceFeedService priceFeedService; + private final TradeStatistics3StorageService tradeStatistics3StorageService; + private final TradeStatisticsConverter tradeStatisticsConverter; + private final File storageDir; + private final boolean dumpStatistics; + private final ObservableSet observableTradeStatisticsSet = FXCollections.observableSet(); + private JsonFileManager jsonFileManager; + + @Inject + public TradeStatisticsManager(P2PService p2PService, + PriceFeedService priceFeedService, + TradeStatistics3StorageService tradeStatistics3StorageService, + AppendOnlyDataStoreService appendOnlyDataStoreService, + TradeStatisticsConverter tradeStatisticsConverter, + @Named(Config.STORAGE_DIR) File storageDir, + @Named(Config.DUMP_STATISTICS) boolean dumpStatistics) { + this.p2PService = p2PService; + this.priceFeedService = priceFeedService; + this.tradeStatistics3StorageService = tradeStatistics3StorageService; + this.tradeStatisticsConverter = tradeStatisticsConverter; + this.storageDir = storageDir; + this.dumpStatistics = dumpStatistics; + + + appendOnlyDataStoreService.addService(tradeStatistics3StorageService); + } + + public void shutDown() { + tradeStatisticsConverter.shutDown(); + if (jsonFileManager != null) { + jsonFileManager.shutDown(); + } + } + + public void onAllServicesInitialized() { + p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { + if (payload instanceof TradeStatistics3) { + TradeStatistics3 tradeStatistics = (TradeStatistics3) payload; + if (!tradeStatistics.isValid()) { + return; + } + observableTradeStatisticsSet.add(tradeStatistics); + priceFeedService.applyLatestBisqMarketPrice(observableTradeStatisticsSet); + maybeDumpStatistics(); + } + }); + + Set set = tradeStatistics3StorageService.getMapOfAllData().values().stream() + .filter(e -> e instanceof TradeStatistics3) + .map(e -> (TradeStatistics3) e) + .filter(TradeStatistics3::isValid) + .collect(Collectors.toSet()); + observableTradeStatisticsSet.addAll(set); + priceFeedService.applyLatestBisqMarketPrice(observableTradeStatisticsSet); + maybeDumpStatistics(); + } + + public ObservableSet getObservableTradeStatisticsSet() { + return observableTradeStatisticsSet; + } + + private void maybeDumpStatistics() { + if (!dumpStatistics) { + return; + } + + if (jsonFileManager == null) { + jsonFileManager = new JsonFileManager(storageDir); + + // We only dump once the currencies as they do not change during runtime + ArrayList fiatCurrencyList = CurrencyUtil.getAllSortedFiatCurrencies().stream() + .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) + .collect(Collectors.toCollection(ArrayList::new)); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(fiatCurrencyList), "fiat_currency_list"); + + ArrayList cryptoCurrencyList = CurrencyUtil.getAllSortedCryptoCurrencies().stream() + .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) + .collect(Collectors.toCollection(ArrayList::new)); + cryptoCurrencyList.add(0, new CurrencyTuple(Res.getBaseCurrencyCode(), Res.getBaseCurrencyName(), 8)); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(cryptoCurrencyList), "crypto_currency_list"); + + Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365)); + Set activeCurrencies = observableTradeStatisticsSet.stream() + .filter(e -> e.getDate().toInstant().isAfter(yearAgo)) + .map(p -> p.getCurrency()) + .collect(Collectors.toSet()); + + ArrayList activeFiatCurrencyList = fiatCurrencyList.stream() + .filter(e -> activeCurrencies.contains(e.code)) + .map(e -> new CurrencyTuple(e.code, e.name, 8)) + .collect(Collectors.toCollection(ArrayList::new)); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeFiatCurrencyList), "active_fiat_currency_list"); + + ArrayList activeCryptoCurrencyList = cryptoCurrencyList.stream() + .filter(e -> activeCurrencies.contains(e.code)) + .map(e -> new CurrencyTuple(e.code, e.name, 8)) + .collect(Collectors.toCollection(ArrayList::new)); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeCryptoCurrencyList), "active_crypto_currency_list"); + } + + List list = observableTradeStatisticsSet.stream() + .map(TradeStatisticsForJson::new) + .sorted((o1, o2) -> (Long.compare(o2.tradeDate, o1.tradeDate))) + .collect(Collectors.toList()); + TradeStatisticsForJson[] array = new TradeStatisticsForJson[list.size()]; + list.toArray(array); + jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(array), "trade_statistics"); + } + + public void maybeRepublishTradeStatistics(Set trades, + @Nullable String referralId, + boolean isTorNetworkNode) { + long ts = System.currentTimeMillis(); + Set hashes = tradeStatistics3StorageService.getMapOfAllData().keySet(); + trades.forEach(trade -> { + if (trade instanceof BuyerTrade) { + log.debug("Trade: {} is a buyer trade, we only republish we have been seller.", + trade.getShortId()); + return; + } + + TradeStatistics3 tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode); + boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); + if (hasTradeStatistics3) { + log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", + trade.getShortId()); + return; + } + + // If we did not find a TradeStatistics3 we look up if we find a TradeStatistics3 converted from + // TradeStatistics2 where we used the original hash, which is not the native hash of the + // TradeStatistics3 but of TradeStatistics2. + TradeStatistics2 tradeStatistics2 = TradeStatistics2.from(trade, referralId, isTorNetworkNode); + boolean hasTradeStatistics2 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics2.getHash())); + if (hasTradeStatistics2) { + log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics2. ", + trade.getShortId()); + return; + } + + if (!tradeStatistics3.isValid()) { + log.warn("Trade: {}. Trade statistics is invalid. We do not publish it.", tradeStatistics3); + return; + } + + log.info("Trade: {}. We republish tradeStatistics3 as we did not find it in the existing trade statistics. ", + trade.getShortId()); + p2PService.addPersistableNetworkPayload(tradeStatistics3, true); + }); + log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}", + System.currentTimeMillis() - ts, hashes.size(), trades.size()); + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofHttpClient.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofHttpClient.java new file mode 100644 index 0000000000..3700ab21e7 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofHttpClient.java @@ -0,0 +1,23 @@ +/* + * 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 bisq.core.trade.txproof; + +import bisq.network.http.HttpClient; + +public interface AssetTxProofHttpClient extends HttpClient { +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofModel.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofModel.java new file mode 100644 index 0000000000..a153a99b28 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofModel.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.core.trade.txproof; + +public interface AssetTxProofModel { +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofParser.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofParser.java new file mode 100644 index 0000000000..ae0a803ff3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofParser.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.core.trade.txproof; + +public interface AssetTxProofParser { + R parse(T model, String jsonTxt); +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequest.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequest.java new file mode 100644 index 0000000000..44835749b0 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequest.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.core.trade.txproof; + +import bisq.common.handlers.FaultHandler; + +import java.util.function.Consumer; + +public interface AssetTxProofRequest { + interface Result { + } + + void requestFromService(Consumer resultHandler, FaultHandler faultHandler); + + void terminate(); +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequestsPerTrade.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequestsPerTrade.java new file mode 100644 index 0000000000..8d1e969dbb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofRequestsPerTrade.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.trade.txproof; + +import bisq.common.handlers.FaultHandler; + +import java.util.function.Consumer; + +public interface AssetTxProofRequestsPerTrade { + void requestFromAllServices(Consumer resultHandler, FaultHandler faultHandler); + + void terminate(); +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java new file mode 100644 index 0000000000..b49d81b660 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.core.trade.txproof; + +import lombok.Getter; + +public enum AssetTxProofResult { + UNDEFINED, + + FEATURE_DISABLED, + TRADE_LIMIT_EXCEEDED, + INVALID_DATA, // Peer provided invalid data. Might be a scam attempt (e.g. txKey reused) + PAYOUT_TX_ALREADY_PUBLISHED, + DISPUTE_OPENED, + + REQUESTS_STARTED(false), + PENDING(false), + + // All services completed with a success state + COMPLETED, + + // Any service had an error (network, API service) + ERROR, + + // Any service failed. Might be that the tx is invalid. + FAILED; + + // If isTerminal is set it means that we stop the service + @Getter + private final boolean isTerminal; + @Getter + private String details = ""; + @Getter + private int numSuccessResults; + @Getter + private int numRequiredSuccessResults; + @Getter + private int numConfirmations; + @Getter + private int numRequiredConfirmations; + + + AssetTxProofResult() { + this(true); + } + + AssetTxProofResult(boolean isTerminal) { + this.isTerminal = isTerminal; + } + + + public AssetTxProofResult numSuccessResults(int numSuccessResults) { + this.numSuccessResults = numSuccessResults; + return this; + } + + public AssetTxProofResult numRequiredSuccessResults(int numRequiredSuccessResults) { + this.numRequiredSuccessResults = numRequiredSuccessResults; + return this; + } + + public AssetTxProofResult numConfirmations(int numConfirmations) { + this.numConfirmations = numConfirmations; + return this; + } + + public AssetTxProofResult numRequiredConfirmations(int numRequiredConfirmations) { + this.numRequiredConfirmations = numRequiredConfirmations; + return this; + } + + public AssetTxProofResult details(String details) { + this.details = details; + return this; + } + + @Override + public String toString() { + return "AssetTxProofResult{" + + "\n details='" + details + '\'' + + ",\n isTerminal=" + isTerminal + + ",\n numSuccessResults=" + numSuccessResults + + ",\n numRequiredSuccessResults=" + numRequiredSuccessResults + + ",\n numConfirmations=" + numConfirmations + + ",\n numRequiredConfirmations=" + numRequiredConfirmations + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java new file mode 100644 index 0000000000..c5c8476a41 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.core.trade.txproof; + +public interface AssetTxProofService { + void onAllServicesInitialized(); + + void shutDown(); +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java new file mode 100644 index 0000000000..ac0a4148de --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java @@ -0,0 +1,32 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.trade.txproof.AssetTxProofHttpClient; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class XmrTxProofHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { + XmrTxProofHttpClient(Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java new file mode 100644 index 0000000000..6d386437be --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java @@ -0,0 +1,102 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.monetary.Volume; +import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; +import bisq.core.trade.txproof.AssetTxProofModel; +import bisq.core.user.AutoConfirmSettings; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.Date; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@SuppressWarnings("SpellCheckingInspection") +@Slf4j +@Value +public class XmrTxProofModel implements AssetTxProofModel { + // Those are values from a valid tx which are set automatically if DevEnv.isDevMode is enabled + public static final String DEV_ADDRESS = "85q13WDADXE26W6h7cStpPMkn8tWpvWgHbpGWWttFEafGXyjsBTXxxyQms4UErouTY5sdKpYHVjQm6SagiCqytseDkzfgub"; + public static final String DEV_TX_KEY = "f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906"; + public static final String DEV_TX_HASH = "5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802"; + public static final long DEV_AMOUNT = 8902597360000L; + + private final String serviceAddress; + private final AutoConfirmSettings autoConfirmSettings; + private final String tradeId; + private final String txHash; + private final String txKey; + private final String recipientAddress; + private final long amount; + private final Date tradeDate; + + XmrTxProofModel(Trade trade, String serviceAddress, AutoConfirmSettings autoConfirmSettings) { + this.serviceAddress = serviceAddress; + this.autoConfirmSettings = autoConfirmSettings; + + Volume volume = trade.getTradeVolume(); + amount = DevEnv.isDevMode() ? + XmrTxProofModel.DEV_AMOUNT : // For dev testing we need to add the matching address to the dev tx key and dev view key + volume != null ? volume.getValue() * 10000L : 0L; // XMR satoshis have 12 decimal places vs. bitcoin's 8 + PaymentAccountPayload sellersPaymentAccountPayload = checkNotNull(trade.getContract()).getSellerPaymentAccountPayload(); + recipientAddress = DevEnv.isDevMode() ? + XmrTxProofModel.DEV_ADDRESS : // For dev testing we need to add the matching address to the dev tx key and dev view key + ((AssetsAccountPayload) sellersPaymentAccountPayload).getAddress(); + txHash = trade.getCounterCurrencyTxId(); + txKey = trade.getCounterCurrencyExtraData(); + tradeDate = trade.getDate(); + tradeId = trade.getId(); + } + + // NumRequiredConfirmations is read just in time. If user changes autoConfirmSettings during requests it will + // be reflected at next result parsing. + int getNumRequiredConfirmations() { + return autoConfirmSettings.getRequiredConfirmations(); + } + + // Used only for testing + // TODO Use mocking framework in testing to avoid that constructor... + @VisibleForTesting + XmrTxProofModel(String tradeId, + String txHash, + String txKey, + String recipientAddress, + long amount, + Date tradeDate, + AutoConfirmSettings autoConfirmSettings) { + this.tradeId = tradeId; + this.txHash = txHash; + this.txKey = txKey; + this.recipientAddress = recipientAddress; + this.amount = amount; + this.tradeDate = tradeDate; + this.autoConfirmSettings = autoConfirmSettings; + this.serviceAddress = autoConfirmSettings.getServiceAddresses().get(0); + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java new file mode 100644 index 0000000000..cff4db23fb --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java @@ -0,0 +1,176 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.trade.txproof.AssetTxProofParser; + +import bisq.asset.CryptoNoteUtils; + +import bisq.common.app.DevEnv; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class XmrTxProofParser implements AssetTxProofParser { + public static final long MAX_DATE_TOLERANCE = TimeUnit.HOURS.toSeconds(2); + + XmrTxProofParser() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("SpellCheckingInspection") + @Override + public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) { + String txHash = model.getTxHash(); + try { + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Empty json")); + } + // there should always be "data" and "status" at the top level + if (json.get("data") == null || !json.get("data").isJsonObject() || json.get("status") == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing data / status fields")); + } + JsonObject jsonData = json.get("data").getAsJsonObject(); + String jsonStatus = json.get("status").getAsString(); + if (jsonStatus.matches("fail")) { + // The API returns "fail" until the transaction has successfully reached the mempool or if request + // contained invalid data. + // We return TX_NOT_FOUND which will cause a retry later + return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.TX_NOT_FOUND); + } else if (!jsonStatus.matches("success")) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Unhandled status value")); + } + + // validate that the address matches + JsonElement jsonAddress = jsonData.get("address"); + if (jsonAddress == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing address field")); + } else { + String expectedAddressHex = CryptoNoteUtils.getRawSpendKeyAndViewKey(model.getRecipientAddress()); + if (!jsonAddress.getAsString().equalsIgnoreCase(expectedAddressHex)) { + log.warn("Address from json result (convertToRawHex):\n{}\nExpected (convertToRawHex):\n{}\nRecipient address:\n{}", + jsonAddress.getAsString(), expectedAddressHex, model.getRecipientAddress()); + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.ADDRESS_INVALID); + } + } + + // validate that the txHash matches + JsonElement jsonTxHash = jsonData.get("tx_hash"); + if (jsonTxHash == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_hash field")); + } else { + if (!jsonTxHash.getAsString().equalsIgnoreCase(txHash)) { + log.warn("txHash {}, expected: {}", jsonTxHash.getAsString(), txHash); + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TX_HASH_INVALID); + } + } + + // validate that the txKey matches + JsonElement jsonViewkey = jsonData.get("viewkey"); + if (jsonViewkey == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing viewkey field")); + } else { + if (!jsonViewkey.getAsString().equalsIgnoreCase(model.getTxKey())) { + log.warn("viewkey {}, expected: {}", jsonViewkey.getAsString(), model.getTxKey()); + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TX_KEY_INVALID); + } + } + + // validate that the txDate matches within tolerance + // (except that in dev mode we let this check pass anyway) + JsonElement jsonTimestamp = jsonData.get("tx_timestamp"); + if (jsonTimestamp == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_timestamp field")); + } else { + long tradeDateSeconds = model.getTradeDate().getTime() / 1000; + long difference = tradeDateSeconds - jsonTimestamp.getAsLong(); + // Accept up to 2 hours difference. Some tolerance is needed if users clock is out of sync + if (difference > MAX_DATE_TOLERANCE && !DevEnv.isDevMode()) { + log.warn("tx_timestamp {}, tradeDate: {}, difference {}", + jsonTimestamp.getAsLong(), tradeDateSeconds, difference); + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING); + } + } + + // calculate how many confirms are still needed + int confirmations; + JsonElement jsonConfirmations = jsonData.get("tx_confirmations"); + if (jsonConfirmations == null) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_confirmations field")); + } else { + confirmations = jsonConfirmations.getAsInt(); + log.info("Confirmations: {}, xmr txHash: {}", confirmations, txHash); + } + + // iterate through the list of outputs, one of them has to match the amount we are trying to verify. + // check that the "match" field is true as well as validating the amount value + // (except that in dev mode we allow any amount as valid) + JsonArray jsonOutputs = jsonData.get("outputs").getAsJsonArray(); + boolean anyMatchFound = false; + boolean amountMatches = false; + for (int i = 0; i < jsonOutputs.size(); i++) { + JsonObject out = jsonOutputs.get(i).getAsJsonObject(); + if (out.get("match").getAsBoolean()) { + anyMatchFound = true; + long jsonAmount = out.get("amount").getAsLong(); + amountMatches = jsonAmount == model.getAmount(); + if (amountMatches) { + break; + } else { + log.warn("amount {}, expected: {}", jsonAmount, model.getAmount()); + } + } + } + + // None of the outputs had a match entry + if (!anyMatchFound) { + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.NO_MATCH_FOUND); + } + + // None of the outputs had a match entry + if (!amountMatches) { + return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.AMOUNT_NOT_MATCHING); + } + + int confirmsRequired = model.getNumRequiredConfirmations(); + if (confirmations < confirmsRequired) { + return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS.numConfirmations(confirmations)); + } else { + return XmrTxProofRequest.Result.SUCCESS.with(XmrTxProofRequest.Detail.SUCCESS.numConfirmations(confirmations)); + } + + } catch (JsonParseException | NullPointerException e) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error(e.toString())); + } catch (CryptoNoteUtils.CryptoNoteException e) { + return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.ADDRESS_INVALID.error(e.toString())); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java new file mode 100644 index 0000000000..5e4e21dd47 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java @@ -0,0 +1,296 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.trade.txproof.AssetTxProofHttpClient; +import bisq.core.trade.txproof.AssetTxProofParser; +import bisq.core.trade.txproof.AssetTxProofRequest; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.handlers.FaultHandler; +import bisq.common.util.Utilities; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +/** + * Requests for the XMR tx proof for a particular trade from a particular service. + * Repeats every 90 sec requests if tx is not confirmed or found yet until MAX_REQUEST_PERIOD of 12 hours is reached. + */ +@Slf4j +@EqualsAndHashCode +class XmrTxProofRequest implements AssetTxProofRequest { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Enums + /////////////////////////////////////////////////////////////////////////////////////////// + + enum Result implements AssetTxProofRequest.Result { + PENDING, // Tx not visible in network yet, unconfirmed or not enough confirmations + SUCCESS, // Proof succeeded + FAILED, // Proof failed + ERROR; // Error from service, does not mean that proof failed + + @Nullable + @Getter + private Detail detail; + + Result with(Detail detail) { + this.detail = detail; + return this; + } + + @Override + public String toString() { + return "Result{" + + "\n detail=" + detail + + "\n} " + super.toString(); + } + } + + enum Detail { + // Pending + TX_NOT_FOUND, // Tx not visible in network yet. Could be also other error + PENDING_CONFIRMATIONS, + + SUCCESS, + + // Error states + CONNECTION_FAILURE, + API_INVALID, + + // Failure states + TX_HASH_INVALID, + TX_KEY_INVALID, + ADDRESS_INVALID, + NO_MATCH_FOUND, + AMOUNT_NOT_MATCHING, + TRADE_DATE_NOT_MATCHING, + NO_RESULTS_TIMEOUT; + + @Getter + private int numConfirmations; + @Nullable + @Getter + private String errorMsg; + + public Detail error(String errorMsg) { + this.errorMsg = errorMsg; + return this; + } + + public Detail numConfirmations(int numConfirmations) { + this.numConfirmations = numConfirmations; + return this; + } + + @Override + public String toString() { + return "Detail{" + + "\n numConfirmations=" + numConfirmations + + ",\n errorMsg='" + errorMsg + '\'' + + "\n} " + super.toString(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final long REPEAT_REQUEST_PERIOD = TimeUnit.SECONDS.toMillis(90); + private static final long MAX_REQUEST_PERIOD = TimeUnit.HOURS.toMillis(12); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final ListeningExecutorService executorService = Utilities.getListeningExecutorService( + "XmrTransferProofRequester", 3, 5, 10 * 60); + + private final AssetTxProofParser parser; + private final XmrTxProofModel model; + private final AssetTxProofHttpClient httpClient; + private final long firstRequest; + + private boolean terminated; + @Getter + @Nullable + private Result result; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider, + XmrTxProofModel model) { + this.parser = new XmrTxProofParser(); + this.model = model; + + httpClient = new XmrTxProofHttpClient(socks5ProxyProvider); + + // localhost, LAN address, or *.local FQDN starts with http://, don't use Tor + if (model.getServiceAddress().regionMatches(0, "http:", 0, 5)) { + httpClient.setBaseUrl(model.getServiceAddress()); + httpClient.setIgnoreSocks5Proxy(true); + // any non-onion FQDN starts with https://, use Tor + } else if (model.getServiceAddress().regionMatches(0, "https:", 0, 6)) { + httpClient.setBaseUrl(model.getServiceAddress()); + httpClient.setIgnoreSocks5Proxy(false); + // it's a raw onion so add http:// and use Tor proxy + } else { + httpClient.setBaseUrl("http://" + model.getServiceAddress()); + httpClient.setIgnoreSocks5Proxy(false); + } + + terminated = false; + firstRequest = System.currentTimeMillis(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("SpellCheckingInspection") + @Override + public void requestFromService(Consumer resultHandler, FaultHandler faultHandler) { + if (terminated) { + // the XmrTransferProofService has asked us to terminate i.e. not make any further api calls + // this scenario may happen if a re-request is scheduled from the callback below + log.warn("Not starting {} as we have already terminated.", this); + return; + } + + if (httpClient.hasPendingRequest()) { + log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient); + return; + } + + // Timeout handing is delegated to the connection timeout handling in httpClient. + + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId()); + String param = "/api/outputs?txhash=" + model.getTxHash() + + "&address=" + model.getRecipientAddress() + + "&viewkey=" + model.getTxKey() + + "&txprove=1"; + log.info("Param {} for {}", param, this); + String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION); + try { + String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json)); + log.info("Response json from {}\n{}", this, prettyJson); + } catch (Throwable error) { + log.error("Pretty print caused a {}: raw json={}", error, json); + } + + Result result = parser.parse(model, json); + log.info("Result from {}\n{}", this, result); + return result; + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(Result result) { + XmrTxProofRequest.this.result = result; + + if (terminated) { + log.warn("We received {} but {} was terminated already. We do not process result.", result, this); + return; + } + + switch (result) { + case PENDING: + if (isTimeOutReached()) { + log.warn("{} took too long without a success or failure/error result We give up. " + + "Might be that the transaction was never published.", this); + // If we reached out timeout we return with an error. + UserThread.execute(() -> resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.NO_RESULTS_TIMEOUT))); + } else { + UserThread.runAfter(() -> requestFromService(resultHandler, faultHandler), REPEAT_REQUEST_PERIOD, TimeUnit.MILLISECONDS); + // We update our listeners + UserThread.execute(() -> resultHandler.accept(result)); + } + break; + case SUCCESS: + log.info("{} succeeded", result); + UserThread.execute(() -> resultHandler.accept(result)); + terminate(); + break; + case FAILED: + case ERROR: + UserThread.execute(() -> resultHandler.accept(result)); + terminate(); + break; + default: + log.warn("Unexpected result {}", result); + break; + } + } + + public void onFailure(@NotNull Throwable throwable) { + String errorMessage = this + " failed with error " + throwable.toString(); + faultHandler.handleFault(errorMessage, throwable); + UserThread.execute(() -> + resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage)))); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public void terminate() { + terminated = true; + } + + // Convenient for logging + @Override + public String toString() { + return "Request at: " + model.getServiceAddress() + " for trade: " + model.getTradeId(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getShortId() { + return Utilities.getShortId(model.getTradeId()) + " @ " + model.getServiceAddress().substring(0, 6); + } + + private boolean isTimeOutReached() { + return System.currentTimeMillis() - firstRequest > MAX_REQUEST_PERIOD; + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java new file mode 100644 index 0000000000..2d0efe887d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java @@ -0,0 +1,343 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.filter.FilterManager; +import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.Trade; +import bisq.core.trade.txproof.AssetTxProofRequestsPerTrade; +import bisq.core.trade.txproof.AssetTxProofResult; +import bisq.core.user.AutoConfirmSettings; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.handlers.FaultHandler; + +import org.bitcoinj.core.Coin; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Handles the XMR tx proof requests for multiple services per trade. + */ +@Slf4j +class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { + @Getter + private final Trade trade; + private final AutoConfirmSettings autoConfirmSettings; + private final MediationManager mediationManager; + private final FilterManager filterManager; + private final RefundManager refundManager; + private final Socks5ProxyProvider socks5ProxyProvider; + + private int numRequiredSuccessResults; + private final Set requests = new HashSet<>(); + + private int numSuccessResults; + private ChangeListener tradeStateListener; + private AutoConfirmSettings.Listener autoConfirmSettingsListener; + private ListChangeListener mediationListener, refundListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + XmrTxProofRequestsPerTrade(Socks5ProxyProvider socks5ProxyProvider, + Trade trade, + AutoConfirmSettings autoConfirmSettings, + MediationManager mediationManager, + FilterManager filterManager, + RefundManager refundManager) { + this.socks5ProxyProvider = socks5ProxyProvider; + this.trade = trade; + this.autoConfirmSettings = autoConfirmSettings; + this.mediationManager = mediationManager; + this.filterManager = filterManager; + this.refundManager = refundManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void requestFromAllServices(Consumer resultHandler, FaultHandler faultHandler) { + // isTradeAmountAboveLimit + if (isTradeAmountAboveLimit(trade)) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.TRADE_LIMIT_EXCEEDED); + return; + } + + // isPayoutPublished + if (trade.isPayoutPublished()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED); + return; + } + + // IsEnabled() + // We will stop all our services if the user changes the enable state in the AutoConfirmSettings + if (!autoConfirmSettings.isEnabled()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED); + return; + } + addSettingsListener(resultHandler); + + // TradeState + setupTradeStateListener(resultHandler); + // We checked initially for current trade state so no need to check again here + + // Check if mediation dispute and add listener + ObservableList mediationDisputes = mediationManager.getDisputesAsObservableList(); + if (isDisputed(mediationDisputes)) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + return; + } + setupMediationListener(resultHandler, mediationDisputes); + + // Check if arbitration dispute and add listener + ObservableList refundDisputes = refundManager.getDisputesAsObservableList(); + if (isDisputed(refundDisputes)) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + return; + } + setupArbitrationListener(resultHandler, refundDisputes); + + // All good so we start + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.REQUESTS_STARTED); + + // We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started + // it will have no impact on serviceAddresses and numRequiredSuccessResults. + // Thought numRequiredConfirmations can be changed during request process and will be read from + // autoConfirmSettings at result parsing. + List serviceAddresses = autoConfirmSettings.getServiceAddresses(); + numRequiredSuccessResults = serviceAddresses.size(); + + for (String serviceAddress : serviceAddresses) { + if (filterManager.isAutoConfExplorerBanned(serviceAddress)) { + log.warn("Filtered out auto-confirmation address: {}", serviceAddress); + continue; // #4683: filter for auto-confirm explorers + } + XmrTxProofModel model = new XmrTxProofModel(trade, serviceAddress, autoConfirmSettings); + XmrTxProofRequest request = new XmrTxProofRequest(socks5ProxyProvider, model); + + log.info("{} created", request); + requests.add(request); + + request.requestFromService(result -> { + // If we ever received an error or failed result we terminate and do not process any + // future result anymore to avoid that we overwrite out state with success. + if (wasTerminated()) { + return; + } + + AssetTxProofResult assetTxProofResult; + if (trade.isPayoutPublished()) { + assetTxProofResult = AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED; + callResultHandlerAndMaybeTerminate(resultHandler, assetTxProofResult); + return; + } + + switch (result) { + case PENDING: + // We expect repeated PENDING results with different details + assetTxProofResult = getAssetTxProofResultForPending(result); + break; + case SUCCESS: + numSuccessResults++; + if (numSuccessResults < numRequiredSuccessResults) { + // Request is success but not all have completed yet. + int remaining = numRequiredSuccessResults - numSuccessResults; + log.info("{} succeeded. We have {} remaining request(s) open.", + request, remaining); + assetTxProofResult = getAssetTxProofResultForPending(result); + } else { + // All our services have returned a SUCCESS result so we + // have completed on the service level. + log.info("All {} tx proof requests for trade {} have been successful.", + numRequiredSuccessResults, trade.getShortId()); + XmrTxProofRequest.Detail detail = result.getDetail(); + assetTxProofResult = AssetTxProofResult.COMPLETED + .numSuccessResults(numSuccessResults) + .numRequiredSuccessResults(numRequiredSuccessResults) + .numConfirmations(detail != null ? detail.getNumConfirmations() : 0) + .numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations()); + } + break; + case FAILED: + log.warn("{} failed. " + + "This might not mean that the XMR transfer was invalid but you have to check yourself " + + "if the XMR transfer was correct. {}", + request, result); + + assetTxProofResult = AssetTxProofResult.FAILED; + break; + case ERROR: + default: + log.warn("{} resulted in an error. " + + "This might not mean that the XMR transfer was invalid but can be a network or " + + "service problem. {}", + request, result); + + assetTxProofResult = AssetTxProofResult.ERROR; + break; + } + + callResultHandlerAndMaybeTerminate(resultHandler, assetTxProofResult); + }, + faultHandler); + } + } + + private boolean wasTerminated() { + return requests.isEmpty(); + } + + private void addSettingsListener(Consumer resultHandler) { + autoConfirmSettingsListener = () -> { + if (!autoConfirmSettings.isEnabled()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED); + } + }; + autoConfirmSettings.addListener(autoConfirmSettingsListener); + } + + private void setupTradeStateListener(Consumer resultHandler) { + tradeStateListener = (observable, oldValue, newValue) -> { + if (trade.isPayoutPublished()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED); + } + }; + trade.stateProperty().addListener(tradeStateListener); + } + + private void setupArbitrationListener(Consumer resultHandler, + ObservableList refundDisputes) { + refundListener = c -> { + c.next(); + if (c.wasAdded() && isDisputed(c.getAddedSubList())) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + } + }; + refundDisputes.addListener(refundListener); + } + + private void setupMediationListener(Consumer resultHandler, + ObservableList mediationDisputes) { + mediationListener = c -> { + c.next(); + if (c.wasAdded() && isDisputed(c.getAddedSubList())) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + } + }; + mediationDisputes.addListener(mediationListener); + } + + @Override + public void terminate() { + requests.forEach(XmrTxProofRequest::terminate); + requests.clear(); + + if (tradeStateListener != null) { + trade.stateProperty().removeListener(tradeStateListener); + } + + if (autoConfirmSettingsListener != null) { + autoConfirmSettings.removeListener(autoConfirmSettingsListener); + } + + if (mediationListener != null) { + mediationManager.getDisputesAsObservableList().removeListener(mediationListener); + } + + if (refundListener != null) { + refundManager.getDisputesAsObservableList().removeListener(refundListener); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void callResultHandlerAndMaybeTerminate(Consumer resultHandler, + AssetTxProofResult assetTxProofResult) { + resultHandler.accept(assetTxProofResult); + if (assetTxProofResult.isTerminal()) { + terminate(); + } + } + + private AssetTxProofResult getAssetTxProofResultForPending(XmrTxProofRequest.Result result) { + XmrTxProofRequest.Detail detail = result.getDetail(); + int numConfirmations = detail != null ? detail.getNumConfirmations() : 0; + log.info("{} returned with numConfirmations {}", + result, numConfirmations); + + String detailString = ""; + if (XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS == detail) { + detailString = Res.get("portfolio.pending.autoConf.state.confirmations", + numConfirmations, autoConfirmSettings.getRequiredConfirmations()); + + } else if (XmrTxProofRequest.Detail.TX_NOT_FOUND == detail) { + detailString = Res.get("portfolio.pending.autoConf.state.txNotFound"); + } + + return AssetTxProofResult.PENDING + .numSuccessResults(numSuccessResults) + .numRequiredSuccessResults(numRequiredSuccessResults) + .numConfirmations(detail != null ? detail.getNumConfirmations() : 0) + .numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations()) + .details(detailString); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isTradeAmountAboveLimit(Trade trade) { + Coin tradeAmount = trade.getTradeAmount(); + Coin tradeLimit = Coin.valueOf(autoConfirmSettings.getTradeLimit()); + if (tradeAmount != null && tradeAmount.isGreaterThan(tradeLimit)) { + log.warn("Trade amount {} is higher than limit from auto-conf setting {}.", + tradeAmount.toFriendlyString(), tradeLimit.toFriendlyString()); + return true; + } + return false; + } + + private boolean isDisputed(List disputes) { + return disputes.stream().anyMatch(e -> e.getTradeId().equals(trade.getId())); + } +} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java new file mode 100644 index 0000000000..469b1b4fc4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -0,0 +1,397 @@ +/* + * 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 bisq.core.trade.txproof.xmr; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.filter.FilterManager; +import bisq.core.locale.Res; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.SellerTrade; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; +import bisq.core.trade.protocol.SellerProtocol; +import bisq.core.trade.txproof.AssetTxProofResult; +import bisq.core.trade.txproof.AssetTxProofService; +import bisq.core.user.AutoConfirmSettings; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; + +import bisq.common.app.DevEnv; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Entry point for clients to request tx proof and trigger auto-confirm if all conditions + * are met. + */ +@Slf4j +@Singleton +public class XmrTxProofService implements AssetTxProofService { + private final FilterManager filterManager; + private final Preferences preferences; + private final TradeManager tradeManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; + private final MediationManager mediationManager; + private final RefundManager refundManager; + private final P2PService p2PService; + private final WalletsSetup walletsSetup; + private final Socks5ProxyProvider socks5ProxyProvider; + private final Map servicesByTradeId = new HashMap<>(); + private AutoConfirmSettings autoConfirmSettings; + private final Map> tradeStateListenerMap = new HashMap<>(); + private ChangeListener btcPeersListener, btcBlockListener; + private BootstrapListener bootstrapListener; + private MonadicBinding p2pNetworkAndWalletReady; + private ChangeListener p2pNetworkAndWalletReadyListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess") + @Inject + public XmrTxProofService(FilterManager filterManager, + Preferences preferences, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, + MediationManager mediationManager, + RefundManager refundManager, + P2PService p2PService, + WalletsSetup walletsSetup, + Socks5ProxyProvider socks5ProxyProvider) { + this.filterManager = filterManager; + this.preferences = preferences; + this.tradeManager = tradeManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; + this.mediationManager = mediationManager; + this.refundManager = refundManager; + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + this.socks5ProxyProvider = socks5ProxyProvider; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAllServicesInitialized() { + // As we might trigger the payout tx we want to be sure that we are well connected to the Bitcoin network. + // onAllServicesInitialized is called once we have received the initial data but we want to have our + // hidden service published and upDatedDataResponse received before we start. + BooleanProperty isP2pBootstrapped = isP2pBootstrapped(); + BooleanProperty hasSufficientBtcPeers = hasSufficientBtcPeers(); + BooleanProperty isBtcBlockDownloadComplete = isBtcBlockDownloadComplete(); + if (isP2pBootstrapped.get() && hasSufficientBtcPeers.get() && isBtcBlockDownloadComplete.get()) { + onP2pNetworkAndWalletReady(); + } else { + p2pNetworkAndWalletReady = EasyBind.combine(isP2pBootstrapped, hasSufficientBtcPeers, isBtcBlockDownloadComplete, + (bootstrapped, sufficientPeers, downloadComplete) -> + bootstrapped && sufficientPeers && downloadComplete); + + p2pNetworkAndWalletReadyListener = (observable, oldValue, newValue) -> { + if (newValue) { + onP2pNetworkAndWalletReady(); + } + }; + p2pNetworkAndWalletReady.subscribe(p2pNetworkAndWalletReadyListener); + } + } + + @Override + public void shutDown() { + servicesByTradeId.values().forEach(XmrTxProofRequestsPerTrade::terminate); + servicesByTradeId.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onP2pNetworkAndWalletReady() { + if (p2pNetworkAndWalletReady != null) { + p2pNetworkAndWalletReady.removeListener(p2pNetworkAndWalletReadyListener); + p2pNetworkAndWalletReady = null; + p2pNetworkAndWalletReadyListener = null; + } + + if (!preferences.findAutoConfirmSettings("XMR").isPresent()) { + log.error("AutoConfirmSettings is not present"); + return; + } + autoConfirmSettings = preferences.findAutoConfirmSettings("XMR").get(); + + // We register a listener to stop running services. For new trades we check anyway in the trade validation + filterManager.filterProperty().addListener((observable, oldValue, newValue) -> { + if (isAutoConfDisabledByFilter()) { + servicesByTradeId.values().stream().map(XmrTxProofRequestsPerTrade::getTrade).forEach(trade -> + trade.setAssetTxProofResult(AssetTxProofResult.FEATURE_DISABLED + .details(Res.get("portfolio.pending.autoConf.state.filterDisabledFeature")))); + tradeManager.requestPersistence(); + shutDown(); + } + }); + + // We listen on new trades + ObservableList tradableList = tradeManager.getObservableList(); + tradableList.addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + processTrades(c.getAddedSubList()); + } + }); + + // Process existing trades + processTrades(tradableList); + } + + private void processTrades(List trades) { + trades.stream() + .filter(trade -> trade instanceof SellerTrade) + .map(trade -> (SellerTrade) trade) + .filter(this::isXmrTrade) + .filter(trade -> !trade.isFiatReceived()) // Phase name is from the time when it was fiat only. Means counter currency (XMR) received. + .forEach(this::processTradeOrAddListener); + } + + // Basic requirements are fulfilled. + // We process further if we are in the expected state or register a listener + private void processTradeOrAddListener(SellerTrade trade) { + if (isExpectedTradeState(trade.getState())) { + startRequestsIfValid(trade); + } else { + // We are expecting SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG in the future, so listen on changes + ChangeListener tradeStateListener = (observable, oldValue, newValue) -> { + if (isExpectedTradeState(newValue)) { + ChangeListener listener = tradeStateListenerMap.remove(trade.getId()); + if (listener != null) { + trade.stateProperty().removeListener(listener); + } + + startRequestsIfValid(trade); + } + }; + tradeStateListenerMap.put(trade.getId(), tradeStateListener); + trade.stateProperty().addListener(tradeStateListener); + } + } + + private void startRequestsIfValid(SellerTrade trade) { + String txId = trade.getCounterCurrencyTxId(); + String txHash = trade.getCounterCurrencyExtraData(); + if (is32BitHexStringInValid(txId) || is32BitHexStringInValid(txHash)) { + trade.setAssetTxProofResult(AssetTxProofResult.INVALID_DATA.details(Res.get("portfolio.pending.autoConf.state.txKeyOrTxIdInvalid"))); + tradeManager.requestPersistence(); + return; + } + + if (isAutoConfDisabledByFilter()) { + trade.setAssetTxProofResult(AssetTxProofResult.FEATURE_DISABLED + .details(Res.get("portfolio.pending.autoConf.state.filterDisabledFeature"))); + tradeManager.requestPersistence(); + return; + } + + if (wasTxKeyReUsed(trade, tradeManager.getObservableList())) { + trade.setAssetTxProofResult(AssetTxProofResult.INVALID_DATA + .details(Res.get("portfolio.pending.autoConf.state.xmr.txKeyReused"))); + tradeManager.requestPersistence(); + return; + } + + startRequests(trade); + } + + private void startRequests(SellerTrade trade) { + XmrTxProofRequestsPerTrade service = new XmrTxProofRequestsPerTrade(socks5ProxyProvider, + trade, + autoConfirmSettings, + mediationManager, + filterManager, + refundManager); + servicesByTradeId.put(trade.getId(), service); + service.requestFromAllServices( + assetTxProofResult -> { + trade.setAssetTxProofResult(assetTxProofResult); + + if (assetTxProofResult == AssetTxProofResult.COMPLETED) { + log.info("###########################################################################################"); + log.info("We auto-confirm trade {} as our all our services for the tx proof completed successfully", trade.getShortId()); + log.info("###########################################################################################"); + + ((SellerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentReceived(() -> { + }, errorMessage -> { + }); + } + + if (assetTxProofResult.isTerminal()) { + servicesByTradeId.remove(trade.getId()); + } + + tradeManager.requestPersistence(); + }, + (errorMessage, throwable) -> { + log.error(errorMessage); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Startup checks + /////////////////////////////////////////////////////////////////////////////////////////// + + private BooleanProperty isBtcBlockDownloadComplete() { + BooleanProperty result = new SimpleBooleanProperty(); + if (walletsSetup.isDownloadComplete()) { + result.set(true); + } else { + btcBlockListener = (observable, oldValue, newValue) -> { + if (walletsSetup.isDownloadComplete()) { + walletsSetup.downloadPercentageProperty().removeListener(btcBlockListener); + result.set(true); + } + }; + walletsSetup.downloadPercentageProperty().addListener(btcBlockListener); + } + return result; + } + + private BooleanProperty hasSufficientBtcPeers() { + BooleanProperty result = new SimpleBooleanProperty(); + if (walletsSetup.hasSufficientPeersForBroadcast()) { + result.set(true); + } else { + btcPeersListener = (observable, oldValue, newValue) -> { + if (walletsSetup.hasSufficientPeersForBroadcast()) { + walletsSetup.numPeersProperty().removeListener(btcPeersListener); + result.set(true); + } + }; + walletsSetup.numPeersProperty().addListener(btcPeersListener); + } + return result; + } + + private BooleanProperty isP2pBootstrapped() { + BooleanProperty result = new SimpleBooleanProperty(); + if (p2PService.isBootstrapped()) { + result.set(true); + } else { + bootstrapListener = new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + p2PService.removeP2PServiceListener(bootstrapListener); + result.set(true); + } + }; + p2PService.addP2PServiceListener(bootstrapListener); + } + return result; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isXmrTrade(Trade trade) { + return (checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR")); + } + + private boolean isExpectedTradeState(Trade.State newValue) { + return newValue == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; + } + + private boolean is32BitHexStringInValid(String hexString) { + if (hexString == null || hexString.isEmpty() || !hexString.matches("[a-fA-F0-9]{64}")) { + log.warn("Invalid hexString: {}", hexString); + return true; + } + + return false; + } + + private boolean isAutoConfDisabledByFilter() { + return filterManager.getFilter() != null && + filterManager.getFilter().isDisableAutoConf(); + } + + private boolean wasTxKeyReUsed(Trade trade, List activeTrades) { + // For dev testing we reuse test data so we ignore that check + if (DevEnv.isDevMode()) { + return false; + } + + // We need to prevent that a user tries to scam by reusing a txKey and txHash of a previous XMR trade with + // the same user (same address) and same amount. We check only for the txKey as a same txHash but different + // txKey is not possible to get a valid result at proof. + Stream failedAndOpenTrades = Stream.concat(activeTrades.stream(), failedTradesManager.getObservableList().stream()); + Stream closedTrades = closedTradableManager.getObservableList().stream() + .filter(tradable -> tradable instanceof Trade) + .map(tradable -> (Trade) tradable); + Stream allTrades = Stream.concat(failedAndOpenTrades, closedTrades); + String txKey = trade.getCounterCurrencyExtraData(); + return allTrades + .filter(t -> !t.getId().equals(trade.getId())) // ignore same trade + .anyMatch(t -> { + String extra = t.getCounterCurrencyExtraData(); + if (extra == null) { + return false; + } + + boolean alreadyUsed = extra.equals(txKey); + if (alreadyUsed) { + log.warn("Peer used the XMR tx key already at another trade with trade ID {}. " + + "This might be a scam attempt.", t.getId()); + } + return alreadyUsed; + }); + } +} diff --git a/core/src/main/java/bisq/core/user/AutoConfirmSettings.java b/core/src/main/java/bisq/core/user/AutoConfirmSettings.java new file mode 100644 index 0000000000..f94e053849 --- /dev/null +++ b/core/src/main/java/bisq/core/user/AutoConfirmSettings.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.user; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.Message; + +import org.bitcoinj.core.Coin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +public final class AutoConfirmSettings implements PersistablePayload { + public interface Listener { + void onChange(); + } + + private boolean enabled; + private int requiredConfirmations; + private long tradeLimit; + private List serviceAddresses; + private String currencyCode; + private List listeners = new CopyOnWriteArrayList<>(); + + @SuppressWarnings("SameParameterValue") + static Optional getDefault(List serviceAddresses, String currencyCode) { + //noinspection SwitchStatementWithTooFewBranches + switch (currencyCode) { + case "XMR": + return Optional.of(new AutoConfirmSettings( + false, + 5, + Coin.COIN.value, + serviceAddresses, + "XMR")); + default: + log.error("No AutoConfirmSettings supported yet for currency {}", currencyCode); + return Optional.empty(); + } + } + + public AutoConfirmSettings(boolean enabled, + int requiredConfirmations, + long tradeLimit, + List serviceAddresses, + String currencyCode) { + this.enabled = enabled; + this.requiredConfirmations = requiredConfirmations; + this.tradeLimit = tradeLimit; + this.serviceAddresses = serviceAddresses; + this.currencyCode = currencyCode; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + return protobuf.AutoConfirmSettings.newBuilder() + .setEnabled(enabled) + .setRequiredConfirmations(requiredConfirmations) + .setTradeLimit(tradeLimit) + .addAllServiceAddresses(serviceAddresses) + .setCurrencyCode(currencyCode) + .build(); + } + + public static AutoConfirmSettings fromProto(protobuf.AutoConfirmSettings proto) { + List serviceAddresses = proto.getServiceAddressesList().isEmpty() ? + new ArrayList<>() : new ArrayList<>(proto.getServiceAddressesList()); + return new AutoConfirmSettings( + proto.getEnabled(), + proto.getRequiredConfirmations(), + proto.getTradeLimit(), + serviceAddresses, + proto.getCurrencyCode()); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + private void notifyListeners() { + listeners.forEach(Listener::onChange); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + notifyListeners(); + } + + public void setRequiredConfirmations(int requiredConfirmations) { + this.requiredConfirmations = requiredConfirmations; + notifyListeners(); + } + + public void setTradeLimit(long tradeLimit) { + this.tradeLimit = tradeLimit; + notifyListeners(); + } + + public void setServiceAddresses(List serviceAddresses) { + this.serviceAddresses = serviceAddresses; + notifyListeners(); + } + + public void setCurrencyCode(String currencyCode) { + this.currencyCode = currencyCode; + notifyListeners(); + } +} diff --git a/core/src/main/java/bisq/core/user/BlockChainExplorer.java b/core/src/main/java/bisq/core/user/BlockChainExplorer.java new file mode 100644 index 0000000000..a20296ea81 --- /dev/null +++ b/core/src/main/java/bisq/core/user/BlockChainExplorer.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.user; + +import bisq.common.proto.persistable.PersistablePayload; + +import com.google.protobuf.Message; + +public final class BlockChainExplorer implements PersistablePayload { + public final String name; + public final String txUrl; + public final String addressUrl; + + public BlockChainExplorer(String name, String txUrl, String addressUrl) { + this.name = name; + this.txUrl = txUrl; + this.addressUrl = addressUrl; + } + + @Override + public Message toProtoMessage() { + return protobuf.BlockChainExplorer.newBuilder().setName(name).setTxUrl(txUrl).setAddressUrl(addressUrl).build(); + } + + public static BlockChainExplorer fromProto(protobuf.BlockChainExplorer proto) { + return new BlockChainExplorer(proto.getName(), + proto.getTxUrl(), + proto.getAddressUrl()); + } +} diff --git a/core/src/main/java/bisq/core/user/Cookie.java b/core/src/main/java/bisq/core/user/Cookie.java new file mode 100644 index 0000000000..f57554b6ff --- /dev/null +++ b/core/src/main/java/bisq/core/user/Cookie.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.user; + +import bisq.common.proto.ProtoUtil; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nullable; + +/** + * Serves as flexible container for persisting UI states, layout,... + * Should not be over-used for domain specific data where type safety and data integrity is important. + */ +public class Cookie extends HashMap { + + public void putAsDouble(CookieKey key, double value) { + put(key, String.valueOf(value)); + } + + public Optional getAsOptionalDouble(CookieKey key) { + try { + return containsKey(key) ? + Optional.of(Double.parseDouble(get(key))) : + Optional.empty(); + } catch (Throwable t) { + return Optional.empty(); + } + } + + public void putAsBoolean(CookieKey key, boolean value) { + put(key, value ? "1" : "0"); + } + + public Optional getAsOptionalBoolean(CookieKey key) { + return containsKey(key) ? + Optional.of(get(key).equals("1")) : + Optional.empty(); + } + + public Map toProtoMessage() { + Map protoMap = new HashMap<>(); + this.forEach((key, value) -> { + if (key != null) { + String name = key.name(); + protoMap.put(name, value); + } + }); + return protoMap; + } + + public static Cookie fromProto(@Nullable Map protoMap) { + Cookie cookie = new Cookie(); + if (protoMap != null) { + protoMap.forEach((key, value) -> cookie.put(ProtoUtil.enumFromProto(CookieKey.class, key), value)); + } + return cookie; + } + + +} diff --git a/core/src/main/java/bisq/core/user/CookieKey.java b/core/src/main/java/bisq/core/user/CookieKey.java new file mode 100644 index 0000000000..2dc3c8a043 --- /dev/null +++ b/core/src/main/java/bisq/core/user/CookieKey.java @@ -0,0 +1,28 @@ +/* + * 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 bisq.core.user; + +// Used for persistence of Cookie. Entries must not be changes or removed. Only adding entries is permitted. +public enum CookieKey { + STAGE_X, + STAGE_Y, + STAGE_W, + STAGE_H, + TRADE_STAT_CHART_USE_USD, + CLEAN_TOR_DIR_AT_RESTART +} diff --git a/core/src/main/java/bisq/core/user/DontShowAgainLookup.java b/core/src/main/java/bisq/core/user/DontShowAgainLookup.java new file mode 100644 index 0000000000..7bb95e98e0 --- /dev/null +++ b/core/src/main/java/bisq/core/user/DontShowAgainLookup.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.core.user; + +public class DontShowAgainLookup { + + private static Preferences preferences; + + public static void setPreferences(Preferences preferences) { + DontShowAgainLookup.preferences = preferences; + } + + public static boolean showAgain(String key) { + return preferences.showAgain(key); + } + + public static void dontShowAgain(String key, boolean dontShowAgain) { + preferences.dontShowAgain(key, dontShowAgain); + } +} diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java new file mode 100644 index 0000000000..644cbf5e27 --- /dev/null +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -0,0 +1,1120 @@ +/* + * 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 bisq.core.user; + +import bisq.core.btc.nodes.BtcNodes; +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.btc.wallet.Restrictions; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.provider.fee.FeeService; +import bisq.core.setup.CoreNetworkCapabilities; + +import bisq.network.p2p.network.BridgeAddressProvider; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class Preferences implements PersistedDataHost, BridgeAddressProvider { + + private static final ArrayList BTC_MAIN_NET_EXPLORERS = new ArrayList<>(Arrays.asList( + new BlockChainExplorer("mempool.space (@wiz)", "https://mempool.space/tx/", "https://mempool.space/address/"), + new BlockChainExplorer("mempool.space Tor V3", "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/tx/", "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/"), + new BlockChainExplorer("mempool.emzy.de (@emzy)", "https://mempool.emzy.de/tx/", "https://mempool.emzy.de/address/"), + new BlockChainExplorer("mempool.emzy.de Tor V3", "http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/tx/", "http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/address/"), + new BlockChainExplorer("mempool.bisq.services (@devinbileck)", "https://mempool.bisq.services/tx/", "https://mempool.bisq.services/address/"), + new BlockChainExplorer("mempool.bisq.services Tor V3", "http://mempoolusb2f67qi7mz2it7n5e77a6komdzx6wftobcduxszkdfun2yd.onion/tx/", "http://mempoolusb2f67qi7mz2it7n5e77a6komdzx6wftobcduxszkdfun2yd.onion/address/"), + new BlockChainExplorer("Blockstream.info", "https://blockstream.info/tx/", "https://blockstream.info/address/"), + new BlockChainExplorer("Blockstream.info Tor V3", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/tx/", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/address/"), + new BlockChainExplorer("OXT", "https://oxt.me/transaction/", "https://oxt.me/address/"), + new BlockChainExplorer("Bitaps", "https://bitaps.com/", "https://bitaps.com/"), + new BlockChainExplorer("Blockcypher", "https://live.blockcypher.com/btc/tx/", "https://live.blockcypher.com/btc/address/"), + new BlockChainExplorer("Tradeblock", "https://tradeblock.com/bitcoin/tx/", "https://tradeblock.com/bitcoin/address/"), + new BlockChainExplorer("Biteasy", "https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/"), + new BlockChainExplorer("Blockonomics", "https://www.blockonomics.co/api/tx?txid=", "https://www.blockonomics.co/#/search?q="), + new BlockChainExplorer("Chainflyer", "http://chainflyer.bitflyer.jp/Transaction/", "http://chainflyer.bitflyer.jp/Address/"), + new BlockChainExplorer("Smartbit", "https://www.smartbit.com.au/tx/", "https://www.smartbit.com.au/address/"), + new BlockChainExplorer("SoChain. Wow.", "https://chain.so/tx/BTC/", "https://chain.so/address/BTC/"), + new BlockChainExplorer("Blockchain.info", "https://blockchain.info/tx/", "https://blockchain.info/address/"), + new BlockChainExplorer("Insight", "https://insight.bitpay.com/tx/", "https://insight.bitpay.com/address/"), + new BlockChainExplorer("Blockchair", "https://blockchair.com/bitcoin/transaction/", "https://blockchair.com/bitcoin/address/") + )); + private static final ArrayList BTC_TEST_NET_EXPLORERS = new ArrayList<>(Arrays.asList( + new BlockChainExplorer("Blockstream.info", "https://blockstream.info/testnet/tx/", "https://blockstream.info/testnet/address/"), + new BlockChainExplorer("Blockstream.info Tor V3", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/tx/", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/address/"), + new BlockChainExplorer("Blockcypher", "https://live.blockcypher.com/btc-testnet/tx/", "https://live.blockcypher.com/btc-testnet/address/"), + new BlockChainExplorer("Blocktrail", "https://www.blocktrail.com/tBTC/tx/", "https://www.blocktrail.com/tBTC/address/"), + new BlockChainExplorer("Biteasy", "https://www.biteasy.com/testnet/transactions/", "https://www.biteasy.com/testnet/addresses/"), + new BlockChainExplorer("Smartbit", "https://testnet.smartbit.com.au/tx/", "https://testnet.smartbit.com.au/address/"), + new BlockChainExplorer("SoChain. Wow.", "https://chain.so/tx/BTCTEST/", "https://chain.so/address/BTCTEST/"), + new BlockChainExplorer("Blockchair", "https://blockchair.com/bitcoin/testnet/transaction/", "https://blockchair.com/bitcoin/testnet/address/") + )); + private static final ArrayList BTC_DAO_TEST_NET_EXPLORERS = new ArrayList<>(Collections.singletonList( + new BlockChainExplorer("BTC DAO-testnet explorer", "https://bisq.network/explorer/btc/dao_testnet/tx/", "https://bisq.network/explorer/btc/dao_testnet/address/") + )); + + public static final ArrayList BSQ_MAIN_NET_EXPLORERS = new ArrayList<>(Arrays.asList( + new BlockChainExplorer("mempool.space (@wiz)", "https://mempool.space/bisq/tx/", "https://mempool.space/bisq/address/"), + new BlockChainExplorer("mempool.emzy.de (@emzy)", "https://mempool.emzy.de/bisq/tx/", "https://mempool.emzy.de/bisq/address/"), + new BlockChainExplorer("mempool.bisq.services (@devinbileck)", "https://mempool.bisq.services/bisq/tx/", "https://mempool.bisq.services/bisq/address/") + )); + + private static final ArrayList XMR_TX_PROOF_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( + "xmrblocks.monero.emzy.de", // @emzy + //"explorer.monero.wiz.biz", // @wiz + "xmrblocks.bisq.services" // @devinbileck + )); + private static final ArrayList XMR_TX_PROOF_SERVICES = new ArrayList<>(Arrays.asList( + "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion", // @emzy + //"wizxmr4hbdxdszqm5rfyqvceyca5jq62ppvtuznasnk66wvhhvgm3uyd.onion", // @wiz + "devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck + )); + + + private static final ArrayList TX_BROADCAST_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( + "https://mempool.space/api/tx", // @wiz + "https://mempool.emzy.de/api/tx", // @emzy + "https://mempool.bisq.services/api/tx" // @devinbileck + )); + + private static final ArrayList TX_BROADCAST_SERVICES = new ArrayList<>(Arrays.asList( + "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/tx", // @wiz + "http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/api/tx", // @emzy + "http://mempoolusb2f67qi7mz2it7n5e77a6komdzx6wftobcduxszkdfun2yd.onion/api/tx" // @devinbileck + )); + + public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; + + + // payload is initialized so the default values are available for Property initialization. + @Setter + @Delegate(excludes = ExcludesDelegateMethods.class) + private PreferencesPayload prefPayload = new PreferencesPayload(); + private boolean initialReadDone = false; + + @Getter + private final BooleanProperty useAnimationsProperty = new SimpleBooleanProperty(prefPayload.isUseAnimations()); + @Getter + private final IntegerProperty cssThemeProperty = new SimpleIntegerProperty(prefPayload.getCssTheme()); + + private final ObservableList fiatCurrenciesAsObservable = FXCollections.observableArrayList(); + private final ObservableList cryptoCurrenciesAsObservable = FXCollections.observableArrayList(); + private final ObservableList tradeCurrenciesAsObservable = FXCollections.observableArrayList(); + private final ObservableMap dontShowAgainMapAsObservable = FXCollections.observableHashMap(); + + private final PersistenceManager persistenceManager; + private final Config config; + private final FeeService feeService; + private final LocalBitcoinNode localBitcoinNode; + private final String btcNodesFromOptions, referralIdFromOptions, + rpcUserFromOptions, rpcPwFromOptions; + private final int blockNotifyPortFromOptions; + private final boolean fullDaoNodeFromOptions; + @Getter + private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode()); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public Preferences(PersistenceManager persistenceManager, + Config config, + FeeService feeService, + LocalBitcoinNode localBitcoinNode, + @Named(Config.BTC_NODES) String btcNodesFromOptions, + @Named(Config.REFERRAL_ID) String referralId, + @Named(Config.FULL_DAO_NODE) boolean fullDaoNode, + @Named(Config.RPC_USER) String rpcUser, + @Named(Config.RPC_PASSWORD) String rpcPassword, + @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort) { + + this.persistenceManager = persistenceManager; + this.config = config; + this.feeService = feeService; + this.localBitcoinNode = localBitcoinNode; + this.btcNodesFromOptions = btcNodesFromOptions; + this.referralIdFromOptions = referralId; + this.fullDaoNodeFromOptions = fullDaoNode; + this.rpcUserFromOptions = rpcUser; + this.rpcPwFromOptions = rpcPassword; + this.blockNotifyPortFromOptions = rpcBlockNotificationPort; + + useAnimationsProperty.addListener((ov) -> { + prefPayload.setUseAnimations(useAnimationsProperty.get()); + GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); + requestPersistence(); + }); + + cssThemeProperty.addListener((ov) -> { + prefPayload.setCssTheme(cssThemeProperty.get()); + requestPersistence(); + }); + + useStandbyModeProperty.addListener((ov) -> { + prefPayload.setUseStandbyMode(useStandbyModeProperty.get()); + requestPersistence(); + }); + + fiatCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { + prefPayload.getFiatCurrencies().clear(); + prefPayload.getFiatCurrencies().addAll(fiatCurrenciesAsObservable); + prefPayload.getFiatCurrencies().sort(TradeCurrency::compareTo); + requestPersistence(); + }); + cryptoCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { + prefPayload.getCryptoCurrencies().clear(); + prefPayload.getCryptoCurrencies().addAll(cryptoCurrenciesAsObservable); + prefPayload.getCryptoCurrencies().sort(TradeCurrency::compareTo); + requestPersistence(); + }); + + fiatCurrenciesAsObservable.addListener(this::updateTradeCurrencies); + cryptoCurrenciesAsObservable.addListener(this::updateTradeCurrencies); + } + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted("PreferencesPayload", + persisted -> { + initFromPersistedPreferences(persisted); + completeHandler.run(); + }, + () -> { + initNewPreferences(); + completeHandler.run(); + }); + } + + private void initFromPersistedPreferences(PreferencesPayload persisted) { + prefPayload = persisted; + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); + GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); + TradeCurrency preferredTradeCurrency = checkNotNull(prefPayload.getPreferredTradeCurrency(), "preferredTradeCurrency must not be null"); + setPreferredTradeCurrency(preferredTradeCurrency); + setFiatCurrencies(prefPayload.getFiatCurrencies()); + setCryptoCurrencies(prefPayload.getCryptoCurrencies()); + setBsqBlockChainExplorer(prefPayload.getBsqBlockChainExplorer()); + GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + + // If a user has updated and the field was not set and get set to 0 by protobuf + // As there is no way to detect that a primitive value field was set we cannot apply + // a "marker" value like -1 to it. We also do not want to wrap the value in a new + // proto message as thats too much for that feature... So we accept that if the user + // sets the value to 0 it will be overwritten by the default at next startup. + if (prefPayload.getBsqAverageTrimThreshold() == 0) { + prefPayload.setBsqAverageTrimThreshold(0.05); + } + + setupPreferences(); + } + + private void initNewPreferences() { + prefPayload = new PreferencesPayload(); + prefPayload.setUserLanguage(GlobalSettings.getLocale().getLanguage()); + prefPayload.setUserCountry(CountryUtil.getDefaultCountry()); + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); + TradeCurrency preferredTradeCurrency = checkNotNull(CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code), + "preferredTradeCurrency must not be null"); + prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); + setFiatCurrencies(CurrencyUtil.getMainFiatCurrencies()); + setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); + + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + if ("BTC".equals(baseCurrencyNetwork.getCurrencyCode())) { + setBlockChainExplorerMainNet(BTC_MAIN_NET_EXPLORERS.get(0)); + setBlockChainExplorerTestNet(BTC_TEST_NET_EXPLORERS.get(0)); + } else { + throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); + } + + prefPayload.setDirectoryChooserPath(Utilities.getSystemHomeDirectory()); + + prefPayload.setOfferBookChartScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setTradeChartsScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setBuyScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setSellScreenCurrencyCode(preferredTradeCurrency.getCode()); + GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + setupPreferences(); + } + + private void setupPreferences() { + persistenceManager.initialize(prefPayload, PersistenceManager.Source.PRIVATE); + + // We don't want to pass Preferences to all popups where the don't show again checkbox is used, so we use + // that static lookup class to avoid static access to the Preferences directly. + DontShowAgainLookup.setPreferences(this); + + // set all properties + useAnimationsProperty.set(prefPayload.isUseAnimations()); + useStandbyModeProperty.set(prefPayload.isUseStandbyMode()); + cssThemeProperty.set(prefPayload.getCssTheme()); + + // a list of previously-used federated explorers + // if user preference references any deprecated explorers we need to select a new valid explorer + String deprecatedExplorers = "(bsq.bisq.cc|bsq.vante.me|bsq.emzy.de|bsq.sqrrm.net|bsq.bisq.services|bsq.ninja).*"; + + // if no valid Bitcoin block explorer is set, select the 1st valid Bitcoin block explorer + ArrayList btcExplorers = getBlockChainExplorers(); + if (getBlockChainExplorer() == null || + getBlockChainExplorer().name.length() == 0 || + getBlockChainExplorer().name.matches(deprecatedExplorers)) { + setBlockChainExplorer(btcExplorers.get(0)); + } + + // if no valid BSQ block explorer is set, randomly select a valid BSQ block explorer + ArrayList bsqExplorers = getBsqBlockChainExplorers(); + if (getBsqBlockChainExplorer() == null || + getBsqBlockChainExplorer().name.length() == 0 || + getBsqBlockChainExplorer().name.matches(deprecatedExplorers)) { + setBsqBlockChainExplorer(bsqExplorers.get((new Random()).nextInt(bsqExplorers.size()))); + } + + tradeCurrenciesAsObservable.addAll(prefPayload.getFiatCurrencies()); + tradeCurrenciesAsObservable.addAll(prefPayload.getCryptoCurrencies()); + dontShowAgainMapAsObservable.putAll(getDontShowAgainMap()); + + // Override settings with options if set + if (config.useTorForBtcOptionSetExplicitly) + setUseTorForBitcoinJ(config.useTorForBtc); + + if (btcNodesFromOptions != null && !btcNodesFromOptions.isEmpty()) { + if (getBitcoinNodes() != null && !getBitcoinNodes().equals(btcNodesFromOptions)) { + log.warn("The Bitcoin node(s) from the program argument and the one(s) persisted in the UI are different. " + + "The Bitcoin node(s) {} from the program argument will be used.", btcNodesFromOptions); + } + setBitcoinNodes(btcNodesFromOptions); + setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.CUSTOM.ordinal()); + } + if (referralIdFromOptions != null && !referralIdFromOptions.isEmpty()) + setReferralId(referralIdFromOptions); + + if (prefPayload.getIgnoreDustThreshold() < Restrictions.getMinNonDustOutput().value) { + setIgnoreDustThreshold(600); + } + + // For users from old versions the 4 flags a false but we want to have it true by default + // PhoneKeyAndToken is also null so we can use that to enable the flags + if (prefPayload.getPhoneKeyAndToken() == null) { + setUseSoundForMobileNotifications(true); + setUseTradeNotifications(true); + setUseMarketNotifications(true); + setUsePriceNotifications(true); + } + + if (prefPayload.getAutoConfirmSettingsList().isEmpty()) { + List defaultXmrTxProofServices = getDefaultXmrTxProofServices(); + AutoConfirmSettings.getDefault(defaultXmrTxProofServices, "XMR") + .ifPresent(xmrAutoConfirmSettings -> { + getAutoConfirmSettingsList().add(xmrAutoConfirmSettings); + }); + } + + // We set the capability in CoreNetworkCapabilities if the program argument is set. + // If we have set it in the preferences view we handle it here. + CoreNetworkCapabilities.maybeApplyDaoFullMode(config); + + initialReadDone = true; + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void dontShowAgain(String key, boolean dontShowAgain) { + prefPayload.getDontShowAgainMap().put(key, dontShowAgain); + requestPersistence(); + dontShowAgainMapAsObservable.put(key, dontShowAgain); + } + + public void resetDontShowAgain() { + prefPayload.getDontShowAgainMap().clear(); + dontShowAgainMapAsObservable.clear(); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setUseAnimations(boolean useAnimations) { + this.useAnimationsProperty.set(useAnimations); + } + + public void setCssTheme(boolean useDarkMode) { + this.cssThemeProperty.set(useDarkMode ? 1 : 0); + } + + public void addFiatCurrency(FiatCurrency tradeCurrency) { + if (!fiatCurrenciesAsObservable.contains(tradeCurrency)) + fiatCurrenciesAsObservable.add(tradeCurrency); + } + + public void removeFiatCurrency(FiatCurrency tradeCurrency) { + if (tradeCurrenciesAsObservable.size() > 1) { + fiatCurrenciesAsObservable.remove(tradeCurrency); + + if (prefPayload.getPreferredTradeCurrency() != null && + prefPayload.getPreferredTradeCurrency().equals(tradeCurrency)) + setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); + } else { + log.error("you cannot remove the last currency"); + } + } + + public void addCryptoCurrency(CryptoCurrency tradeCurrency) { + if (!cryptoCurrenciesAsObservable.contains(tradeCurrency)) + cryptoCurrenciesAsObservable.add(tradeCurrency); + } + + public void removeCryptoCurrency(CryptoCurrency tradeCurrency) { + if (tradeCurrenciesAsObservable.size() > 1) { + cryptoCurrenciesAsObservable.remove(tradeCurrency); + + if (prefPayload.getPreferredTradeCurrency() != null && + prefPayload.getPreferredTradeCurrency().equals(tradeCurrency)) + setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); + } else { + log.error("you cannot remove the last currency"); + } + } + + public void setBlockChainExplorer(BlockChainExplorer blockChainExplorer) { + if (Config.baseCurrencyNetwork().isMainnet()) + setBlockChainExplorerMainNet(blockChainExplorer); + else + setBlockChainExplorerTestNet(blockChainExplorer); + } + + public void setTacAccepted(boolean tacAccepted) { + prefPayload.setTacAccepted(tacAccepted); + requestPersistence(); + } + + public void setTacAcceptedV120(boolean tacAccepted) { + prefPayload.setTacAcceptedV120(tacAccepted); + requestPersistence(); + } + + public void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold) { + prefPayload.setBsqAverageTrimThreshold(bsqAverageTrimThreshold); + requestPersistence(); + } + + public Optional findAutoConfirmSettings(String currencyCode) { + return prefPayload.getAutoConfirmSettingsList().stream() + .filter(e -> e.getCurrencyCode().equals(currencyCode)) + .findAny(); + } + + public void setAutoConfServiceAddresses(String currencyCode, List serviceAddresses) { + findAutoConfirmSettings(currencyCode).ifPresent(e -> { + e.setServiceAddresses(serviceAddresses); + requestPersistence(); + }); + } + + public void setAutoConfEnabled(String currencyCode, boolean enabled) { + findAutoConfirmSettings(currencyCode).ifPresent(e -> { + e.setEnabled(enabled); + requestPersistence(); + }); + } + + public void setAutoConfRequiredConfirmations(String currencyCode, int requiredConfirmations) { + findAutoConfirmSettings(currencyCode).ifPresent(e -> { + e.setRequiredConfirmations(requiredConfirmations); + requestPersistence(); + }); + } + + public void setAutoConfTradeLimit(String currencyCode, long tradeLimit) { + findAutoConfirmSettings(currencyCode).ifPresent(e -> { + e.setTradeLimit(tradeLimit); + requestPersistence(); + }); + } + + public void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods) { + prefPayload.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods); + requestPersistence(); + } + + private void requestPersistence() { + if (initialReadDone) + persistenceManager.requestPersistence(); + } + + public void setUserLanguage(@NotNull String userLanguageCode) { + prefPayload.setUserLanguage(userLanguageCode); + if (prefPayload.getUserCountry() != null && prefPayload.getUserLanguage() != null) + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); + requestPersistence(); + } + + public void setUserCountry(@NotNull Country userCountry) { + prefPayload.setUserCountry(userCountry); + if (prefPayload.getUserLanguage() != null) + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), userCountry.code)); + requestPersistence(); + } + + public void setPreferredTradeCurrency(TradeCurrency preferredTradeCurrency) { + if (preferredTradeCurrency != null) { + prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); + GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + requestPersistence(); + } + } + + public void setUseTorForBitcoinJ(boolean useTorForBitcoinJ) { + prefPayload.setUseTorForBitcoinJ(useTorForBitcoinJ); + requestPersistence(); + } + + public void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook) { + prefPayload.setShowOwnOffersInOfferBook(showOwnOffersInOfferBook); + requestPersistence(); + } + + public void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent) { + prefPayload.setMaxPriceDistanceInPercent(maxPriceDistanceInPercent); + requestPersistence(); + } + + public void setBackupDirectory(String backupDirectory) { + prefPayload.setBackupDirectory(backupDirectory); + requestPersistence(); + } + + public void setAutoSelectArbitrators(boolean autoSelectArbitrators) { + prefPayload.setAutoSelectArbitrators(autoSelectArbitrators); + requestPersistence(); + } + + public void setUsePercentageBasedPrice(boolean usePercentageBasedPrice) { + prefPayload.setUsePercentageBasedPrice(usePercentageBasedPrice); + requestPersistence(); + } + + public void setTagForPeer(String fullAddress, String tag) { + prefPayload.getPeerTagMap().put(fullAddress, tag); + requestPersistence(); + } + + public void setOfferBookChartScreenCurrencyCode(String offerBookChartScreenCurrencyCode) { + prefPayload.setOfferBookChartScreenCurrencyCode(offerBookChartScreenCurrencyCode); + requestPersistence(); + } + + public void setBuyScreenCurrencyCode(String buyScreenCurrencyCode) { + prefPayload.setBuyScreenCurrencyCode(buyScreenCurrencyCode); + requestPersistence(); + } + + public void setSellScreenCurrencyCode(String sellScreenCurrencyCode) { + prefPayload.setSellScreenCurrencyCode(sellScreenCurrencyCode); + requestPersistence(); + } + + public void setIgnoreTradersList(List ignoreTradersList) { + prefPayload.setIgnoreTradersList(ignoreTradersList); + requestPersistence(); + } + + public void setDirectoryChooserPath(String directoryChooserPath) { + prefPayload.setDirectoryChooserPath(directoryChooserPath); + requestPersistence(); + } + + public void setTradeChartsScreenCurrencyCode(String tradeChartsScreenCurrencyCode) { + prefPayload.setTradeChartsScreenCurrencyCode(tradeChartsScreenCurrencyCode); + requestPersistence(); + } + + public void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex) { + prefPayload.setTradeStatisticsTickUnitIndex(tradeStatisticsTickUnitIndex); + requestPersistence(); + } + + public void setSortMarketCurrenciesNumerically(boolean sortMarketCurrenciesNumerically) { + prefPayload.setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically); + requestPersistence(); + } + + public void setBitcoinNodes(String bitcoinNodes) { + prefPayload.setBitcoinNodes(bitcoinNodes); + requestPersistence(); + } + + public void setUseCustomWithdrawalTxFee(boolean useCustomWithdrawalTxFee) { + prefPayload.setUseCustomWithdrawalTxFee(useCustomWithdrawalTxFee); + requestPersistence(); + } + + public void setWithdrawalTxFeeInVbytes(long withdrawalTxFeeInVbytes) { + prefPayload.setWithdrawalTxFeeInVbytes(withdrawalTxFeeInVbytes); + requestPersistence(); + } + + public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) { + double max = Restrictions.getMaxBuyerSecurityDepositAsPercent(); + double min = Restrictions.getMinBuyerSecurityDepositAsPercent(); + + if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) + prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + else + prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + requestPersistence(); + } + + public void setSelectedPaymentAccountForCreateOffer(@Nullable PaymentAccount paymentAccount) { + prefPayload.setSelectedPaymentAccountForCreateOffer(paymentAccount); + requestPersistence(); + } + + public void setPayFeeInBtc(boolean payFeeInBtc) { + prefPayload.setPayFeeInBtc(payFeeInBtc); + requestPersistence(); + } + + private void setFiatCurrencies(List currencies) { + fiatCurrenciesAsObservable.setAll(currencies.stream() + .map(fiatCurrency -> new FiatCurrency(fiatCurrency.getCurrency())) + .distinct().collect(Collectors.toList())); + requestPersistence(); + } + + private void setCryptoCurrencies(List currencies) { + cryptoCurrenciesAsObservable.setAll(currencies.stream().distinct().collect(Collectors.toList())); + requestPersistence(); + } + + public void setBsqBlockChainExplorer(BlockChainExplorer bsqBlockChainExplorer) { + prefPayload.setBsqBlockChainExplorer(bsqBlockChainExplorer); + requestPersistence(); + } + + private void setBlockChainExplorerTestNet(BlockChainExplorer blockChainExplorerTestNet) { + prefPayload.setBlockChainExplorerTestNet(blockChainExplorerTestNet); + requestPersistence(); + } + + private void setBlockChainExplorerMainNet(BlockChainExplorer blockChainExplorerMainNet) { + prefPayload.setBlockChainExplorerMainNet(blockChainExplorerMainNet); + requestPersistence(); + } + + public void setResyncSpvRequested(boolean resyncSpvRequested) { + prefPayload.setResyncSpvRequested(resyncSpvRequested); + // We call that before shutdown so we dont want a delay here + requestPersistence(); + } + + public void setBridgeAddresses(List bridgeAddresses) { + prefPayload.setBridgeAddresses(bridgeAddresses); + // We call that before shutdown so we dont want a delay here + requestPersistence(); + } + + // Only used from PB but keep it explicit as it may be used from the client and then we want to persist + public void setPeerTagMap(Map peerTagMap) { + prefPayload.setPeerTagMap(peerTagMap); + requestPersistence(); + } + + public void setBridgeOptionOrdinal(int bridgeOptionOrdinal) { + prefPayload.setBridgeOptionOrdinal(bridgeOptionOrdinal); + requestPersistence(); + } + + public void setTorTransportOrdinal(int torTransportOrdinal) { + prefPayload.setTorTransportOrdinal(torTransportOrdinal); + requestPersistence(); + } + + public void setCustomBridges(String customBridges) { + prefPayload.setCustomBridges(customBridges); + requestPersistence(); + } + + public void setBitcoinNodesOptionOrdinal(int bitcoinNodesOptionOrdinal) { + prefPayload.setBitcoinNodesOptionOrdinal(bitcoinNodesOptionOrdinal); + requestPersistence(); + } + + public void setReferralId(String referralId) { + prefPayload.setReferralId(referralId); + requestPersistence(); + } + + public void setPhoneKeyAndToken(String phoneKeyAndToken) { + prefPayload.setPhoneKeyAndToken(phoneKeyAndToken); + requestPersistence(); + } + + public void setUseSoundForMobileNotifications(boolean value) { + prefPayload.setUseSoundForMobileNotifications(value); + requestPersistence(); + } + + public void setUseTradeNotifications(boolean value) { + prefPayload.setUseTradeNotifications(value); + requestPersistence(); + } + + public void setUseMarketNotifications(boolean value) { + prefPayload.setUseMarketNotifications(value); + requestPersistence(); + } + + public void setUsePriceNotifications(boolean value) { + prefPayload.setUsePriceNotifications(value); + requestPersistence(); + } + + public void setUseStandbyMode(boolean useStandbyMode) { + this.useStandbyModeProperty.set(useStandbyMode); + } + + public void setTakeOfferSelectedPaymentAccountId(String value) { + prefPayload.setTakeOfferSelectedPaymentAccountId(value); + requestPersistence(); + } + + public void setDaoFullNode(boolean value) { + // We only persist if we have not set the program argument + if (config.fullDaoNodeOptionSetExplicitly) { + prefPayload.setDaoFullNode(value); + requestPersistence(); + } + } + + public void setRpcUser(String value) { + // We only persist if we have not set the program argument + if (!rpcUserFromOptions.isEmpty()) { + prefPayload.setRpcUser(value); + } + prefPayload.setRpcUser(value); + requestPersistence(); + } + + public void setRpcPw(String value) { + // We only persist if we have not set the program argument + if (rpcPwFromOptions.isEmpty()) { + prefPayload.setRpcPw(value); + requestPersistence(); + } + } + + public void setBlockNotifyPort(int value) { + // We only persist if we have not set the program argument + if (blockNotifyPortFromOptions == Config.UNSPECIFIED_PORT) { + prefPayload.setBlockNotifyPort(value); + requestPersistence(); + } + } + + public void setIgnoreDustThreshold(int value) { + prefPayload.setIgnoreDustThreshold(value); + requestPersistence(); + } + + public void setShowOffersMatchingMyAccounts(boolean value) { + prefPayload.setShowOffersMatchingMyAccounts(value); + requestPersistence(); + } + + public void setDenyApiTaker(boolean value) { + prefPayload.setDenyApiTaker(value); + requestPersistence(); + } + + public void setNotifyOnPreRelease(boolean value) { + prefPayload.setNotifyOnPreRelease(value); + requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + public BooleanProperty useAnimationsProperty() { + return useAnimationsProperty; + } + + public ObservableList getFiatCurrenciesAsObservable() { + return fiatCurrenciesAsObservable; + } + + public ObservableList getCryptoCurrenciesAsObservable() { + return cryptoCurrenciesAsObservable; + } + + public ObservableList getTradeCurrenciesAsObservable() { + return tradeCurrenciesAsObservable; + } + + public ObservableMap getDontShowAgainMapAsObservable() { + return dontShowAgainMapAsObservable; + } + + public BlockChainExplorer getBlockChainExplorer() { + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + switch (baseCurrencyNetwork) { + case BTC_MAINNET: + return prefPayload.getBlockChainExplorerMainNet(); + case BTC_TESTNET: + case BTC_REGTEST: + return prefPayload.getBlockChainExplorerTestNet(); + case BTC_DAO_TESTNET: + return BTC_DAO_TEST_NET_EXPLORERS.get(0); + case BTC_DAO_BETANET: + return prefPayload.getBlockChainExplorerMainNet(); + case BTC_DAO_REGTEST: + return BTC_DAO_TEST_NET_EXPLORERS.get(0); + default: + throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); + } + } + + public ArrayList getBlockChainExplorers() { + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + switch (baseCurrencyNetwork) { + case BTC_MAINNET: + return BTC_MAIN_NET_EXPLORERS; + case BTC_TESTNET: + case BTC_REGTEST: + return BTC_TEST_NET_EXPLORERS; + case BTC_DAO_TESTNET: + return BTC_DAO_TEST_NET_EXPLORERS; + case BTC_DAO_BETANET: + return BTC_MAIN_NET_EXPLORERS; + case BTC_DAO_REGTEST: + return BTC_DAO_TEST_NET_EXPLORERS; + default: + throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); + } + } + + public ArrayList getBsqBlockChainExplorers() { + return BSQ_MAIN_NET_EXPLORERS; + } + + public boolean showAgain(String key) { + return !prefPayload.getDontShowAgainMap().containsKey(key) || !prefPayload.getDontShowAgainMap().get(key); + } + + public boolean getUseTorForBitcoinJ() { + // We override the useTorForBitcoinJ and set it to false if we will use a + // localhost Bitcoin node or if we are not on mainnet, unless the useTorForBtc + // parameter is explicitly provided. On testnet there are very few Bitcoin tor + // nodes and we don't provide tor nodes. + + if ((!Config.baseCurrencyNetwork().isMainnet() + || localBitcoinNode.shouldBeUsed()) + && !config.useTorForBtcOptionSetExplicitly) + return false; + else + return prefPayload.isUseTorForBitcoinJ(); + } + + public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) { + double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? + prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent(); + + if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) { + value = Restrictions.getMinBuyerSecurityDepositAsPercent(); + setBuyerSecurityDepositAsPercent(value, paymentAccount); + } + + return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value; + } + + //TODO remove and use isPayFeeInBtc instead + public boolean getPayFeeInBtc() { + return prefPayload.isPayFeeInBtc(); + } + + @Override + @Nullable + public List getBridgeAddresses() { + return prefPayload.getBridgeAddresses(); + } + + public long getWithdrawalTxFeeInVbytes() { + return Math.max(prefPayload.getWithdrawalTxFeeInVbytes(), + feeService.getMinFeePerVByte()); + } + + public boolean isDaoFullNode() { + if (config.fullDaoNodeOptionSetExplicitly) { + return fullDaoNodeFromOptions; + } else { + return prefPayload.isDaoFullNode(); + } + } + + public String getRpcUser() { + if (!rpcUserFromOptions.isEmpty()) { + return rpcUserFromOptions; + } else { + return prefPayload.getRpcUser(); + } + } + + public String getRpcPw() { + if (!rpcPwFromOptions.isEmpty()) { + return rpcPwFromOptions; + } else { + return prefPayload.getRpcPw(); + } + } + + public int getBlockNotifyPort() { + if (blockNotifyPortFromOptions != Config.UNSPECIFIED_PORT) { + try { + return blockNotifyPortFromOptions; + } catch (Throwable ignore) { + return 0; + } + + } else { + return prefPayload.getBlockNotifyPort(); + } + } + + public List getDefaultXmrTxProofServices() { + if (config.useLocalhostForP2P) { + return XMR_TX_PROOF_SERVICES_CLEAR_NET; + } else { + return XMR_TX_PROOF_SERVICES; + } + } + + public List getDefaultTxBroadcastServices() { + if (config.useLocalhostForP2P) { + return TX_BROADCAST_SERVICES_CLEAR_NET; + } else { + return TX_BROADCAST_SERVICES; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateTradeCurrencies(ListChangeListener.Change change) { + change.next(); + if (change.wasAdded() && change.getAddedSize() == 1 && initialReadDone) + tradeCurrenciesAsObservable.add(change.getAddedSubList().get(0)); + else if (change.wasRemoved() && change.getRemovedSize() == 1 && initialReadDone) + tradeCurrenciesAsObservable.remove(change.getRemoved().get(0)); + + requestPersistence(); + } + + private interface ExcludesDelegateMethods { + void setTacAccepted(boolean tacAccepted); + + void setUseAnimations(boolean useAnimations); + + void setCssTheme(int cssTheme); + + void setUserLanguage(@NotNull String userLanguageCode); + + void setUserCountry(@NotNull Country userCountry); + + void setPreferredTradeCurrency(TradeCurrency preferredTradeCurrency); + + void setUseTorForBitcoinJ(boolean useTorForBitcoinJ); + + void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook); + + void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent); + + void setBackupDirectory(String backupDirectory); + + void setAutoSelectArbitrators(boolean autoSelectArbitrators); + + void setUsePercentageBasedPrice(boolean usePercentageBasedPrice); + + void setTagForPeer(String hostName, String tag); + + void setOfferBookChartScreenCurrencyCode(String offerBookChartScreenCurrencyCode); + + void setBuyScreenCurrencyCode(String buyScreenCurrencyCode); + + void setSellScreenCurrencyCode(String sellScreenCurrencyCode); + + void setIgnoreTradersList(List ignoreTradersList); + + void setDirectoryChooserPath(String directoryChooserPath); + + void setTradeChartsScreenCurrencyCode(String tradeChartsScreenCurrencyCode); + + void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex); + + void setSortMarketCurrenciesNumerically(boolean sortMarketCurrenciesNumerically); + + void setBitcoinNodes(String bitcoinNodes); + + void setUseCustomWithdrawalTxFee(boolean useCustomWithdrawalTxFee); + + void setWithdrawalTxFeeInVbytes(long withdrawalTxFeeInVbytes); + + void setSelectedPaymentAccountForCreateOffer(@Nullable PaymentAccount paymentAccount); + + void setBsqBlockChainExplorer(BlockChainExplorer bsqBlockChainExplorer); + + void setPayFeeInBtc(boolean payFeeInBtc); + + void setFiatCurrencies(List currencies); + + void setCryptoCurrencies(List currencies); + + void setBlockChainExplorerTestNet(BlockChainExplorer blockChainExplorerTestNet); + + void setBlockChainExplorerMainNet(BlockChainExplorer blockChainExplorerMainNet); + + void setResyncSpvRequested(boolean resyncSpvRequested); + + void setDontShowAgainMap(Map dontShowAgainMap); + + void setPeerTagMap(Map peerTagMap); + + void setBridgeAddresses(List bridgeAddresses); + + void setBridgeOptionOrdinal(int bridgeOptionOrdinal); + + void setTorTransportOrdinal(int torTransportOrdinal); + + void setCustomBridges(String customBridges); + + void setBitcoinNodesOptionOrdinal(int bitcoinNodesOption); + + void setReferralId(String referralId); + + void setPhoneKeyAndToken(String phoneKeyAndToken); + + void setUseSoundForMobileNotifications(boolean value); + + void setUseTradeNotifications(boolean value); + + void setUseMarketNotifications(boolean value); + + void setUsePriceNotifications(boolean value); + + List getBridgeAddresses(); + + long getWithdrawalTxFeeInVbytes(); + + void setUseStandbyMode(boolean useStandbyMode); + + void setTakeOfferSelectedPaymentAccountId(String value); + + void setIgnoreDustThreshold(int value); + + void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent); + + double getBuyerSecurityDepositAsPercent(); + + void setDaoFullNode(boolean value); + + void setRpcUser(String value); + + void setRpcPw(String value); + + void setBlockNotifyPort(int value); + + boolean isDaoFullNode(); + + String getRpcUser(); + + String getRpcPw(); + + int getBlockNotifyPort(); + + void setTacAcceptedV120(boolean tacAccepted); + + void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); + + void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); + + void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods); + + void setShowOffersMatchingMyAccounts(boolean value); + + void setDenyApiTaker(boolean value); + + void setNotifyOnPreRelease(boolean value); + } +} diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java new file mode 100644 index 0000000000..3eb6ecfa7b --- /dev/null +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -0,0 +1,305 @@ +/* + * 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 bisq.core.user; + +import bisq.core.locale.Country; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.CoreProtoResolver; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistableEnvelope; + +import com.google.protobuf.Message; + +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; + +@Slf4j +@Data +@AllArgsConstructor +public final class PreferencesPayload implements PersistableEnvelope { + private String userLanguage; + private Country userCountry; + private List fiatCurrencies = new ArrayList<>(); + private List cryptoCurrencies = new ArrayList<>(); + private BlockChainExplorer blockChainExplorerMainNet; + private BlockChainExplorer blockChainExplorerTestNet; + @Nullable + private BlockChainExplorer bsqBlockChainExplorer; + @Nullable + private String backupDirectory; + private boolean autoSelectArbitrators = true; + private Map dontShowAgainMap = new HashMap<>(); + private boolean tacAccepted; + private boolean useTorForBitcoinJ = true; + private boolean showOwnOffersInOfferBook = true; + @Nullable + private TradeCurrency preferredTradeCurrency; + private long withdrawalTxFeeInVbytes = 100; + private boolean useCustomWithdrawalTxFee = false; + private double maxPriceDistanceInPercent = 0.3; + @Nullable + private String offerBookChartScreenCurrencyCode; + @Nullable + private String tradeChartsScreenCurrencyCode; + @Nullable + private String buyScreenCurrencyCode; + @Nullable + private String sellScreenCurrencyCode; + private int tradeStatisticsTickUnitIndex = 3; + private boolean resyncSpvRequested; + private boolean sortMarketCurrenciesNumerically = true; + private boolean usePercentageBasedPrice = true; + private Map peerTagMap = new HashMap<>(); + // custom btc nodes + private String bitcoinNodes = ""; + private List ignoreTradersList = new ArrayList<>(); + private String directoryChooserPath; + + @Deprecated // Superseded by buyerSecurityDepositAsPercent + private long buyerSecurityDepositAsLong; + + private boolean useAnimations; + private int cssTheme; + @Nullable + private PaymentAccount selectedPaymentAccountForCreateOffer; + private boolean payFeeInBtc = true; + @Nullable + private List bridgeAddresses; + private int bridgeOptionOrdinal; + private int torTransportOrdinal; + @Nullable + private String customBridges; + private int bitcoinNodesOptionOrdinal; + @Nullable + private String referralId; + @Nullable + private String phoneKeyAndToken; + private boolean useSoundForMobileNotifications = true; + private boolean useTradeNotifications = true; + private boolean useMarketNotifications = true; + private boolean usePriceNotifications = true; + private boolean useStandbyMode = false; + private boolean isDaoFullNode = false; + @Nullable + private String rpcUser; + @Nullable + private String rpcPw; + @Nullable + private String takeOfferSelectedPaymentAccountId; + private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent(); + private int ignoreDustThreshold = 600; + private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); + private int blockNotifyPort; + private boolean tacAcceptedV120; + private double bsqAverageTrimThreshold = 0.05; + + // Added at 1.3.8 + private List autoConfirmSettingsList = new ArrayList<>(); + + // Added in 1.5.5 + private boolean hideNonAccountPaymentMethods; + private boolean showOffersMatchingMyAccounts; + private boolean denyApiTaker; + private boolean notifyOnPreRelease; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + PreferencesPayload() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + protobuf.PreferencesPayload.Builder builder = protobuf.PreferencesPayload.newBuilder() + .setUserLanguage(userLanguage) + .setUserCountry((protobuf.Country) userCountry.toProtoMessage()) + .addAllFiatCurrencies(fiatCurrencies.stream() + .map(fiatCurrency -> ((protobuf.TradeCurrency) fiatCurrency.toProtoMessage())) + .collect(Collectors.toList())) + .addAllCryptoCurrencies(cryptoCurrencies.stream() + .map(cryptoCurrency -> ((protobuf.TradeCurrency) cryptoCurrency.toProtoMessage())) + .collect(Collectors.toList())) + .setBlockChainExplorerMainNet((protobuf.BlockChainExplorer) blockChainExplorerMainNet.toProtoMessage()) + .setBlockChainExplorerTestNet((protobuf.BlockChainExplorer) blockChainExplorerTestNet.toProtoMessage()) + .setAutoSelectArbitrators(autoSelectArbitrators) + .putAllDontShowAgainMap(dontShowAgainMap) + .setTacAccepted(tacAccepted) + .setUseTorForBitcoinJ(useTorForBitcoinJ) + .setShowOwnOffersInOfferBook(showOwnOffersInOfferBook) + .setWithdrawalTxFeeInVbytes(withdrawalTxFeeInVbytes) + .setUseCustomWithdrawalTxFee(useCustomWithdrawalTxFee) + .setMaxPriceDistanceInPercent(maxPriceDistanceInPercent) + .setTradeStatisticsTickUnitIndex(tradeStatisticsTickUnitIndex) + .setResyncSpvRequested(resyncSpvRequested) + .setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically) + .setUsePercentageBasedPrice(usePercentageBasedPrice) + .putAllPeerTagMap(peerTagMap) + .setBitcoinNodes(bitcoinNodes) + .addAllIgnoreTradersList(ignoreTradersList) + .setDirectoryChooserPath(directoryChooserPath) + .setBuyerSecurityDepositAsLong(buyerSecurityDepositAsLong) + .setUseAnimations(useAnimations) + .setCssTheme(cssTheme) + .setPayFeeInBtc(payFeeInBtc) + .setBridgeOptionOrdinal(bridgeOptionOrdinal) + .setTorTransportOrdinal(torTransportOrdinal) + .setBitcoinNodesOptionOrdinal(bitcoinNodesOptionOrdinal) + .setUseSoundForMobileNotifications(useSoundForMobileNotifications) + .setUseTradeNotifications(useTradeNotifications) + .setUseMarketNotifications(useMarketNotifications) + .setUsePriceNotifications(usePriceNotifications) + .setUseStandbyMode(useStandbyMode) + .setIsDaoFullNode(isDaoFullNode) + .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) + .setIgnoreDustThreshold(ignoreDustThreshold) + .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) + .setBlockNotifyPort(blockNotifyPort) + .setTacAcceptedV120(tacAcceptedV120) + .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) + .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() + .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) + .collect(Collectors.toList())) + .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) + .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) + .setDenyApiTaker(denyApiTaker) + .setNotifyOnPreRelease(notifyOnPreRelease); + + Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); + Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); + Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode); + Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode); + Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode); + Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode); + Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent( + account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage())); + Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses); + Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges); + Optional.ofNullable(referralId).ifPresent(builder::setReferralId); + Optional.ofNullable(phoneKeyAndToken).ifPresent(builder::setPhoneKeyAndToken); + Optional.ofNullable(rpcUser).ifPresent(builder::setRpcUser); + Optional.ofNullable(rpcPw).ifPresent(builder::setRpcPw); + Optional.ofNullable(takeOfferSelectedPaymentAccountId).ifPresent(builder::setTakeOfferSelectedPaymentAccountId); + Optional.ofNullable(bsqBlockChainExplorer).ifPresent(e -> builder.setBsqBlockChainExplorer((protobuf.BlockChainExplorer) e.toProtoMessage())); + + return protobuf.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build(); + } + + public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, CoreProtoResolver coreProtoResolver) { + final protobuf.Country userCountry = proto.getUserCountry(); + PaymentAccount paymentAccount = null; + if (proto.hasSelectedPaymentAccountForCreateOffer() && proto.getSelectedPaymentAccountForCreateOffer().hasPaymentMethod()) + paymentAccount = PaymentAccount.fromProto(proto.getSelectedPaymentAccountForCreateOffer(), coreProtoResolver); + + return new PreferencesPayload( + proto.getUserLanguage(), + Country.fromProto(userCountry), + proto.getFiatCurrenciesList().isEmpty() ? new ArrayList<>() : + new ArrayList<>(proto.getFiatCurrenciesList().stream() + .map(FiatCurrency::fromProto) + .collect(Collectors.toList())), + proto.getCryptoCurrenciesList().isEmpty() ? new ArrayList<>() : + new ArrayList<>(proto.getCryptoCurrenciesList().stream() + .map(CryptoCurrency::fromProto) + .collect(Collectors.toList())), + BlockChainExplorer.fromProto(proto.getBlockChainExplorerMainNet()), + BlockChainExplorer.fromProto(proto.getBlockChainExplorerTestNet()), + proto.hasBsqBlockChainExplorer() ? BlockChainExplorer.fromProto(proto.getBsqBlockChainExplorer()) : null, + ProtoUtil.stringOrNullFromProto(proto.getBackupDirectory()), + proto.getAutoSelectArbitrators(), + Maps.newHashMap(proto.getDontShowAgainMapMap()), + proto.getTacAccepted(), + proto.getUseTorForBitcoinJ(), + proto.getShowOwnOffersInOfferBook(), + proto.hasPreferredTradeCurrency() ? TradeCurrency.fromProto(proto.getPreferredTradeCurrency()) : null, + proto.getWithdrawalTxFeeInVbytes(), + proto.getUseCustomWithdrawalTxFee(), + proto.getMaxPriceDistanceInPercent(), + ProtoUtil.stringOrNullFromProto(proto.getOfferBookChartScreenCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getTradeChartsScreenCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getSellScreenCurrencyCode()), + proto.getTradeStatisticsTickUnitIndex(), + proto.getResyncSpvRequested(), + proto.getSortMarketCurrenciesNumerically(), + proto.getUsePercentageBasedPrice(), + Maps.newHashMap(proto.getPeerTagMapMap()), + proto.getBitcoinNodes(), + proto.getIgnoreTradersListList(), + proto.getDirectoryChooserPath(), + proto.getBuyerSecurityDepositAsLong(), + proto.getUseAnimations(), + proto.getCssTheme(), + paymentAccount, + proto.getPayFeeInBtc(), + proto.getBridgeAddressesList().isEmpty() ? null : new ArrayList<>(proto.getBridgeAddressesList()), + proto.getBridgeOptionOrdinal(), + proto.getTorTransportOrdinal(), + ProtoUtil.stringOrNullFromProto(proto.getCustomBridges()), + proto.getBitcoinNodesOptionOrdinal(), + proto.getReferralId().isEmpty() ? null : proto.getReferralId(), + proto.getPhoneKeyAndToken().isEmpty() ? null : proto.getPhoneKeyAndToken(), + proto.getUseSoundForMobileNotifications(), + proto.getUseTradeNotifications(), + proto.getUseMarketNotifications(), + proto.getUsePriceNotifications(), + proto.getUseStandbyMode(), + proto.getIsDaoFullNode(), + proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), + proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), + proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), + proto.getBuyerSecurityDepositAsPercent(), + proto.getIgnoreDustThreshold(), + proto.getBuyerSecurityDepositAsPercentForCrypto(), + proto.getBlockNotifyPort(), + proto.getTacAcceptedV120(), + proto.getBsqAverageTrimThreshold(), + proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : + new ArrayList<>(proto.getAutoConfirmSettingsList().stream() + .map(AutoConfirmSettings::fromProto) + .collect(Collectors.toList())), + proto.getHideNonAccountPaymentMethods(), + proto.getShowOffersMatchingMyAccounts(), + proto.getDenyApiTaker(), + proto.getNotifyOnPreRelease() + ); + } +} diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java new file mode 100644 index 0000000000..04edaf82b5 --- /dev/null +++ b/core/src/main/java/bisq/core/user/User.java @@ -0,0 +1,521 @@ +/* + * 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 bisq.core.user; + +import bisq.core.alert.Alert; +import bisq.core.filter.Filter; +import bisq.core.locale.LanguageUtil; +import bisq.core.locale.TradeCurrency; +import bisq.core.notifications.alerts.market.MarketAlertFilter; +import bisq.core.notifications.alerts.price.PriceAlertFilter; +import bisq.core.payment.PaymentAccount; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.KeyRing; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * The User is persisted locally. + * It must never be transmitted over the wire (messageKeyPair contains private key!). + */ +@Slf4j +@AllArgsConstructor +@Singleton +public class User implements PersistedDataHost { + private final PersistenceManager persistenceManager; + private final KeyRing keyRing; + + private ObservableSet paymentAccountsAsObservable; + private ObjectProperty currentPaymentAccountProperty; + + private UserPayload userPayload = new UserPayload(); + private boolean isPaymentAccountImport = false; + + @Inject + public User(PersistenceManager persistenceManager, KeyRing keyRing) { + this.persistenceManager = persistenceManager; + this.keyRing = keyRing; + } + + // for unit tests + public User() { + persistenceManager = null; + keyRing = null; + } + + @Override + public void readPersisted(Runnable completeHandler) { + checkNotNull(persistenceManager).readPersisted("UserPayload", + persisted -> { + userPayload = persisted; + init(); + completeHandler.run(); + }, + () -> { + init(); + completeHandler.run(); + }); + } + + private void init() { + checkNotNull(persistenceManager).initialize(userPayload, PersistenceManager.Source.PRIVATE); + + checkNotNull(userPayload.getPaymentAccounts(), "userPayload.getPaymentAccounts() must not be null"); + checkNotNull(userPayload.getAcceptedLanguageLocaleCodes(), "userPayload.getAcceptedLanguageLocaleCodes() must not be null"); + paymentAccountsAsObservable = FXCollections.observableSet(userPayload.getPaymentAccounts()); + currentPaymentAccountProperty = new SimpleObjectProperty<>(userPayload.getCurrentPaymentAccount()); + userPayload.setAccountId(String.valueOf(Math.abs(checkNotNull(keyRing).getPubKeyRing().hashCode()))); + + // language setup + if (!userPayload.getAcceptedLanguageLocaleCodes().contains(LanguageUtil.getDefaultLanguageLocaleAsCode())) + userPayload.getAcceptedLanguageLocaleCodes().add(LanguageUtil.getDefaultLanguageLocaleAsCode()); + String english = LanguageUtil.getEnglishLanguageLocaleCode(); + if (!userPayload.getAcceptedLanguageLocaleCodes().contains(english)) + userPayload.getAcceptedLanguageLocaleCodes().add(english); + + paymentAccountsAsObservable.addListener((SetChangeListener) change -> { + userPayload.setPaymentAccounts(new HashSet<>(paymentAccountsAsObservable)); + requestPersistence(); + }); + currentPaymentAccountProperty.addListener((ov) -> { + userPayload.setCurrentPaymentAccount(currentPaymentAccountProperty.get()); + requestPersistence(); + }); + + requestPersistence(); + } + + public void requestPersistence() { + if (persistenceManager != null) + persistenceManager.requestPersistence(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { + final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); + if (acceptedArbitrators != null) { + Optional arbitratorOptional = acceptedArbitrators.stream() + .filter(e -> e.getNodeAddress().equals(nodeAddress)) + .findFirst(); + return arbitratorOptional.orElse(null); + } else { + return null; + } + } + + @Nullable + public Mediator getAcceptedMediatorByAddress(NodeAddress nodeAddress) { + final List acceptedMediators = userPayload.getAcceptedMediators(); + if (acceptedMediators != null) { + Optional mediatorOptionalOptional = acceptedMediators.stream() + .filter(e -> e.getNodeAddress().equals(nodeAddress)) + .findFirst(); + return mediatorOptionalOptional.orElse(null); + } else { + return null; + } + } + + @Nullable + public RefundAgent getAcceptedRefundAgentByAddress(NodeAddress nodeAddress) { + final List acceptedRefundAgents = userPayload.getAcceptedRefundAgents(); + if (acceptedRefundAgents != null) { + Optional refundAgentOptional = acceptedRefundAgents.stream() + .filter(e -> e.getNodeAddress().equals(nodeAddress)) + .findFirst(); + return refundAgentOptional.orElse(null); + } else { + return null; + } + } + + @Nullable + public PaymentAccount findFirstPaymentAccountWithCurrency(TradeCurrency tradeCurrency) { + if (userPayload.getPaymentAccounts() != null) { + for (PaymentAccount paymentAccount : userPayload.getPaymentAccounts()) { + for (TradeCurrency currency : paymentAccount.getTradeCurrencies()) { + if (currency.equals(tradeCurrency)) + return paymentAccount; + } + } + return null; + } else { + return null; + } + } + + public boolean hasPaymentAccountForCurrency(TradeCurrency tradeCurrency) { + return findFirstPaymentAccountWithCurrency(tradeCurrency) != null; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Collection operations + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addPaymentAccountIfNotExists(PaymentAccount paymentAccount) { + if (!paymentAccountExists(paymentAccount)) { + addPaymentAccount(paymentAccount); + requestPersistence(); + } + } + + public void addPaymentAccount(PaymentAccount paymentAccount) { + paymentAccount.onAddToUser(); + + boolean changed = paymentAccountsAsObservable.add(paymentAccount); + setCurrentPaymentAccount(paymentAccount); + if (changed) + requestPersistence(); + } + + public void addImportedPaymentAccounts(Collection paymentAccounts) { + isPaymentAccountImport = true; + + boolean changed = paymentAccountsAsObservable.addAll(paymentAccounts); + paymentAccounts.stream().findFirst().ifPresent(this::setCurrentPaymentAccount); + if (changed) + requestPersistence(); + + isPaymentAccountImport = false; + } + + public void removePaymentAccount(PaymentAccount paymentAccount) { + boolean changed = paymentAccountsAsObservable.remove(paymentAccount); + if (changed) + requestPersistence(); + } + + public boolean addAcceptedArbitrator(Arbitrator arbitrator) { + List arbitrators = userPayload.getAcceptedArbitrators(); + if (arbitrators != null && !arbitrators.contains(arbitrator) && !isMyOwnRegisteredArbitrator(arbitrator)) { + arbitrators.add(arbitrator); + requestPersistence(); + return true; + } else { + return false; + } + } + + public void removeAcceptedArbitrator(Arbitrator arbitrator) { + if (userPayload.getAcceptedArbitrators() != null) { + boolean changed = userPayload.getAcceptedArbitrators().remove(arbitrator); + if (changed) + requestPersistence(); + } + } + + public void clearAcceptedArbitrators() { + if (userPayload.getAcceptedArbitrators() != null) { + userPayload.getAcceptedArbitrators().clear(); + requestPersistence(); + } + } + + public boolean addAcceptedMediator(Mediator mediator) { + List mediators = userPayload.getAcceptedMediators(); + if (mediators != null && !mediators.contains(mediator) && !isMyOwnRegisteredMediator(mediator)) { + mediators.add(mediator); + requestPersistence(); + return true; + } else { + return false; + } + } + + public void removeAcceptedMediator(Mediator mediator) { + if (userPayload.getAcceptedMediators() != null) { + boolean changed = userPayload.getAcceptedMediators().remove(mediator); + if (changed) + requestPersistence(); + } + } + + public void clearAcceptedMediators() { + if (userPayload.getAcceptedMediators() != null) { + userPayload.getAcceptedMediators().clear(); + requestPersistence(); + } + } + + public boolean addAcceptedRefundAgent(RefundAgent refundAgent) { + List refundAgents = userPayload.getAcceptedRefundAgents(); + if (refundAgents != null && !refundAgents.contains(refundAgent) && !isMyOwnRegisteredRefundAgent(refundAgent)) { + refundAgents.add(refundAgent); + requestPersistence(); + return true; + } else { + return false; + } + } + + public void removeAcceptedRefundAgent(RefundAgent refundAgent) { + if (userPayload.getAcceptedRefundAgents() != null) { + boolean changed = userPayload.getAcceptedRefundAgents().remove(refundAgent); + if (changed) + requestPersistence(); + } + } + + public void clearAcceptedRefundAgents() { + if (userPayload.getAcceptedRefundAgents() != null) { + userPayload.getAcceptedRefundAgents().clear(); + requestPersistence(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setCurrentPaymentAccount(PaymentAccount paymentAccount) { + currentPaymentAccountProperty.set(paymentAccount); + requestPersistence(); + } + + public void setRegisteredArbitrator(@Nullable Arbitrator arbitrator) { + userPayload.setRegisteredArbitrator(arbitrator); + requestPersistence(); + } + + public void setRegisteredMediator(@Nullable Mediator mediator) { + userPayload.setRegisteredMediator(mediator); + requestPersistence(); + } + + public void setRegisteredRefundAgent(@Nullable RefundAgent refundAgent) { + userPayload.setRegisteredRefundAgent(refundAgent); + requestPersistence(); + } + + public void setDevelopersFilter(@Nullable Filter developersFilter) { + userPayload.setDevelopersFilter(developersFilter); + requestPersistence(); + } + + public void setDevelopersAlert(@Nullable Alert developersAlert) { + userPayload.setDevelopersAlert(developersAlert); + requestPersistence(); + } + + public void setDisplayedAlert(@Nullable Alert displayedAlert) { + userPayload.setDisplayedAlert(displayedAlert); + requestPersistence(); + } + + public void addMarketAlertFilter(MarketAlertFilter filter) { + getMarketAlertFilters().add(filter); + requestPersistence(); + } + + public void removeMarketAlertFilter(MarketAlertFilter filter) { + getMarketAlertFilters().remove(filter); + requestPersistence(); + } + + public void setPriceAlertFilter(PriceAlertFilter filter) { + userPayload.setPriceAlertFilter(filter); + requestPersistence(); + } + + public void removePriceAlertFilter() { + userPayload.setPriceAlertFilter(null); + requestPersistence(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Nullable + public PaymentAccount getPaymentAccount(String paymentAccountId) { + Optional optional = userPayload.getPaymentAccounts() != null ? + userPayload.getPaymentAccounts().stream().filter(e -> e.getId().equals(paymentAccountId)).findAny() : + Optional.empty(); + return optional.orElse(null); + } + + public String getAccountId() { + return userPayload.getAccountId(); + } + + public ReadOnlyObjectProperty currentPaymentAccountProperty() { + return currentPaymentAccountProperty; + } + + @Nullable + public Set getPaymentAccounts() { + return userPayload.getPaymentAccounts(); + } + + public ObservableSet getPaymentAccountsAsObservable() { + return paymentAccountsAsObservable; + } + + /** + * If this user is an arbitrator it returns the registered arbitrator. + * + * @return The arbitrator registered for this user + */ + @Nullable + public Arbitrator getRegisteredArbitrator() { + return userPayload.getRegisteredArbitrator(); + } + + @Nullable + public Mediator getRegisteredMediator() { + return userPayload.getRegisteredMediator(); + } + + @Nullable + public RefundAgent getRegisteredRefundAgent() { + return userPayload.getRegisteredRefundAgent(); + } + + @Nullable + public List getAcceptedArbitrators() { + return userPayload.getAcceptedArbitrators(); + } + + @Nullable + public List getAcceptedMediators() { + return userPayload.getAcceptedMediators(); + } + + @Nullable + public List getAcceptedRefundAgents() { + return userPayload.getAcceptedRefundAgents(); + } + + @Nullable + public List getAcceptedArbitratorAddresses() { + return userPayload.getAcceptedArbitrators() != null ? + userPayload.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()) : + null; + } + + @Nullable + public List getAcceptedMediatorAddresses() { + return userPayload.getAcceptedMediators() != null ? + userPayload.getAcceptedMediators().stream().map(Mediator::getNodeAddress).collect(Collectors.toList()) : + null; + } + + @Nullable + public List getAcceptedRefundAgentAddresses() { + return userPayload.getAcceptedRefundAgents() != null ? + userPayload.getAcceptedRefundAgents().stream().map(RefundAgent::getNodeAddress).collect(Collectors.toList()) : + null; + } + + public boolean hasAcceptedArbitrators() { + return getAcceptedArbitrators() != null && !getAcceptedArbitrators().isEmpty(); + } + + public boolean hasAcceptedMediators() { + return getAcceptedMediators() != null && !getAcceptedMediators().isEmpty(); + } + + public boolean hasAcceptedRefundAgents() { + return getAcceptedRefundAgents() != null && !getAcceptedRefundAgents().isEmpty(); + } + + @Nullable + public Filter getDevelopersFilter() { + return userPayload.getDevelopersFilter(); + } + + @Nullable + public Alert getDevelopersAlert() { + return userPayload.getDevelopersAlert(); + } + + @Nullable + public Alert getDisplayedAlert() { + return userPayload.getDisplayedAlert(); + } + + public boolean isMyOwnRegisteredArbitrator(Arbitrator arbitrator) { + return arbitrator.equals(userPayload.getRegisteredArbitrator()); + } + + public boolean isMyOwnRegisteredMediator(Mediator mediator) { + return mediator.equals(userPayload.getRegisteredMediator()); + } + + public boolean isMyOwnRegisteredRefundAgent(RefundAgent refundAgent) { + return refundAgent.equals(userPayload.getRegisteredRefundAgent()); + } + + public List getMarketAlertFilters() { + return userPayload.getMarketAlertFilters(); + } + + @Nullable + public PriceAlertFilter getPriceAlertFilter() { + return userPayload.getPriceAlertFilter(); + } + + public boolean isPaymentAccountImport() { + return isPaymentAccountImport; + } + + private boolean paymentAccountExists(PaymentAccount paymentAccount) { + return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount)); + } + + public Cookie getCookie() { + return userPayload.getCookie(); + } +} diff --git a/core/src/main/java/bisq/core/user/UserPayload.java b/core/src/main/java/bisq/core/user/UserPayload.java new file mode 100644 index 0000000000..3f2a892f06 --- /dev/null +++ b/core/src/main/java/bisq/core/user/UserPayload.java @@ -0,0 +1,160 @@ +/* + * 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 bisq.core.user; + +import bisq.core.alert.Alert; +import bisq.core.filter.Filter; +import bisq.core.notifications.alerts.market.MarketAlertFilter; +import bisq.core.notifications.alerts.price.PriceAlertFilter; +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; + +import bisq.common.proto.ProtoUtil; +import bisq.common.proto.persistable.PersistableEnvelope; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Data +@AllArgsConstructor +public class UserPayload implements PersistableEnvelope { + @Nullable + private String accountId; + @Nullable + private Set paymentAccounts = new HashSet<>(); + @Nullable + private PaymentAccount currentPaymentAccount; + @Nullable + private List acceptedLanguageLocaleCodes = new ArrayList<>(); + @Nullable + private Alert developersAlert; + @Nullable + private Alert displayedAlert; + @Nullable + private Filter developersFilter; + @Nullable + private Arbitrator registeredArbitrator; + @Nullable + private Mediator registeredMediator; + @Nullable + private List acceptedArbitrators = new ArrayList<>(); + @Nullable + private List acceptedMediators = new ArrayList<>(); + @Nullable + private PriceAlertFilter priceAlertFilter; + @Nullable + private List marketAlertFilters = new ArrayList<>(); + + // Added v1.2.0 + @Nullable + private RefundAgent registeredRefundAgent; + @Nullable + private List acceptedRefundAgents = new ArrayList<>(); + + // Added at 1.5.3 + // Generic map for persisting various UI states. We keep values un-typed as string to + // provide sufficient flexibility. + private Cookie cookie = new Cookie(); + + public UserPayload() { + } + + @Override + public protobuf.PersistableEnvelope toProtoMessage() { + protobuf.UserPayload.Builder builder = protobuf.UserPayload.newBuilder(); + Optional.ofNullable(accountId).ifPresent(e -> builder.setAccountId(accountId)); + Optional.ofNullable(paymentAccounts) + .ifPresent(e -> builder.addAllPaymentAccounts(ProtoUtil.collectionToProto(paymentAccounts, protobuf.PaymentAccount.class))); + Optional.ofNullable(currentPaymentAccount) + .ifPresent(e -> builder.setCurrentPaymentAccount(currentPaymentAccount.toProtoMessage())); + Optional.ofNullable(acceptedLanguageLocaleCodes) + .ifPresent(e -> builder.addAllAcceptedLanguageLocaleCodes(acceptedLanguageLocaleCodes)); + Optional.ofNullable(developersAlert) + .ifPresent(developersAlert -> builder.setDevelopersAlert(developersAlert.toProtoMessage().getAlert())); + Optional.ofNullable(displayedAlert) + .ifPresent(displayedAlert -> builder.setDisplayedAlert(displayedAlert.toProtoMessage().getAlert())); + Optional.ofNullable(developersFilter) + .ifPresent(developersFilter -> builder.setDevelopersFilter(developersFilter.toProtoMessage().getFilter())); + Optional.ofNullable(registeredArbitrator) + .ifPresent(registeredArbitrator -> builder.setRegisteredArbitrator(registeredArbitrator.toProtoMessage().getArbitrator())); + Optional.ofNullable(registeredMediator) + .ifPresent(registeredMediator -> builder.setRegisteredMediator(registeredMediator.toProtoMessage().getMediator())); + Optional.ofNullable(acceptedArbitrators) + .ifPresent(e -> builder.addAllAcceptedArbitrators(ProtoUtil.collectionToProto(acceptedArbitrators, + message -> ((protobuf.StoragePayload) message).getArbitrator()))); + Optional.ofNullable(acceptedMediators) + .ifPresent(e -> builder.addAllAcceptedMediators(ProtoUtil.collectionToProto(acceptedMediators, + message -> ((protobuf.StoragePayload) message).getMediator()))); + Optional.ofNullable(priceAlertFilter).ifPresent(priceAlertFilter -> builder.setPriceAlertFilter(priceAlertFilter.toProtoMessage())); + Optional.ofNullable(marketAlertFilters) + .ifPresent(e -> builder.addAllMarketAlertFilters(ProtoUtil.collectionToProto(marketAlertFilters, protobuf.MarketAlertFilter.class))); + + Optional.ofNullable(registeredRefundAgent) + .ifPresent(registeredRefundAgent -> builder.setRegisteredRefundAgent(registeredRefundAgent.toProtoMessage().getRefundAgent())); + Optional.ofNullable(acceptedRefundAgents) + .ifPresent(e -> builder.addAllAcceptedRefundAgents(ProtoUtil.collectionToProto(acceptedRefundAgents, + message -> ((protobuf.StoragePayload) message).getRefundAgent()))); + Optional.ofNullable(cookie).ifPresent(e -> builder.putAllCookie(cookie.toProtoMessage())); + return protobuf.PersistableEnvelope.newBuilder().setUserPayload(builder).build(); + } + + public static UserPayload fromProto(protobuf.UserPayload proto, CoreProtoResolver coreProtoResolver) { + return new UserPayload( + ProtoUtil.stringOrNullFromProto(proto.getAccountId()), + proto.getPaymentAccountsList().isEmpty() ? new HashSet<>() : new HashSet<>(proto.getPaymentAccountsList().stream() + .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) + .collect(Collectors.toSet())), + proto.hasCurrentPaymentAccount() ? PaymentAccount.fromProto(proto.getCurrentPaymentAccount(), coreProtoResolver) : null, + proto.getAcceptedLanguageLocaleCodesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedLanguageLocaleCodesList()), + proto.hasDevelopersAlert() ? Alert.fromProto(proto.getDevelopersAlert()) : null, + proto.hasDisplayedAlert() ? Alert.fromProto(proto.getDisplayedAlert()) : null, + proto.hasDevelopersFilter() ? Filter.fromProto(proto.getDevelopersFilter()) : null, + proto.hasRegisteredArbitrator() ? Arbitrator.fromProto(proto.getRegisteredArbitrator()) : null, + proto.hasRegisteredMediator() ? Mediator.fromProto(proto.getRegisteredMediator()) : null, + proto.getAcceptedArbitratorsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedArbitratorsList().stream() + .map(Arbitrator::fromProto) + .collect(Collectors.toList())), + proto.getAcceptedMediatorsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedMediatorsList().stream() + .map(Mediator::fromProto) + .collect(Collectors.toList())), + PriceAlertFilter.fromProto(proto.getPriceAlertFilter()), + proto.getMarketAlertFiltersList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getMarketAlertFiltersList().stream() + .map(e -> MarketAlertFilter.fromProto(e, coreProtoResolver)) + .collect(Collectors.toSet())), + proto.hasRegisteredRefundAgent() ? RefundAgent.fromProto(proto.getRegisteredRefundAgent()) : null, + proto.getAcceptedRefundAgentsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedRefundAgentsList().stream() + .map(RefundAgent::fromProto) + .collect(Collectors.toList())), + Cookie.fromProto(proto.getCookieMap()) + ); + } +} diff --git a/core/src/main/java/bisq/core/util/AveragePriceUtil.java b/core/src/main/java/bisq/core/util/AveragePriceUtil.java new file mode 100644 index 0000000000..beb320e0e2 --- /dev/null +++ b/core/src/main/java/bisq/core/util/AveragePriceUtil.java @@ -0,0 +1,139 @@ +/* + * 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 bisq.core.util; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; + +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import org.bitcoinj.utils.Fiat; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.stream.Collectors; + +public class AveragePriceUtil { + private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10; + + public static Tuple2 getAveragePriceTuple(Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + int days) { + double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); + Date pastXDays = getPastDate(days); + List bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("BSQ")) + .filter(e -> e.getDate().after(pastXDays)) + .collect(Collectors.toList()); + List bsqTradePastXDays = percentToTrim > 0 ? + removeOutliers(bsqAllTradePastXDays, percentToTrim) : + bsqAllTradePastXDays; + + List usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("USD")) + .filter(e -> e.getDate().after(pastXDays)) + .collect(Collectors.toList()); + List usdTradePastXDays = percentToTrim > 0 ? + removeOutliers(usdAllTradePastXDays, percentToTrim) : + usdAllTradePastXDays; + + Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays)); + Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays)); + return new Tuple2<>(usdPrice, bsqPrice); + } + + private static List removeOutliers(List list, double percentToTrim) { + List yValues = list.stream() + .filter(TradeStatistics3::isValid) + .map(e -> (double) e.getPrice()) + .collect(Collectors.toList()); + + Tuple2 tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER); + double lowerBound = tuple.first; + double upperBound = tuple.second; + return list.stream() + .filter(e -> e.getPrice() > lowerBound) + .filter(e -> e.getPrice() < upperBound) + .collect(Collectors.toList()); + } + + private static long getBTCAverage(List list) { + long accumulatedVolume = 0; + long accumulatedAmount = 0; + + for (TradeStatistics3 item : list) { + accumulatedVolume += item.getTradeVolume().getValue(); + accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded + } + long averagePrice; + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); + averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0; + + return averagePrice; + } + + private static long getUSDAverage(List bsqList, List usdList) { + // Use next USD/BTC print as price to calculate BSQ/USD rate + // Store each trade as amount of USD and amount of BSQ traded + List> usdBsqList = new ArrayList<>(bsqList.size()); + usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); + var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all + + for (TradeStatistics3 item : bsqList) { + // Find usdprice for trade item + usdBTCPrice = usdList.stream() + .filter(usd -> usd.getDateAsLong() > item.getDateAsLong()) + .map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(), + Fiat.SMALLEST_UNIT_EXPONENT)) + .findFirst() + .orElse(usdBTCPrice); + var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(), + Altcoin.SMALLEST_UNIT_EXPONENT); + var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(), + Altcoin.SMALLEST_UNIT_EXPONENT); + usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount)); + } + long averagePrice; + var usdTraded = usdBsqList.stream() + .mapToDouble(item -> item.first) + .sum(); + var bsqTraded = usdBsqList.stream() + .mapToDouble(item -> item.second) + .sum(); + var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d; + var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT); + averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0; + + return averagePrice; + } + + private static Date getPastDate(int days) { + Calendar cal = new GregorianCalendar(); + cal.setTime(new Date()); + cal.add(Calendar.DAY_OF_MONTH, -1 * days); + return cal.getTime(); + } +} diff --git a/core/src/main/java/bisq/core/util/FeeReceiverSelector.java b/core/src/main/java/bisq/core/util/FeeReceiverSelector.java new file mode 100644 index 0000000000..2854a0e09f --- /dev/null +++ b/core/src/main/java/bisq/core/util/FeeReceiverSelector.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.util; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.param.Param; +import bisq.core.filter.FilterManager; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeeReceiverSelector { + public static String getAddress(DaoFacade daoFacade, FilterManager filterManager) { + return getAddress(daoFacade, filterManager, new Random()); + } + + @VisibleForTesting + static String getAddress(DaoFacade daoFacade, FilterManager filterManager, Random rnd) { + List feeReceivers = Optional.ofNullable(filterManager.getFilter()) + .flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses())) + .orElse(List.of()); + + List amountList = new ArrayList<>(); + List receiverAddressList = new ArrayList<>(); + + feeReceivers.forEach(e -> { + try { + String[] tokens = e.split("#"); + amountList.add(Coin.parseCoin(tokens[1]).longValue()); // total amount the victim should receive + receiverAddressList.add(tokens[0]); // victim's receiver address + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + }); + + if (!amountList.isEmpty()) { + return receiverAddressList.get(weightedSelection(amountList, rnd)); + } + + // We keep default value as fallback in case no filter value is available or user has old version. + return daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS); + } + + @VisibleForTesting + static int weightedSelection(List weights, Random rnd) { + long sum = weights.stream().mapToLong(n -> n).sum(); + long target = rnd.longs(0, sum).findFirst().orElseThrow(); + int i; + for (i = 0; i < weights.size() && target >= 0; i++) { + target -= weights.get(i); + } + return i - 1; + } +} diff --git a/core/src/main/java/bisq/core/util/FormattingUtils.java b/core/src/main/java/bisq/core/util/FormattingUtils.java new file mode 100644 index 0000000000..d6658e7129 --- /dev/null +++ b/core/src/main/java/bisq/core/util/FormattingUtils.java @@ -0,0 +1,299 @@ +package bisq.core.util; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.Fiat; +import org.bitcoinj.utils.MonetaryFormat; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DurationFormatUtils; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class FormattingUtils { + public static final String BTC_FORMATTER_KEY = "BTC"; + + public final static String RANGE_SEPARATOR = " - "; + + private static final MonetaryFormat fiatPriceFormat = new MonetaryFormat().shift(0).minDecimals(4).repeatOptionalDecimals(0, 0); + private static final MonetaryFormat altcoinFormat = new MonetaryFormat().shift(0).minDecimals(8).repeatOptionalDecimals(0, 0); + private static final DecimalFormat decimalFormat = new DecimalFormat("#.#"); + + public static String formatCoinWithCode(long value, MonetaryFormat coinFormat) { + return formatCoinWithCode(Coin.valueOf(value), coinFormat); + } + + public static String formatCoinWithCode(Coin coin, MonetaryFormat coinFormat) { + if (coin != null) { + try { + // we don't use the code feature from coinFormat as it does automatic switching between mBTC and BTC and + // pre and post fixing + return coinFormat.postfixCode().format(coin).toString(); + } catch (Throwable t) { + log.warn("Exception at formatCoinWithCode: " + t.toString()); + return ""; + } + } else { + return ""; + } + } + + public static String formatCoin(long value, MonetaryFormat coinFormat) { + return formatCoin(Coin.valueOf(value), -1, false, 0, coinFormat); + } + + public static String formatCoin(Coin coin, + int decimalPlaces, + boolean decimalAligned, + int maxNumberOfDigits, + MonetaryFormat coinFormat) { + String formattedCoin = ""; + + if (coin != null) { + try { + if (decimalPlaces < 0 || decimalPlaces > 4) { + formattedCoin = coinFormat.noCode().format(coin).toString(); + } else { + formattedCoin = coinFormat.noCode().minDecimals(decimalPlaces).repeatOptionalDecimals(1, decimalPlaces).format(coin).toString(); + } + } catch (Throwable t) { + log.warn("Exception at formatBtc: " + t.toString()); + } + } + + if (decimalAligned) { + formattedCoin = fillUpPlacesWithEmptyStrings(formattedCoin, maxNumberOfDigits); + } + + return formattedCoin; + } + + public static String formatFiat(Fiat fiat, MonetaryFormat format, boolean appendCurrencyCode) { + if (fiat != null) { + try { + final String res = format.noCode().format(fiat).toString(); + if (appendCurrencyCode) + return res + " " + fiat.getCurrencyCode(); + else + return res; + } catch (Throwable t) { + log.warn("Exception at formatFiatWithCode: " + t.toString()); + return Res.get("shared.na") + " " + fiat.getCurrencyCode(); + } + } else { + return Res.get("shared.na"); + } + } + + private static String formatAltcoin(Altcoin altcoin, boolean appendCurrencyCode) { + if (altcoin != null) { + try { + String res = altcoinFormat.noCode().format(altcoin).toString(); + if (appendCurrencyCode) + return res + " " + altcoin.getCurrencyCode(); + else + return res; + } catch (Throwable t) { + log.warn("Exception at formatAltcoin: " + t.toString()); + return Res.get("shared.na") + " " + altcoin.getCurrencyCode(); + } + } else { + return Res.get("shared.na"); + } + } + + public static String formatAltcoinVolume(Altcoin altcoin, boolean appendCurrencyCode) { + if (altcoin != null) { + try { + // TODO quick hack... + String res; + if (altcoin.getCurrencyCode().equals("BSQ")) + res = altcoinFormat.noCode().minDecimals(2).repeatOptionalDecimals(0, 0).format(altcoin).toString(); + else + res = altcoinFormat.noCode().format(altcoin).toString(); + if (appendCurrencyCode) + return res + " " + altcoin.getCurrencyCode(); + else + return res; + } catch (Throwable t) { + log.warn("Exception at formatAltcoinVolume: " + t.toString()); + return Res.get("shared.na") + " " + altcoin.getCurrencyCode(); + } + } else { + return Res.get("shared.na"); + } + } + + public static String formatPrice(Price price, MonetaryFormat fiatPriceFormat, boolean appendCurrencyCode) { + if (price != null) { + Monetary monetary = price.getMonetary(); + if (monetary instanceof Fiat) + return formatFiat((Fiat) monetary, fiatPriceFormat, appendCurrencyCode); + else + return formatAltcoin((Altcoin) monetary, appendCurrencyCode); + } else { + return Res.get("shared.na"); + } + } + + public static String formatPrice(Price price, boolean appendCurrencyCode) { + return formatPrice(price, fiatPriceFormat, appendCurrencyCode); + } + + public static String formatPrice(Price price) { + return formatPrice(price, fiatPriceFormat, false); + } + + public static String formatMarketPrice(double price, String currencyCode) { + if (CurrencyUtil.isFiatCurrency(currencyCode)) + return formatMarketPrice(price, 2); + else + return formatMarketPrice(price, 8); + } + + public static String formatMarketPrice(double price, int precision) { + return formatRoundedDoubleWithPrecision(price, precision); + } + + public static String formatRoundedDoubleWithPrecision(double value, int precision) { + decimalFormat.setMinimumFractionDigits(precision); + decimalFormat.setMaximumFractionDigits(precision); + return decimalFormat.format(MathUtils.roundDouble(value, precision)).replace(",", "."); + } + + public static String formatDateTime(Date date, boolean useLocaleAndLocalTimezone) { + Locale locale = useLocaleAndLocalTimezone ? GlobalSettings.getLocale() : Locale.US; + DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); + DateFormat timeInstance = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); + if (!useLocaleAndLocalTimezone) { + dateInstance.setTimeZone(TimeZone.getTimeZone("UTC")); + timeInstance.setTimeZone(TimeZone.getTimeZone("UTC")); + } + return formatDateTime(date, dateInstance, timeInstance); + } + + public static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { + if (date != null) { + return dateFormatter.format(date) + " " + timeFormatter.format(date); + } else { + return ""; + } + } + + public static String getDateFromBlockHeight(long blockHeight) { + long now = new Date().getTime(); + SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM", Locale.getDefault()); + SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault()); + return formatDateTime(new Date(now + blockHeight * 10 * 60 * 1000L), dateFormatter, timeFormatter); + } + + public static String formatToPercentWithSymbol(double value) { + return formatToPercent(value) + "%"; + } + + public static String formatToRoundedPercentWithSymbol(double value) { + return formatToPercent(value, new DecimalFormat("#")) + "%"; + } + + public static String formatPercentagePrice(double value) { + return formatToPercentWithSymbol(value); + } + + public static String formatToPercent(double value) { + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + decimalFormat.setMinimumFractionDigits(2); + decimalFormat.setMaximumFractionDigits(2); + + return formatToPercent(value, decimalFormat); + } + + public static String formatToPercent(double value, DecimalFormat decimalFormat) { + return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", "."); + } + + public static String formatDurationAsWords(long durationMillis) { + return formatDurationAsWords(durationMillis, false, true); + } + + public static String formatDurationAsWords(long durationMillis, boolean showSeconds, boolean showZeroValues) { + String format = ""; + String second = Res.get("time.second"); + String minute = Res.get("time.minute"); + String hour = Res.get("time.hour").toLowerCase(); + String day = Res.get("time.day").toLowerCase(); + String days = Res.get("time.days"); + String hours = Res.get("time.hours"); + String minutes = Res.get("time.minutes"); + String seconds = Res.get("time.seconds"); + + if (durationMillis >= TimeUnit.DAYS.toMillis(1)) { + format = "d\' " + days + ", \'"; + } + + if (showSeconds) { + format += "H\' " + hours + ", \'m\' " + minutes + ", \'s\' " + seconds + "\'"; + } else { + format += "H\' " + hours + ", \'m\' " + minutes + "\'"; + } + + String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : ""; + + duration = StringUtils.replacePattern(duration, "^1 " + seconds + "|\\b1 " + seconds, "1 " + second); + duration = StringUtils.replacePattern(duration, "^1 " + minutes + "|\\b1 " + minutes, "1 " + minute); + duration = StringUtils.replacePattern(duration, "^1 " + hours + "|\\b1 " + hours, "1 " + hour); + duration = StringUtils.replacePattern(duration, "^1 " + days + "|\\b1 " + days, "1 " + day); + + if (!showZeroValues) { + duration = duration.replace(", 0 seconds", ""); + duration = duration.replace(", 0 minutes", ""); + duration = duration.replace(", 0 hours", ""); + duration = StringUtils.replacePattern(duration, "^0 days, ", ""); + duration = StringUtils.replacePattern(duration, "^0 hours, ", ""); + duration = StringUtils.replacePattern(duration, "^0 minutes, ", ""); + duration = StringUtils.replacePattern(duration, "^0 seconds, ", ""); + } + return duration.trim(); + } + + public static String formatBytes(long bytes) { + double kb = 1024; + double mb = kb * kb; + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + if (bytes < kb) + return bytes + " bytes"; + else if (bytes < mb) + return decimalFormat.format(bytes / kb) + " KB"; + else + return decimalFormat.format(bytes / mb) + " MB"; + } + + @NotNull + public static String fillUpPlacesWithEmptyStrings(String formattedNumber, @SuppressWarnings("unused") int maxNumberOfDigits) { + //FIXME: temporary deactivate adding spaces in front of numbers as we don't use a monospace font right now. + /*int numberOfPlacesToFill = maxNumberOfDigits - formattedNumber.length(); + for (int i = 0; i < numberOfPlacesToFill; i++) { + formattedNumber = " " + formattedNumber; + }*/ + return formattedNumber; + } +} diff --git a/core/src/main/java/bisq/core/util/InlierUtil.java b/core/src/main/java/bisq/core/util/InlierUtil.java new file mode 100644 index 0000000000..41aefac866 --- /dev/null +++ b/core/src/main/java/bisq/core/util/InlierUtil.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.util; + +import bisq.common.util.DoubleSummaryStatisticsWithStdDev; +import bisq.common.util.Tuple2; + +import javafx.collections.FXCollections; + +import java.util.DoubleSummaryStatistics; +import java.util.List; +import java.util.stream.Collectors; + +public class InlierUtil { + + /* Finds the minimum and maximum inlier values. The returned values may be NaN. + * See `computeInlierThreshold` for the definition of inlier. + */ + public static Tuple2 findInlierRange( + List yValues, + double percentToTrim, + double howManyStdDevsConstituteOutlier + ) { + Tuple2 inlierThreshold = + computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier); + + DoubleSummaryStatistics inlierStatistics = + yValues + .stream() + .filter(y -> withinBounds(inlierThreshold, y)) + .mapToDouble(Double::doubleValue) + .summaryStatistics(); + + var inlierMin = inlierStatistics.getMin(); + var inlierMax = inlierStatistics.getMax(); + + return new Tuple2<>(inlierMin, inlierMax); + } + + private static boolean withinBounds(Tuple2 bounds, double number) { + var lowerBound = bounds.first; + var upperBound = bounds.second; + return (lowerBound <= number) && (number <= upperBound); + } + + /* Computes the lower and upper inlier thresholds. A point lying outside + * these thresholds is considered an outlier, and a point lying within + * is considered an inlier. + * The thresholds are found by trimming the dataset (see method `trim`), + * then adding or subtracting a multiple of its (trimmed) standard + * deviation from its (trimmed) mean. + */ + private static Tuple2 computeInlierThreshold( + List numbers, double percentToTrim, double howManyStdDevsConstituteOutlier + ) { + if (howManyStdDevsConstituteOutlier <= 0) { + throw new IllegalArgumentException( + "howManyStdDevsConstituteOutlier should be a positive number"); + } + + List trimmed = trim(percentToTrim, numbers); + + DoubleSummaryStatisticsWithStdDev summaryStatistics = + trimmed.stream() + .collect( + DoubleSummaryStatisticsWithStdDev::new, + DoubleSummaryStatisticsWithStdDev::accept, + DoubleSummaryStatisticsWithStdDev::combine); + + double mean = summaryStatistics.getAverage(); + double stdDev = summaryStatistics.getStandardDeviation(); + + var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier); + var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier); + + return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold); + } + + /* Sorts the data and discards given percentage from the left and right sides each. + * E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded. + * Used in calculating trimmed mean (and in turn trimmed standard deviation), + * which is more robust to outliers than a simple mean. + */ + private static List trim(double percentToTrim, List numbers) { + var minPercentToTrim = 0; + var maxPercentToTrim = 50; + if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) { + throw new IllegalArgumentException( + String.format( + "The percentage of data points to trim must be in the range [%d,%d].", + minPercentToTrim, maxPercentToTrim)); + } + + var totalPercentTrim = percentToTrim * 2; + if (totalPercentTrim == 0) { + return numbers; + } + if (totalPercentTrim == 100) { + return FXCollections.emptyObservableList(); + } + + if (numbers.isEmpty()) { + return numbers; + } + + var count = numbers.size(); + int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0? + if (countToDropFromEachSide == 0) { + return numbers; + } + + var sorted = numbers.stream().sorted(); + + var oneSideTrimmed = sorted.skip(countToDropFromEachSide); + + // Here, having already trimmed the left-side, we are implicitly trimming + // the right-side by specifying a limit to the stream's length. + // An explicit right-side drop/trim/skip is not supported by the Stream API. + var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count? + var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim); + + return bothSidesTrimmed.collect(Collectors.toList()); + } + +} diff --git a/core/src/main/java/bisq/core/util/ParsingUtils.java b/core/src/main/java/bisq/core/util/ParsingUtils.java new file mode 100644 index 0000000000..b9910f5fbd --- /dev/null +++ b/core/src/main/java/bisq/core/util/ParsingUtils.java @@ -0,0 +1,84 @@ +package bisq.core.util; + +import bisq.core.monetary.Price; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.MonetaryFormat; + +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ParsingUtils { + public static Coin parseToCoin(String input, CoinFormatter coinFormatter) { + return parseToCoin(input, coinFormatter.getMonetaryFormat()); + } + + public static Coin parseToCoin(String input, MonetaryFormat coinFormat) { + if (input != null && input.length() > 0) { + try { + return coinFormat.parse(cleanDoubleInput(input)); + } catch (Throwable t) { + log.warn("Exception at parseToBtc: " + t.toString()); + return Coin.ZERO; + } + } else { + return Coin.ZERO; + } + } + + public static double parseNumberStringToDouble(String input) throws NumberFormatException { + return Double.parseDouble(cleanDoubleInput(input)); + } + + public static double parsePercentStringToDouble(String percentString) throws NumberFormatException { + String input = percentString.replace("%", ""); + input = cleanDoubleInput(input); + double value = Double.parseDouble(input); + return MathUtils.roundDouble(value / 100d, 4); + } + + public static long parsePriceStringToLong(String currencyCode, String amount, int precision) { + if (amount == null || amount.isEmpty()) + return 0; + + long value = 0; + try { + double amountValue = Double.parseDouble(amount); + amount = FormattingUtils.formatRoundedDoubleWithPrecision(amountValue, precision); + value = Price.parse(currencyCode, amount).getValue(); + } catch (NumberFormatException ignore) { + // expected NumberFormatException if input is not a number + } catch (Throwable t) { + log.error("parsePriceStringToLong: " + t.toString()); + } + + return value; + } + + public static String convertCharsForNumber(String input) { + // Some languages like Finnish use the long dash for the minus + input = input.replace("−", "-"); + input = StringUtils.deleteWhitespace(input); + return input.replace(",", "."); + } + + public static String cleanDoubleInput(String input) { + input = convertCharsForNumber(input); + if (input.equals(".")) + input = input.replace(".", "0."); + if (input.equals("-.")) + input = input.replace("-.", "-0."); + // don't use String.valueOf(Double.parseDouble(input)) as return value as it gives scientific + // notation (1.0E-6) which screw up coinFormat.parse + //noinspection ResultOfMethodCallIgnored + // Just called to check if we have a valid double, throws exception otherwise + //noinspection ResultOfMethodCallIgnored + Double.parseDouble(input); + return input; + } +} diff --git a/core/src/main/java/bisq/core/util/Validator.java b/core/src/main/java/bisq/core/util/Validator.java new file mode 100644 index 0000000000..52da492774 --- /dev/null +++ b/core/src/main/java/bisq/core/util/Validator.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.core.util; + +import bisq.core.trade.messages.TradeMessage; + +import org.bitcoinj.core.Coin; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Utility class for validating domain data. + */ +public class Validator { + + public static String nonEmptyStringOf(String value) { + checkNotNull(value); + checkArgument(value.length() > 0); + return value; + } + + public static long nonNegativeLongOf(long value) { + checkArgument(value >= 0); + return value; + } + + public static Coin nonZeroCoinOf(Coin value) { + checkNotNull(value); + checkArgument(!value.isZero()); + return value; + } + + public static Coin positiveCoinOf(Coin value) { + checkNotNull(value); + checkArgument(value.isPositive()); + return value; + } + + public static void checkTradeId(String tradeId, TradeMessage tradeMessage) { + checkArgument(isTradeIdValid(tradeId, tradeMessage)); + } + + public static boolean isTradeIdValid(String tradeId, TradeMessage tradeMessage) { + return tradeId.equals(tradeMessage.getTradeId()); + } +} diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java new file mode 100644 index 0000000000..4150aaa875 --- /dev/null +++ b/core/src/main/java/bisq/core/util/VolumeUtil.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.core.util; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.AltcoinExchangeRate; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + +public class VolumeUtil { + + public static Volume getRoundedFiatVolume(Volume volumeByAmount) { + // We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR. + return getAdjustedFiatVolume(volumeByAmount, 1); + } + + public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) { + // EUR has precision 4 and we want multiple of 10 so we divide by 100000 then + // round and multiply with 10 + return getAdjustedFiatVolume(volumeByAmount, 10); + } + + /** + * + * @param volumeByAmount The volume generated from an amount + * @param factor The factor used for rounding. E.g. 1 means rounded to + * units of 1 EUR, 10 means rounded to 10 EUR. + * @return The adjusted Fiat volume + */ + public static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) { + // Fiat currencies use precision 4 and we want multiple of factor so we divide by 10000 * factor then + // round and multiply with factor + long roundedVolume = Math.round((double) volumeByAmount.getValue() / (10000d * factor)) * factor; + // Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...) + roundedVolume = Math.max(factor, roundedVolume); + return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); + } + + public static Volume getVolume(Coin amount, Price price) { + if (price.getMonetary() instanceof Altcoin) { + return new Volume(new AltcoinExchangeRate((Altcoin) price.getMonetary()).coinToAltcoin(amount)); + } else { + return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount)); + } + } +} diff --git a/core/src/main/java/bisq/core/util/coin/BsqFormatter.java b/core/src/main/java/bisq/core/util/coin/BsqFormatter.java new file mode 100644 index 0000000000..ab476097be --- /dev/null +++ b/core/src/main/java/bisq/core/util/coin/BsqFormatter.java @@ -0,0 +1,252 @@ +/* + * 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 bisq.core.util.coin; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.validation.BtcAddressValidator; +import bisq.core.util.validation.InputValidator; + +import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.utils.MonetaryFormat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.util.Locale; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class BsqFormatter implements CoinFormatter { + private final ImmutableCoinFormatter immutableCoinFormatter; + + // We don't support localized formatting. Format is always using "." as decimal mark and no grouping separator. + // Input of "," as decimal mark (like in German locale) will be replaced with ".". + // Input of a group separator (1,123,45) leads to a validation error. + // Note: BtcFormat was intended to be used, but it leads to many problems (automatic format to mBit, + // no way to remove grouping separator). It seems to be not optimal for user input formatting. + @Getter + private MonetaryFormat monetaryFormat; + + + @SuppressWarnings("PointlessBooleanExpression") + private static final boolean useBsqAddressFormat = true || !DevEnv.isDevMode(); + private final String prefix = "B"; + private DecimalFormat amountFormat; + private DecimalFormat marketCapFormat; + private final MonetaryFormat btcCoinFormat; + + @Inject + public BsqFormatter() { + this.btcCoinFormat = Config.baseCurrencyNetworkParameters().getMonetaryFormat(); + this.monetaryFormat = new MonetaryFormat().shift(6).code(6, "BSQ").minDecimals(2); + this.immutableCoinFormatter = new ImmutableCoinFormatter(monetaryFormat); + + GlobalSettings.localeProperty().addListener((observable, oldValue, newValue) -> switchLocale(newValue)); + switchLocale(GlobalSettings.getLocale()); + + amountFormat.setMinimumFractionDigits(2); + } + + private void switchLocale(Locale locale) { + amountFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); + amountFormat.setMinimumFractionDigits(2); + amountFormat.setMaximumFractionDigits(2); + + marketCapFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); + marketCapFormat = new DecimalFormat(); + marketCapFormat.setMaximumFractionDigits(0); + } + + /** + * Returns the base-58 encoded String representation of this + * object, including version and checksum bytes. + */ + public String getBsqAddressStringFromAddress(LegacyAddress address) { + final String addressString = address.toString(); + if (useBsqAddressFormat) + return prefix + addressString; + else + return addressString; + + } + + public LegacyAddress getAddressFromBsqAddress(String encoded) { + String maybeUpdatedEncoded = encoded; + if (useBsqAddressFormat) + maybeUpdatedEncoded = encoded.substring(prefix.length(), encoded.length()); + + try { + return LegacyAddress.fromBase58(Config.baseCurrencyNetworkParameters(), maybeUpdatedEncoded); + } catch (AddressFormatException e) { + throw new RuntimeException(e); + } + } + + public String formatAmountWithGroupSeparatorAndCode(Coin amount) { + return amountFormat.format(MathUtils.scaleDownByPowerOf10(amount.value, 2)) + " BSQ"; + } + + public String formatMarketCap(Price usdBsqPrice, Coin issuedAmount) { + if (usdBsqPrice != null && issuedAmount != null) { + double marketCap = usdBsqPrice.getValue() * (MathUtils.scaleDownByPowerOf10(issuedAmount.value, 6)); + return marketCapFormat.format(MathUtils.doubleToLong(marketCap)) + " USD"; + } else { + return ""; + } + } + + public String formatBSQSatoshis(long satoshi) { + return FormattingUtils.formatCoin(satoshi, monetaryFormat); + } + + public String formatBSQSatoshisWithCode(long satoshi) { + return FormattingUtils.formatCoinWithCode(satoshi, monetaryFormat); + } + + public String formatBTCSatoshis(long satoshi) { + return FormattingUtils.formatCoin(satoshi, btcCoinFormat); + } + + public String formatBTCWithCode(long satoshi) { + return FormattingUtils.formatCoinWithCode(satoshi, btcCoinFormat); + } + + public String formatBTCWithCode(Coin coin) { + return FormattingUtils.formatCoinWithCode(coin, btcCoinFormat); + } + + private String formatBTC(Coin coin) { + return FormattingUtils.formatCoin(coin.value, btcCoinFormat); + } + + public Coin parseToBTC(String input) { + return ParsingUtils.parseToCoin(input, btcCoinFormat); + } + + public String formatParamValue(Param param, String value) { + switch (param.getParamType()) { + case UNDEFINED: + // In case we add a new param old clients will not know that enum and fall back to UNDEFINED. + return Res.get("shared.na"); + case BSQ: + return formatCoinWithCode(ParsingUtils.parseToCoin(value, this)); + case BTC: + return formatBTCWithCode(parseToBTC(value)); + case PERCENT: + return FormattingUtils.formatToPercentWithSymbol(ParsingUtils.parsePercentStringToDouble(value)); + case BLOCK: + return Res.get("dao.param.blocks", Integer.parseInt(value)); + case ADDRESS: + return value; + default: + log.warn("Param type {} not handled in switch case at formatParamValue", param.getParamType()); + return Res.get("shared.na"); + } + } + + public Coin parseParamValueToCoin(Param param, String inputValue) { + switch (param.getParamType()) { + case BSQ: + return ParsingUtils.parseToCoin(inputValue, this); + case BTC: + return parseToBTC(inputValue); + default: + throw new IllegalArgumentException("Unsupported paramType. param: " + param); + } + } + + private int parseParamValueToBlocks(Param param, String inputValue) { + switch (param.getParamType()) { + case BLOCK: + return Integer.parseInt(inputValue); + default: + throw new IllegalArgumentException("Unsupported paramType. param: " + param); + } + } + + public String parseParamValueToString(Param param, String inputValue) throws ProposalValidationException { + switch (param.getParamType()) { + case UNDEFINED: + return Res.get("shared.na"); + case BSQ: + return formatCoin(parseParamValueToCoin(param, inputValue)); + case BTC: + return formatBTC(parseParamValueToCoin(param, inputValue)); + case PERCENT: + return FormattingUtils.formatToPercent(ParsingUtils.parsePercentStringToDouble(inputValue)); + case BLOCK: + return Integer.toString(parseParamValueToBlocks(param, inputValue)); + case ADDRESS: + InputValidator.ValidationResult validationResult = new BtcAddressValidator().validate(inputValue); + if (validationResult.isValid) + return inputValue; + else + throw new ProposalValidationException(validationResult.errorMessage); + default: + log.warn("Param type {} not handled in switch case at parseParamValueToString", param.getParamType()); + return Res.get("shared.na"); + } + } + + public String formatCoin(Coin coin) { + return formatCoin(coin, false); + } + + public String formatCoin(Coin coin, boolean appendCode) { + return appendCode ? + immutableCoinFormatter.formatCoinWithCode(coin) : + immutableCoinFormatter.formatCoin(coin); + } + + public String formatCoin(Coin coin, int decimalPlaces) { + return immutableCoinFormatter.formatCoin(coin, decimalPlaces); + } + + public String formatCoin(Coin coin, + int decimalPlaces, + boolean decimalAligned, + int maxNumberOfDigits) { + return immutableCoinFormatter.formatCoin(coin, decimalPlaces, decimalAligned, maxNumberOfDigits); + } + + public String formatCoinWithCode(Coin coin) { + return formatCoin(coin, true); + } + + public String formatCoinWithCode(long value) { + return immutableCoinFormatter.formatCoinWithCode(value); + } +} diff --git a/core/src/main/java/bisq/core/util/coin/CoinFormatter.java b/core/src/main/java/bisq/core/util/coin/CoinFormatter.java new file mode 100644 index 0000000000..2b65460e6b --- /dev/null +++ b/core/src/main/java/bisq/core/util/coin/CoinFormatter.java @@ -0,0 +1,19 @@ +package bisq.core.util.coin; + +import org.bitcoinj.core.Coin; + +public interface CoinFormatter { + String formatCoin(Coin coin); + + String formatCoin(Coin coin, boolean appendCode); + + String formatCoin(Coin coin, int decimalPlaces); + + String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits); + + String formatCoinWithCode(Coin coin); + + String formatCoinWithCode(long value); + + org.bitcoinj.utils.MonetaryFormat getMonetaryFormat(); +} diff --git a/core/src/main/java/bisq/core/util/coin/CoinUtil.java b/core/src/main/java/bisq/core/util/coin/CoinUtil.java new file mode 100644 index 0000000000..3f8b16bda0 --- /dev/null +++ b/core/src/main/java/bisq/core/util/coin/CoinUtil.java @@ -0,0 +1,187 @@ +/* + * 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 bisq.core.util.coin; + +import bisq.core.btc.wallet.Restrictions; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.provider.fee.FeeService; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import javax.annotation.Nullable; + +import static bisq.core.util.VolumeUtil.getAdjustedFiatVolume; +import static com.google.common.base.Preconditions.checkArgument; + +public class CoinUtil { + + // Get the fee per amount + public static Coin getFeePerBtc(Coin feePerBtc, Coin amount) { + double feePerBtcAsDouble = feePerBtc != null ? (double) feePerBtc.value : 0; + double amountAsDouble = amount != null ? (double) amount.value : 0; + double btcAsDouble = (double) Coin.COIN.value; + double fact = amountAsDouble / btcAsDouble; + return Coin.valueOf(Math.round(feePerBtcAsDouble * fact)); + } + + public static Coin minCoin(Coin a, Coin b) { + return a.compareTo(b) <= 0 ? a : b; + } + + public static Coin maxCoin(Coin a, Coin b) { + return a.compareTo(b) >= 0 ? a : b; + } + + public static double getFeePerVbyte(Coin miningFee, int txVsize) { + double value = miningFee != null ? miningFee.value : 0; + return MathUtils.roundDouble((value / (double) txVsize), 2); + } + + /** + * @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC) + * @return The percentage value as double (e.g. 1% is 0.01) + */ + public static double getAsPercentPerBtc(Coin value) { + return getAsPercentPerBtc(value, Coin.COIN); + } + + /** + * @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 + * + * @return The percentage value as double (e.g. 1% is 0.01) + */ + public static double getAsPercentPerBtc(Coin part, Coin total) { + double asDouble = part != null ? (double) part.value : 0; + double btcAsDouble = total != null ? (double) total.value : 1; + return MathUtils.roundDouble(asDouble / btcAsDouble, 4); + } + + /** + * @param percent The percentage value as double (e.g. 1% is 0.01) + * @param amount The amount as Coin for the percentage calculation + * @return The percentage as Coin (e.g. 1% of 1 BTC is 0.01 BTC) + */ + public static Coin getPercentOfAmountAsCoin(double percent, Coin amount) { + double amountAsDouble = amount != null ? (double) amount.value : 0; + return Coin.valueOf(Math.round(percent * amountAsDouble)); + } + + + /** + * Calculates the maker fee for the given amount, marketPrice and marketPriceMargin. + * + * @param isCurrencyForMakerFeeBtc {@code true} to pay fee in BTC, {@code false} to pay fee in BSQ + * @param amount the amount of BTC to trade + * @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null} + */ + @Nullable + public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin amount) { + if (amount != null) { + Coin feePerBtc = getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount); + return maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc)); + } else { + return null; + } + } + + /** + * Calculate the possibly adjusted amount for {@code amount}, taking into account the + * {@code price} and {@code maxTradeLimit} and {@code factor}. + * + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation to that amount. + * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @return The adjusted amount + */ + public static Coin getRoundedFiatAmount(Coin amount, Price price, long maxTradeLimit) { + return getAdjustedAmount(amount, price, maxTradeLimit, 1); + } + + public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long maxTradeLimit) { + return getAdjustedAmount(amount, price, maxTradeLimit, 10); + } + + /** + * Calculate the possibly adjusted amount for {@code amount}, taking into account the + * {@code price} and {@code maxTradeLimit} and {@code factor}. + * + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation to that amount. + * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @param factor The factor used for rounding. E.g. 1 means rounded to units of + * 1 EUR, 10 means rounded to 10 EUR, etc. + * @return The adjusted amount + */ + @VisibleForTesting + static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int factor) { + checkArgument( + amount.getValue() >= 10_000, + "amount needs to be above minimum of 10k satoshis" + ); + checkArgument( + factor > 0, + "factor needs to be positive" + ); + // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or + // 10 EUR in case of HalCash. + Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); + if (smallestUnitForVolume.getValue() <= 0) + return Coin.ZERO; + + Coin smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); + long minTradeAmount = Restrictions.getMinTradeAmount().value; + + // We use 10 000 satoshi as min allowed amount + checkArgument( + minTradeAmount >= 10_000, + "MinTradeAmount must be at least 10k satoshis" + ); + smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value)); + // We don't allow smaller amount values than smallestUnitForAmount + boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; + + // We get the adjusted volume from our amount + Volume volume = useSmallestUnitForAmount + ? getAdjustedFiatVolume(price.getVolumeByAmount(smallestUnitForAmount), factor) + : getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor); + if (volume.getValue() <= 0) + return Coin.ZERO; + + // From that adjusted volume we calculate back the amount. It might be a bit different as + // the amount used as input before due rounding. + Coin amountByVolume = price.getAmountByVolume(volume); + + // For the amount we allow only 4 decimal places + long adjustedAmount = Math.round((double) amountByVolume.value / 10000d) * 10000; + + // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + while (adjustedAmount > maxTradeLimit) { + adjustedAmount -= smallestUnitForAmount.value; + } + adjustedAmount = Math.max(minTradeAmount, adjustedAmount); + adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + return Coin.valueOf(adjustedAmount); + } +} diff --git a/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java b/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java new file mode 100644 index 0000000000..80a9d7ef86 --- /dev/null +++ b/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.util.coin; + +import bisq.core.util.FormattingUtils; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.MonetaryFormat; + +import javax.inject.Inject; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ImmutableCoinFormatter implements CoinFormatter { + + // We don't support localized formatting. Format is always using "." as decimal mark and no grouping separator. + // Input of "," as decimal mark (like in german locale) will be replaced with ".". + // Input of a group separator (1,123,45) lead to an validation error. + // Note: BtcFormat was intended to be used, but it lead to many problems (automatic format to mBit, + // no way to remove grouping separator). It seems to be not optimal for user input formatting. + @Getter + private MonetaryFormat monetaryFormat; + + @Inject + public ImmutableCoinFormatter(MonetaryFormat monetaryFormat) { + this.monetaryFormat = monetaryFormat; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BTC + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String formatCoin(Coin coin) { + return formatCoin(coin, -1); + } + + @Override + public String formatCoin(Coin coin, boolean appendCode) { + return appendCode ? formatCoinWithCode(coin) : formatCoin(coin); + } + + @Override + public String formatCoin(Coin coin, int decimalPlaces) { + return formatCoin(coin, decimalPlaces, false, 0); + } + + @Override + public String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits) { + return FormattingUtils.formatCoin(coin, decimalPlaces, decimalAligned, maxNumberOfDigits, monetaryFormat); + } + + @Override + public String formatCoinWithCode(Coin coin) { + return FormattingUtils.formatCoinWithCode(coin, monetaryFormat); + } + + @Override + public String formatCoinWithCode(long value) { + return FormattingUtils.formatCoinWithCode(Coin.valueOf(value), monetaryFormat); + } +} diff --git a/core/src/main/java/bisq/core/util/validation/BtcAddressValidator.java b/core/src/main/java/bisq/core/util/validation/BtcAddressValidator.java new file mode 100644 index 0000000000..38eda4094c --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/BtcAddressValidator.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.core.util.validation; + +import bisq.core.locale.Res; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; + +import javax.inject.Inject; + +public final class BtcAddressValidator extends InputValidator { + + @Inject + public BtcAddressValidator() { + } + + @Override + public ValidationResult validate(String input) { + + ValidationResult result = validateIfNotEmpty(input); + if (result.isValid) + return validateBtcAddress(input); + else + return result; + } + + private ValidationResult validateBtcAddress(String input) { + try { + Address.fromString(Config.baseCurrencyNetworkParameters(), input); + return new ValidationResult(true); + } catch (AddressFormatException e) { + return new ValidationResult(false, Res.get("validation.btc.invalidFormat")); + } + } +} diff --git a/core/src/main/java/bisq/core/util/validation/HexStringValidator.java b/core/src/main/java/bisq/core/util/validation/HexStringValidator.java new file mode 100644 index 0000000000..32067b76e9 --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/HexStringValidator.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.core.util.validation; + +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; + +@EqualsAndHashCode(callSuper = true) +@Data +public class HexStringValidator extends InputValidator { + @Setter + private int minLength = Integer.MIN_VALUE; + @Setter + private int maxLength = Integer.MAX_VALUE; + + public HexStringValidator() { + } + + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + if (input.length() > maxLength || input.length() < minLength) + new ValidationResult(false, Res.get("validation.length", minLength, maxLength)); + + try { + Utilities.decodeFromHex(input); + return validationResult; + } catch (Throwable t) { + return new ValidationResult(false, Res.get("validation.noHexString", input)); + } + + } +} diff --git a/core/src/main/java/bisq/core/util/validation/InputValidator.java b/core/src/main/java/bisq/core/util/validation/InputValidator.java new file mode 100644 index 0000000000..7ce3d68a5e --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/InputValidator.java @@ -0,0 +1,119 @@ +/* + * 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 bisq.core.util.validation; + +import bisq.core.locale.Res; + +import java.math.BigInteger; + +import java.util.Objects; +import java.util.function.Function; + +public class InputValidator { + + public ValidationResult validate(String input) { + return validateIfNotEmpty(input); + } + + protected ValidationResult validateIfNotEmpty(String input) { + //trim added to avoid empty input + if (input == null || input.trim().length() == 0) + return new ValidationResult(false, Res.get("validation.empty")); + else + return new ValidationResult(true); + } + + public static class ValidationResult { + public final boolean isValid; + public final String errorMessage; + + public ValidationResult(boolean isValid, String errorMessage) { + this.isValid = isValid; + this.errorMessage = errorMessage; + } + + public ValidationResult(boolean isValid) { + this(isValid, null); + } + + public ValidationResult and(ValidationResult next) { + if (this.isValid) + return next; + else + return this; + } + + @Override + public String toString() { + return "ValidationResult{" + + "isValid=" + isValid + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } + + public boolean errorMessageEquals(ValidationResult other) { + if (this == other) return true; + if (other == null) return false; + return Objects.equals(errorMessage, other.errorMessage); + } + + public interface Validator extends Function { + + } + + /* + This function validates the input with array of validator functions. + If any function validation result is false, it short circuits + as in && (and) operation. + */ + public ValidationResult andValidation(String input, Validator... validators) { + ValidationResult result = null; + for (Validator validator : validators) { + result = validator.apply(input); + if (!result.isValid) + return result; + } + return result; + } + } + + protected boolean isPositiveNumber(String input) { + try { + return input != null && new BigInteger(input).compareTo(BigInteger.ZERO) >= 0; + } catch (Throwable t) { + return false; + } + } + + protected boolean isNumberWithFixedLength(String input, int length) { + return isPositiveNumber(input) && input.length() == length; + } + + protected boolean isNumberInRange(String input, int minLength, int maxLength) { + return isPositiveNumber(input) && input.length() >= minLength && input.length() <= maxLength; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + protected boolean isStringWithFixedLength(String input, int length) { + return input != null && input.length() == length; + } + + protected boolean isStringInRange(String input, int minLength, int maxLength) { + return input != null && input.length() >= minLength && input.length() <= maxLength; + } +} diff --git a/core/src/main/java/bisq/core/util/validation/IntegerValidator.java b/core/src/main/java/bisq/core/util/validation/IntegerValidator.java new file mode 100644 index 0000000000..d904eb497a --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/IntegerValidator.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.util.validation; + +import bisq.core.locale.Res; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class IntegerValidator extends InputValidator { + private int minValue = Integer.MIN_VALUE; + private int maxValue = Integer.MAX_VALUE; + private int intValue; + + public IntegerValidator() { + } + + public IntegerValidator(int minValue, int maxValue) { + this.minValue = minValue; + this.maxValue = maxValue; + } + + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + if (!isInteger(input)) + return new ValidationResult(false, Res.get("validation.notAnInteger")); + + if (isBelowMinValue(intValue)) + return new ValidationResult(false, Res.get("validation.btc.toSmall", minValue)); + + if (isAboveMaxValue(intValue)) + return new ValidationResult(false, Res.get("validation.btc.toLarge", maxValue)); + + return validationResult; + } + + private boolean isBelowMinValue(int intValue) { + return intValue < minValue; + } + + private boolean isAboveMaxValue(int intValue) { + return intValue > maxValue; + } + + private boolean isInteger(String input) { + try { + intValue = Integer.parseInt(input); + return true; + } catch (Throwable t) { + return false; + } + } +} diff --git a/core/src/main/java/bisq/core/util/validation/RegexValidator.java b/core/src/main/java/bisq/core/util/validation/RegexValidator.java new file mode 100644 index 0000000000..2ee772f36c --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/RegexValidator.java @@ -0,0 +1,35 @@ +package bisq.core.util.validation; + +import bisq.core.locale.Res; + +public class RegexValidator extends InputValidator { + private String pattern; + private String errorMessage; + + @Override + public ValidationResult validate(String input) { + ValidationResult result = new ValidationResult(true); + String message = (this.errorMessage == null) ? Res.get("validation.pattern", this.pattern) : this.errorMessage; + String testStr = input == null ? "" : input; + + if (this.pattern == null) + return result; + + if (!testStr.matches(this.pattern)) + result = new ValidationResult(false, message); + + return result; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/core/src/main/java/bisq/core/util/validation/RegexValidatorFactory.java b/core/src/main/java/bisq/core/util/validation/RegexValidatorFactory.java new file mode 100644 index 0000000000..2eab99db9a --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/RegexValidatorFactory.java @@ -0,0 +1,174 @@ +/* + * 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 bisq.core.util.validation; + +public class RegexValidatorFactory { + public static RegexValidator addressRegexValidator() { + RegexValidator regexValidator = new RegexValidator(); + String portRegexPattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"; + String onionV2RegexPattern = String.format("[a-zA-Z2-7]{16}\\.onion(?:\\:%1$s)?", portRegexPattern); + String onionV3RegexPattern = String.format("[a-zA-Z2-7]{56}\\.onion(?:\\:%1$s)?", portRegexPattern); + String ipv4RegexPattern = String.format("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", portRegexPattern); + String ipv6RegexPattern = "(" + + "([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + // 1:2:3:4:5:6:7:8 + "([0-9a-fA-F]{1,4}:){1,7}:|" + // 1:: 1:2:3:4:5:6:7:: + "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + // 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 + "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + // 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 + "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + // 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 + "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + // 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 + "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + // 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 + "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + // 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 + ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: + "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + // fe80::7:8%eth0 fe80::7:8%1 + "::(ffff(:0{1,4}){0,1}:){0,1}" + // (link-local IPv6 addresses with zone index) + "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" + // ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 + "([0-9a-fA-F]{1,4}:){1,4}:" + // (IPv4-mapped IPv6 addresses and IPv4-translated addresses) + "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])" + // 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 + ")"; // (IPv4-Embedded IPv6 Address) + ipv6RegexPattern = String.format("(?:%1$s)|(?:\\[%1$s\\]\\:%2$s)", ipv6RegexPattern, portRegexPattern); + String fqdnRegexPattern = String.format("(((?!-)[a-zA-Z0-9-]{1,63}(?. + */ + +package bisq.core.util.validation; + +import bisq.core.locale.Res; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class StringValidator extends InputValidator { + private int length = 0; + + public StringValidator() { + } + + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + if (!isStringWithFixedLength(input, length)) + return new ValidationResult(false, Res.get("validation.invalidInput", input)); + + return validationResult; + } +} diff --git a/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java b/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java new file mode 100644 index 0000000000..6b088eebf9 --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java @@ -0,0 +1,45 @@ +/* + * 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 bisq.core.util.validation; + +import bisq.core.locale.Res; + +import java.net.URL; + +import static com.google.common.base.Preconditions.checkArgument; + +public class UrlInputValidator extends InputValidator { + + public UrlInputValidator() { + } + + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + try { + new URL(input); // does not cover all invalid urls, so we use a regex as well + String regex = "^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"; + checkArgument(input.matches(regex), "URL does not match regex"); + return validationResult; + } catch (Throwable t) { + return new ValidationResult(false, Res.get("validation.invalidUrl")); + } + } +} diff --git a/core/src/main/resources/bisq.policy b/core/src/main/resources/bisq.policy new file mode 100644 index 0000000000..71c75aa4cd --- /dev/null +++ b/core/src/main/resources/bisq.policy @@ -0,0 +1,120 @@ +grant { + permission "java.util.PropertyPermission" "idea.launcher.*", "read"; + permission "java.util.PropertyPermission" "slf4j.detectLoggerNameMismatch", "read"; + permission "java.util.PropertyPermission" "user.home", "read"; + permission "java.util.PropertyPermission" "java.runtime.name", "read"; + permission "java.util.PropertyPermission" "java.runtime.version", "read"; + permission "java.util.PropertyPermission" "sun.arch.data.model", "read"; + permission "java.util.PropertyPermission" "ignoreDevMsg", "read"; + permission "java.util.PropertyPermission" "baseCryptoNetwork", "read"; + permission "java.util.PropertyPermission" "appDataDir", "read"; + permission "java.util.PropertyPermission" "logLevel", "read"; + permission "java.util.PropertyPermission" "storageDir", "read"; + permission "java.util.PropertyPermission" "keyStorageDir", "read"; + permission "java.util.PropertyPermission" "dumpStatistics", "read"; + permission "java.util.PropertyPermission" "torDir", "read"; + permission "java.util.PropertyPermission" "maxConnections", "read"; + permission "java.util.PropertyPermission" "networkId", "read"; + permission "java.util.PropertyPermission" "banList", "read"; + permission "java.util.PropertyPermission" "socks5ProxyBtcAddress", "read"; + permission "java.util.PropertyPermission" "socks5ProxyHttpAddress", "read"; + permission "java.util.PropertyPermission" "useragent.name", "read"; + permission "java.util.PropertyPermission" "useragent.version", "read"; + permission "java.util.PropertyPermission" "walletDir", "read"; + permission "java.util.PropertyPermission" "useTorForBtc", "read"; + permission "java.util.PropertyPermission" "providers", "read"; + permission "java.util.PropertyPermission" "rpcUser", "read"; + permission "java.util.PropertyPermission" "rpcPassword", "read"; + permission "java.util.PropertyPermission" "rpcPort", "read"; + permission "java.util.PropertyPermission" "rpcBlockPort", "read"; + permission "java.util.PropertyPermission" "rpcWalletPort", "read"; + permission "java.util.PropertyPermission" "logback.*", "read"; + permission "java.util.PropertyPermission" "org.apache.commons.logging.*", "read"; + permission "java.util.PropertyPermission" "spring.getenv.ignore", "read"; + permission "java.util.PropertyPermission" "javafx.toolkit", "read"; + permission "java.util.PropertyPermission" "guice.custom.loader", "read"; + permission "java.util.PropertyPermission" "cglib.debugLocation", "read"; + permission "java.util.PropertyPermission" "useLocalhost", "read"; + permission "java.util.PropertyPermission" "nodePort", "read"; + permission "java.util.PropertyPermission" "seedNodes", "read"; + permission "java.util.PropertyPermission" "bitcoinRegtestHost", "read"; + permission "java.util.PropertyPermission" "btcNodes", "read"; + permission "java.util.PropertyPermission" "appName", "read"; + permission "java.util.PropertyPermission" "socks5DiscoverMode", "read"; + permission "java.util.PropertyPermission" "priceFeedProviders", "read"; + permission "java.util.PropertyPermission" "maxMemory", "read"; + + /* Why ??? no permission exceptions, no dependency in bisq nor in bitcoinj. Local problem? */ + permission "java.util.PropertyPermission" "com.google.appengine.runtime.environment", "read"; + + permission "java.lang.RuntimePermission" "accessDeclaredMembers"; + permission "java.lang.RuntimePermission" "setDefaultUncaughtExceptionHandler"; + permission "java.lang.RuntimePermission" "accessClassInPackage.sun.misc"; + permission "java.lang.RuntimePermission" "accessClassInPackage.sun.util.logging.resources"; + permission "java.lang.RuntimePermission" "accessClassInPackage.com.sun.javafx.tk.quantum"; + permission "java.lang.RuntimePermission" "accessClassInPackage.sun.reflect"; + permission "java.lang.RuntimePermission" "getProtectionDomain"; + permission "java.lang.RuntimePermission" "setContextClassLoader"; + permission "java.lang.RuntimePermission" "shutdownHooks"; + permission "java.lang.RuntimePermission" "getenv.logLevel"; + permission "java.lang.RuntimePermission" "getenv.storageDir"; + permission "java.lang.RuntimePermission" "getenv.keyStorageDir"; + permission "java.lang.RuntimePermission" "getenv.dumpStatistics"; + permission "java.lang.RuntimePermission" "getenv.torDir"; + permission "java.lang.RuntimePermission" "getenv.maxConnections"; + permission "java.lang.RuntimePermission" "getenv.networkId"; + permission "java.lang.RuntimePermission" "getenv.banList"; + permission "java.lang.RuntimePermission" "getenv.socks5ProxyBtcAddress"; + permission "java.lang.RuntimePermission" "getenv.socks5ProxyHttpAddress"; + permission "java.lang.RuntimePermission" "getenv.useragent.name"; + permission "java.lang.RuntimePermission" "getenv.useragent.version"; + permission "java.lang.RuntimePermission" "getenv.walletDir"; + permission "java.lang.RuntimePermission" "getenv.useTorForBtc"; + permission "java.lang.RuntimePermission" "getenv.providers"; + permission "java.lang.RuntimePermission" "getenv.rpcPassword"; + permission "java.lang.RuntimePermission" "getenv.rpcUser"; + permission "java.lang.RuntimePermission" "getenv.rpcPort"; + permission "java.lang.RuntimePermission" "getenv.rpcBlockPort"; + permission "java.lang.RuntimePermission" "getenv.rpcWalletPort"; + permission "java.lang.RuntimePermission" "getenv.ignoreDevMsg"; + permission "java.lang.RuntimePermission" "getenv.ignoreDevMsg"; + permission "java.lang.RuntimePermission" "getenv.baseCryptoNetwork"; + permission "java.lang.RuntimePermission" "getenv.appDataDir"; + permission "java.lang.RuntimePermission" "getenv.socks5DiscoverMode"; + permission "java.lang.RuntimePermission" "getenv.priceFeedProviders"; + permission "java.lang.RuntimePermission" "getenv.seedNodes"; + permission "java.lang.RuntimePermission" "getenv.bitcoinRegtestHost"; + permission "java.lang.RuntimePermission" "getenv.btcNodes"; + permission "java.lang.RuntimePermission" "getenv.maxMemory"; + permission "java.lang.RuntimePermission" "getClassLoader"; + permission "java.lang.RuntimePermission" "accessUserInformation"; + permission "java.lang.RuntimePermission" "loadLibrary.javasecp256k1"; + permission "java.lang.RuntimePermission" "modifyThread"; + + /* user data dir for Mac, Linux, Windows */ + permission "java.io.FilePermission" "${user.home}${/}Library${/}Application Support${/}-", "read,write,delete"; + permission "java.io.FilePermission" "${user.home}${/}.local${/}share${/}bisq-", "read,write,delete"; + permission "java.io.FilePermission" "${appdata}${/}bisq-", "read,write,delete"; + + /* temp dir Mac, Linux, Windows TODO */ + permission "java.io.FilePermission" "/var/folders/-", "read,write,delete"; + + /* only for dev + permission "java.io.FilePermission" "${user.home}${/}.m2${/}-", "read"; + permission "java.io.FilePermission" "/Users/me/dev/bitcoin_projects/bisq/-", "read"; + permission "java.lang.reflect.ReflectPermission" "suppressAccessChecks"; +*/ + + + permission "java.net.SocketPermission" "127.0.0.1:*", "listen,connect,resolve, accept"; + permission "java.net.URLPermission" "http://95.85.11.205:8080/all", "GET:User-Agent"; + permission "java.net.URLPermission" "http://95.85.11.205:8080/getFees", "GET:User-Agent"; + permission "java.net.URLPermission" "http://95.85.11.205:8080/getAllMarketPrices", "GET:User-Agent"; + permission "java.net.SocketPermission" "*:8333", "connect,resolve"; + permission "java.net.SocketPermission" "*.onion:80", "connect,resolve"; + + permission "java.awt.AWTPermission" "accessSystemTray"; + permission "java.awt.AWTPermission" "showWindowWithoutWarningBanner"; + permission "java.security.SecurityPermission" "insertProvider"; + permission "java.util.logging.LoggingPermission" "control"; +}; diff --git a/core/src/main/resources/bisq.properties b/core/src/main/resources/bisq.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/main/resources/btc_dao_betanet.seednodes b/core/src/main/resources/btc_dao_betanet.seednodes new file mode 100644 index 0000000000..c260a87394 --- /dev/null +++ b/core/src/main/resources/btc_dao_betanet.seednodes @@ -0,0 +1,2 @@ +# nodeaddress.onion:port [(@owner)] +csmijmjs7ftqfw6v.onion:8004 diff --git a/core/src/main/resources/btc_dao_regtest.seednodes b/core/src/main/resources/btc_dao_regtest.seednodes new file mode 100644 index 0000000000..4c6367383f --- /dev/null +++ b/core/src/main/resources/btc_dao_regtest.seednodes @@ -0,0 +1,6 @@ +# nodeaddress.onion:port [(@owner)] +2bnvhfkdrnx5hrlv.onion:8005 +b3jnw7fyph2jsu6n.onion:8005 + +# omentgpxrxy5lehq.onion:8005 +# r7cucuwouvhdhdgo.onion:8005 diff --git a/core/src/main/resources/btc_dao_testnet.seednodes b/core/src/main/resources/btc_dao_testnet.seednodes new file mode 100644 index 0000000000..a57515edb1 --- /dev/null +++ b/core/src/main/resources/btc_dao_testnet.seednodes @@ -0,0 +1,3 @@ +# nodeaddress.onion:port [(@owner)] +fjr5w4eckjghqtnu.onion:8003 +74w2sttlo4qk6go3.onion:8003 diff --git a/core/src/main/resources/btc_mainnet.seednodes b/core/src/main/resources/btc_mainnet.seednodes new file mode 100644 index 0000000000..eb2b0a4bc1 --- /dev/null +++ b/core/src/main/resources/btc_mainnet.seednodes @@ -0,0 +1,17 @@ +# nodeaddress.onion:port [(@owner,@backup)] +wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000 (@wiz) +wizseed3d376esppbmbjxk2fhk2jg5fpucddrzj2kxtbxbx4vrnwclad.onion:8000 (@wiz) +wizseed7ab2gi3x267xahrp2pkndyrovczezzb46jk6quvguciuyqrid.onion:8000 (@wiz) +devinv3rhon24gqf5v6ondoqgyrbzyqihzyouzv7ptltsewhfmox2zqd.onion:8000 (@devinbileck) +devinsn2teu33efff62bnvwbxmfgbfjlgqsu3ad4b4fudx3a725eqnyd.onion:8000 (@devinbileck) +devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbileck) +sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy) +sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy) +sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy) +sn2bisqad7ncazupgbd3dcedqh5ptirgwofw63djwpdtftwhddo75oid.onion:8000 (@miker) +sn3bsq3evqkpshdmc3sbdxafkhfnk7ctop44jsxbxyys5ridsaw5abyd.onion:8000 (@miker) +sn4bsqpc7eb2ntvpsycxbzqt6fre72l4krp2fl5svphfh2eusrqtq3qd.onion:8000 (@miker) +5quyxpxheyvzmb2d.onion:8000 (@miker) +rm7b56wbrcczpjvl.onion:8000 (@miker) +s67qglwhkgkyvr74.onion:8000 (@emzy) +fl3mmribyxgrv63c.onion:8000 (@devinbileck) diff --git a/core/src/main/resources/btc_regtest.seednodes b/core/src/main/resources/btc_regtest.seednodes new file mode 100644 index 0000000000..44cd4ec296 --- /dev/null +++ b/core/src/main/resources/btc_regtest.seednodes @@ -0,0 +1,3 @@ +# nodeaddress.onion:port [(@owner,@backup)] +localhost:2002 (@devtest1) +localhost:3002 (@devtest2) diff --git a/core/src/main/resources/btc_testnet.seednodes b/core/src/main/resources/btc_testnet.seednodes new file mode 100644 index 0000000000..4051e84128 --- /dev/null +++ b/core/src/main/resources/btc_testnet.seednodes @@ -0,0 +1,2 @@ +# nodeaddress.onion:port [(@owner)] +m5izk3fvjsjbmkqi.onion:8001 diff --git a/core/src/main/resources/help/canceloffer-help.txt b/core/src/main/resources/help/canceloffer-help.txt new file mode 100644 index 0000000000..0155d9c185 --- /dev/null +++ b/core/src/main/resources/help/canceloffer-help.txt @@ -0,0 +1,26 @@ +canceloffer + +NAME +---- +canceloffer - cancel an existing offer to buy or sell BTC + +SYNOPSIS +-------- +canceloffer + --offer-id= + +DESCRIPTION +----------- +Cancel an existing offer. The offer will be removed from other Bisq users' offer views, +and paid transaction fees will be forfeited. + +OPTIONS +------- +--offer-id + The ID of the buy or sell offer to cancel. + +EXAMPLES +-------- +To cancel an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 canceloffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea + diff --git a/core/src/main/resources/help/confirmpaymentreceived-help.txt b/core/src/main/resources/help/confirmpaymentreceived-help.txt new file mode 100644 index 0000000000..4e460b4bfb --- /dev/null +++ b/core/src/main/resources/help/confirmpaymentreceived-help.txt @@ -0,0 +1,27 @@ +confirmpaymentreceived + +NAME +---- +confirmpaymentreceived - confirm payment has been received + +SYNOPSIS +-------- +confirmpaymentreceived + --trade-id= + +DESCRIPTION +----------- +After the seller receives payment from the BTC buyer, confirmpaymentreceived notifies +the buyer the payment has arrived. The seller can release locked BTC only after the +this confirmation message has been sent. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +EXAMPLES +-------- +A BTC seller has taken an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and has recently +received the required fiat payment from the buyer's fiat account: +$ ./bisq-cli --password=xyz --port=9998 confirmpaymentreceived --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/core/src/main/resources/help/confirmpaymentstarted-help.txt b/core/src/main/resources/help/confirmpaymentstarted-help.txt new file mode 100644 index 0000000000..c83907b58c --- /dev/null +++ b/core/src/main/resources/help/confirmpaymentstarted-help.txt @@ -0,0 +1,26 @@ +confirmpaymentstarted + +NAME +---- +confirmpaymentstarted - confirm payment has been sent + +SYNOPSIS +-------- +confirmpaymentstarted + --trade-id= + +DESCRIPTION +----------- +After the buyer initiates payment to the BTC seller, confirmpaymentstarted notifies +the seller to begin watching for a funds deposit in her payment account. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +EXAMPLES +-------- +A BTC buyer has taken an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and has recently +initiated the required fiat payment to the seller's fiat account: +$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/core/src/main/resources/help/createcryptopaymentacct-help.txt b/core/src/main/resources/help/createcryptopaymentacct-help.txt new file mode 100644 index 0000000000..83fa2e9f63 --- /dev/null +++ b/core/src/main/resources/help/createcryptopaymentacct-help.txt @@ -0,0 +1,47 @@ +createcryptopaymentacct + +NAME +---- +createcryptopaymentacct - create a cryptocurrency payment account + +SYNOPSIS +-------- +createcryptopaymentacct + --account-name= + --currency-code= + --address= + [--trade-instant=] + +DESCRIPTION +----------- +Create an cryptocurrency (altcoin) payment account. Only BSQ payment accounts are currently supported. + +OPTIONS +------- +--account-name + The name of the cryptocurrency payment account used to create and take altcoin offers. + +--currency-code + The three letter code for the altcoin, e.g., BSQ. + +--address + The altcoin address to be used receive cryptocurrency payment when selling BTC. + +--trade-instant + True for creating an instant cryptocurrency payment account, false otherwise. + Default is false. + +EXAMPLES +-------- + +To create a BSQ Altcoin payment account: +$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My BSQ Account" \ + --currency-code=bsq \ + --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \ + --trade-instant=false + +To create a BSQ Instant Altcoin payment account: +$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My Instant BSQ Account" \ + --currency-code=bsq \ + --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \ + --trade-instant=true diff --git a/core/src/main/resources/help/createoffer-help.txt b/core/src/main/resources/help/createoffer-help.txt new file mode 100644 index 0000000000..38ec0fd8da --- /dev/null +++ b/core/src/main/resources/help/createoffer-help.txt @@ -0,0 +1,82 @@ +createoffer + +NAME +---- +createoffer - create offer to buy or sell BTC + +SYNOPSIS +-------- +createoffer + --payment-account= + --direction= + --currency-code= + --market-price-margin= | --fixed-price= + --amount= + --min-amount= + --security-deposit= + [--fee-currency=] + +DESCRIPTION +----------- +Create and place an offer to buy or sell BTC using a fiat account. + +OPTIONS +------- +--payment-account + The ID of the fiat payment account used to send or receive funds during the trade. + +--direction + The direction of the trade (BUY or SELL). + +--currency-code + The three letter code for the fiat used to buy or sell BTC, e.g., EUR, USD, BRL, ... + +--market-price-margin + The % above or below market BTC price, e.g., 1.00 (1%). + If --market-price-margin is not present, --fixed-price must be. + +--fixed-price + The fixed BTC price in fiat used to buy or sell BTC, e.g., 34000 (USD). + If --fixed-price is not present, --market-price-margin must be. + +--amount + The amount of BTC to buy or sell, e.g., 0.125. + +--min-amount + The minimum amount of BTC to buy or sell, e.g., 0.006. + If --min-amount is not present, it defaults to the --amount value. + +--security-deposit + The percentage of the BTC amount being traded for the security deposit, e.g., 60.0 (60%). + +--fee-currency + The wallet currency used to pay the Bisq trade maker fee (BSQ|BTC). Default is BTC + +EXAMPLES +-------- + +To create a BUY 0.125 BTC with EUR offer + at the current market price, + using a payment account with ID 7413d263-225a-4f1b-837a-1e3094dc0d77, + putting up a 30 percent security deposit, + and paying the Bisq maker trading fee in BSQ: +$ ./bisq-cli --password=xyz --port=9998 createoffer --payment-account=7413d263-225a-4f1b-837a-1e3094dc0d77 \ + --direction=buy \ + --currency-code=eur \ + --amount=0.125 \ + --market-price-margin=0.00 \ + --security-deposit=30.0 \ + --fee-currency=bsq + +To create a SELL 0.006 BTC for USD offer + at a fixed price of 40,000 USD, + using a payment account with ID 7413d263-225a-4f1b-837a-1e3094dc0d77, + putting up a 25 percent security deposit, + and paying the Bisq maker trading fee in BTC: +$ ./bisq-cli --password=xyz --port=9998 createoffer --payment-account=7413d263-225a-4f1b-837a-1e3094dc0d77 \ + --direction=sell \ + --currency-code=usd \ + --amount=0.006 \ + --fixed-price=40000 \ + --security-deposit=25.0 \ + --fee-currency=btc diff --git a/core/src/main/resources/help/createpaymentacct-help.txt b/core/src/main/resources/help/createpaymentacct-help.txt new file mode 100644 index 0000000000..cdc837f7f1 --- /dev/null +++ b/core/src/main/resources/help/createpaymentacct-help.txt @@ -0,0 +1,46 @@ +createpaymentacct + +NAME +---- +createpaymentacct - create a payment account + +SYNOPSIS +-------- +createpaymentacct + --payment-account-form= + +DESCRIPTION +----------- +Create a Bisq trading account with a payment account form. + +The details of the payment account are defined in a manually edited json file generated +by a getpaymentacctform command, e.g., + + { + "_COMMENTS_": [ + "Do not manually edit the paymentMethodId field.", + "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." + ], + "paymentMethodId": "SEPA", + "accountName": "your accountname", + "bic": "your bic", + "country": "your country", + "holderName": "your holdername", + "iban": "your iban", + "salt": "" + } + + +EXAMPLES +-------- +To create a new SEPA payment account, find the payment-method-id for the getpaymentacctform command: +$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods + +Get a new, blank SEPA payment account form: +$ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=SEPA + +The previous command created a json file named sepa_1610817857085.json. The timestamp +in the file name is to ensure each generated file is uniquely named (you can rename the file). + +Manually edit the json file, and pass the file's path to the createpaymentacct command: +$ ./bisq-cli --password=xyz --port=9998 createpaymentacct --payment-account-form=sepa_1610817857085.json diff --git a/core/src/main/resources/help/getaddressbalance-help.txt b/core/src/main/resources/help/getaddressbalance-help.txt new file mode 100644 index 0000000000..279c58309d --- /dev/null +++ b/core/src/main/resources/help/getaddressbalance-help.txt @@ -0,0 +1,23 @@ +getaddressbalance + +NAME +---- +getaddressbalance - get btc address balance + +SYNOPSIS +-------- +getaddressbalance + --address= + +DESCRIPTION +----------- +Returns the balance of a BTC address in the Bisq server's wallet. + +OPTIONS +------- +--address= + The BTC address. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getaddressbalance --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 diff --git a/core/src/main/resources/help/getbalance-help.txt b/core/src/main/resources/help/getbalance-help.txt new file mode 100644 index 0000000000..9426373be7 --- /dev/null +++ b/core/src/main/resources/help/getbalance-help.txt @@ -0,0 +1,30 @@ +getbalance + +NAME +---- +getbalance - get wallet balance(s) + +SYNOPSIS +-------- +getbalance + [--currency-code=] + +DESCRIPTION +----------- +Returns full balance information for Bisq BSQ and/or BTC wallets. + +OPTIONS +------- +--currency-code= + The three letter Bisq wallet crypto currency code. + +EXAMPLES +-------- +Show full BSQ and BTC wallet balance information: +$ ./bisq-cli --password=xyz --port=9998 getbalance + +Show full BSQ wallet balance information: +$ ./bisq-cli --password=xyz --port=9998 getbalance --currency-code=bsq + +Show full BTC wallet balance information: +$ ./bisq-cli --password=xyz --port=9998 getbalance --currency-code=btc diff --git a/core/src/main/resources/help/getbtcprice-help.txt b/core/src/main/resources/help/getbtcprice-help.txt new file mode 100644 index 0000000000..cd0e79f027 --- /dev/null +++ b/core/src/main/resources/help/getbtcprice-help.txt @@ -0,0 +1,30 @@ +getbtcprice + +NAME +---- +getbtcprice - get current btc market price + +SYNOPSIS +-------- +getbtcprice + --currency-code= + +DESCRIPTION +----------- +Returns the current market BTC price for the given currency-code. + +OPTIONS +------- + +--currency-code + The three letter code for the fiat currency code, e.g., EUR, USD, BRL, ... + +EXAMPLES +-------- +Get the current BTC market price in Euros: +$ ./bisq-cli --password=xyz --port=9998 getbtcprice --currency-code=eur + +Get the current BTC market price in Brazilian Reais: +$ ./bisq-cli --password=xyz --port=9998 getbtcprice --currency-code=brl + + diff --git a/core/src/main/resources/help/getfundingaddresses-help.txt b/core/src/main/resources/help/getfundingaddresses-help.txt new file mode 100644 index 0000000000..0d3e7ea629 --- /dev/null +++ b/core/src/main/resources/help/getfundingaddresses-help.txt @@ -0,0 +1,17 @@ +getfundingaddresses + +NAME +---- +getfundingaddresses - list BTC receiving address + +SYNOPSIS +-------- +getfundingaddresses + +DESCRIPTION +----------- +Returns a list of receiving BTC addresses. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getfundingaddresses diff --git a/core/src/main/resources/help/getmyoffer-help.txt b/core/src/main/resources/help/getmyoffer-help.txt new file mode 100644 index 0000000000..66cabd9348 --- /dev/null +++ b/core/src/main/resources/help/getmyoffer-help.txt @@ -0,0 +1,25 @@ +getmyoffer + +NAME +---- +getmyoffer - get your offer to buy or sell BTC + +SYNOPSIS +-------- +getmyoffer + --offer-id= + +DESCRIPTION +----------- +List one of your existing offers' details. + +OPTIONS +------- +--offer-id + The ID of your buy or sell offer. + +EXAMPLES +-------- +To view your offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea + diff --git a/core/src/main/resources/help/getmyoffers-help.txt b/core/src/main/resources/help/getmyoffers-help.txt new file mode 100644 index 0000000000..7d2c3d3eb8 --- /dev/null +++ b/core/src/main/resources/help/getmyoffers-help.txt @@ -0,0 +1,33 @@ +getmyoffers + +NAME +---- +getmyoffers - get your own buy or sell BTC offers for a fiat currency + +SYNOPSIS +-------- +getmyoffers + --direction= + --currency-code= + +DESCRIPTION +----------- +List your existing offers for a direction (SELL|BUY) and currency (EUR|GBP|USD|BRL|...). + +OPTIONS +------- +--direction + The direction of the offer (BUY or SELL). + +--currency-code + The three letter code for the fiat used to buy or sell BTC, e.g., EUR, USD, BRL, ... + +EXAMPLES +-------- +List all of your existing BUY offers for BRL: +$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=buy --currency-code=brl + +List all of your existing SELL offers for EUR: +$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=sell --currency-code=eur + + diff --git a/core/src/main/resources/help/getoffer-help.txt b/core/src/main/resources/help/getoffer-help.txt new file mode 100644 index 0000000000..a722f1f428 --- /dev/null +++ b/core/src/main/resources/help/getoffer-help.txt @@ -0,0 +1,26 @@ +getoffer + +NAME +---- +getoffer - get an offer to buy or sell BTC + +SYNOPSIS +-------- +getoffer + --offer-id= + +DESCRIPTION +----------- +List an existing offer's details. The offer must not be one of your own. +The offer must be available to take with one of your matching payment accounts. + +OPTIONS +------- +--offer-id + The ID of the buy or sell offer to view. + +EXAMPLES +-------- +To view an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea + diff --git a/core/src/main/resources/help/getoffers-help.txt b/core/src/main/resources/help/getoffers-help.txt new file mode 100644 index 0000000000..c76c2dfbda --- /dev/null +++ b/core/src/main/resources/help/getoffers-help.txt @@ -0,0 +1,39 @@ +getoffers + +NAME +---- +getoffers - get available buy or sell BTC offers for a fiat currency + +SYNOPSIS +-------- +getoffers + --direction= + --currency-code= + +DESCRIPTION +----------- +List existing offers for a direction (SELL|BUY) and currency (EUR|GBP|USD|BRL|...). +All of the listed offers will be available for the taking because you have a +matching payment account, and none of the offers listed will be one of yours. + +OPTIONS +------- +--direction + The direction of the offer (BUY or SELL). + +--currency-code + The three letter code for the fiat used to buy or sell BTC, e.g., EUR, USD, BRL, ... + +EXAMPLES +-------- +You have one Brazilian Real payment account with a face-to-face payment method type. +To view available offers to BUY BTC with BRL, created by other users with the same +face-to-fact account type: +$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=buy --currency-code=brl + +You have several EUR payment accounts, each with a different payment method type. +To view available offers to SELL BTC with EUR, created by other users having at +least one payment account that matches any of your own: +$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=sell --currency-code=eur + + diff --git a/core/src/main/resources/help/getpaymentacctform-help.txt b/core/src/main/resources/help/getpaymentacctform-help.txt new file mode 100644 index 0000000000..5086728c35 --- /dev/null +++ b/core/src/main/resources/help/getpaymentacctform-help.txt @@ -0,0 +1,44 @@ +getpaymentacctform + +NAME +---- +getpaymentacctform - get a blank payment account form for a payment method + +SYNOPSIS +-------- +getpaymentacctform + --payment-method-id= + +DESCRIPTION +----------- +Returns a new, blank payment account form as a json file, e.g., + + { + "_COMMENTS_": [ + "Do not manually edit the paymentMethodId field.", + "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." + ], + "paymentMethodId": "CLEAR_X_CHANGE", + "accountName": "your accountname", + "emailOrMobileNr": "your emailormobilenr", + "holderName": "your holdername", + "salt": "" + } + +This form is manually edited, and used as a parameter to the createpaymentacct command, +which creates the new payment account. + +EXAMPLES +-------- +To create a new CLEAR_X_CHANGE (Zelle) payment account, find the payment-method-id for +the getpaymentacctform command: +$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods + +Get a new, blank CLEAR_X_CHANGE (Zelle) payment account form: +$ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=CLEAR_X_CHANGE_ID + +The previous command created a json file named clear_x_change_1610818248040.json. The timestamp +in the file name is to ensure each generated file is uniquely named (you can rename the file). + +Manually edit the json file, and pass the file's path to the createpaymentacct command: +$ ./bisq-cli --password=xyz --port=9998 createpaymentacct --payment-account-form=clear_x_change_1610818248040.json diff --git a/core/src/main/resources/help/getpaymentaccts-help.txt b/core/src/main/resources/help/getpaymentaccts-help.txt new file mode 100644 index 0000000000..f12b5bd453 --- /dev/null +++ b/core/src/main/resources/help/getpaymentaccts-help.txt @@ -0,0 +1,17 @@ +getpaymentaccts + +NAME +---- +getpaymentaccts - list user payment accounts + +SYNOPSIS +-------- +getpaymentaccts + +DESCRIPTION +----------- +Returns the list of user payment accounts. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getpaymentaccts diff --git a/core/src/main/resources/help/getpaymentmethods-help.txt b/core/src/main/resources/help/getpaymentmethods-help.txt new file mode 100644 index 0000000000..b7f860548d --- /dev/null +++ b/core/src/main/resources/help/getpaymentmethods-help.txt @@ -0,0 +1,17 @@ +getpaymentmethods + +NAME +---- +getpaymentmethods - list fiat payment methods + +SYNOPSIS +-------- +getpaymentmethods + +DESCRIPTION +----------- +Returns a list of currently supported fiat payment method IDs. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods diff --git a/core/src/main/resources/help/gettrade-help.txt b/core/src/main/resources/help/gettrade-help.txt new file mode 100644 index 0000000000..4cf64584cc --- /dev/null +++ b/core/src/main/resources/help/gettrade-help.txt @@ -0,0 +1,32 @@ +gettrade + +NAME +---- +gettrade - get a buy or sell BTC trade + +SYNOPSIS +-------- +gettrade + --trade-id= + [--show-contract=] + +DESCRIPTION +----------- +List details of a trade with the given trade-id. If the trade has not yet been completed, +the details can inform each side of the trade of the current phase of the trade protocol. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +--show-contract + Optionally display the trade's full contract details in json format. The default = false. + +EXAMPLES +-------- +To see the summary of a trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea + +To see the full contract for a trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea --show-contract=true diff --git a/core/src/main/resources/help/gettransaction-help.txt b/core/src/main/resources/help/gettransaction-help.txt new file mode 100644 index 0000000000..3d16f3c297 --- /dev/null +++ b/core/src/main/resources/help/gettransaction-help.txt @@ -0,0 +1,27 @@ +gettransaction + +NAME +---- +gettransaction - get transaction summary + +SYNOPSIS +-------- +gettransaction + --transaction-id= + +DESCRIPTION +----------- +Returns a very brief summary of a BTC transaction created by the Bisq server. + +To see full transaction details, use a bitcoin-core client or an online block explorer. + +OPTIONS +------- +--transaction-id + The ID of the BTC transaction. + +EXAMPLES +-------- +To see the summary of a transaction with ID 282dc2a5755219a49ee9f6d46a31a2cbaec6624beba96548180eccb1f004cdd8: +$ ./bisq-cli --password=xyz --port=9998 gettransaction \ + --transaction-id=282dc2a5755219a49ee9f6d46a31a2cbaec6624beba96548180eccb1f004cdd8 diff --git a/core/src/main/resources/help/gettxfeerate-help.txt b/core/src/main/resources/help/gettxfeerate-help.txt new file mode 100644 index 0000000000..3582d5dcf0 --- /dev/null +++ b/core/src/main/resources/help/gettxfeerate-help.txt @@ -0,0 +1,17 @@ +gettxfeerate + +NAME +---- +gettxfeerate - get transaction fee rate + +SYNOPSIS +-------- +gettxfeerate + +DESCRIPTION +----------- +Returns the most recent bitcoin network transaction fee the Bisq server could find. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 gettxfeerate diff --git a/core/src/main/resources/help/getunusedbsqaddress-help.txt b/core/src/main/resources/help/getunusedbsqaddress-help.txt new file mode 100644 index 0000000000..308ad01ba7 --- /dev/null +++ b/core/src/main/resources/help/getunusedbsqaddress-help.txt @@ -0,0 +1,17 @@ +getunusedbsqaddress + +NAME +---- +getunusedbsqaddress - get BSQ receiving address + +SYNOPSIS +-------- +getunusedbsqaddress + +DESCRIPTION +----------- +Returns an unused BSQ receiving address. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress diff --git a/core/src/main/resources/help/getversion-help.txt b/core/src/main/resources/help/getversion-help.txt new file mode 100644 index 0000000000..ce3b801db4 --- /dev/null +++ b/core/src/main/resources/help/getversion-help.txt @@ -0,0 +1,17 @@ +getversion + +NAME +---- +getversion - get server version + +SYNOPSIS +-------- +getversion + +DESCRIPTION +----------- +Returns the Bisq server version. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getversion diff --git a/core/src/main/resources/help/keepfunds-help.txt b/core/src/main/resources/help/keepfunds-help.txt new file mode 100644 index 0000000000..4e8232df04 --- /dev/null +++ b/core/src/main/resources/help/keepfunds-help.txt @@ -0,0 +1,31 @@ +keepfunds + +NAME +---- +keepfunds - keep BTC received during a trade in Bisq wallet + +SYNOPSIS +-------- +keepfunds + --trade-id= + +DESCRIPTION +----------- +A BTC buyer completes the final step in the trade protocol by keeping received BTC in his +Bisq wallet. This step may not seem necessary from the buyer's perspective, but it is +necessary for correct transition of a trade's state to CLOSED, within the Bisq server. + +The alternative way to close out the trade is to send the received BTC to an external +BTC wallet, using the withdrawfunds command. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +EXAMPLES +-------- +A BTC seller has informed the buyer that fiat payment has been received for trade with ID +83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and locked BTC has been released to the buyer. +The BTC buyer closes out the trade by keeping the received BTC in her Bisq wallet: +$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/core/src/main/resources/help/lockwallet-help.txt b/core/src/main/resources/help/lockwallet-help.txt new file mode 100644 index 0000000000..58cb5668ab --- /dev/null +++ b/core/src/main/resources/help/lockwallet-help.txt @@ -0,0 +1,18 @@ +lockwallet + +NAME +---- +lockwallet - lock Bisq wallet + +SYNOPSIS +-------- +lockwallet + +DESCRIPTION +----------- +Locks an unlocked wallet before an unlockwallet timeout expires. + +EXAMPLES +-------- +Immediately lock an encrypted wallet set to automatically lock in the future: +$ ./bisq-cli --password=xyz --port=9998 lockwallet diff --git a/core/src/main/resources/help/removewalletpassword-help.txt b/core/src/main/resources/help/removewalletpassword-help.txt new file mode 100644 index 0000000000..73b6d311e9 --- /dev/null +++ b/core/src/main/resources/help/removewalletpassword-help.txt @@ -0,0 +1,19 @@ +removewalletpassword + +NAME +---- +removewalletpassword - remove a Bisq wallet's encryption password + +SYNOPSIS +-------- +removewalletpassword + --wallet-password= + --timeout= + +DESCRIPTION +----------- +Remove an encryption password from an encrypted Bisq wallet. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 removewalletpassword --wallet-password=mypassword diff --git a/core/src/main/resources/help/sendbsq-help.txt b/core/src/main/resources/help/sendbsq-help.txt new file mode 100644 index 0000000000..8a3e7d938a --- /dev/null +++ b/core/src/main/resources/help/sendbsq-help.txt @@ -0,0 +1,38 @@ +sendbsq + +NAME +---- +sendbsq - send BSQ to an external wallet + +SYNOPSIS +-------- +sendbsq + --address= + --amount= + [--tx-fee-rate=] + +DESCRIPTION +----------- +Send BSQ from your Bisq wallet to an external BSQ address. + +OPTIONS +------- +--address + The destination BSQ address for the send transaction. + +--amount + The amount of BSQ to send. + +--tx-fee-rate + An optional transaction fee rate (sats/byte) for the transaction. The user is + responsible for choosing a fee rate that will be accepted by the network in a + reasonable amount of time, and the fee rate must be greater than 1 (sats/byte). + +EXAMPLES +-------- +Send 500 BSQ to address Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne with a default transaction fee rate: +$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne --amount=500.00 + +Send 3000 BSQ to address Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne with transaction fee rate of 40 sats/byte: +$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne --amount=3000.00 \ + --tx-fee-rate=40 diff --git a/core/src/main/resources/help/sendbtc-help.txt b/core/src/main/resources/help/sendbtc-help.txt new file mode 100644 index 0000000000..833612a648 --- /dev/null +++ b/core/src/main/resources/help/sendbtc-help.txt @@ -0,0 +1,51 @@ +sendbtc + +NAME +---- +sendbtc - send BTC to an external wallet + +SYNOPSIS +-------- +sendbtc + --address= + --amount= + [--tx-fee-rate=] + [--memo=<"memo">] + +DESCRIPTION +----------- +Send BTC from your Bisq wallet to an external BTC address. + +OPTIONS +------- +--address + The destination BTC address for the send transaction. + +--amount + The amount of BTC to send. + +--tx-fee-rate + An optional transaction fee rate (sats/byte) for the transaction. The user is + responsible for choosing a fee rate that will be accepted by the network in a + reasonable amount of time, and the fee rate must be greater than 1 (sats/byte). + +--memo + An optional memo to be saved with the send btc transaction. + A multi word memo must be enclosed in double quotes. + +EXAMPLES +-------- +Send 0.10 BTC to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a default +transaction fee rate: +$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.10 + +Send 0.05 BTC to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a transaction +fee rate of 10 sats/byte: +$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.05 \ + --tx-fee-rate=10 + +Send 0.005 BTC to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a transaction +fee rate of 40 sats/byte, and save a memo with the send transaction: +$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.005 \ + --tx-fee-rate=40 \ + --memo="note to self" diff --git a/core/src/main/resources/help/settxfeerate-help.txt b/core/src/main/resources/help/settxfeerate-help.txt new file mode 100644 index 0000000000..557d7ee5c5 --- /dev/null +++ b/core/src/main/resources/help/settxfeerate-help.txt @@ -0,0 +1,19 @@ +settxfeerate + +NAME +---- +settxfeerate - set custom transaction fee rate preference + +SYNOPSIS +-------- +settxfeerate + --tx-fee-rate= + +DESCRIPTION +----------- +Sets the user's custom transaction fee rate preference. + +EXAMPLES +-------- +Set custom transaction fee rate to 25 sats/byte: +$ ./bisq-cli --password=xyz --port=9998 settxfeerate --tx-fee-rate=25 diff --git a/core/src/main/resources/help/setwalletpassword-help.txt b/core/src/main/resources/help/setwalletpassword-help.txt new file mode 100644 index 0000000000..da372d9857 --- /dev/null +++ b/core/src/main/resources/help/setwalletpassword-help.txt @@ -0,0 +1,25 @@ +setwalletpassword + +NAME +---- +setwalletpassword - set Bisq wallet password + +SYNOPSIS +-------- +setwalletpassword + --wallet-password= + --new-wallet-password= + +DESCRIPTION +----------- +Encrypts a Bisq wallet with a password. If the optional new wallet password option is +present, a new wallet password replaces the existing password + +EXAMPLES +-------- +Encrypt an unencrypted Bisq wallet with a password: +$ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password=mypassword + +Set a new password on a Bisq wallet that is already encrypted: +$ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password=myoldpassword \ + --new-wallet-password=mynewpassword diff --git a/core/src/main/resources/help/stop-help.txt b/core/src/main/resources/help/stop-help.txt new file mode 100644 index 0000000000..ad28fc24bd --- /dev/null +++ b/core/src/main/resources/help/stop-help.txt @@ -0,0 +1,22 @@ +stop + +NAME +---- +stop - stop the server + +SYNOPSIS +-------- +stop + +DESCRIPTION +----------- +Shutdown the RPC server. + +OPTIONS +------- + +EXAMPLES +-------- +To shutdown the server: +$ ./bisq-cli --password=xyz --port=9998 stop + diff --git a/core/src/main/resources/help/takeoffer-help.txt b/core/src/main/resources/help/takeoffer-help.txt new file mode 100644 index 0000000000..1290a00839 --- /dev/null +++ b/core/src/main/resources/help/takeoffer-help.txt @@ -0,0 +1,37 @@ +takeoffer + +NAME +---- +takeoffer - take an offer to buy or sell BTC + +SYNOPSIS +-------- +takeoffer + --offer-id= + --payment-account= + --fee-currency= + +DESCRIPTION +----------- +Take an existing offer using a matching payment method. The Bisq trade fee can be paid in BSQ or BTC. + +OPTIONS +------- +--offer-id + The ID of the buy or sell offer to take. + +--payment-account + The ID of the fiat payment account used to send or receive funds during the trade. + The payment account's payment method must match that of the offer. + +--fee-currency + The wallet currency used to pay the Bisq trade taker fee (BSQ|BTC). Default is BTC + +EXAMPLES +-------- +To take an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea + using a payment account with ID fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e, + and paying the Bisq trading fee in BSQ: +$ ./bisq-cli --password=xyz --port=9998 takeoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \ + -fee-currency=bsq diff --git a/core/src/main/resources/help/unlockwallet-help.txt b/core/src/main/resources/help/unlockwallet-help.txt new file mode 100644 index 0000000000..388117caf2 --- /dev/null +++ b/core/src/main/resources/help/unlockwallet-help.txt @@ -0,0 +1,21 @@ +unlockwallet + +NAME +---- +unlockwallet - unlock an encrypted Bisq wallet + +SYNOPSIS +-------- +unlockwallet + --wallet-password= + --timeout= + +DESCRIPTION +----------- +Unlocks an encrypted Bisq wallet for a specified number of seconds. +The timeout can be manually overridden with the lockwallet command. + +EXAMPLES +-------- +Unlock a wallet encrypted with the wallet-password "mypassword" for 30 seconds: +$ ./bisq-cli --password=xyz --port=9998 unlockwallet --wallet-password=mypassword --timeout=30 diff --git a/core/src/main/resources/help/unsettxfeerate-help.txt b/core/src/main/resources/help/unsettxfeerate-help.txt new file mode 100644 index 0000000000..d3ec0f96a1 --- /dev/null +++ b/core/src/main/resources/help/unsettxfeerate-help.txt @@ -0,0 +1,18 @@ +unsettxfeerate + +NAME +---- +unsettxfeerate - unset transaction fee rate preference + +SYNOPSIS +-------- +unsettxfeerate + +DESCRIPTION +----------- +Unsets (removes) the transaction fee rate user preference. + +EXAMPLES +-------- +Remove the user's custom transaction fee rate preference: +$ ./bisq-cli --password=xyz --port=9998 unsettxfeerate diff --git a/core/src/main/resources/help/verifybsqsenttoaddress-help.txt b/core/src/main/resources/help/verifybsqsenttoaddress-help.txt new file mode 100644 index 0000000000..47cb09d790 --- /dev/null +++ b/core/src/main/resources/help/verifybsqsenttoaddress-help.txt @@ -0,0 +1,39 @@ +verifybsqsenttoaddress + +NAME +---- +verifybsqsenttoaddress - verify BSQ sent to wallet address + +SYNOPSIS +-------- +verifybsqsenttoaddress + --address= + --amount= + +DESCRIPTION +----------- +Verify an exact amount of BSQ was sent to a specific Bisq wallet's BSQ address. +Receipt of BSQ to a BSQ (altcoin) payment account address should always be verified +before a BSQ seller sends a confirmpaymentreceived message for a BSQ/BTC trade. + +Warning: The verification result should be considered a false positive if a BSQ wallet +address has received the same amount of BSQ in more than one transaction. A way to +avoid this problem is to use different BSQ payment accounts for different trades, so +the payment account receiving address will vary from trade to trade. Another way is to +slightly vary your offer amounts and BSQ prices (if you are the maker), to make sure the +received BSQ amounts vary from trade to trade. Doing all of the above further reduces +the chance of a false positive. Another step is to check your BSQ wallet balance when +you verify BSQ has been received to an address. + +OPTIONS +------- +--address + The receiving BSQ address. + +--amount + The amount of BSQ received. + +EXAMPLES +-------- +Verify 500.00 BSQ was sent to address Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne: +$ ./bisq-cli --password=xyz --port=9998 verifybsqsenttoaddress --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne --amount=500.00 diff --git a/core/src/main/resources/help/withdrawfunds-help.txt b/core/src/main/resources/help/withdrawfunds-help.txt new file mode 100644 index 0000000000..edc69dddcb --- /dev/null +++ b/core/src/main/resources/help/withdrawfunds-help.txt @@ -0,0 +1,50 @@ +withdrawfunds + +NAME +---- +withdrawfunds - send BTC received during a trade to an external BTC wallet + +SYNOPSIS +-------- +withdrawfunds + --trade-id= + --address= + [--memo=<"memo">] + +DESCRIPTION +----------- +A BTC buyer completes the final step in the trade protocol by sending received BTC to +an external BTC wallet. + +The alternative way to close out the trade is to keep the received BTC in the Bisq wallet, +using the keepfunds command. + +The buyer needs to complete the trade protocol using the keepfunds or withdrawfunds or command. +This step may not seem necessary from the buyer's perspective, but it is necessary for correct +transition of a trade's state to CLOSED, within the Bisq server. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +--address + The destination btc address for the send btc transaction. + +--memo + An optional memo to be saved with the send btc transaction. + A multi word memo must be enclosed in double quotes. + +EXAMPLES +-------- +A BTC seller has informed the buyer that fiat payment has been received for trade with ID +83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and locked BTC has been released to the buyer. +The BTC buyer closes out the trade by sending the received BTC to an external BTC wallet: +$ ./bisq-cli --password=xyz --port=9998 withdrawfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --address=2N5J6MyjAsWnashimGiNwoRzUXThsQzRmbv (bitcoin regtest address) + + +A seller sends a trade's BTC proceeds to an external wallet, and includes an optional memo: +$ ./bisq-cli --password=xyz --port=9998 withdrawfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --address=2N5J6MyjAsWnashimGiNwoRzUXThsQzRmbv \ + --memo="note to self" diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties new file mode 100644 index 0000000000..5d989228b2 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -0,0 +1,3698 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Read more +shared.openHelp=Open Help +shared.warning=Warning +shared.close=Close +shared.cancel=Cancel +shared.ok=OK +shared.yes=Yes +shared.no=No +shared.iUnderstand=I understand +shared.na=N/A +shared.shutDown=Shut down +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=Buy bitcoin +shared.sellBitcoin=Sell bitcoin +shared.buyCurrency=Buy {0} +shared.sellCurrency=Sell {0} +shared.buyingBTCWith=buying BTC with {0} +shared.sellingBTCFor=selling BTC for {0} +shared.buyingCurrency=buying {0} (selling BTC) +shared.sellingCurrency=selling {0} (buying BTC) +shared.buy=buy +shared.sell=sell +shared.buying=buying +shared.selling=selling +shared.P2P=P2P +shared.oneOffer=offer +shared.multipleOffers=offers +shared.Offer=Offer +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=open offers +shared.trade=trade +shared.trades=trades +shared.openTrades=open trades +shared.dateTime=Date/Time +shared.price=Price +shared.priceWithCur=Price in {0} +shared.priceInCurForCur=Price in {0} for 1 {1} +shared.fixedPriceInCurForCur=Fixed price in {0} for 1 {1} +shared.amount=Amount +shared.txFee=Transaction Fee +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Buyer Deposit +shared.sellerSecurityDeposit=Seller Deposit +shared.amountWithCur=Amount in {0} +shared.volumeWithCur=Volume in {0} +shared.currency=Currency +shared.market=Market +shared.deviation=Deviation +shared.paymentMethod=Payment method +shared.tradeCurrency=Trade currency +shared.offerType=Offer type +shared.details=Details +shared.address=Address +shared.balanceWithCur=Balance in {0} +shared.utxo=Unspent transaction output +shared.txId=Transaction ID +shared.confirmations=Confirmations +shared.revert=Revert Tx +shared.select=Select +shared.usage=Usage +shared.state=Status +shared.tradeId=Trade ID +shared.offerId=Offer ID +shared.bankName=Bank name +shared.acceptedBanks=Accepted banks +shared.amountMinMax=Amount (min - max) +shared.amountHelp=If an offer has a minimum and a maximum amount set, then you can trade any amount within this range. +shared.remove=Remove +shared.goTo=Go to {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Remove offer +shared.dontRemoveOffer=Don't remove offer +shared.editOffer=Edit offer +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=Trading account +shared.faq=Visit FAQ page +shared.yesCancel=Yes, cancel +shared.nextStep=Next step +shared.selectTradingAccount=Select trading account +shared.fundFromSavingsWalletButton=Transfer funds from Bisq wallet +shared.fundFromExternalWalletButton=Open your external wallet for funding +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=Below % from market price +shared.aboveInPercent=Above % from market price +shared.enterPercentageValue=Enter % value +shared.OR=OR +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=Waiting for funds... +shared.depositTransactionId=Deposit transaction ID +shared.TheBTCBuyer=The BTC buyer +shared.You=You +shared.sendingConfirmation=Sending confirmation... +shared.sendingConfirmationAgain=Please send confirmation again +shared.exportCSV=Export to CSV +shared.exportJSON=Export to JSON +shared.summary=Show summary +shared.noDateAvailable=No date available +shared.noDetailsAvailable=No details available +shared.notUsedYet=Not used yet +shared.date=Date +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.copyToClipboard=Copy to clipboard +shared.language=Language +shared.country=Country +shared.applyAndShutDown=Apply and shut down +shared.selectPaymentMethod=Select payment method +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Do you really want to delete the selected account? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=There are no accounts set up yet +shared.manageAccounts=Manage accounts +shared.addNewAccount=Add new account +shared.ExportAccounts=Export Accounts +shared.importAccounts=Import Accounts +shared.createNewAccount=Create new account +shared.saveNewAccount=Save new account +shared.selectedAccount=Selected account +shared.deleteAccount=Delete account +shared.errorMessageInline=\nError message: {0} +shared.errorMessage=Error message +shared.information=Information +shared.name=Name +shared.id=ID +shared.dashboard=Dashboard +shared.accept=Accept +shared.balance=Balance +shared.save=Save +shared.onionAddress=Onion address +shared.supportTicket=support ticket +shared.dispute=dispute +shared.mediationCase=mediation case +shared.seller=seller +shared.buyer=buyer +shared.allEuroCountries=All Euro countries +shared.acceptedTakerCountries=Accepted taker countries +shared.tradePrice=Trade price +shared.tradeAmount=Trade amount +shared.tradeVolume=Trade volume +shared.invalidKey=The key you entered was not correct. +shared.enterPrivKey=Enter private key to unlock +shared.makerFeeTxId=Maker fee transaction ID +shared.takerFeeTxId=Taker fee transaction ID +shared.payoutTxId=Payout transaction ID +shared.contractAsJson=Contract in JSON format +shared.viewContractAsJson=View contract in JSON format +shared.contract.title=Contract for trade with ID: {0} +shared.paymentDetails=BTC {0} payment details +shared.securityDeposit=Security deposit +shared.yourSecurityDeposit=Your security deposit +shared.contract=Contract +shared.messageArrived=Message arrived. +shared.messageStoredInMailbox=Message stored in mailbox. +shared.messageSendingFailed=Message sending failed. Error: {0} +shared.unlock=Unlock +shared.toReceive=to receive +shared.toSpend=to spend +shared.btcAmount=BTC amount +shared.yourLanguage=Your languages +shared.addLanguage=Add language +shared.total=Total +shared.totalsNeeded=Funds needed +shared.tradeWalletAddress=Trade wallet address +shared.tradeWalletBalance=Trade wallet balance +shared.makerTxFee=Maker: {0} +shared.takerTxFee=Taker: {0} +shared.iConfirm=I confirm +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Open {0} +shared.fiat=Fiat +shared.crypto=Crypto +shared.all=All +shared.edit=Edit +shared.advancedOptions=Advanced options +shared.interval=Interval +shared.actions=Actions +shared.buyerUpperCase=Buyer +shared.sellerUpperCase=Seller +shared.new=NEW +shared.blindVoteTxId=Blind vote transaction ID +shared.proposal=Proposal +shared.votes=Votes +shared.learnMore=Learn more +shared.dismiss=Dismiss +shared.selectedArbitrator=Selected arbitrator +shared.selectedMediator=Selected mediator +shared.selectedRefundAgent=Selected arbitrator +shared.mediator=Mediator +shared.arbitrator=Arbitrator +shared.refundAgent=Arbitrator +shared.refundAgentForSupportStaff=Refund agent +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Market +mainView.menu.buyBtc=Buy BTC +mainView.menu.sellBtc=Sell BTC +mainView.menu.portfolio=Portfolio +mainView.menu.funds=Funds +mainView.menu.support=Support +mainView.menu.settings=Settings +mainView.menu.account=Account +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Market price by {0} +mainView.marketPrice.bisqInternalPrice=Price of latest Bisq trade +mainView.marketPrice.tooltip.bisqInternalPrice=There is no market price from external price feed providers available.\n\ + The displayed price is the latest Bisq trade price for that currency. +mainView.marketPrice.tooltip=Market price is provided by {0}{1}\nLast update: {2}\nProvider node URL: {3} +mainView.balance.available=Available balance +mainView.balance.reserved=Reserved in offers +mainView.balance.locked=Locked in trades +mainView.balance.reserved.short=Reserved +mainView.balance.locked.short=Locked + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Connecting to Bitcoin network +mainView.footer.bsqInfo.synchronizing=/ Synchronizing DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Connecting to +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=DAO full node + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Connecting to Tor network... +mainView.bootstrapState.torNodeCreated=(2/4) Tor node created +mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service published +mainView.bootstrapState.initialDataReceived=(4/4) Initial data received + +mainView.bootstrapWarning.noSeedNodesAvailable=No seed nodes available +mainView.bootstrapWarning.noNodesAvailable=No seed nodes and peers available +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Bisq network failed + +mainView.p2pNetworkWarnMsg.noNodesAvailable=There are no seed nodes or persisted peers available for requesting data.\nPlease check your internet connection or try to restart the application. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Bisq network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. + +mainView.walletServiceErrorMsg.timeout=Connecting to the Bitcoin network failed because of a timeout. +mainView.walletServiceErrorMsg.connectionError=Connection to the Bitcoin network failed because of an error: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} + +mainView.networkWarning.allConnectionsLost=You lost the connection to all {0} network peers.\nMaybe you lost your internet connection or your computer was in standby mode. +mainView.networkWarning.localhostBitcoinLost=You lost the connection to the localhost Bitcoin node.\nPlease restart the Bisq application to connect to other Bitcoin nodes or restart the localhost Bitcoin node. +mainView.version.update=(Update available) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Offer book +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Trades + +# OfferBookChartView +market.offerBook.buyAltcoin=Buy {0} (sell {1}) +market.offerBook.sellAltcoin=Sell {0} (buy {1}) +market.offerBook.buyWithFiat=Buy {0} +market.offerBook.sellWithFiat=Sell {0} +market.offerBook.sellOffersHeaderLabel=Sell {0} to +market.offerBook.buyOffersHeaderLabel=Buy {0} from +market.offerBook.buy=I want to buy bitcoin +market.offerBook.sell=I want to sell bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=All offers ({0}) +market.spread.numberOfBuyOffersColumn=Buy BTC ({0}) +market.spread.numberOfSellOffersColumn=Sell BTC ({0}) +market.spread.totalAmountColumn=Total BTC ({0}) +market.spread.spreadColumn=Spread +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Trades: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Open: +market.trades.tooltip.candle.close=Close: +market.trades.tooltip.candle.high=High: +market.trades.tooltip.candle.low=Low: +market.trades.tooltip.candle.average=Average: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=Date: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=Create offer +offerbook.takeOffer=Take offer +offerbook.takeOfferToBuy=Take offer to buy {0} +offerbook.takeOfferToSell=Take offer to sell {0} +offerbook.trader=Trader +offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} +offerbook.offerersBankName=Maker''s bank name: {0} +offerbook.offerersBankSeat=Maker''s seat of bank country: {0} +offerbook.offerersAcceptedBankSeatsEuro=Accepted seat of bank countries (taker): All Euro countries +offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {0} +offerbook.availableOffers=Available offers +offerbook.filterByCurrency=Filter by currency +offerbook.filterByPaymentMethod=Filter by payment method +offerbook.matchingOffers=Offers matching my accounts +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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) +offerbook.timeSinceSigning.info.banned=account was banned +offerbook.timeSinceSigning.daysSinceSigning={0} days +offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing +offerbook.xmrAutoConf=Is auto-confirm enabled + +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ + {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.ageDays={0} days +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=No. of offers: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Create new offer to buy {0} +offerbook.createOfferToSell=Create new offer to sell {0} +offerbook.createOfferToBuy.withFiat=Create new offer to buy {0} with {1} +offerbook.createOfferToSell.forFiat=Create new offer to sell {0} for {1} +offerbook.createOfferToBuy.withCrypto=Create new offer to sell {0} (buy {1}) +offerbook.createOfferToSell.forCrypto=Create new offer to buy {0} (sell {1}) + +offerbook.takeOfferButton.tooltip=Take offer for {0} +offerbook.yesCreateOffer=Yes, create offer +offerbook.setupNewAccount=Set up a new trading account +offerbook.removeOffer.success=Remove offer was successful. +offerbook.removeOffer.failed=Remove offer failed:\n{0} +offerbook.deactivateOffer.failed=Deactivating of offer failed:\n{0} +offerbook.activateOffer.failed=Publishing of offer failed:\n{0} +offerbook.withdrawFundsHint=You can withdraw the funds you paid in from the {0} screen. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + +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://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ + - The buyer''s account has not been signed by an arbitrator or a peer\n\ + - The time since signing of the buyer''s account is not at least 30 days\n\ + - The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ + - Your account has not been signed by an arbitrator or a peer\n\ + - The time since signing of your account is not at least 30 days\n\ + - The payment method for this offer is considered risky for bank chargebacks\n\n{1} + +offerbook.warning.wrongTradeProtocol=That offer requires a different protocol version as the one used in your version of the software.\n\nPlease check if you have the latest version installed, otherwise the user who created the offer has used an older version.\n\nUsers cannot trade with an incompatible trade protocol version. +offerbook.warning.userIgnored=You have added that user's onion address to your ignore list. +offerbook.warning.offerBlocked=That offer was blocked by the Bisq developers.\nProbably there is an unhandled bug causing issues when taking that offer. +offerbook.warning.currencyBanned=The currency used in that offer was blocked by the Bisq developers.\nPlease visit the Bisq Forum for more information. +offerbook.warning.paymentMethodBanned=The payment method used in that offer was blocked by the Bisq developers.\nPlease visit the Bisq Forum for more information. +offerbook.warning.nodeBlocked=The onion address of that trader was blocked by the Bisq developers.\nProbably there is an unhandled bug causing issues when taking offers from that trader. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\n\ + Please update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=You will sell at market price (updated every minute). +offerbook.info.buyAtMarketPrice=You will buy at market price (updated every minute). +offerbook.info.sellBelowMarketPrice=You will get {0} less than the current market price (updated every minute). +offerbook.info.buyAboveMarketPrice=You will pay {0} more than the current market price (updated every minute). +offerbook.info.sellAboveMarketPrice=You will get {0} more than the current market price (updated every minute). +offerbook.info.buyBelowMarketPrice=You will pay {0} less than the current market price (updated every minute). +offerbook.info.buyAtFixedPrice=You will buy at this fixed price. +offerbook.info.sellAtFixedPrice=You will sell at this fixed price. +offerbook.info.noArbitrationInUserLanguage=In case of a dispute, please note that arbitration for this offer will be handled in {0}. Language is currently set to {1}. +offerbook.info.roundedFiatVolume=The amount was rounded to increase the privacy of your trade. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Enter amount in BTC +createOffer.price.prompt=Enter price +createOffer.volume.prompt=Enter amount in {0} +createOffer.amountPriceBox.amountDescription=Amount of BTC to {0} +createOffer.amountPriceBox.buy.volumeDescription=Amount in {0} to spend +createOffer.amountPriceBox.sell.volumeDescription=Amount in {0} to receive +createOffer.amountPriceBox.minAmountDescription=Minimum amount of BTC +createOffer.securityDeposit.prompt=Security deposit +createOffer.fundsBox.title=Fund your offer +createOffer.fundsBox.offerFee=Trade fee +createOffer.fundsBox.networkFee=Mining fee +createOffer.fundsBox.placeOfferSpinnerInfo=Offer publishing is in progress ... +createOffer.fundsBox.paymentLabel=Bisq trade with ID {0} +createOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) +createOffer.fundsBox.fundsStructure.BSQ=({0} security deposit, {1} mining fee) + {2} trade fee +createOffer.success.headline=Your offer has been published +createOffer.success.info=You can manage your open offers at \"Portfolio/My open offers\". +createOffer.info.sellAtMarketPrice=You will always sell at market price as the price of your offer will be continuously updated. +createOffer.info.buyAtMarketPrice=You will always buy at market price as the price of your offer will be continuously updated. +createOffer.info.sellAboveMarketPrice=You will always get {0}% more than the current market price as the price of your offer will be continuously updated. +createOffer.info.buyBelowMarketPrice=You will always pay {0}% less than the current market price as the price of your offer will be continuously updated. +createOffer.warning.sellBelowMarketPrice=You will always get {0}% less than the current market price as the price of your offer will be continuously updated. +createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the current market price as the price of your offer will be continuously updated. +createOffer.tradeFee.descriptionBTCOnly=Trade fee +createOffer.tradeFee.descriptionBSQEnabled=Select trade fee currency + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which \ + 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} + +# new entries +createOffer.placeOfferButton=Review: Place offer to {0} bitcoin +createOffer.createOfferFundWalletInfo.headline=Fund your offer +# suppress inspection "TrailingSpacesInProperty" +createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n +createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\ + Those funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\n\ + The amount is the sum of:\n\ + {1}\ + - Your security deposit: {2}\n\ + - Trading fee: {3}\n\ + - Mining fee: {4}\n\n\ + You can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. + +# 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=An error occurred when placing the offer:\n\n{0}\n\n\ +No funds have left your wallet yet.\n\ +Please restart your application and check your network connection. +createOffer.setAmountPrice=Set amount and price +createOffer.warnCancelOffer=You have already funded that offer.\nIf you cancel now, your funds will be moved to your local Bisq wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? +createOffer.timeoutAtPublishing=A timeout occurred at publishing the offer. +createOffer.errorInfo=\n\nThe maker fee is already paid. In the worst case you have lost that fee.\nPlease try to restart your application and check your network connection to see if you can resolve the issue. +createOffer.tooLowSecDeposit.warning=You have set the security deposit to a lower value than the recommended default value of {0}.\n\ + Are you sure you want to use a lower security deposit? +createOffer.tooLowSecDeposit.makerIsSeller=It gives you less protection in case the trading peer does not follow the trade protocol. +createOffer.tooLowSecDeposit.makerIsBuyer=It gives less protection for the trading peer that you follow the trade protocol as you have less deposit at risk. \ + Other users might prefer to take other offers instead of yours. +createOffer.resetToDefault=No, reset to the default value +createOffer.useLowerValue=Yes, use my lower value +createOffer.priceOutSideOfDeviation=The price you have entered is outside the max. allowed deviation from the market price.\nThe max. allowed deviation is {0} and can be adjusted in the preferences. +createOffer.changePrice=Change price +createOffer.tac=With publishing this offer I agree to trade with any trader who fulfills the conditions as defined in this screen. +createOffer.currencyForFee=Trade fee +createOffer.setDeposit=Set buyer's security deposit (%) +createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) +createOffer.setDepositForBothTraders=Set both traders' security deposit (%) +createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} +createOffer.minSecurityDepositUsed=Min. buyer security deposit is used + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Enter amount in BTC +takeOffer.amountPriceBox.buy.amountDescription=Amount of BTC to sell +takeOffer.amountPriceBox.sell.amountDescription=Amount of BTC to buy +takeOffer.amountPriceBox.priceDescription=Price per bitcoin in {0} +takeOffer.amountPriceBox.amountRangeDescription=Possible amount range +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=The amount you have entered exceeds the number of allowed decimal places.\nThe amount has been adjusted to 4 decimal places. +takeOffer.validation.amountSmallerThanMinAmount=Amount cannot be smaller than minimum amount defined in the offer. +takeOffer.validation.amountLargerThanOfferAmount=Input amount cannot be higher than the amount defined in the offer. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=That input amount would create dust change for the BTC seller. +takeOffer.fundsBox.title=Fund your trade +takeOffer.fundsBox.isOfferAvailable=Checking if the offer is still available ... +takeOffer.fundsBox.tradeAmount=Amount to sell +takeOffer.fundsBox.offerFee=Trade fee +takeOffer.fundsBox.networkFee=Total mining fees +takeOffer.fundsBox.takeOfferSpinnerInfo=Take offer in progress ... +takeOffer.fundsBox.paymentLabel=Bisq trade with ID {0} +takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) +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} + +# new entries +takeOffer.takeOfferButton=Review: Take offer to {0} bitcoin +takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentage price based on the market price but there is no price feed available. +takeOffer.takeOfferFundWalletInfo.headline=Fund your trade +# suppress inspection "TrailingSpacesInProperty" +takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n +takeOffer.takeOfferFundWalletInfo.msg=You need to deposit {0} for taking this offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}\n- Total mining fees: {4}\n\nYou can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. +takeOffer.alreadyPaidInFunds=If you have already paid in funds you can withdraw it in the \"Funds/Send funds\" screen. +takeOffer.paymentInfo=Payment info +takeOffer.setAmountPrice=Set amount +takeOffer.alreadyFunded.askCancel=You have already funded that offer.\nIf you cancel now, your funds will be moved to your local Bisq wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? +takeOffer.failed.offerNotAvailable=Take offer request failed because the offer is not available anymore. Maybe another trader has taken the offer in the meantime. +takeOffer.failed.offerTaken=You cannot take that offer because the offer was already taken by another trader. +takeOffer.failed.offerRemoved=You cannot take that offer because the offer has been removed in the meantime. +takeOffer.failed.offererNotOnline=Take offer request failed because maker is not online anymore. +takeOffer.failed.offererOffline=You cannot take that offer because the maker is offline. +takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. + +takeOffer.error.noFundsLost=\n\nNo funds have left your wallet yet.\nPlease try to restart your application and check your network connection to see if you can resolve the issue. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nThe deposit transaction is already published.\nPlease try to restart your application and check your network connection to see if you can resolve the issue.\nIf the problem still remains please contact the developers for support. +takeOffer.error.payoutPublished=\n\nThe payout transaction is already published.\nPlease try to restart your application and check your network connection to see if you can resolve the issue.\nIf the problem still remains please contact the developers for support. +takeOffer.tac=With taking this offer I agree to the trade conditions as defined in this screen. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Trigger price +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 + +editOffer.setPrice=Set price +editOffer.confirmEdit=Confirm: Edit offer +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 the Bisq DAO and can no longer be edited. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=My open offers +portfolio.tab.pendingTrades=Open trades +portfolio.tab.history=History +portfolio.tab.failed=Failed +portfolio.tab.editOpenOffer=Edit offer +portfolio.tab.duplicateOffer=Duplicate offer +portfolio.context.offerLikeThis=Create new offer like this... +portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\n\ + Please do NOT send the fiat or altcoin payment.\n\n\ + Open a support ticket to get assistance from a Mediator.\n\n\ + Error message: {0} + +portfolio.pending.unconfirmedTooLong=Security deposit transaction on trade {0} is still unconfirmed after {1} hours. \ + Check the deposit transaction at a blockchain explorer; if it has been confirmed but is not being displayed \ + as confirmed in Bisq: \n\ + ● Make a data backup [HYPERLINK:https://bisq.wiki/Backing_up_application_data] \n\ + ● Do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file]\n\n\ + Contact Bisq support [HYPERLINK:https://keybase.io/team/bisq] if you have doubts or the issue persists. + +portfolio.pending.step1.waitForConf=Wait for blockchain confirmation +portfolio.pending.step2_buyer.startPayment=Start payment +portfolio.pending.step2_seller.waitPaymentStarted=Wait until payment has started +portfolio.pending.step3_buyer.waitPaymentArrived=Wait until payment arrived +portfolio.pending.step3_seller.confirmPaymentReceived=Confirm payment received +portfolio.pending.step5.completed=Completed + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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 at least one blockchain confirmation before starting the payment. +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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field \ + empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. \ + You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Please transfer from your external {0} wallet\n{1} to the BTC seller.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Please go to a bank and pay {0} to the BTC seller.\n\n +portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment write on the paper receipt: NO REFUNDS.\nThen tear it in 2 parts, make a photo and send it to the BTC seller's email address. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the BTC seller by using MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the BTC seller.\n\ + The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the BTC seller by using Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the BTC seller.\n\ + The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the BTC seller.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. \ + Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ + See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Please contact the BTC seller by the provided contact and arrange a meeting to pay {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer +portfolio.pending.step2_buyer.sellersAddress=Seller''s {0} address +portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used +portfolio.pending.step2_buyer.paymentStarted=Payment started +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.\ + Please contact the mediator for assistance. +portfolio.pending.step2_buyer.paperReceipt.headline=Did you send the paper receipt to the BTC seller? +portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\n\ + You need to write on the paper receipt: NO REFUNDS.\n\ + Then tear it in 2 parts, make a photo and send it to the BTC seller's email address. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Send Authorisation number and receipt +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=You need to send the Authorisation number and a photo of the receipt by email to the BTC seller.\n\ + The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}.\n\n\ + Did you send the Authorisation number and contract to the seller? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Send MTCN and receipt +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=You need to send the MTCN (tracking number) and a photo of the receipt by email to the BTC seller.\n\ + The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}.\n\n\ + Did you send the MTCN and contract to the seller? +portfolio.pending.step2_buyer.halCashInfo.headline=Send HalCash code +portfolio.pending.step2_buyer.halCashInfo.msg=You need to send a text message with the HalCash code as well as the \ + trade ID ({0}) to the BTC seller.\nThe seller''s mobile nr. is {1}.\n\n\ + Did you send the code to the seller? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. \ + Faster Payments accounts created in old Bisq clients do not provide the receiver's name, \ + so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=Confirm that you have started the payment +portfolio.pending.step2_buyer.confirmStart.msg=Did you initiate the {0} payment to your trading partner? +portfolio.pending.step2_buyer.confirmStart.yes=Yes, I have started the payment +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\n\ + By not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\n\ + Beside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\n\ + See more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=Wait for payment +portfolio.pending.step2_seller.f2fInfo.headline=Buyer's contact information +portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction has at least one blockchain confirmation.\nYou need to wait until the BTC buyer starts the {0} payment. +portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. +disputeChat.chatWindowTitle=Dispute chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Trader Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ + It is not mandatory to reply in the chat.\n\ + If a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\n\ + Chat rules:\n\ + \t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\ + \t● Do not send your seed words, private keys, passwords or other sensitive information!\n\ + \t● Do not encourage trading outside of Bisq (no security).\n\ + \t● Do not engage in any form of social engineering scam attempts.\n\ + \t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\ + \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\ + \t● Keep conversation friendly and respectful. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +message.state.SENT=Message sent +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Message arrived at peer +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Peer confirmed message receipt +# suppress inspection "UnusedProperty" +message.state.FAILED=Sending message failed + +portfolio.pending.step3_buyer.wait.headline=Wait for BTC seller's payment confirmation +portfolio.pending.step3_buyer.wait.info=Waiting for the BTC seller''s confirmation for the receipt of the {0} payment. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Payment started message status +portfolio.pending.step3_buyer.warn.part1a=on the {0} blockchain +portfolio.pending.step3_buyer.warn.part1b=at your payment provider (e.g. bank) +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the \ + payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the \ + trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Your trading partner has confirmed that they have initiated the {0} payment.\n\n +portfolio.pending.step3_seller.altcoin.explorer=on your favorite {0} blockchain explorer +portfolio.pending.step3_seller.altcoin.wallet=at your {0} wallet +portfolio.pending.step3_seller.altcoin={0}Please check {1} if the transaction to your receiving address\n\ +{2}\n\ +has already sufficient blockchain confirmations.\nThe payment amount has to be {3}\n\n\ +You can copy & paste your {4} address from the main screen after closing that popup. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\n\ + Please go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Because the payment is done via Cash Deposit the BTC buyer has to write \"NO REFUND\" on the paper receipt, tear it in 2 parts and send you a photo by email.\n\n\ +To avoid chargeback risk, only confirm if you received the email and if you are sure the paper receipt is valid.\n\ +If you are not sure, {0} +portfolio.pending.step3_seller.moneyGram=The buyer has to send you the Authorisation number and a photo of the receipt by email.\n\ + The receipt must clearly show your full name, country, state and the amount. Please check your email if you received the Authorisation number.\n\n\ + After closing that popup you will see the BTC buyer's name and address for picking up the money from MoneyGram.\n\n\ + Only confirm receipt after you have successfully picked up the money! +portfolio.pending.step3_seller.westernUnion=The buyer has to send you the MTCN (tracking number) and a photo of the receipt by email.\n\ + The receipt must clearly show your full name, city, country and the amount. Please check your email if you received the MTCN.\n\n\ + After closing that popup you will see the BTC buyer's name and address for picking up the money from Western Union.\n\n\ + Only confirm receipt after you have successfully picked up the money! +portfolio.pending.step3_seller.halCash=The buyer has to send you the HalCash code as text message. Beside that you will receive a message from HalCash with the required information to withdraw the EUR from a HalCash supporting ATM.\n\n\ + After you have picked up the money from the ATM please confirm here the receipt of the payment! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text \ + message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted \ + confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\n\ + If the names are not exactly the same, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Confirm payment receipt +portfolio.pending.step3_seller.amountToReceive=Amount to receive +portfolio.pending.step3_seller.yourAddress=Your {0} address +portfolio.pending.step3_seller.buyersAddress=Buyers {0} address +portfolio.pending.step3_seller.yourAccount=Your trading account +portfolio.pending.step3_seller.xmrTxHash=Transaction ID +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt +portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Check at your trading account (e.g. bank account) and confirm when you have received the payment. +portfolio.pending.step3_seller.warn.part1a=on the {0} blockchain +portfolio.pending.step3_seller.warn.part1b=at your payment provider (e.g. bank) +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. \ + Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\n\ + The max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirm that you have received the payment +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Yes, I have received the payment +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also \ + verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, \ + you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. + +portfolio.pending.step5_buyer.groupTitle=Summary of completed trade +portfolio.pending.step5_buyer.tradeFee=Trade fee +portfolio.pending.step5_buyer.makersMiningFee=Mining fee +portfolio.pending.step5_buyer.takersMiningFee=Total mining fees +portfolio.pending.step5_buyer.refunded=Refunded security deposit +portfolio.pending.step5_buyer.withdrawBTC=Withdraw your bitcoin +portfolio.pending.step5_buyer.amount=Amount to withdraw +portfolio.pending.step5_buyer.withdrawToAddress=Withdraw to address +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=Withdraw to external wallet +portfolio.pending.step5_buyer.alreadyWithdrawn=Your funds have already been withdrawn.\nPlease check the transaction history. +portfolio.pending.step5_buyer.confirmWithdrawal=Confirm withdrawal request +portfolio.pending.step5_buyer.amountTooLow=The amount to transfer is lower than the transaction fee and the min. possible tx value (dust). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Withdrawal completed +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Your completed trades are stored under \"Portfolio/History\".\nYou can review all your bitcoin transactions under \"Funds/Transactions\" +portfolio.pending.step5_buyer.bought=You have bought +portfolio.pending.step5_buyer.paid=You have paid + +portfolio.pending.step5_seller.sold=You have sold +portfolio.pending.step5_seller.received=You have received + +tradeFeedbackWindow.title=Congratulations on completing your trade +tradeFeedbackWindow.msg.part1=We'd love to hear back from you about your experience. It'll help us to improve the software \ + and to smooth out any rough edges. If you'd like to provide feedback, please fill out this short survey \ + (no registration required) at: +tradeFeedbackWindow.msg.part2=If you have any questions, or experienced any problems, please get in touch with other users and contributors via the Bisq forum at: +tradeFeedbackWindow.msg.part3=Thanks for using Bisq! + +portfolio.pending.role=My role +portfolio.pending.tradeInformation=Trade information +portfolio.pending.remainingTime=Remaining time +portfolio.pending.remainingTimeDetail={0} (until {1}) +portfolio.pending.tradePeriodInfo=After the first blockchain confirmation, the trade period starts. Based on the payment method used, a different maximum allowed trade period is applied. +portfolio.pending.tradePeriodWarning=If the period is exceeded both traders can open a dispute. +portfolio.pending.tradeNotCompleted=Trade not completed in time (until {0}) +portfolio.pending.tradeProcess=Trade process +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived \ + (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask \ + for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Open dispute again +portfolio.pending.openSupportTicket.headline=Open support ticket +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \ + \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and \ + handled by a mediator or arbitrator. + +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. +portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute \ + without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\n\ + For further help please contact the Bisq support channel at the Bisq Keybase team. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the \ + trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the \ + trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute \ + with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\n\ + For further help please contact the Bisq support channel at the Bisq Keybase team. + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade \ + chat or ask the Bisq community at https://bisq.community. \ + If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Trade period is over + +portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested +portfolio.pending.openSupport=Open support ticket +portfolio.pending.supportTicketOpened=Support ticket opened +portfolio.pending.communicateWithArbitrator=Please communicate in the \"Support\" screen with the arbitrator. +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. +portfolio.pending.disputeOpenedMyUser=You opened already a dispute.\n{0} +portfolio.pending.disputeOpenedByPeer=Your trading peer opened a dispute\n{0} +portfolio.pending.noReceiverAddressDefined=No receiver address defined + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=View proposed resolution +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\n\ + You receive: {0}\n\ + Your trading peer receives: {1}\n\n\ + You can accept or reject this suggested payout.\n\n\ + By accepting, you sign the proposed payout transaction. \ + If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\ + If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \ + second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ + The arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. \ + Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \ + exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \ + (or if the other peer is unresponsive).\n\n\ + More details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ + but it seems that your trading peer has not accepted it.\n\n\ + Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ + investigate the case again and do a payout based on their findings.\n\n\ + You can find more details about the arbitration model at:\ + [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\n\ + Without 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\n\ + Without this tx, the trade cannot be completed. No funds have been locked. \ + Your offer is still available to other traders, so you have not lost the maker fee. \ + You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\n\ + Without this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. \ + You can make a request to be reimbursed the trade fee here: \ + [HYPERLINK:https://github.com/bisq-network/support/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\ + Please do NOT send the fiat or altcoin payment to the BTC 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\n\ + You 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\n\ + If 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\n\ + If 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\n\ + You 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\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 Bisq 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] +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] +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\ + You 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\n\ + The 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\n\ + Do you want to move the trade to failed trades?\n\n\ + You 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.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Completed +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated +portfolio.closed.canceled=Canceled +portfolio.failed.Failed=Failed +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\n\ + Do you want to move this trade back to open trades?\n\ + This is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \n\ + Try again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Receive funds +funds.tab.withdrawal=Send funds +funds.tab.reserved=Reserved funds +funds.tab.locked=Locked funds +funds.tab.transactions=Transactions + +funds.deposit.unused=Unused +funds.deposit.usedInTx=Used in {0} transaction(s) +funds.deposit.fundBisqWallet=Fund Bisq wallet +funds.deposit.noAddresses=No deposit addresses have been generated yet +funds.deposit.fundWallet=Fund your wallet +funds.deposit.withdrawFromWallet=Send funds from wallet +funds.deposit.amount=Amount in BTC (optional) +funds.deposit.generateAddress=Generate new address +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Please select an unused address from the table above rather than generating a new one. + +funds.withdrawal.arbitrationFee=Arbitration fee +funds.withdrawal.inputs=Inputs selection +funds.withdrawal.useAllInputs=Use all available inputs +funds.withdrawal.useCustomInputs=Use custom inputs +funds.withdrawal.receiverAmount=Receiver's amount +funds.withdrawal.senderAmount=Sender's amount +funds.withdrawal.feeExcluded=Amount excludes mining fee +funds.withdrawal.feeIncluded=Amount includes mining fee +funds.withdrawal.fromLabel=Withdraw from address +funds.withdrawal.toLabel=Withdraw to address +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Withdraw selected +funds.withdrawal.noFundsAvailable=No funds are available for withdrawal +funds.withdrawal.confirmWithdrawalRequest=Confirm withdrawal request +funds.withdrawal.withdrawMultipleAddresses=Withdraw from multiple addresses ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Withdraw from multiple addresses:\n{0} +funds.withdrawal.notEnoughFunds=You don't have enough funds in your wallet. +funds.withdrawal.selectAddress=Select a source address from the table +funds.withdrawal.setAmount=Set the amount to withdraw +funds.withdrawal.fillDestAddress=Fill in your destination address +funds.withdrawal.warn.noSourceAddressSelected=You need to select a source address in the table above. +funds.withdrawal.warn.amountExceeds=You don't have sufficient funds available from the selected address.\n\ + Consider to select multiple addresses in the table above or change the fee toggle to include the miner fee. +funds.withdrawal.txFee=Withdrawal transaction fee (satoshis/vbyte) +funds.withdrawal.useCustomFeeValueInfo=Insert a custom transaction fee value +funds.withdrawal.useCustomFeeValue=Use custom value +funds.withdrawal.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +funds.withdrawal.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. + +funds.reserved.noFunds=No funds are reserved in open offers +funds.reserved.reserved=Reserved in local wallet for offer with ID: {0} + +funds.locked.noFunds=No funds are locked in trades +funds.locked.locked=Locked in multisig for trade with ID: {0} + +funds.tx.direction.sentTo=Sent to: +funds.tx.direction.receivedWith=Received with: +funds.tx.direction.genesisTx=From Genesis tx: +funds.tx.txFeePaymentForBsqTx=Miner fee for BSQ tx +funds.tx.createOfferFee=Maker and tx fee: {0} +funds.tx.takeOfferFee=Taker and tx fee: {0} +funds.tx.multiSigDeposit=Multisig deposit: {0} +funds.tx.multiSigPayout=Multisig payout: {0} +funds.tx.disputePayout=Dispute payout: {0} +funds.tx.disputeLost=Lost dispute case: {0} +funds.tx.collateralForRefund=Refund collateral: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} +funds.tx.unknown=Unknown reason: {0} +funds.tx.noFundsFromDispute=No refund from dispute +funds.tx.receivedFunds=Received funds +funds.tx.withdrawnFromWallet=Withdrawn from wallet +funds.tx.withdrawnFromBSQWallet=BTC withdrawn from BSQ wallet +funds.tx.memo=Memo +funds.tx.noTxAvailable=No transactions available +funds.tx.revert=Revert +funds.tx.txSent=Transaction successfully sent to a new address in the local Bisq wallet. +funds.tx.direction.self=Sent to yourself +funds.tx.daoTxFee=Miner fee for BSQ tx +funds.tx.reimbursementRequestTxFee=Reimbursement request +funds.tx.compensationRequestTxFee=Compensation request +funds.tx.dustAttackTx=Received dust +funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount to your wallet and might be an attempt \ + from chain analysis companies to spy on your wallet.\n\n\ + If you use that transaction output in a spending transaction they will learn that you are likely the owner of the \ + other address as well (coin merge).\n\n\ + To protect your privacy the Bisq wallet ignores such dust outputs for spending purposes and in the balance display. \ + You can set the threshold amount when an output is considered dust in the settings. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.legacyArbitration.support=Legacy Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets +support.filter=Search disputes +support.filter.prompt=Enter trade ID, date, onion address or account data + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the \ + mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can \ + check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Private notification +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=There are no open tickets +support.sendingMessage=Sending Message... +support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. +support.sendMessageError=Sending message failed. Error: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=The offer in that dispute has been created with an older version of Bisq.\n\ +You cannot close that dispute with your version of the application.\n\n\ +Please use an older version with protocol version {0} +support.openFile=Open file to attach (max. file size: {0} kb) +support.attachmentTooLarge=The total size of your attachments is {0} kb and is exceeding the max. allowed message size of {1} kB. +support.maxSize=The max. allowed file size is {0} kB. +support.attachment=Attachment +support.tooManyAttachments=You cannot send more than 3 attachments in one message. +support.save=Save file to disk +support.messages=Messages +support.input.prompt=Enter message... +support.send=Send +support.addAttachments=Add attachments +support.closeTicket=Close ticket +support.attachments=Attachments: +support.savedInMailbox=Message saved in receiver's mailbox +support.arrived=Message arrived at receiver +support.acknowledged=Message arrival confirmed by receiver +support.error=Receiver could not process message. Error: {0} +support.buyerAddress=BTC buyer address +support.sellerAddress=BTC seller address +support.role=Role +support.agent=Support agent +support.state=State +support.chat=Chat +support.closed=Closed +support.open=Open +support.process=Process +support.buyerOfferer=BTC buyer/Maker +support.sellerOfferer=BTC seller/Maker +support.buyerTaker=BTC buyer/Taker +support.sellerTaker=BTC seller/Taker + +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\n\ +Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \ + If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and suggest a \ + payout of trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. \ + If one or both traders do not agree to the mediator's suggested payout, they can request arbitration.\ + The arbitrator will re-evaluate the situation and, if warranted, personally pay the trader back and request reimbursement \ + for this payment from the Bisq DAO. +support.initialInfo=Please enter a description of your problem in the text field below. \ + Add as much information as possible to speed up dispute resolution time.\n\n\ + Here is a check list for information you should provide:\n\ + \t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' \ + button in the application?\n\ + \t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' \ + button in the application?\n\ + \t● Which version of Bisq are you using?\n\ + \t● Which operating system are you using?\n\ + \t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\ + \t Sometimes the data directory gets corrupted and leads to strange bugs. \n\ + \t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\n\ + Please make yourself familiar with the basic rules for the dispute process:\n\ +\t● You need to respond to the {0}''s requests within 2 days.\n\ +\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\ +\t● The maximum period for a dispute is 14 days.\n\ +\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\ +\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\ +You can read more about the dispute process at: {2} +support.systemMsg=System message: {0} +support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nBisq version: {1} +support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nBisq version: {1} +support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq version: {1} +support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ + It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ + Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\ + Address used in the dispute: {0}\n\n\ + All DAO param donation addresses: {1}\n\n\ + Trade ID: {2}\ + {3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preferences +settings.tab.network=Network info +settings.tab.about=About + +setting.preferences.general=General preferences +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Max. deviation from market price +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Avoid standby mode +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Values higher than {0}% are not allowed. +setting.preferences.txFee=BSQ Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Use custom value +setting.preferences.ignorePeers=Ignored peers [onion address:port] +setting.preferences.ignoreDustThreshold=Min. non-dust output value +setting.preferences.currenciesInList=Currencies in market price feed list +setting.preferences.prefCurrency=Preferred currency +setting.preferences.displayFiat=Display national currencies +setting.preferences.noFiat=There are no national currencies selected +setting.preferences.cannotRemovePrefCurrency=You cannot remove your selected preferred display currency +setting.preferences.displayAltcoins=Display altcoins +setting.preferences.noAltcoins=There are no altcoins selected +setting.preferences.addFiat=Add national currency +setting.preferences.addAltcoin=Add altcoin +setting.preferences.displayOptions=Display options +setting.preferences.showOwnOffers=Show my own offers in offer book +setting.preferences.useAnimations=Use animations +setting.preferences.useDarkMode=Use dark mode +setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Reset all \"Don't show again\" flags +settings.preferences.languageChange=To apply the language change to all screens requires a restart. +settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. +setting.preferences.daoOptions=DAO options +setting.preferences.dao.resyncFromGenesis.label=Rebuild DAO state from genesis tx +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from \ + the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU \ + resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\n\ + If you proceed, after an application restart the Bisq network governance data will be reloaded from \ + the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Run Bisq as DAO full node +setting.preferences.dao.rpcUser=RPC username +setting.preferences.dao.rpcPw=RPC password +setting.preferences.dao.blockNotifyPort=Block notify port +setting.preferences.dao.fullNodeInfo=For running Bisq as DAO full node you need to have Bitcoin Core locally running \ + and RPC enabled. All requirements are documented in ''{0}''.\n\n\ + After changing the mode you need to restart. +setting.preferences.dao.fullNodeInfo.ok=Open docs page +setting.preferences.dao.fullNodeInfo.cancel=No, I stick with lite node mode +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ + customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Name +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Bitcoin network +settings.net.p2pHeader=Bisq network +settings.net.onionAddressLabel=My onion address +settings.net.btcNodesLabel=Use custom Bitcoin Core nodes +settings.net.bitcoinPeersLabel=Connected peers +settings.net.useTorForBtcJLabel=Use Tor for Bitcoin network +settings.net.bitcoinNodesLabel=Bitcoin Core nodes to connect to +settings.net.useProvidedNodesRadio=Use provided Bitcoin Core nodes +settings.net.usePublicNodesRadio=Use public Bitcoin network +settings.net.useCustomNodesRadio=Use custom Bitcoin Core nodes +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\n\ + Please read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\n\ + Are you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=No, use provided nodes +settings.net.warn.usePublicNodes.usePublic=Yes, use public network +settings.net.warn.useCustomNodes.B2XWarning=Please be sure that your Bitcoin node is a trusted Bitcoin Core node!\n\n\ + Connecting to nodes which do not follow the Bitcoin Core consensus rules could corrupt your wallet and cause problems in the trade process.\n\n\ + Users who connect to nodes that violate consensus rules are responsible for any resulting damage. \ + Any resulting disputes will be decided in favor of the other peer. No technical support will be given \ + to users who ignore this warning and protection mechanisms! +settings.net.warn.invalidBtcConfig=Connection to the Bitcoin network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Bitcoin nodes instead. You will need to restart the application. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=Connected peers +settings.net.onionAddressColumn=Onion address +settings.net.creationDateColumn=Established +settings.net.connectionTypeColumn=In/Out +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Roundtrip +settings.net.sentBytesColumn=Sent +settings.net.receivedBytesColumn=Received +settings.net.peerTypeColumn=Peer type +settings.net.openTorSettingsButton=Open Tor settings + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Height + +settings.net.needRestart=You need to restart the application to apply that change.\nDo you want to do that now? +settings.net.notKnownYet=Not known yet... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[IP address:port | host name:port | onion address:port] (comma separated). Port can be omitted if default is used (8333). +settings.net.seedNode=Seed node +settings.net.directPeer=Peer (direct) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Peer +settings.net.inbound=inbound +settings.net.outbound=outbound +settings.net.reSyncSPVChainLabel=Resync SPV chain +settings.net.reSyncSPVChainButton=Delete SPV file and resync +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\n\ +After the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\n\ + Depending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. \ + Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=The SPV chain file has been deleted. Please be patient. It can take a while to resync with the network. +settings.net.reSyncSPVAfterRestartCompleted=The resync is now completed. Please restart the application. +settings.net.reSyncSPVFailed=Could not delete SPV chain file.\nError: {0} +setting.about.aboutBisq=About Bisq +setting.about.about=Bisq is open-source software which facilitates the exchange of bitcoin with national currencies (and other cryptocurrencies) through a decentralized peer-to-peer network in a way that strongly protects user privacy. Learn more about Bisq on our project web page. +setting.about.web=Bisq web page +setting.about.code=Source code +setting.about.agpl=AGPL License +setting.about.support=Support Bisq +setting.about.def=Bisq is not a company—it is a project open to the community. If you want to participate or support Bisq please follow the links below. +setting.about.contribute=Contribute +setting.about.providers=Data providers +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Market prices provided by +setting.about.feeEstimation.label=Mining fee estimation provided by +setting.about.versionDetails=Version details +setting.about.version=Application version +setting.about.subsystems.label=Versions of subsystems +setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Local DB version: {2}; Trade protocol version: {3} + +setting.about.shortcuts=Short cuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigate main menu +setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' + +setting.about.shortcuts.close=Close Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Close popup or dialog window +setting.about.shortcuts.closePopup.value='ESCAPE' key + +setting.about.shortcuts.chatSendMsg=Send trader chat message +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Open dispute +setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} + +setting.about.shortcuts.walletDetails=Open wallet details window + +setting.about.shortcuts.openEmergencyBtcWalletTool=Open emergency wallet tool for BTC wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool for BSQ wallet + +setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN + +setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) +setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} + +setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) +setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} + +setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) + +setting.about.shortcuts.sendFilter=Set Filter (privileged activity) + +setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of \ + XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\n\ + Auto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided \ + by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your \ + own XMR explorer node for maximum privacy and security.\n\n\ + You can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required \ + confirmations here in Settings.\n\n\ + See more details (including how to set up your own explorer node) on the Bisq wiki \ + [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing +account.info.headline=Welcome to your Bisq Account +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\n\ +A new Bitcoin wallet was created the first time you started Bisq.\n\n\ +We strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a \ + password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\n\ +Privacy & security note: \ +because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no \ + access to your personal info, your funds, or even your IP address. Data such as bank account numbers, \ + altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate \ + (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). + +account.menu.paymentAccount=National currency accounts +account.menu.altCoinsAccountView=Altcoin accounts +account.menu.password=Wallet password +account.menu.seedWords=Wallet seed +account.menu.walletInfo=Wallet info +account.menu.backup=Backup +account.menu.notifications=Notifications + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\n\ + For BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the \ + path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\n\ + Keep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet \ + data, which can lead to failed trades.\n\n\ + NEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Public key + +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=Revoke +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.warn.min1Language=You need to set at least 1 language.\nWe added the default language for you. +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} + +account.altcoin.yourAltcoinAccounts=Your altcoin accounts +account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as \ +described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or \ +(b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ +not a {2} specialist and cannot help in such cases. +account.altcoin.popup.wallet.confirm=I understand and confirm that I know which wallet I need to use. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +For sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the \ +store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as \ +that would be required in case of a dispute.\n\ +uplexa-wallet-cli (use the command get_tx_key)\n\ +uplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ +At normal block explorers the transfer is not verifiable.\n\n\ +You need to provide the arbitrator the following data in case of a dispute:\n\ +- The tx private key\n\ +- The transaction hash\n\ +- The recipient's public address\n\n\ +Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ +dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the \ +arbitrator in case of a dispute.\n\n\ +There is no payment ID required, just the normal public address.\n\ +If you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) \ +or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +For sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the \ +store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as \ +that would be required in case of a dispute.\n\ +arqma-wallet-cli (use the command get_tx_key)\n\ +arqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ +At normal block explorers the transfer is not verifiable.\n\n\ +You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ +- The tx private key\n\ +- The transaction hash\n\ +- The recipient's public address\n\n\ +Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ +dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the \ +mediator or arbitrator in case of a dispute.\n\n\ +There is no payment ID required, just the normal public address.\n\ +If you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) \ +or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\n\ +If selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n\ +- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n\ +- the transaction ID (Tx ID or Tx Hash)\n\ +- the destination address (recipient's address)\n\n\ +See the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\n\ +Failure to provide the required transaction data will result in losing disputes.\n\n\ +Also note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, \ +but you need to enable it in Settings.\n\n\ +See the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +For sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the \ +store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as \ +that would be required in case of a dispute.\n\ +masari-wallet-cli (use the command get_tx_key)\n\ +masari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ +Masari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\n\ +Verification can be accomplished in-wallet.\n\ +masari-wallet-cli : using command (check_tx_key).\n\ +masari-wallet-gui : on the Advanced > Prove/Check page.\n\ +Verification can be accomplished in the block explorer \n\ +Open block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\n\ +Once transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\n\ +You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ +- The tx private key\n\ +- The transaction hash\n\ +- The recipient's public address\n\n\ +Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ +dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the \ +mediator or arbitrator in case of a dispute.\n\n\ +There is no payment ID required, just the normal public address.\n\ +If you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +To send BLUR you must use the Blur Network CLI or GUI Wallet. \n\n\ +If you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save \ +this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the \ +transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\n\ +If you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently \ +in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the \ +lower-right corner of the box containing the transaction. You must save this information. \n\n\ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ +2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR \ +transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ +If you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +To send Solo you must use the Solo Network CLI Wallet. \n\n\ +If you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save \ +this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the \ +transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\n\ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ +2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo \ +transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\n\ +failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ +If you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +To send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\n\ +After a transaction is sent, the transaction ID will be displayed. You must save this information. \ +Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the \ +transaction secret key. \n\n\ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ +2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 \ +transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ +If you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +To send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\n\ +After a transaction is sent, the transaction ID will be displayed. You must save this information. \ +Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the \ +transaction secret key. \n\n\ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ +2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC \ +transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ +If you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +Because of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you \ +can prove your payment through the use of your TXN-Private-Key.\n\ +The TXN-Private Key is a one-time key automatically generated for every transaction that can \ +only be accessed from within your DRGL wallet.\n\ +Either by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\n\ +DRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\n\ +In case of a dispute, you must provide the mediator or arbitrator the following data:\n\ +- The TXN-Private key\n\ +- The transaction hash\n\ +- The recipient's public address\n\n\ +Verification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\n\ +Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ +dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the \ +mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\n\ +If you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not \ +the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not \ +the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the \ + transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN \ + (the receiver needs to be online or at least be online during a certain time frame). \n\n\ + Bisq supports only the Grinbox (Wallet713) wallet URL format. \n\n\ + The GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a \ + potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the \ + latest Grinbox software which supports the transaction proof and that you understand the process of transferring and \ + receiving GRIN as well as how to create the proof. \n\n\ + See https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more \ + information about the Grinbox proof tool. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the \ + transaction. \n\n\ + Be sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM \ + (the receiver needs to be online or at least be online during a certain time frame). \n\n\ + The BEAM sender is required to provide proof that they sent BEAM successfully. \ + Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential \ + dispute will be resolved in favor of the BEAM receiver. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill \ +the following requirements:\n\n\ +To send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\n\ +You can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) \ +You need to right Click on the Transaction and then click on show details. \n\n\ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, \ +2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS \ +transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ +If you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\n\ +Burnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: \ +OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. \ +For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\n\ +OP_RETURN OP_PUSHDATA 666f6f\n\n\ +To create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\n\ +For possible use cases, one may look at https://ibo.laboratorium.ee .\n\n\ +As burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means \ +burning ordinary blackcoins (with associated data equal to the destination address).\n\n\ +In case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Trading L-BTC on Bisq requires that you understand the following:\n\n\ +When receiving L-BTC for a trade on Bisq, you cannot use the mobile Blockstream Green Wallet app or a \ +custodial/exchange wallet. You must only receive L-BTC into the Liquid Elements Core wallet, or another \ +L-BTC wallet which allows you to obtain the blinding key for your blinded L-BTC address.\n\n\ +In the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for \ +your receiving L-BTC address to the Bisq mediator or refund agent so they can verify the details of \ +your Confidential Transaction on their own Elements Core full node.\n\n\ +Failure to provide the required information to the mediator or refund agent will result in losing the \ +dispute case. In all cases of dispute, the L-BTC receiver bears 100% of the burden of responsibility in \ +providing cryptographic proof to the mediator or refund agent.\n\n\ +If you do not understand these requirements, do not trade L-BTC on Bisq. + +account.fiat.yourFiatAccounts=Your national currency accounts + +account.backup.title=Backup wallet +account.backup.location=Backup location +account.backup.selectLocation=Select backup location +account.backup.backupNow=Backup now (backup is not encrypted!) +account.backup.appDir=Application data directory +account.backup.openDirectory=Open directory +account.backup.openLogFile=Open Log file +account.backup.success=Backup successfully saved at:\n{0} +account.backup.directoryNotAccessible=The directory you have chosen is not accessible. {0} + +account.password.removePw.button=Remove password +account.password.removePw.headline=Remove password protection for wallet +account.password.setPw.button=Set password +account.password.setPw.headline=Set password protection for wallet +account.password.info=With password protection you'll need to enter your password at application startup, when withdrawing bitcoin \ +out of your wallet, and when restoring your wallet from seed words. + +account.seed.backup.title=Backup your wallets seed words +account.seed.info=Please write down both wallet seed words and the date! \ +You can recover your wallet any time with seed words and the date.\n\ +The same seed words are used for the BTC and BSQ wallet.\n\n\ +You should write down the seed words on a sheet of paper. Do not save them on your computer.\n\n\ +Please note that the seed words are NOT a replacement for a backup.\n\ +You need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\n\ +Importing seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\n\ +You need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\n\ +Importing seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\n\ +See the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=You have not setup a wallet password which would protect the display of the seed words.\n\n\ +Do you want to display the seed words? +account.seed.warn.noPw.yes=Yes, and don't ask me again +account.seed.enterPw=Enter password to view seed words +account.seed.restore.info=Please make a backup before applying restore from seed words. Be aware that wallet restore is \ + only for emergency cases and might cause problems with the internal wallet database.\n\ + It is not a way for applying a backup! Please use a backup from the application data directory for restoring a \ + previous application state.\n\n\ + After restoring the application will shut down automatically. After you have restarted the application it will resync \ + with the Bitcoin network. This can take a while and can consume a lot of CPU, especially if the wallet was older and \ + had many transactions. Please avoid interrupting that process, otherwise you might need to delete the SPV chain file \ + again or repeat the restore process. +account.seed.restore.ok=Ok, do the restore and shut down Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Setup +account.notifications.download.label=Download mobile app +account.notifications.waitingForWebCam=Waiting for webcam... +account.notifications.webCamWindow.headline=Scan QR-code from phone +account.notifications.webcam.label=Use webcam +account.notifications.webcam.button=Scan QR code +account.notifications.noWebcam.button=I don't have a webcam +account.notifications.erase.label=Clear notifications on phone +account.notifications.erase.title=Clear notifications +account.notifications.email.label=Pairing token +account.notifications.email.prompt=Enter pairing token you received by email +account.notifications.settings.title=Settings +account.notifications.useSound.label=Play notification sound on phone +account.notifications.trade.label=Receive trade messages +account.notifications.market.label=Receive offer alerts +account.notifications.price.label=Receive price alerts +account.notifications.priceAlert.title=Price alerts +account.notifications.priceAlert.high.label=Notify if BTC price is above +account.notifications.priceAlert.low.label=Notify if BTC price is below +account.notifications.priceAlert.setButton=Set price alert +account.notifications.priceAlert.removeButton=Remove price alert +account.notifications.trade.message.title=Trade state changed +account.notifications.trade.message.msg.conf=The deposit transaction for the trade with ID {0} is confirmed. \ + Please open your Bisq application and start the payment. +account.notifications.trade.message.msg.started=The BTC buyer has started the payment for the trade with ID {0}. +account.notifications.trade.message.msg.completed=The trade with ID {0} is completed. +account.notifications.offer.message.title=Your offer was taken +account.notifications.offer.message.msg=Your offer with ID {0} was taken +account.notifications.dispute.message.title=New dispute message +account.notifications.dispute.message.msg=You received a dispute message for trade with ID {0} + +account.notifications.marketAlert.title=Offer alerts +account.notifications.marketAlert.selectPaymentAccount=Offers matching payment account +account.notifications.marketAlert.offerType.label=Offer type I am interested in +account.notifications.marketAlert.offerType.buy=Buy offers (I want to sell BTC) +account.notifications.marketAlert.offerType.sell=Sell offers (I want to buy BTC) +account.notifications.marketAlert.trigger=Offer price distance (%) +account.notifications.marketAlert.trigger.info=With a price distance set, you will only receive an alert when an offer \ + that meets (or exceeds) your requirements is published. Example: you want to sell BTC, but you will only sell at \ + a 2% premium to the current market price. Setting this field to 2% will ensure you only receive alerts for offers \ + with prices that are 2% (or more) above the current market price. +account.notifications.marketAlert.trigger.prompt=Percentage distance from market price (e.g. 2.50%, -0.50%, etc) +account.notifications.marketAlert.addButton=Add offer alert +account.notifications.marketAlert.manageAlertsButton=Manage offer alerts +account.notifications.marketAlert.manageAlerts.title=Manage offer alerts +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Payment account +account.notifications.marketAlert.manageAlerts.header.trigger=Trigger price +account.notifications.marketAlert.manageAlerts.header.offerType=Offer type +account.notifications.marketAlert.message.title=Offer alert +account.notifications.marketAlert.message.msg.below=below +account.notifications.marketAlert.message.msg.above=above +account.notifications.marketAlert.message.msg=A new ''{0} {1}'' offer with price {2} ({3} {4} market price) and \ + payment method ''{5}'' was published to the Bisq offerbook.\n\ + Offer ID: {6}. +account.notifications.priceAlert.message.title=Price alert for {0} +account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2} +account.notifications.noWebCamFound.warning=No webcam found.\n\n\ + Please use the email option to send the token and encryption key from your mobile phone to the Bisq application. +account.notifications.priceAlert.warning.highPriceTooLow=The higher price must be larger than the lower price. +account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must be lower than the higher price. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Facts & Figures +dao.tab.bsqWallet=BSQ wallet +dao.tab.proposals=Governance +dao.tab.bonding=Bonding +dao.tab.proofOfBurn=Asset listing fee/Proof of burn +dao.tab.monitor=Network monitor +dao.tab.news=News + +dao.paidWithBsq=paid with BSQ +dao.availableBsqBalance=Available for spending (verified + unconfirmed change outputs) +dao.verifiedBsqBalance=Balance of all verified UTXOs +dao.unconfirmedChangeBalance=Balance of all unconfirmed change outputs +dao.unverifiedBsqBalance=Balance of all unverified transactions (awaiting block confirmation) +dao.lockedForVoteBalance=Used for voting +dao.lockedInBonds=Locked in bonds +dao.availableNonBsqBalance=Available non-BSQ balance (BTC) +dao.reputationBalance=Merit Value (not spendable) + +dao.tx.published.success=Your transaction has been successfully published. +dao.proposal.menuItem.make=Make proposal +dao.proposal.menuItem.browse=Browse open proposals +dao.proposal.menuItem.vote=Vote on proposals +dao.proposal.menuItem.result=Vote results +dao.cycle.headline=Voting cycle +dao.cycle.overview.headline=Voting cycle overview +dao.cycle.currentPhase=Current phase +dao.cycle.currentBlockHeight=Current block height +dao.cycle.proposal=Proposal phase +dao.cycle.proposal.next=Next proposal phase +dao.cycle.blindVote=Blind vote phase +dao.cycle.voteReveal=Vote reveal phase +dao.cycle.voteResult=Vote result +dao.cycle.phaseDuration={0} blocks (≈{1}); Block {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Block {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Vote reveal transaction published +dao.voteReveal.txPublished=Your vote reveal transaction with transaction ID {0} was successfully published.\n\n\ + This happens automatically by the software if you have participated in the DAO voting. + +dao.results.cycles.header=Cycles +dao.results.cycles.table.header.cycle=Cycle +dao.results.cycles.table.header.numProposals=Proposals +dao.results.cycles.table.header.voteWeight=Vote weight +dao.results.cycles.table.header.issuance=Issuance + +dao.results.results.table.item.cycle=Cycle {0} started: {1} + +dao.results.proposals.header=Proposals of selected cycle +dao.results.proposals.table.header.nameLink=Name/link +dao.results.proposals.table.header.details=Details +dao.results.proposals.table.header.myVote=My vote +dao.results.proposals.table.header.result=Vote result +dao.results.proposals.table.header.threshold=Threshold +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Vote results for selected proposal + +dao.results.exceptions=Vote result exception(s) + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Undefined + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQ maker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQ taker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Min. BSQ maker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Min. BSQ taker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTC maker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTC taker fee +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Min. BTC maker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Min. BTC taker fee +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Proposal fee in BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Voting fee in BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Compensation request min. BSQ amount +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Compensation request max. BSQ amount +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Reimbursement request min. BSQ amount +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Reimbursement request max. BSQ amount + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Required quorum in BSQ for generic proposal +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Required quorum in BSQ for compensation request +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Required quorum in BSQ for reimbursement request +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Required quorum in BSQ for changing a parameter +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Required quorum in BSQ for removing an asset +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Required quorum in BSQ for a confiscation request +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Required quorum in BSQ for bonded role requests + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Required threshold in % for generic proposal +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Required threshold in % for compensation request +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Required threshold in % for reimbursement request +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Required threshold in % for changing a parameter +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Required threshold in % for removing an asset +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Required threshold in % for a confiscation request +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Required threshold in % for bonded role requests + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Recipient BTC address + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Asset listing fee per day +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Min. trade volume for assets + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Lock time for alternative trade payout tx +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Arbitrator fee in BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Max. trade limit in BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Bonded role unit factor in BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Issuance limit per cycle in BSQ + +dao.param.currentValue=Current value: {0} +dao.param.currentAndPastValue=Current value: {0} (Value when proposal was made: {1}) +dao.param.blocks={0} blocks + +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was \ + not distributed well in the Bisq network.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Proposal phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Break 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Blind vote phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Break 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Vote reveal phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Break 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Result phase + +dao.results.votes.table.header.stakeAndMerit=Vote weight +dao.results.votes.table.header.stake=Stake +dao.results.votes.table.header.merit=Earned +dao.results.votes.table.header.vote=Vote + +dao.bond.menuItem.bondedRoles=Bonded roles +dao.bond.menuItem.reputation=Bonded reputation +dao.bond.menuItem.bonds=Bonds + +dao.bond.dashboard.bondsHeadline=Bonded BSQ +dao.bond.dashboard.lockupAmount=Lockup funds +dao.bond.dashboard.unlockingAmount=Unlocking funds (wait until lock time is over) + + +dao.bond.reputation.header=Lockup a bond for reputation +dao.bond.reputation.table.header=My reputation bonds +dao.bond.reputation.amount=Amount of BSQ to lockup +dao.bond.reputation.time=Unlock time in blocks +dao.bond.reputation.salt=Salt +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Lockup +dao.bond.reputation.lockup.headline=Confirm lockup transaction +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\n\ + Mining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Confirm unlock transaction +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\n\ + Mining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=All bonds + +dao.bond.bondedReputation=Bonded Reputation +dao.bond.bondedRoles=Bonded roles + +dao.bond.details.header=Role details +dao.bond.details.role=Role +dao.bond.details.requiredBond=Required BSQ bond +dao.bond.details.unlockTime=Unlock time in blocks +dao.bond.details.link=Link to role description +dao.bond.details.isSingleton=Can be taken by multiple role holders +dao.bond.details.blocks={0} blocks + +dao.bond.table.column.name=Name +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Bond type +dao.bond.table.column.details=Details +dao.bond.table.column.lockupTxId=Lockup Tx ID +dao.bond.table.column.bondState=Bond state +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=Lockup date + +dao.bond.table.button.lockup=Lockup +dao.bond.table.button.unlock=Unlock +dao.bond.table.button.revoke=Revoke + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Not bonded yet +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Lockup pending +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Bond locked up +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Unlock pending +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Unlock tx confirmed +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Bond unlocking +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Bond unlocked +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Bond confiscated + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Bonded role +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Bonded reputation + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=GitHub admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Forum admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-fork maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Website operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Forum operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Seed node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Price node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Mobile notifications relay operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Domain name holder +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Arbitrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC donation address owner + +dao.burnBsq.assetFee=Asset listing +dao.burnBsq.menuItem.assetFee=Asset listing fee +dao.burnBsq.menuItem.proofOfBurn=Proof of burn +dao.burnBsq.header=Fee for asset listing +dao.burnBsq.selectAsset=Select Asset +dao.burnBsq.fee=Fee +dao.burnBsq.trialPeriod=Trial period +dao.burnBsq.payFee=Pay fee +dao.burnBsq.allAssets=All assets +dao.burnBsq.assets.nameAndCode=Asset name +dao.burnBsq.assets.state=State +dao.burnBsq.assets.tradeVolume=Trade volume +dao.burnBsq.assets.lookBackPeriod=Verification period +dao.burnBsq.assets.trialFee=Fee for trial period +dao.burnBsq.assets.totalFee=Total fees paid +dao.burnBsq.assets.days={0} days +dao.burnBsq.assets.toFewDays=The asset fee is too low. The min. amount of days for the trial period is {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=In trial period +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Actively traded +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=De-listed due to inactivity +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Removed by voting + +dao.proofOfBurn.header=Proof of burn +dao.proofOfBurn.amount=Amount +dao.proofOfBurn.preImage=Pre-image +dao.proofOfBurn.burn=Burn +dao.proofOfBurn.allTxs=All proof of burn transactions +dao.proofOfBurn.myItems=My proof of burn transactions +dao.proofOfBurn.date=Date +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transactions +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Sign a message with key from proof of burn transaction +dao.proofOfBurn.verify.window.title=Verify a message with key from proof of burn transaction +dao.proofOfBurn.copySig=Copy signature to clipboard +dao.proofOfBurn.sign=Sign +dao.proofOfBurn.message=Message +dao.proofOfBurn.sig=Signature +dao.proofOfBurn.verify=Verify +dao.proofOfBurn.verificationResult.ok=Verification succeeded +dao.proofOfBurn.verificationResult.failed=Verification failed + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Proposal phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Break before blind vote phase +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Blind vote phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Break before vote reveal phase +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Vote reveal phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Break before result phase +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Vote result phase + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Proposal phase +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Blind vote +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Vote reveal +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Vote result + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Compensation request +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Reimbursement request +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Proposal for a bonded role +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Proposal for removing an asset +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Proposal for changing a parameter +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Generic proposal +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Proposal for confiscating a bond + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Compensation request +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Reimbursement request +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Bonded role +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Removing an altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Changing a parameter +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Generic proposal +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscating a bond + +dao.proposal.details=Proposal details +dao.proposal.selectedProposal=Selected proposal +dao.proposal.active.header=Proposals of current cycle +dao.proposal.active.remove.confirm=Are you sure you want to remove that proposal?\n\ + The already paid proposal fee will be lost. +dao.proposal.active.remove.doRemove=Yes, remove my proposal +dao.proposal.active.remove.failed=Could not remove proposal. +dao.proposal.myVote.title=Voting +dao.proposal.myVote.accept=Accept proposal +dao.proposal.myVote.reject=Reject proposal +dao.proposal.myVote.removeMyVote=Ignore proposal +dao.proposal.myVote.merit=Vote weight from earned BSQ +dao.proposal.myVote.stake=Vote weight from stake +dao.proposal.myVote.revealTxId=Vote reveal transaction ID +dao.proposal.myVote.stake.prompt=Max. available stake for voting: {0} +dao.proposal.votes.header=Set stake for voting and publish your votes +dao.proposal.myVote.button=Publish votes +dao.proposal.myVote.setStake.description=After voting on all proposals you have to set your stake for voting by locking up \ + BSQ. The more BSQ you lock up, the more weight your vote will have. \n\n\ + BSQ locked up for voting will be unlocked again during the vote reveal phase. +dao.proposal.create.selectProposalType=Select proposal type +dao.proposal.create.phase.inactive=Please wait until the next proposal phase +dao.proposal.create.proposalType=Proposal type +dao.proposal.create.new=Make new proposal +dao.proposal.create.button=Make proposal +dao.proposal.create.publish=Publish proposal +dao.proposal.create.publishing=Proposal publishing is in progress ... +dao.proposal=proposal +dao.proposal.display.type=Proposal type +dao.proposal.display.name=Exact GitHub username +dao.proposal.display.link=Link to detailed info +dao.proposal.display.link.prompt=Link to proposal +dao.proposal.display.requestedBsq=Requested amount in BSQ +dao.proposal.display.txId=Proposal transaction ID +dao.proposal.display.proposalFee=Proposal fee +dao.proposal.display.myVote=My vote +dao.proposal.display.voteResult=Vote result summary +dao.proposal.display.bondedRoleComboBox.label=Bonded role type +dao.proposal.display.requiredBondForRole.label=Required bond for role +dao.proposal.display.option=Option + +dao.proposal.table.header.proposalType=Proposal type +dao.proposal.table.header.link=Link +dao.proposal.table.header.myVote=My vote +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Remove +dao.proposal.table.icon.tooltip.removeProposal=Remove my proposal +dao.proposal.table.icon.tooltip.changeVote=Current vote: ''{0}''. Change vote to: ''{1}'' + +dao.proposal.display.myVote.accepted=Accepted +dao.proposal.display.myVote.rejected=Rejected +dao.proposal.display.myVote.ignored=Ignored +dao.proposal.display.myVote.unCounted=Vote was not included in result +dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Vote was invalid + +dao.proposal.voteResult.success=Accepted +dao.proposal.voteResult.failed=Rejected +dao.proposal.voteResult.summary=Result: {0}; Threshold: {1} (required > {2}); Quorum: {3} (required > {4}) + +dao.proposal.display.paramComboBox.label=Select parameter to change +dao.proposal.display.paramValue=Parameter value + +dao.proposal.display.confiscateBondComboBox.label=Choose bond +dao.proposal.display.assetComboBox.label=Asset to remove + +dao.blindVote=blind vote + +dao.blindVote.startPublishing=Publishing blind vote transaction... +dao.blindVote.success=Your blind vote transaction has been successfully published.\n\nPlease note, that you have to be \ + online in the vote reveal phase so that your Bisq application can publish the vote reveal transaction. \ + Without the vote reveal transaction your vote would be invalid! + +dao.wallet.menuItem.send=Send +dao.wallet.menuItem.receive=Receive +dao.wallet.menuItem.transactions=Transactions + +dao.wallet.dashboard.myBalance=My wallet balance + +dao.wallet.receive.fundYourWallet=Your BSQ receive address +dao.wallet.receive.bsqAddress=BSQ wallet address (Fresh unused address) + +dao.wallet.send.sendFunds=Send funds +dao.wallet.send.sendBtcFunds=Send non-BSQ funds (BTC) +dao.wallet.send.amount=Amount in BSQ +dao.wallet.send.btcAmount=Amount in BTC (non-BSQ funds) +dao.wallet.send.setAmount=Set amount to withdraw (min. amount is {0}) +dao.wallet.send.receiverAddress=Receiver's BSQ address +dao.wallet.send.receiverBtcAddress=Receiver's BTC address +dao.wallet.send.setDestinationAddress=Fill in your destination address +dao.wallet.send.send=Send BSQ funds +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Send BTC funds +dao.wallet.send.sendFunds.headline=Confirm withdrawal request +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Latest verified block: {0} +dao.wallet.chainHeightSyncing=Awaiting blocks... Verified {0} blocks out of {1} +dao.wallet.tx.type=Type + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Not recognized +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Unverified BSQ transaction +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Invalid BSQ transaction +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Genesis transaction +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Transfer BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=Received BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=Sent BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Trading fee +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Fee for compensation request +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Fee for reimbursement request +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Fee for proposal +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Fee for blind vote +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Vote reveal +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Lock up bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Unlock bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Asset listing fee +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Proof of burn +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregular + +dao.tx.withdrawnFromWallet=BTC withdrawn from wallet +dao.tx.issuanceFromCompReq=Compensation request/issuance +dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\n\ + Issuance date: {0} +dao.tx.issuanceFromReimbursement=Reimbursement request/issuance +dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\n\ + Issuance date: {0} +dao.proposal.create.missingBsqFunds=You don''t have sufficient BSQ funds for creating the proposal. If you have an \ + unconfirmed BSQ transaction you need to wait for a blockchain confirmation because BSQ is validated only if it is \ + included in a block.\n\ + Missing: {0} + +dao.proposal.create.missingBsqFundsForBond=You don''t have sufficient BSQ funds for this role. You can still \ + publish this proposal, but you''ll need the full BSQ amount required for this role if it gets accepted. \n\ + Missing: {0} + +dao.proposal.create.missingMinerFeeFunds=You don''t have sufficient BTC funds for creating the proposal transaction. \ + All BSQ transactions require a miner fee in BTC.\n\ + Missing: {0} + +dao.proposal.create.missingIssuanceFunds=You don''t have sufficient BTC funds for creating the proposal transaction. \ + All BSQ transactions require a miner fee in BTC, and issuance transactions also require BTC for the requested BSQ \ + amount ({0} Satoshis/BSQ).\n\ + Missing: {1} + +dao.feeTx.confirm=Confirm {0} transaction +dao.feeTx.confirm.details={0} fee: {1}\n\ + Mining fee: {2} ({3} Satoshis/vbyte)\n\ + Transaction vsize: {4} vKb\n\n\ + Are you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\n\ + BTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\n\ + Mining fee: {4} ({5} Satoshis/vbyte)\n\ + Transaction vsize: {6} vKb\n\n\ + If your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\n\ + Are you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=THE BISQ DAO +dao.news.bisqDAO.description=Just as the Bisq exchange is decentralized and censorship-resistant, so is its \ + governance model - and the Bisq DAO and BSQ token are the tools that make it possible. +dao.news.bisqDAO.readMoreLink=Learn More About the Bisq DAO + +dao.news.pastContribution.title=MADE PAST CONTRIBUTIONS? REQUEST BSQ +dao.news.pastContribution.description=If you have contributed to Bisq please use the BSQ address below and make a \ + request for taking part of the BSQ genesis distribution. +dao.news.pastContribution.yourAddress=Your BSQ Wallet Address +dao.news.pastContribution.requestNow=Request now + +dao.news.DAOOnTestnet.title=RUN THE BISQ DAO ON OUR TESTNET +dao.news.DAOOnTestnet.description=The mainnet Bisq DAO is not launched yet but you can learn about the Bisq DAO \ + by running it on our testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Switch to DAO Testnet Mode +dao.news.DAOOnTestnet.firstSection.content=Switch to DAO Testnet from the Settings screen. +dao.news.DAOOnTestnet.secondSection.title=2. Acquire Some BSQ +dao.news.DAOOnTestnet.secondSection.content=Request BSQ on Slack or Buy BSQ on Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Participate in a Voting Cycle +dao.news.DAOOnTestnet.thirdSection.content=Making proposals and voting on proposals to change various aspects of Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Explore a BSQ Block Explorer +dao.news.DAOOnTestnet.fourthSection.content=Since BSQ is just bitcoin, you can see BSQ transactions on our bitcoin block explorer. +dao.news.DAOOnTestnet.readMoreLink=Read the full documentation + +dao.monitor.daoState=DAO state +dao.monitor.proposals=Proposals state +dao.monitor.blindVotes=Blind votes state + +dao.monitor.table.peers=Peers +dao.monitor.table.conflicts=Conflicts +dao.monitor.state=Status +dao.monitor.requestAlHashes=Request all hashes +dao.monitor.resync=Resync DAO state +dao.monitor.table.header.cycleBlockHeight=Cycle / block height +dao.monitor.table.cycleBlockHeight=Cycle {0} / block {1} +dao.monitor.table.seedPeers=Seed node: {0} + +dao.monitor.daoState.headline=DAO state +dao.monitor.daoState.table.headline=Chain of DAO state hashes +dao.monitor.daoState.table.blockHeight=Block height +dao.monitor.daoState.table.hash=Hash of DAO state +dao.monitor.daoState.table.prev=Previous hash +dao.monitor.daoState.conflictTable.headline=DAO state hashes from peers in conflict +dao.monitor.daoState.utxoConflicts=UTXO conflicts +dao.monitor.daoState.utxoConflicts.blockHeight=Block height: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Sum of all UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Sum of all BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. \ + After restart the DAO state will resync. + +dao.monitor.proposal.headline=Proposals state +dao.monitor.proposal.table.headline=Chain of proposal state hashes +dao.monitor.proposal.conflictTable.headline=Proposal state hashes from peers in conflict + +dao.monitor.proposal.table.hash=Hash of proposal state +dao.monitor.proposal.table.prev=Previous hash +dao.monitor.proposal.table.numProposals=No. proposals + +dao.monitor.isInConflictWithSeedNode=Your local data is not in consensus with at least one seed node. \ + Please resync the DAO state. +dao.monitor.isInConflictWithNonSeedNode=One of your peers is not in consensus with the network but your node \ + is in sync with the seed nodes. +dao.monitor.daoStateInSync=Your local node is in consensus with the network + +dao.monitor.blindVote.headline=Blind votes state +dao.monitor.blindVote.table.headline=Chain of blind vote state hashes +dao.monitor.blindVote.conflictTable.headline=Blind vote state hashes from peers in conflict +dao.monitor.blindVote.table.hash=Hash of blind vote state +dao.monitor.blindVote.table.prev=Previous hash +dao.monitor.blindVote.table.numBlindVotes=No. blind votes + +dao.factsAndFigures.menuItem.supply=BSQ Supply +dao.factsAndFigures.menuItem.transactions=BSQ Transactions + +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=Total available BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt + +dao.factsAndFigures.supply.issued=BSQ issued +dao.factsAndFigures.supply.compReq=Compensation requests +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ issued at genesis transaction +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation requests +dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ burnt + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Trade volume +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Global state of locked BSQ +dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds +dao.factsAndFigures.supply.totalUnlockingAmount=Unlocking BSQ from bonds +dao.factsAndFigures.supply.totalUnlockedAmount=Unlocked BSQ from bonds +dao.factsAndFigures.supply.totalConfiscatedAmount=Confiscated BSQ from bonds +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Genesis transaction +dao.factsAndFigures.transactions.genesisBlockHeight=Genesis block height +dao.factsAndFigures.transactions.genesisTxId=Genesis transaction ID +dao.factsAndFigures.transactions.txDetails=BSQ transactions statistics +dao.factsAndFigures.transactions.allTx=No. of all BSQ transactions +dao.factsAndFigures.transactions.utxo=No. of all unspent transaction outputs +dao.factsAndFigures.transactions.compensationIssuanceTx=No. of all compensation request issuance transactions +dao.factsAndFigures.transactions.reimbursementIssuanceTx=No. of all reimbursement request issuance transactions +dao.factsAndFigures.transactions.burntTx=No. of all fee payments transactions +dao.factsAndFigures.transactions.invalidTx=No. of all invalid transactions +dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Available balance + +contractWindow.title=Dispute details +contractWindow.dates=Offer date / Trade date +contractWindow.btcAddresses=Bitcoin address BTC buyer / BTC seller +contractWindow.onions=Network address BTC buyer / BTC seller +contractWindow.accountAge=Account age BTC buyer / BTC seller +contractWindow.numDisputes=No. of disputes BTC buyer / BTC seller +contractWindow.contractHash=Contract hash + +displayAlertMessageWindow.headline=Important information! +displayAlertMessageWindow.update.headline=Important update information! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=Files: +displayUpdateDownloadWindow.downloadingFile=Downloading: {0} +displayUpdateDownloadWindow.verifiedSigs=Signature verified with keys: +displayUpdateDownloadWindow.status.downloading=Downloading files... +displayUpdateDownloadWindow.status.verifying=Verifying signature... +displayUpdateDownloadWindow.button.label=Download installer and verify signature +displayUpdateDownloadWindow.button.downloadLater=Download later +displayUpdateDownloadWindow.button.ignoreDownload=Ignore this version +displayUpdateDownloadWindow.headline=A new Bisq update is available! +displayUpdateDownloadWindow.download.failed.headline=Download failed +displayUpdateDownloadWindow.download.failed=Download failed.\n\ + Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at \ + [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\n\ + Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=The new version has been successfully downloaded and the signature verified.\n\n\ +Please open the download directory, shut down the application and install the new version. +displayUpdateDownloadWindow.download.openDir=Open download directory + +disputeSummaryWindow.title=Summary +disputeSummaryWindow.openDate=Ticket opening date +disputeSummaryWindow.role=Trader's role +disputeSummaryWindow.payout=Trade amount payout +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} gets trade amount payout +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Custom payout +disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount +disputeSummaryWindow.payoutAmount.seller=Seller's payout amount +disputeSummaryWindow.payoutAmount.invert=Use loser as publisher +disputeSummaryWindow.reason=Reason of dispute +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usability +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protocol violation +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=No reply +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Other +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Bank +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Summary notes +disputeSummaryWindow.addSummaryNotes=Add summary notes +disputeSummaryWindow.close.button=Close ticket + +# 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 closed on {0}\n\ + {1} node address: {2}\n\n\ + Summary:\n\ + Trade ID: {3}\n\ + Currency: {4}\n\ + Trade amount: {5}\n\ + Payout amount for BTC buyer: {6}\n\ + Payout amount for BTC seller: {7}\n\n\ + Reason for dispute: {8}\n\n\ + Summary notes:\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=\nNext steps:\n\ +Open trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ +No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n\ + {1}{2}\ + Transaction fee: {3} ({4} satoshis/vbyte)\n\ + Transaction vsize: {5} vKb\n\n\ + Are you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline={0} emergency wallet tool +emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ +Please note that all open offers will be closed automatically when using this tool.\n\n\ +Before you use this tool, please backup your data directory. \ +You can do this at \"Account/Backup\".\n\n\ +Please report us your problem and file a bug report on GitHub or at the Bisq forum so that we can investigate what was causing the problem. +emptyWalletWindow.balance=Your available wallet balance +emptyWalletWindow.bsq.btcBalance=Balance of non-BSQ Satoshis + +emptyWalletWindow.address=Your destination address +emptyWalletWindow.button=Send all funds +emptyWalletWindow.openOffers.warn=You have open offers which will be removed if you empty the wallet.\nAre you sure that you want to empty your wallet? +emptyWalletWindow.openOffers.yes=Yes, I am sure +emptyWalletWindow.sent.success=The balance of your wallet was successfully transferred. + +enterPrivKeyWindow.headline=Enter private key for registration + +filterWindow.headline=Edit filter list +filterWindow.offers=Filtered offers (comma sep.) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] +filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) +filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) +filterWindow.seedNode=Filtered seed nodes (comma sep. onion addresses) +filterWindow.priceRelayNode=Filtered price relay nodes (comma sep. onion addresses) +filterWindow.btcNode=Filtered Bitcoin nodes (comma sep. addresses + port) +filterWindow.preventPublicBtcNetwork=Prevent usage of public Bitcoin network +filterWindow.disableDao=Disable DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Min. version required for DAO +filterWindow.disableTradeBelowVersion=Min. version required for trading +filterWindow.add=Add filter +filterWindow.remove=Remove filter +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Min. BTC amount +offerDetailsWindow.min=(min. {0}) +offerDetailsWindow.distance=(distance from market price: {0}) +offerDetailsWindow.myTradingAccount=My trading account +offerDetailsWindow.bankId=Bank ID (e.g. BIC or SWIFT) +offerDetailsWindow.countryBank=Maker's country of bank +offerDetailsWindow.commitment=Commitment +offerDetailsWindow.agree=I agree +offerDetailsWindow.tac=Terms and conditions +offerDetailsWindow.confirm.maker=Confirm: Place offer to {0} bitcoin +offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} bitcoin +offerDetailsWindow.creationDate=Creation date +offerDetailsWindow.makersOnion=Maker's onion address + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Payment request:\n{0} + +selectDepositTxWindow.headline=Select deposit transaction for dispute +selectDepositTxWindow.msg=The deposit transaction was not stored in the trade.\n\ +Please select one of the existing multisig transactions from your wallet which was the \ +deposit transaction used in the failed trade.\n\n\ +You can find the correct transaction by opening the trade details window (click on the trade ID in the list)\ + and following the trading fee payment transaction output to the next transaction where you see \ +the multisig deposit transaction (the address starts with 3). That transaction ID should be \ +visible in the list presented here. Once you found the correct transaction select that transaction here and continue.\n\n\ +Sorry for the inconvenience but that error case should happen very rarely and in future we will try \ +to find better ways to resolve it. +selectDepositTxWindow.select=Select deposit transaction + +sendAlertMessageWindow.headline=Send global notification +sendAlertMessageWindow.alertMsg=Alert message +sendAlertMessageWindow.enterMsg=Enter message +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=New version no. +sendAlertMessageWindow.send=Send notification +sendAlertMessageWindow.remove=Remove notification + +sendPrivateNotificationWindow.headline=Send private message +sendPrivateNotificationWindow.privateNotification=Private notification +sendPrivateNotificationWindow.enterNotification=Enter notification +sendPrivateNotificationWindow.send=Send private notification + +showWalletDataWindow.walletData=Wallet data +showWalletDataWindow.includePrivKeys=Include private keys + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=User agreement +tacWindow.agree=I agree +tacWindow.disagree=I disagree and quit +tacWindow.arbitrationSystem=Dispute resolution + +tradeDetailsWindow.headline=Trade +tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID: +tradeDetailsWindow.tradeDate=Trade date +tradeDetailsWindow.txFee=Mining fee +tradeDetailsWindow.tradingPeersOnion=Trading peers onion address +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Trade state +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. \ + BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Enter password to unlock + +torNetworkSettingWindow.header=Tor networks settings +torNetworkSettingWindow.noBridges=Don't use bridges +torNetworkSettingWindow.providedBridges=Connect with provided bridges +torNetworkSettingWindow.customBridges=Enter custom bridges +torNetworkSettingWindow.transportType=Transport type +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (recommended) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Enter one or more bridge relays (one per line) +torNetworkSettingWindow.enterBridgePrompt=type address:port +torNetworkSettingWindow.restartInfo=You need to restart to apply the changes +torNetworkSettingWindow.openTorWebPage=Open Tor project web page +torNetworkSettingWindow.deleteFiles.header=Connection problems? +torNetworkSettingWindow.deleteFiles.info=If you have repeated connection problems at start up, deleting outdated Tor files might help. To do that click the button below and restart afterwards. +torNetworkSettingWindow.deleteFiles.button=Delete outdated Tor files and shut down +torNetworkSettingWindow.deleteFiles.progress=Shut down Tor in progress +torNetworkSettingWindow.deleteFiles.success=Outdated Tor files deleted successfully. Please restart. +torNetworkSettingWindow.bridges.header=Is Tor blocked? +torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider or by your country you can try to use Tor bridges.\n\ + Visit the Tor web page at: https://bridges.torproject.org/bridges to learn more about \ + bridges and pluggable transports. + +feeOptionWindow.headline=Choose currency for trade fee payment +feeOptionWindow.info=You can choose to pay the trade fee in BSQ or in BTC. If you choose BSQ you appreciate the discounted trade fee. +feeOptionWindow.optionsLabel=Choose currency for trade fee payment +feeOptionWindow.useBTC=Use BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notification +popup.headline.instruction=Please note: +popup.headline.attention=Attention +popup.headline.backgroundInfo=Background information +popup.headline.feedback=Completed +popup.headline.confirmation=Confirmation +popup.headline.information=Information +popup.headline.warning=Warning +popup.headline.error=Error + +popup.doNotShowAgain=Don't show again +popup.reportError.log=Open log file +popup.reportError.gitHub=Report to GitHub issue tracker +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.\n\ +The above error message will be copied to the clipboard when you click either of the buttons below.\n\ +It will make debugging easier if you include the bisq.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. + +popup.error.tryRestart=Please try to restart your application and check your network connection to see if you can resolve the issue. +popup.error.takeOfferRequestFailed=An error occurred when someone tried to take one of your offers:\n{0} + +error.spvFileCorrupted=An error occurred when reading the SPV chain file.\nIt might be that the SPV chain file is corrupted.\n\nError message: {0}\n\nDo you want to delete it and start a resync? +error.deleteAddressEntryListFailed=Could not delete AddressEntryList file.\nError: {0} +error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still \ + unconfirmed.\n\n\ + Please do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. +error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\n\ + Please restart the application to clean up the closed trades list. + +popup.warning.walletNotInitialized=The wallet is not initialized yet +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application \ + (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\n\ + To avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> \ + 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\n\ + Bisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations \ + (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=You probably have the wrong Bisq version for this computer.\n\ +Your computer''s architecture is: {0}.\n\ +The Bisq binary you installed is: {1}.\n\ +Please shut down and re-install the correct version ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\n\ +Those database file(s) are not compatible with our current code base:\n{0}\n\n\ +We made a backup of the corrupted file(s) and applied the default values to a new database version.\n\n\ +The backup is located at:\n\ +{1}/db/backup_of_corrupted_data.\n\n\ +Please check if you have the latest version of Bisq installed.\n\ +You can download it at: [HYPERLINK:https://bisq.network/downloads].\n\n\ +Please restart the application. +popup.warning.startupFailed.twoInstances=Bisq is already running. You cannot run two instances of Bisq. +popup.warning.tradePeriod.halfReached=Your trade with ID {0} has reached the half of the max. allowed trading period and is still not completed.\n\nThe trade period ends on {1}\n\nPlease check your trade state at \"Portfolio/Open trades\" for further information. +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\n\ + The trade period ended on {1}\n\n\ + Please check your trade at \"Portfolio/Open trades\" for contacting the mediator. +popup.warning.noTradingAccountSetup.headline=You have not setup a trading account +popup.warning.noTradingAccountSetup.msg=You need to setup a national currency or altcoin account before you can create an offer.\nDo you want to setup an account? +popup.warning.noArbitratorsAvailable=There are no arbitrators available. +popup.warning.noMediatorsAvailable=There are no mediators available. +popup.warning.notFullyConnected=You need to wait until you are fully connected to the network.\nThat might take up to about 2 minutes at startup. +popup.warning.notSufficientConnectionsToBtcNetwork=You need to wait until you have at least {0} connections to the Bitcoin network. +popup.warning.downloadNotComplete=You need to wait until the download of missing Bitcoin blocks is complete. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\n\ + You can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Are you sure you want to remove that offer?\nThe maker fee of {0} will be lost if you remove that offer. +popup.warning.tooLargePercentageValue=You cannot set a percentage of 100% or larger. +popup.warning.examplePercentageValue=Please enter a percentage number like \"5.4\" for 5.4% +popup.warning.noPriceFeedAvailable=There is no price feed available for that currency. You cannot use a percent based price.\nPlease select the fixed price. +popup.warning.sendMsgFailed=Sending message to your trading partner failed.\nPlease try again and if it continue to fail report a bug. +popup.warning.insufficientBtcFundsForBsqTx=You don''t have sufficient BTC funds for paying the miner fee for that transaction.\n\ +Please fund your BTC wallet.\nMissing funds: {0} +popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust \ + limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\n\ + You need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your \ + sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\n\ + The dust output is {0}. +popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust \ + limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\n\ + You need to add the dust amount to your sending amount to avoid to generate a dust output.\n\n\ + The dust output is {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last \ + 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\n\ + You can either buy more BSQ or pay trade fees with BTC.\n\n\ + Missing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Your BSQ wallet does not have sufficient funds for paying the trade fee in BSQ. +popup.warning.messageTooLong=Your message exceeds the max. allowed size. Please send it in several parts or upload it to a service like https://pastebin.com. +popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\n\ + Locked up balance: {0} \n\ + Deposit tx address: {1}\n\ + Trade ID: {2}.\n\n\ + Please open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=price relay +popup.warning.seed=seed +popup.warning.mandatoryUpdate.trading=Please update to the latest Bisq version. \ + A mandatory update was released which disables trading for old versions. \ + Please check out the Bisq Forum for more information. +popup.warning.mandatoryUpdate.dao=Please update to the latest Bisq version. \ + A mandatory update was released which disables the Bisq DAO and BSQ for old versions. \ + Please check out the Bisq Forum for more information. +popup.warning.disable.dao=The Bisq DAO and BSQ are temporary disabled. \ + Please check out the Bisq Forum for more information. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 BTC to transfer. + +popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Bitcoin network.\n\ + Transaction ID={1}.\n\ + The offer has been removed to avoid further problems.\n\ + Please go to \"Settings/Network info\" and do a SPV resync.\n\ + For further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.trade.txRejected.tradeFee=trade fee +popup.warning.trade.txRejected.deposit=deposit +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\n\ + Transaction ID={2}\n\ + The trade has been moved to failed trades.\n\ + Please go to \"Settings/Network info\" and do a SPV resync.\n\ + For further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\n\ + Transaction ID={1}.\n\ + Please go to \"Settings/Network info\" and do a SPV resync.\n\ + For further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.info.securityDepositInfo=To ensure both traders follow the trade protocol, both traders need to pay a security \ +deposit.\n\nThis deposit is kept in your trade wallet until your trade has been successfully completed, and then it's \ +refunded to you.\n\nPlease note: if you're creating a new offer, Bisq needs to be running for another trader to take \ +it. To keep your offers online, keep Bisq running and make sure this computer remains online too (i.e., make sure it \ +doesn't switch to standby mode...monitor standby is fine). + +popup.info.cashDepositInfo=Please be sure that you have a bank branch in your area to be able to make the cash deposit.\n\ + The bank ID (BIC/SWIFT) of the seller''s bank is: {0}. +popup.info.cashDepositInfo.confirm=I confirm that I can make the deposit +popup.info.shutDownWithOpenOffers=Bisq is being shut down, but there are open offers. \n\n\ + These offers won't be available on the P2P network while Bisq is shut down, but \ + they will be re-published to the P2P network the next time you start Bisq.\n\n\ + To keep your offers online, keep Bisq running and make sure this computer remains online too \ + (i.e., make sure it doesn't go into standby mode...monitor standby is not a problem). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\ + Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Important private notification! + +popup.securityRecommendation.headline=Important security recommendation +popup.securityRecommendation.msg=We would like to remind you to consider using password protection for your wallet if you have not already enabled that.\n\nIt is also highly recommended to write down the wallet seed words. Those seed words are like a master password for recovering your Bitcoin wallet.\nAt the \"Wallet Seed\" section you find more information.\n\nAdditionally you should backup the complete application data folder at the \"Backup\" section. + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\n\ + Please ensure:\n\ + - the node is fully synced before starting Bisq\n\ + - pruning is disabled ('prune=0' in bitcoin.conf)\n\ + - bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Shut down in progress +popup.shutDownInProgress.msg=Shutting down application can take a few seconds.\nPlease don't interrupt this process. + +popup.attention.forTradeWithId=Attention required for trade with ID {0} +popup.attention.newFeatureDuplicateOffer=Version 1.6.3 introduces a new feature allowing easy re-entry of offers \ + by right-clicking on an existing offer or trade within Portfolio menu and choosing `Create new offer like this`. \ + This is useful for traders who frequently make the same offer. + +popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available +popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. + +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.signAll=Sign all payment methods +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\n\ + For further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \ + and the initial limit for one of your accounts has been lifted.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notification for trade with ID {0} +notification.ticket.headline=Support ticket for trade with ID {0} +notification.trade.completed=The trade is now completed and you can withdraw your funds. +notification.trade.accepted=Your offer has been accepted by a BTC {0}. +notification.trade.confirmed=Your trade has at least one blockchain confirmation.\nYou can start the payment now. +notification.trade.paymentStarted=The BTC buyer has started the payment. +notification.trade.selectTrade=Select trade +notification.trade.peerOpenedDispute=Your trading peer has opened a {0}. +notification.trade.disputeClosed=The {0} has been closed. +notification.walletUpdate.headline=Trading wallet update +notification.walletUpdate.msg=Your trading wallet is sufficiently funded.\nAmount: {0} +notification.takeOffer.walletUpdate.msg=Your trading wallet was already sufficiently funded from an earlier take offer attempt.\nAmount: {0} +notification.tradeCompleted.headline=Trade completed +notification.tradeCompleted.msg=You can withdraw your funds now to your external Bitcoin wallet or transfer it to the Bisq wallet. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Show application window +systemTray.hide=Hide application window +systemTray.info=Info about Bisq +systemTray.exit=Exit +systemTray.tooltip=Bisq: A decentralized bitcoin exchange network + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is \ +at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0} +guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting. +guiUtil.accountExport.selectPath=Select path to {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Trading account with id {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=We did not import trading account with id {0} because it exists already.\n +guiUtil.accountExport.exportFailed=Exporting to CSV failed because of an error.\nError = {0} +guiUtil.accountExport.selectExportPath=Select export path +guiUtil.accountImport.imported=Trading account imported from path:\n{0}\n\nImported accounts:\n{1} +guiUtil.accountImport.noAccountsFound=No exported trading accounts has been found at path: {0}.\nFile name is {1}." +guiUtil.openWebBrowser.warning=You are going to open a web page \ +in your system web browser.\n\ +Do you want to open the web page now?\n\n\ +If you are not using the \"Tor Browser\" as your default system web browser you \ +will connect to the web page in clear net.\n\n\ +URL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Open the web page and don't ask again +guiUtil.openWebBrowser.copyUrl=Copy URL and cancel +guiUtil.ofTradeAmount=of trade amount +guiUtil.requiredMinimum=(required minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Select currency +list.currency.showAll=Show all +list.currency.editList=Edit currency list + +table.placeholder.noItems=Currently there are no {0} available +table.placeholder.noData=Currently there is no data available +table.placeholder.processingData=Processing data... + + +peerInfoIcon.tooltip.tradePeer=Trading peer's +peerInfoIcon.tooltip.maker=Maker's +peerInfoIcon.tooltip.trade.traded={0} onion address: {1}\nYou have already traded {2} time(s) with that peer\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} onion address: {1}\nYou have not traded with that peer so far.\n{2} +peerInfoIcon.tooltip.age=Payment account created {0} ago. +peerInfoIcon.tooltip.unknownAge=Payment account age not known. + +tooltip.openPopupForDetails=Open popup for details +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Open external blockchain explorer for address: {0} +tooltip.openBlockchainForTx=Open external blockchain explorer for transaction: {0} + +confidence.unknown=Unknown transaction status +confidence.seen=Seen by {0} peer(s) / 0 confirmations +confidence.confirmed=Confirmed in {0} block(s) +confidence.invalid=Transaction is invalid + +peerInfo.title=Peer info +peerInfo.nrOfTrades=Number of completed trades +peerInfo.notTradedYet=You have not traded with that user so far. +peerInfo.setTag=Set tag for that peer +peerInfo.age.noRisk=Payment account age +peerInfo.age.chargeBackRisk=Time since signing +peerInfo.unknownAge=Age not known + +addressTextField.openWallet=Open your default Bitcoin wallet +addressTextField.copyToClipboard=Copy address to clipboard +addressTextField.addressCopiedToClipboard=Address has been copied to clipboard +addressTextField.openWallet.failed=Opening a default Bitcoin wallet application has failed. Perhaps you don't have one installed? + +peerInfoIcon.tooltip={0}\nTag: {1} + +txIdTextField.copyIcon.tooltip=Copy transaction ID to clipboard +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Account\" +navigation.account.walletSeed=\"Account/Wallet seed\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"Portfolio/My open offers\" +navigation.portfolio.pending=\"Portfolio/Open trades\" +navigation.portfolio.closedTrades=\"Portfolio/History\" +navigation.funds.depositFunds=\"Funds/Receive funds\" +navigation.settings.preferences=\"Settings/Preferences\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Funds/Transactions\" +navigation.support=\"Support\" +navigation.dao.wallet.receive=\"DAO/BSQ Wallet/Receive\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} amount{1} +formatter.makerTaker=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.fiat=You are creating an offer to {0} {1} +formatter.youAreCreatingAnOffer.altcoin=You are creating an offer to {0} {1} ({2} {3}) +formatter.asMaker={0} {1} as maker +formatter.asTaker={0} {1} as taker + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=Year +time.month=Month +time.week=Week +time.day=Day +time.hour=Hour +time.minute10=10 Minutes +time.hours=hours +time.days=days +time.1hour=1 hour +time.1day=1 day +time.minute=minute +time.second=second +time.minutes=minutes +time.seconds=seconds + + +password.enterPassword=Enter password +password.confirmPassword=Confirm password +password.tooLong=Password must be less than 500 characters. +password.deriveKey=Derive key from password +password.walletDecrypted=Wallet successfully decrypted and password protection removed. +password.wrongPw=You entered the wrong password.\n\nPlease try entering your password again, carefully checking for typos or spelling errors. +password.walletEncrypted=Wallet successfully encrypted and password protection enabled. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=The 2 passwords you entered don't match. +password.forgotPassword=Forgot password? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\n\ + It is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Wallet seed words +seed.enterSeedWords=Enter wallet seed words +seed.date=Wallet date +seed.restore.title=Restore wallets from seed words +seed.restore=Restore wallets +seed.creationDate=Creation date +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\n\ +You must empty this wallet before attempting to restore an older one, as mixing wallets \ +together can lead to invalidated backups.\n\n\ +Please finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\n\ +In case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\n\ +To open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=I want to restore anyway +seed.warn.walletNotEmpty.emptyWallet=I will empty my wallets first +seed.warn.notEncryptedAnymore=Your wallets are encrypted.\n\n\ +After restore, the wallets will no longer be encrypted and you must set a new password.\n\n\ +Do you want to proceed? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\n\ +BIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\n\ +Ideally you should specify the date your wallet seed was created.\n\n\n\ +Are you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Wallets restored successfully with the new seed words.\n\nYou need to shut down and restart the application. +seed.restore.error=An error occurred when restoring the wallets with seed words.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\n\ + Are you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Account +payment.account.no=Account no. +payment.account.name=Account name +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Account owner full name +payment.account.fullName=Full name (first, middle, last) +payment.account.state=State/Province/Region +payment.account.city=City +payment.bank.country=Country of bank +payment.account.name.email=Account owner full name / email +payment.account.name.emailAndHolderId=Account owner full name / email / {0} +payment.bank.name=Bank name +payment.select.account=Select account type +payment.select.region=Select region +payment.select.country=Select country +payment.select.bank.country=Select country of bank +payment.foreign.currency=Are you sure you want to choose a currency other than the country's default currency? +payment.restore.default=No, restore default currency +payment.email=Email +payment.country=Country +payment.extras=Extra requirements +payment.email.mobile=Email or mobile no. +payment.altcoin.address=Altcoin address +payment.altcoin.tradeInstantCheckbox=Trade instant (within 1 hour) with this Altcoin +payment.altcoin.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able \ + to complete the trade in less than 1 hour.\n\n\ + If you have offers open and you are not available please disable \ + those offers under the 'Portfolio' screen. +payment.altcoin=Altcoin +payment.select.altcoin=Select or search Altcoin +payment.secret=Secret question +payment.answer=Answer +payment.wallet=Wallet ID +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Username or email or phone no. +payment.moneyBeam.accountId=Email or phone no. +payment.venmo.venmoUserName=Venmo username +payment.popmoney.accountId=Email or phone no. +payment.promptPay.promptPayId=Citizen ID/Tax ID or phone no. +payment.supportedCurrencies=Supported currencies +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Limitations +payment.salt=Salt for account age verification +payment.error.noHexSalt=The salt needs to be in HEX format.\n\ + It is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. \ + The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Accept trades from these Euro countries +payment.accept.nonEuro=Accept trades from these non-Euro countries +payment.accepted.countries=Accepted countries +payment.accepted.banks=Accepted banks (ID) +payment.mobile=Mobile no. +payment.postal.address=Postal address +payment.national.account.id.AR=CBU number +shared.accountSigningState=Account signing status + +#new +payment.altcoin.address.dyn={0} address +payment.altcoin.receiver.address=Receiver's altcoin address +payment.accountNr=Account number +payment.emailOrMobile=Email or mobile no. +payment.useCustomAccountName=Use custom account name +payment.maxPeriod=Max. allowed trade period +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} +payment.maxPeriodAndLimitCrypto=Max. trade duration: {0} / Max. trade limit: {1} +payment.currencyWithSymbol=Currency: {0} +payment.nameOfAcceptedBank=Name of accepted bank +payment.addAcceptedBank=Add accepted bank +payment.clearAcceptedBanks=Clear accepted banks +payment.bank.nameOptional=Bank name (optional) +payment.bankCode=Bank code +payment.bankId=Bank ID (BIC/SWIFT) +payment.bankIdOptional=Bank ID (BIC/SWIFT) (optional) +payment.branchNr=Branch no. +payment.branchNrOptional=Branch no. (optional) +payment.accountNrLabel=Account no. (IBAN) +payment.accountType=Account type +payment.checking=Checking +payment.savings=Savings +payment.personalId=Personal ID +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\n\ +It is highly recommended to either:\n\ +- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n\ +- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\n\ +Bisq 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 BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\n\ +It 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\n\ +Bisq 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.clearXchange.info=Zelle is a money transfer service that works best *through* another bank.\n\n\ + 1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ + 2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n\ + 3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n\ + 4. The name specified on your Bisq account MUST match the name on your Zelle/bank account. \n\n\ + If you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\n\ + Because of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ + really owns the Zelle account specified in Bisq. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster \ + Payments transfers. Your current Faster Payments account does not specify a full name.\n\n\ + Please consider recreating your Faster Payments account in Bisq to provide future {0} buyers with a full name.\n\n\ + When you recreate the account, make sure to copy the precise sort code, account number and account age verification \ + salt values from your old account to your new account. This will ensure your existing account''s age and signing \ + status are preserved. +payment.moneyGram.info=When using MoneyGram the BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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=When using HalCash the BTC buyer needs to send the BTC seller the HalCash code via a text message from their mobile phone.\n\n\ + Please make sure to not exceed the maximum amount your bank allows you to send with HalCash. \ + The min. amount per withdrawal is 10 EUR and the max. amount is 600 EUR. For repeated withdrawals it is \ + 3000 EUR per receiver per day and 6000 EUR per receiver per month. Please cross check those limits with your \ + bank to be sure they use the same limits as stated here.\n\n\ + The withdrawal amount must be a multiple of 10 EUR as you cannot withdraw other amounts from an ATM. The \ + UI in the create-offer and take-offer screen will adjust the BTC amount so that the EUR amount is correct. You cannot use market \ + based price as the EUR amount would be changing with changing prices.\n\n\ + In case of a dispute the BTC buyer needs to provide the proof that they sent the 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, \ + Bisq sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\ + \n\ + For this payment method, your per-trade limit for buying and selling is {2}.\n\ + \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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade limits for this payment account type based \ + on the following 2 factors:\n\n\ + 1. General chargeback risk for the payment method\n\ + 2. Account signing status\n\ + \n\ + This 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\ + \n\ + Sell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\ + \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://bisq.wiki/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. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\n\ + Your existing Revolut account ({1}) does not have a ''User name''.\n\ + Please enter your Revolut ''User name'' to update your account data.\n\ + This will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. +payment.account.amazonGiftCard.addCountryInfo={0}\n\ + Your existing Amazon Gift Card account ({1}) does not have a Country specified.\n\ + Please enter your Amazon Gift Card Country to update your account data.\n\ + This 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 Bisq requires that you understand the following:\n\ +\n\ +- BTC buyers must write the BTC 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\ +- BTC buyers must send the USPMO to the BTC 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 Bisq 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\ +If you do not understand these requirements, do not trade using USPMO on Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\ + \n\ + ● BTC buyer should package cash in a tamper-evident cash bag.\n\ + ● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n\ + ● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n\ + ● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n\ + ● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n\ + ● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\ + \n\ + CBM trades put the onus to act honestly squarely on both peers.\n\ + \n\ + ● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n\ + ● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n\ + ● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n\ + ● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n\ + ● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n\ + ● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n\ + ● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\ + \n\ + To be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\ + \n\ + If you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Contact info +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=Contact info +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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.cashByMail.extraInfo.prompt=Please state on your offers: \n\n\ +Country you are located (eg France); \n\ +Countries / regions you would accept trades from (eg France, EU, or any European country); \n\ +Any special terms/conditions; \n\ +Any other details. +payment.cashByMail.tradingRestrictions=Please review the maker's terms and conditions.\n\ + If you do not meet the requirements do not take this trade. +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 \ + of what happened at the meeting. In such cases the BTC 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://docs.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Bank +payment.japan.branch=Branch +payment.japan.account=Account +payment.japan.recipient=Name +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 BTC seller via your Amazon account. \n\n\ + Bisq will show the BTC 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://bisq.wiki/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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=National bank transfer +SAME_BANK=Transfer with same bank +SPECIFIC_BANKS=Transfers with specific banks +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=Cash Deposit +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Face to face (in person) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=National banks +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Same bank +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Specific banks +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Cash Deposit +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Empty input is not allowed. +validation.NaN=Input is not a valid number. +validation.notAnInteger=Input is not an integer value. +validation.zero=Input of 0 is not allowed. +validation.negative=A negative value is not allowed. +validation.fiat.toSmall=Input smaller than minimum possible amount is not allowed. +validation.fiat.toLarge=Input larger than maximum possible amount is not allowed. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=Input larger than {0} is not allowed. +validation.btc.toSmall=Input smaller than {0} is not allowed. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=The password you entered is too long. It cannot be longer than 50 characters. +validation.sortCodeNumber={0} must consist of {1} numbers. +validation.sortCodeChars={0} must consist of {1} characters. +validation.bankIdNumber={0} must consist of {1} numbers. +validation.accountNr=Account number must consist of {0} numbers. +validation.accountNrChars=Account number must consist of {0} characters. +validation.btc.invalidAddress=The address is not correct. Please check the address format. +validation.integerOnly=Please enter integer numbers only. +validation.inputError=Your input caused an error:\n{0} +validation.bsq.insufficientBalance=Your available balance is {0}. +validation.btc.exceedsMaxTradeLimit=Your trade limit is {0}. +validation.bsq.amountBelowMinAmount=Min. amount is {0} +validation.nationalAccountId={0} must consist of {1} numbers. + +#new +validation.invalidInput=Invalid input: {0} +validation.accountNrFormat=Account number must be of format: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Address validation failed because it does not match the structure of a {0} address. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Address is not a valid {0} address! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Bank and Country code must be letters +validation.bic.invalidLocationCode=BIC contains invalid location code +validation.bic.invalidBranchCode=BIC contains invalid branch code +validation.bic.sepaRevolutBic=Revolut Sepa accounts are not supported. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Invalid address +validation.iban.invalidCountryCode=Country code invalid +validation.iban.checkSumNotNumeric=Checksum must be numeric +validation.iban.nonNumericChars=Non-alphanumeric character detected +validation.iban.checkSumInvalid=IBAN checksum is invalid +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Non-Canadian area code +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Must contain only letters, numbers, spaces and/or the symbols ' _ , . ? - +validation.interacETransfer.invalidAnswer=Must be one word and contain only letters, numbers, and/or the symbol - +validation.inputTooLarge=Input must not be larger than {0} +validation.inputTooSmall=Input has to be larger than {0} +validation.inputToBeAtLeast=Input has to be at least {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=Length must be between {0} and {1} +validation.fixedLength=Length must be {0} +validation.pattern=Input must be of format: {0} +validation.noHexString=The input is not in HEX format. +validation.advancedCash.invalidFormat=Must be a valid email or wallet id of format: X000000000000 +validation.invalidUrl=This is not a valid URL +validation.mustBeDifferent=Your input must be different from the current value +validation.cannotBeChanged=Parameter cannot be changed +validation.numberFormatException=Number format exception {0} +validation.mustNotBeNegative=Input must not be negative +validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.invalidCharacters=Phone number {0} contains invalid characters +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. \ + The correct dialing code is {2}. +validation.invalidAddressList=Must be comma separated list of valid addresses diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties new file mode 100644 index 0000000000..a7d8c9a421 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Přečíst více +shared.openHelp=Otevřít nápovědu +shared.warning=Varování +shared.close=Zavřít +shared.cancel=Zrušit +shared.ok=OK +shared.yes=Ano +shared.no=Ne +shared.iUnderstand=Rozumím +shared.na=N/A +shared.shutDown=Vypnout +shared.reportBug=Nahlásit chybu na GitHubu +shared.buyBitcoin=Koupit bitcoin +shared.sellBitcoin=Prodat bitcoin +shared.buyCurrency=Koupit {0} +shared.sellCurrency=Prodat {0} +shared.buyingBTCWith=nakoupit BTC za {0} +shared.sellingBTCFor=prodat BTC za {0} +shared.buyingCurrency=nakoupit {0} (prodat BTC) +shared.sellingCurrency=prodat {0} (nakoupit BTC) +shared.buy=koupit +shared.sell=prodat +shared.buying=kupuje +shared.selling=prodává +shared.P2P=P2P +shared.oneOffer=nabídka +shared.multipleOffers=nabídky +shared.Offer=Nabídka +shared.offerVolumeCode={0} Objem nabídky +shared.openOffers=otevřené nabídky +shared.trade=obchod +shared.trades=obchody +shared.openTrades=otevřené obchody +shared.dateTime=Datum/Čas +shared.price=Cena +shared.priceWithCur=Cena v {0} +shared.priceInCurForCur=Cena v {0} za 1 {1} +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.amountWithCur=Množství v {0} +shared.volumeWithCur=Objem v {0} +shared.currency=Měna +shared.market=Trh +shared.deviation=Odchylka +shared.paymentMethod=Platební metoda +shared.tradeCurrency=Obchodní měna +shared.offerType=Typ nabídky +shared.details=Detaily +shared.address=Adresa +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.select=Vybrat +shared.usage=Použití +shared.state=Stav +shared.tradeId=ID obchodu +shared.offerId=ID nabídky +shared.bankName=Jméno banky +shared.acceptedBanks=Přijímané banky +shared.amountMinMax=Množství (min - max) +shared.amountHelp=Pokud je v nabídce nastavena minimální a maximální částka, můžete obchodovat s jakoukoli částkou v tomto rozsahu. +shared.remove=Odstranit +shared.goTo=Přejít na {0} +shared.BTCMinMax=BTC (min - max) +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.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 Bisq peněženky +shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování +shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci bitcoinové peněženky. 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 Bisq 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 Bisq v části Prostředky > Přijmout prostředky. +shared.waitingForFunds=Čekání na finance... +shared.depositTransactionId=ID vkladové transakce +shared.TheBTCBuyer=BTC kupující +shared.You=Vy +shared.sendingConfirmation=Posílám potvrzení... +shared.sendingConfirmationAgain=Prosím pošlete potvrzení znovu +shared.exportCSV=Exportovat do CSV +shared.exportJSON=Exportovat do JSON +shared.summary=Ukázat souhrn +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}\nZ adresy: {1}\nNa přijímací adresu: {2}.\nPožadovaný poplatek za těžbu je: {3} ({4} satoshi/vbyte)\nTransakční vsize: {5} vKb\n\nPříjemce obdrží: {6}\n\nOpravdu chcete tuto částku vybrat? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro bitcoinový konsenzus). 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.language=Jazyk +shared.country=Země +shared.applyAndShutDown=Potvrdit a ukončit +shared.selectPaymentMethod=Vybrat platební metodu +shared.accountNameAlreadyUsed=Toto jméno účtu je již použito pro jiný účet.\nPoužijte prosím jiné jméno. +shared.askConfirmDeleteAccount=Skutečně chcete smazat vybraný účet? +shared.cannotDeleteAccount=Tento účet nemůžete smazat, protože je použit v otevřené nabídce (nebo v otevřeném obchodě). +shared.noAccountsSetupYet=Ještě nejsou nastaveny žádné účty +shared.manageAccounts=Spravovat účty +shared.addNewAccount=Přidat nový účet +shared.ExportAccounts=Exportovat účty +shared.importAccounts=Importovat účty +shared.createNewAccount=Vytvořit nový účet +shared.saveNewAccount=Uložit nový účet +shared.selectedAccount=Vybraný účet +shared.deleteAccount=Smazat účet +shared.errorMessageInline=\nChybová zpráva {0} +shared.errorMessage=Chybová zpráva +shared.information=Informace +shared.name=Jméno +shared.id=ID +shared.dashboard=Dashboard +shared.accept=Přijmout +shared.balance=Zůstatek +shared.save=Uložit +shared.onionAddress=Onion adresa +shared.supportTicket=úkol pro podporu +shared.dispute=spor +shared.mediationCase=mediační případ +shared.seller=prodejce +shared.buyer=kupující +shared.allEuroCountries=Všechny Euro země +shared.acceptedTakerCountries=Země příjemce akceptovány +shared.tradePrice=Tržní cena +shared.tradeAmount=Výše obchodu +shared.tradeVolume=Objem obchodu +shared.invalidKey=Vložený klíč není správný +shared.enterPrivKey=Pro odemknutí vložte privátní klíč +shared.makerFeeTxId=ID transakčního poplatku tvůrce +shared.takerFeeTxId=ID transakčního poplatku příjemce +shared.payoutTxId=ID platební transakce +shared.contractAsJson=Kontakt v JSON formátu +shared.viewContractAsJson=Zobrazit kontrakt v JSON formátu +shared.contract.title=Kontrakt pro obchod s ID: {0} +shared.paymentDetails=BTC {0} detaily platby +shared.securityDeposit=Kauce +shared.yourSecurityDeposit=Vaše kauce +shared.contract=Kontrakt +shared.messageArrived=Zpráva dorazila. +shared.messageStoredInMailbox=Zpráva uložena ve schránce +shared.messageSendingFailed=Odeslání zprávy selhalo. Chyba: {0} +shared.unlock=Odemknout +shared.toReceive=bude přijata +shared.toSpend=bude utracena +shared.btcAmount=Částka BTC +shared.yourLanguage=Vaše jazyky +shared.addLanguage=Přidat jazyk +shared.total=Celkem +shared.totalsNeeded=Potřebné prostředky +shared.tradeWalletAddress=Adresa obchodní peněženky +shared.tradeWalletBalance=Zůstatek obchodní peněženky +shared.makerTxFee=Tvůrce: {0} +shared.takerTxFee=Příjemce: {0} +shared.iConfirm=Potvrzuji +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Otevřené {0} +shared.fiat=Fiat +shared.crypto=Krypto +shared.all=Vše +shared.edit=Upravit +shared.advancedOptions=Pokročilé možnosti +shared.interval=Interval +shared.actions=Akce +shared.buyerUpperCase=Kupující +shared.sellerUpperCase=Prodejce +shared.new=NOVÝ +shared.blindVoteTxId=ID transakce se slepým hlasováním +shared.proposal=Návrh +shared.votes=Hlasy +shared.learnMore=Zjistit více +shared.dismiss=Zavřít +shared.selectedArbitrator=Zvolený rozhodce +shared.selectedMediator=Zvolený mediátor +shared.selectedRefundAgent=Zvolený rozhodce +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.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í + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Trh +mainView.menu.buyBtc=Koupit BTC +mainView.menu.sellBtc=Prodat BTC +mainView.menu.portfolio=Portfolio +mainView.menu.funds=Finance +mainView.menu.support=Podpora +mainView.menu.settings=Nastavení +mainView.menu.account=Účet +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Tržní cena {0} +mainView.marketPrice.bisqInternalPrice=Cena posledního Bisq obchodu +mainView.marketPrice.tooltip.bisqInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\nZobrazená cena je nejnovější obchodní cena Bisq 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 +mainView.balance.locked=Zamčené v obchodech +mainView.balance.reserved.short=Rezervováno +mainView.balance.locked.short=Zamčeno + +mainView.footer.usingTor=(přes Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Aktuální poplatek: {0} sat/vB +mainView.footer.btcInfo.initializing=Připojování do Bitcoinové sítě +mainView.footer.bsqInfo.synchronizing=/ Synchronizace DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizace s {0} v bloku: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synchronizováno s {0} v bloku {1} +mainView.footer.btcInfo.connectingTo=Připojování +mainView.footer.btcInfo.connectionFailed=Připojení se nezdařilo +mainView.footer.p2pInfo=Bitcoin síťové nody: {0} / Bisq síťové nody: {1} +mainView.footer.daoFullNode=DAO full node + +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.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.bootstrappingToP2PFailed=Zavádění do sítě Bisq 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.connectionToP2PFailed=Připojení k síti Bisq selhalo (nahlášená chyba: {0}).\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. + +mainView.walletServiceErrorMsg.timeout=Připojení k síti Bitcoin selhalo kvůli vypršení časového limitu. +mainView.walletServiceErrorMsg.connectionError=Připojení k síti Bitcoin selhalo kvůli chybě {0} + +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.localhostBitcoinLost=Ztratili jste připojení k Bitcoinovému localhost nodu.\nRestartujte aplikaci Bisq a připojte se k jiným Bitcoinovým nodům nebo restartujte Bitcoinový localhost node. +mainView.version.update=(Dostupná aktualizace) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Seznam nabídek +market.tabs.spreadCurrency=Nabídky podle měn +market.tabs.spreadPayment=Nabídky podle způsobů platby +market.tabs.trades=Obchody + +# OfferBookChartView +market.offerBook.buyAltcoin=Koupit {0} (prodat {1}) +market.offerBook.sellAltcoin=Prodat {0} (koupit {1}) +market.offerBook.buyWithFiat=Koupit {0} +market.offerBook.sellWithFiat=Prodat {0} +market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu +market.offerBook.buyOffersHeaderLabel=Koupit {0} od prodejce +market.offerBook.buy=Chci koupit bitcoin +market.offerBook.sell=Chci prodat bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Všechny nabídky ({0}) +market.spread.numberOfBuyOffersColumn=Koupit BTC ({0}) +market.spread.numberOfSellOffersColumn=Prodat BTC ({0}) +market.spread.totalAmountColumn=Celkem BTC ({0}) +market.spread.spreadColumn=Rozptyl +market.spread.expanded=Rozbalit + +# TradesChartsView +market.trades.nrOfTrades=Obchodů: {0} +market.trades.tooltip.volumeBar=Objem: {0} / {1}\nPočet obchodů: {2}\nDatum: {3} +market.trades.tooltip.candle.open=Otevřené: +market.trades.tooltip.candle.close=Zavřít: +market.trades.tooltip.candle.high=Nejvyšší: +market.trades.tooltip.candle.low=Nejnižší: +market.trades.tooltip.candle.average=Průměr: +market.trades.tooltip.candle.median=Medián: +market.trades.tooltip.candle.date=Datum: +market.trades.showVolumeInUSD=Zobrazit objem v USD + +#################################################################### +# OfferView +#################################################################### + +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.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.filterByCurrency=Filtrovat podle měny +offerbook.filterByPaymentMethod=Filtrovat podle platební metody +offerbook.matchingOffers=Nabídky odpovídající mým účtům +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 +offerbook.timeSinceSigning.info.signer=podepsán partnerem a může podepsat účty partnera (pro zrušení limitů) +offerbook.timeSinceSigning.info.banned=účet byl zablokován +offerbook.timeSinceSigning.daysSinceSigning={0} dní +offerbook.timeSinceSigning.daysSinceSigning.long={0} od podpisu +offerbook.xmrAutoConf=Je automatické potvrzení povoleno + +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=Altcoinové účty neprocházejí kontrolou podpisu a stáří + +offerbook.nrOffers=Počet nabídek: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Kauce BTC (%) +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.withFiat=Vytvořit novou nabídku k nákupu {0} za {1} +offerbook.createOfferToSell.forFiat=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.takeOfferButton.tooltip=Využijte nabídku {0} +offerbook.yesCreateOffer=Ano, vytvořit nabídku +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.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? +offerbook.warning.noMatchingAccount.headline=Žádný odpovídající platební účet. +offerbook.warning.noMatchingAccount.msg=Tato nabídka používá platební metodu, kterou jste dosud nenastavili.\n\nChcete nyní založit nový platební účet? + +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.bisq.network/payment-methods#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} + +offerbook.warning.wrongTradeProtocol=Tato nabídka vyžaduje jinou verzi protokolu než ta, která byla použita ve vaší verzi softwaru.\n\nZkontrolujte, zda máte nainstalovanou nejnovější verzi, jinak uživatel, který nabídku vytvořil, použil starší verzi.\n\nUživatelé nemohou obchodovat s nekompatibilní verzí obchodního protokolu. +offerbook.warning.userIgnored=Do seznamu ignorovaných uživatelů jste přidali onion adresu tohoto uživatele. +offerbook.warning.offerBlocked=Tato nabídka byla blokována vývojáři Bisq.\nPravděpodobně existuje neošetřená chyba způsobující problémy při přijetí této nabídky. +offerbook.warning.currencyBanned=Měna použitá v této nabídce byla blokována vývojáři Bisq.\nDalší informace naleznete na fóru Bisq. +offerbook.warning.paymentMethodBanned=Vývojáři Bisq zablokovali způsob platby použitý v této nabídce.\nDalší informace naleznete na fóru Bisq. +offerbook.warning.nodeBlocked=Onion adresa tohoto obchodníka byla zablokována vývojáři Bisq.\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 Bisq již není kompatibilní pro obchodování. Aktualizujte prosím na nejnovější verzi Bisq na adrese [HYPERLINK:https://bisq.network/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.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). +offerbook.info.sellBelowMarketPrice=Získáte o {0} méně než je aktuální tržní cena (aktualizováno každou minutu). +offerbook.info.buyAboveMarketPrice=Platíte o {0} více, než je aktuální tržní cena (aktualizováno každou minutu). +offerbook.info.sellAboveMarketPrice=Získáte o {0} více, než je aktuální tržní cena (aktualizováno každou minutu). +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 BTC +createOffer.price.prompt=Zadejte cenu +createOffer.volume.prompt=Vložte množství v {0} +createOffer.amountPriceBox.amountDescription=Množství BTC, 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.minAmountDescription=Minimální množství BTC +createOffer.securityDeposit.prompt=Kauce +createOffer.fundsBox.title=Financujte svou 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.paymentLabel=Bisq obchod s ID {0} +createOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) +createOffer.fundsBox.fundsStructure.BSQ=(kauce {0}, poplatek za těžbu {1}) + obchodní poplatek {2} +createOffer.success.headline=Vaše nabídka byla publikována +createOffer.success.info=Otevřené nabídky můžete spravovat na stránce \"Portfolio/Moje otevřené nabídky\". +createOffer.info.sellAtMarketPrice=Vždy budete prodávat za tržní cenu, protože cena vaší nabídky bude průběžně aktualizována. +createOffer.info.buyAtMarketPrice=Vždy budete nakupovat za tržní cenu, protože cena vaší nabídky bude průběžně aktualizována. +createOffer.info.sellAboveMarketPrice=Vždy získá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.info.buyBelowMarketPrice=Vždy zaplatí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.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.descriptionBTCOnly=Obchodní poplatek +createOffer.tradeFee.descriptionBSQEnabled=Zvolte měnu obchodního poplatku + +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.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} bitcoin +createOffer.createOfferFundWalletInfo.headline=Financujte svou 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 Bisq (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í. + +# 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.setAmountPrice=Nastavit množství a cenu +createOffer.warnCancelOffer=Tuto nabídku jste již financovali.\nPokud ji nyní zrušíte, budou vaše prostředky přesunuty do lokální peněženky Bisq a jsou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\".\nOpravdu ji chcete 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.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.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.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.securityDepositInfoAsBuyer=Vaše kauce jako kupující bude {0} +createOffer.minSecurityDepositUsed=Je použita min. záloha kupujícího + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Vložte množství v BTC +takeOffer.amountPriceBox.buy.amountDescription=Množství BTC na prodej +takeOffer.amountPriceBox.sell.amountDescription=Množství BTC k nákupu +takeOffer.amountPriceBox.priceDescription=Cena za bitcoin v {0} +takeOffer.amountPriceBox.amountRangeDescription=Možný rozsah množství +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=Čá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 BTC. +takeOffer.fundsBox.title=Financujte svůj obchod +takeOffer.fundsBox.isOfferAvailable=Kontroluje se, zda je nabídka k dispozici ... +takeOffer.fundsBox.tradeAmount=Částka k prodeji +takeOffer.fundsBox.offerFee=Obchodní poplatek +takeOffer.fundsBox.networkFee=Celkové poplatky za těžbu +takeOffer.fundsBox.takeOfferSpinnerInfo=Probíhá využití nabídky... +takeOffer.fundsBox.paymentLabel=Bisq obchod s ID {0} +takeOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) +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} bitcoin +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 +# 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 Bisq (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.\nPokud ji nyní zrušíte, budou vaše prostředky přesunuty do lokální peněženky Bisq a jsou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\".\nOpravdu ji chcete 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.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. +takeOffer.warning.connectionToPeerLost=Ztratili jste spojení s tvůrcem.\nMohli odejít do režimu offline nebo s vámi ukončili připojení kvůli příliš velkému počtu otevřených připojení.\n\nPokud stále jeho nabídku vidíte v seznamu nabídek, můžete zkusit nabídku znovu využít. + +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.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. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +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 + +editOffer.setPrice=Nastavit cenu +editOffer.confirmEdit=Potvrdit: Upravit nabídku +editOffer.publishOffer=Publiková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 Bisq DAO a nemůže být dále upravována. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Moje otevřené nabídky +portfolio.tab.pendingTrades=Otevřené obchody +portfolio.tab.history=Historie +portfolio.tab.failed=Selhalo +portfolio.tab.editOpenOffer=Upravit nabídku + +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 altcoin platby.\n\nOtevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\nChybová zpráva: {0} + +portfolio.pending.step1.waitForConf=Počkejte na potvrzení na blockchainu +portfolio.pending.step2_buyer.startPayment=Zahajte platbu +portfolio.pending.step2_seller.waitPaymentStarted=Počkejte, než začne platba +portfolio.pending.step3_buyer.waitPaymentArrived=Počkejte, než dorazí platba +portfolio.pending.step3_seller.confirmPaymentReceived=Potvrďte přijetí platby +portfolio.pending.step5.completed=Hotovo + +portfolio.pending.step3_seller.autoConf.status.label=Stav automat. potvrzení +portfolio.pending.autoConf=Automaticky potvrzeno +portfolio.pending.autoConf.blocks=Potvrzení XMR: {0} / požadováno: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transakční klíč byl použit znovu. Otevřete prosím spor. +portfolio.pending.autoConf.state.confirmations=Potvrzení XMR: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transakce zatím není v mem-poolu vidět +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=Žádné platné ID transakce / transakční klíč +portfolio.pending.autoConf.state.filterDisabledFeature=Zakázáno vývojáři. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Funkce automatického potvrzení je zakázána. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Částka obchodu překračuje limit částky pro automatické potvrzení +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer poskytl neplatná data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Výplatní transakce již byla zveřejněna. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Spor byl otevřen. Automatické potvrzení je u tohoto obchodu deaktivováno. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Byly zahájeny žádosti o ověření transakce +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Úspěšné výsledky: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Důkaz u všech služeb byl úspěšný +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=Došlo k chybě při požadavku na službu. Není možné automatické potvrzení. +# 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.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. + +# 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ř. 'bitcoin', 'BTC' nebo 'Bisq'. Můžete se se svým obchodním partnerem domluvit pomocí chatu na identifikaci 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.altcoin=Převeďte prosím z vaší externí {0} peněženky\n{1} prodejci BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Přejděte do banky a zaplaťte {0} prodejci BTC.\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 BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Zaplaťte prosím {0} prodejci BTC 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 BTC.\nPotvrzení 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 BTC pomocí Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci BTC 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}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Zašlete prosím {0} prodejci BTC pomocí \"US Postal Money Order\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Zašlete prosím {0} prodejci BTC v poštovní zásilce (\"Cash 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 Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Prosím uhraďte {0} pomocí zvolené platební metody prodejci BTC. V dalším kroku naleznete detaily o účtu prodejce.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Kontaktujte prodejce BTC prostřednictvím poskytnutého kontaktu a domluvte si schůzku kde zaplatíte {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Zahajte platbu pomocí {0} +portfolio.pending.step2_buyer.recipientsAccountData=Příjemci {0} +portfolio.pending.step2_buyer.amountToTransfer=Částka k převodu +portfolio.pending.step2_buyer.sellersAddress={0} adresa prodejce +portfolio.pending.step2_buyer.buyerAccount=Použijte svůj platební účet +portfolio.pending.step2_buyer.paymentStarted=Platba zahájena +portfolio.pending.step2_buyer.fillInBsqWallet=Odeslat z BSQ peněženky +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.paperReceipt.headline=Odeslali jste papírový doklad prodejci BTC? +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 BTC. +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 BTC.\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.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 BTC.\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.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 BTC.\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ů Bisq 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í BTC, jakmile bude přijat XMR.\nKromě toho Bisq vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\nDalší podrobnosti na wiki Bisq: [HYPERLINK:https://bisq.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 +portfolio.pending.step2_seller.f2fInfo.headline=Kontaktní informace kupujícího +portfolio.pending.step2_seller.waitPayment.msg=Vkladová transakce má alespoň jedno potvrzení na blockchainu.\nMusíte počkat, než kupující BTC zahájí platbu {0}. +portfolio.pending.step2_seller.warn=Kupující BTC 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í BTC 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. +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 Bisq (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 +# suppress inspection "UnusedProperty" +message.state.SENT=Zpráva odeslána +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Zpráva dorazila partnerovi +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Zpráva o platbě je odeslaná, ale není dosud přijatá partnerem +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Partner potvrdil přijetí zprávy +# suppress inspection "UnusedProperty" +message.state.FAILED=Odeslání zprávy se nezdařilo + +portfolio.pending.step3_buyer.wait.headline=Počkejte na potvrzení platby prodejce BTC +portfolio.pending.step3_buyer.wait.info=Čekání na potvrzení prodejce BTC na přijetí platby {0}. +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 BTC vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda bylo odeslání platby úspěšné. +portfolio.pending.step3_buyer.openForDispute=Prodejce BTC 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.altcoin.explorer=ve vašem oblíbeném {0} blockchain exploreru +portfolio.pending.step3_seller.altcoin.wallet=na vaší {0} peněžence +portfolio.pending.step3_seller.altcoin={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.postal={0}Zkontrolujte, zda jste od kupujícího BTC obdrželi {1} přes \"US Postal Money Order\". +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Zkontrolujte, zda jste od kupujícího BTC obdrželi {1} přes \"Cash by Mail\". +# 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 BTC 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í BTC 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 BTC 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 BTC 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.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} +# 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 +portfolio.pending.step3_seller.amountToReceive=Částka k přijetí +portfolio.pending.step3_seller.yourAddress=Vaše {0} adresa +portfolio.pending.step3_seller.buyersAddress={0} adresa kupujících +portfolio.pending.step3_seller.yourAccount=Váš obchodní účet +portfolio.pending.step3_seller.xmrTxHash=ID transakce +portfolio.pending.step3_seller.xmrTxKey=Transakční klíč +portfolio.pending.step3_seller.buyersAccount=Údaje o účtu kupujícího +portfolio.pending.step3_seller.confirmReceipt=Potvrďte příjem platby +portfolio.pending.step3_seller.buyerStartedPayment=Kupující BTC zahájil platbu {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Podívejte se na potvrzení na blockchainu ve své altcoin peněžence nebo v blok exploreru a potvrďte platbu, pokud máte dostatečné potvrzení na blockchainu. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=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. +# 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" +portfolio.pending.step3_seller.onPaymentReceived.name=Ověřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje se jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\nPokud jména nejsou úplně stejná, nepotvrzujte příjem platby. Místo toho otevřete spor stisknutím \"alt + o\" nebo \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Vezměte prosím na vědomí, že jakmile potvrdíte příjem, dosud uzamčený obchodovaný BTC 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.step5_buyer.groupTitle=Shrnutí dokončeného obchodu +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.withdrawBTC=Vyberte své bitcoiny +portfolio.pending.step5_buyer.amount=Částka k výběru +portfolio.pending.step5_buyer.withdrawToAddress=Adresa výběru +portfolio.pending.step5_buyer.moveToBisqWallet=Uchovat prostředky v peněžence Bisq +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é bitcoinové transakce si můžete prohlédnout v sekci \"Prostředky/Transakce\" +portfolio.pending.step5_buyer.bought=Koupili jste +portfolio.pending.step5_buyer.paid=Zaplatili jste + +portfolio.pending.step5_seller.sold=Prodali jste +portfolio.pending.step5_seller.received=Přijali jste + +tradeFeedbackWindow.title=Gratulujeme k dokončení obchodu +tradeFeedbackWindow.msg.part1=Rádi bychom od vás slyšeli o vašich zkušenostech. Pomůže nám to vylepšit software a vyhladit všechny nejasnosti. Pokud chcete poskytnout zpětnou vazbu, vyplňte prosím tento krátký dotazník (není nutná registrace) na: +tradeFeedbackWindow.msg.part2=Pokud máte nějaké dotazy nebo máte nějaké problémy, obraťte se prosím na ostatní uživatele a přispěvatele prostřednictvím fóra Bisq na: +tradeFeedbackWindow.msg.part3=Děkujeme, že používáte Bisq! + +portfolio.pending.role=Moje role +portfolio.pending.tradeInformation=Obchodní informace +portfolio.pending.remainingTime=Zbývající čas +portfolio.pending.remainingTimeDetail={0} (do {1}) +portfolio.pending.tradePeriodInfo=Po prvním potvrzení na blockchainu začíná obchodní období. Na základě použitého způsobu platby se použije jiné maximální povolené obchodní období. +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.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 Bisq na adrese [HYPERLINK:https://bisq.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.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 Bisq 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 Bisq Keybase týmu. + +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 Bisq na adrese https://bisq.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.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. +portfolio.pending.communicateWithMediator=Komunikujte prosím na obrazovce \"Podpora\" s mediátorem. +portfolio.pending.disputeOpenedMyUser=Už jste otevřeli spor.\n{0} +portfolio.pending.disputeOpenedByPeer=Váš obchodní partner otevřel spor\n{0} +portfolio.pending.noReceiverAddressDefined=Není definována žádná adresa příjemce + +portfolio.pending.mediationResult.headline=Navrhovaná výplata z mediace +portfolio.pending.mediationResult.info.noneAccepted=Dokončete obchod přijetím návrhu mediátora na výplatu obchodu. +portfolio.pending.mediationResult.info.selfAccepted=Přijali jste návrh mediátora. Čekáte na to, aby ho také přijal partner. +portfolio.pending.mediationResult.info.peerAccepted=Váš obchodní partner přijal návrh mediátora. Přijímáte také? +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.bisq.network/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.bisq.network/trading-rules.html#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 altcoin platbu prodejci BTC, 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ů Bisq.\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.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.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=Přesunout obchod do otevřených obchodů + +portfolio.closed.completed=Dokončeno +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.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á. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Přijmout finanční prostředky +funds.tab.withdrawal=Poslat finanční prostředky +funds.tab.reserved=Vyhrazené prostředky +funds.tab.locked=Zamčené prostředky +funds.tab.transactions=Transakce + +funds.deposit.unused=Nepoužito +funds.deposit.usedInTx=Používá se v {0} transakcích +funds.deposit.fundBisqWallet=Financovat Bisq peněženku +funds.deposit.noAddresses=Dosud nebyly vygenerovány žádné adresy pro vklad +funds.deposit.fundWallet=Financujte svou peněženku +funds.deposit.withdrawFromWallet=Pošlete peníze z peněženky +funds.deposit.amount=Částka v BTC (volitelná) +funds.deposit.generateAddress=Vygenerujte novou adresu +funds.deposit.generateAddressSegwit=Nativní formát segwit (Bech32) +funds.deposit.selectUnused=Vyberte prosím nepoužívanou adresu z výše uvedené tabulky místo generování nové. + +funds.withdrawal.arbitrationFee=Poplatek za arbitráž +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.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.memoLabel=Poznámka k výběru +funds.withdrawal.memo=Volitelně vyplňte poznámku +funds.withdrawal.withdrawButton=Odeslat výběr +funds.withdrawal.noFundsAvailable=Pro výběr nejsou k dispozici žádné finanční prostředky +funds.withdrawal.confirmWithdrawalRequest=Potvrďte žádost o výběr +funds.withdrawal.withdrawMultipleAddresses=Výběr z více adres ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Výběr z více adres:\n{0} +funds.withdrawal.notEnoughFunds=V peněžence nemáte dostatek finančních prostředků. +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.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} + +funds.locked.noFunds=V obchodech nejsou zamčeny žádné prostředky +funds.locked.locked=Uzamčeno v multisig adrese pro obchodování s ID: {0} + +funds.tx.direction.sentTo=Odesláno na: +funds.tx.direction.receivedWith=Přijato z: +funds.tx.direction.genesisTx=Z Genesis tx: +funds.tx.txFeePaymentForBsqTx=Poplatek za těžbu za BSQ tx +funds.tx.createOfferFee=Poplatky tvůrce a tx: {0} +funds.tx.takeOfferFee=Poplatky příjemce a tx: {0} +funds.tx.multiSigDeposit=Vklad na multisig adresu: {0} +funds.tx.multiSigPayout=Výběr z multisig adresy: {0} +funds.tx.disputePayout=Výběr ze sporu: {0} +funds.tx.disputeLost=Prohraných sporů: {0} +funds.tx.collateralForRefund=Zástava na vrácení peněz: {0} +funds.tx.timeLockedPayoutTx=Časově uzamčená výplata tx: {0} +funds.tx.refund=Vrácení peněz z rozhodčího řízení: {0} +funds.tx.unknown=Neznámý důvod: {0} +funds.tx.noFundsFromDispute=Žádná náhrada ze sporu +funds.tx.receivedFunds=Přijaté prostředky +funds.tx.withdrawnFromWallet=Výběr z peněženky +funds.tx.withdrawnFromBSQWallet=Výběr BTC z BSQ peněženky +funds.tx.memo=Poznámka +funds.tx.noTxAvailable=Není k dispozici žádná transakce +funds.tx.revert=Vrátit +funds.tx.txSent=Transakce byla úspěšně odeslána na novou adresu v lokální peněžence Bisq. +funds.tx.direction.self=Posláno sobě +funds.tx.daoTxFee=Poplatek za těžbu za BSQ tx +funds.tx.reimbursementRequestTxFee=Žádost o vyrovnání +funds.tx.compensationRequestTxFee=Žádost o odměnu +funds.tx.dustAttackTx=Přijaté drobné +funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku BTC 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 Bisq 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í. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediace +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.sigCheck.button=Ověřit podpis +support.sigCheck.popup.info=V případě žádosti o vrácení peněz DAO musíte vložit souhrnnou zprávu procesu zprostředkování a rozhodčího řízení do své žádosti o vrácení peněz na Githubu. Aby bylo toto prohlášení ověřitelné, může každý uživatel pomocí tohoto nástroje zkontrolovat, zda se podpis mediátora nebo rozhodce shoduje se souhrnnou zprávou. +support.sigCheck.popup.header=Ověřit podpis výsledku sporu +support.sigCheck.popup.msg.label=Souhrnná zpráva +support.sigCheck.popup.msg.prompt=Zkopírovat a vložit souhrnnou zprávu ze sporu +support.sigCheck.popup.result=Výsledek ověření +support.sigCheck.popup.success=Podpis je platný +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.reOpenButton.label=Znovu otevřít +support.sendNotificationButton.label=Soukromé oznámení +support.reportButton.label=Zpráva +support.fullReportButton.label=Všechny spory +support.noTickets=Žádné otevřené úkoly +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í Bisq.\nTento spor nemůžete ukončit s touto verzí aplikace.\n\nPouž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. +support.attachment=Příloha +support.tooManyAttachments=V jedné zprávě nelze odeslat více než 3 přílohy. +support.save=Uložit soubor na disk +support.messages=Zprávy +support.input.prompt=Vložte zprávu... +support.send=Odeslat +support.addAttachments=Připojit soubory +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.acknowledged=Přijetí zprávy potvrzeno příjemcem +support.error=Příjemce nemohl zpracovat zprávu. Chyba: {0} +support.buyerAddress=Adresa kupujícího BTC +support.sellerAddress=Adresa prodejce BTC +support.role=Role +support.agent=Agent podpory +support.state=Stav +support.chat=Chat +support.closed=Zavřeno +support.open=Otevřené +support.process=Rozhodnout +support.buyerOfferer=Kupující BTC/Tvůrce +support.sellerOfferer=Prodejce BTC/Tvůrce +support.buyerTaker=Kupující BTC/Příjemce +support.sellerTaker=Prodávající BTC/Příjemce + +support.backgroundInfo=Bisq 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 zkusit řešení sporů sami. Pokud to nestačí, může jim pomoci mediátor. Mediátor vyhodnotí situaci a navrhne vyúčtování obchodních prostředků. Pokud oba obchodníci přijmou tento návrh, je výplata dokončena a obchod je uzavřen. Pokud jeden nebo oba obchodníci nesouhlasí s výplatou navrhovanou mediátorem, mohou požádat o rozhodčí řízení. Rozhodce přehodnotí situaci a v odůvodněných případech vrátí osobně prostředky obchodníkovi zpět a požádá o vrácení této platby od Bisq DAO. +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 BTC: Provedli jste převod Fiat nebo Altcoinu? Pokud ano, klikli jste v aplikaci na tlačítko „Platba zahájena“?\n\t● Pokud jste prodejcem BTC: Obdrželi jste platbu Fiat nebo Altcoinu? Pokud ano, klikli jste v aplikaci na tlačítko „Platba přijata“?\n\t● Kterou verzi Bisq 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.bisq.network/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.systemMsg=Systémová zpráva: {0} +support.youOpenedTicket=Otevřeli jste žádost o podporu.\n\n{0}\n\nVerze Bisq: {1} +support.youOpenedDispute=Otevřeli jste žádost o spor.\n\n{0}\n\nVerze Bisq: {1} +support.youOpenedDisputeForMediation=Vyžádali jste si mediaci.\n\n{0}\n\nBisq verze {1} +support.peerOpenedTicket=Váš obchodní partner požádal o podporu kvůli technickým problémům.\n\n{0}\n\nBisq verze: {1} +support.peerOpenedDispute=Váš obchodní partner požádal o spor.\n\n{0}\n\nBisq verze: {1} +support.peerOpenedDisputeForMediation=Váš obchodní partner požádal o mediaci.\n\n{0}\n\nBisq verze: {1} +support.mediatorsDisputeSummary=Systémová zpráva: Shrnutí sporu mediátora:\n{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.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ů. +support.info.disputeReOpened=Spor byl znovuotevřen. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preference +settings.tab.network=Informace o síti +settings.tab.about=O Bisq + +setting.preferences.general=Základní nastavení +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Max. odchylka od tržní ceny +setting.preferences.bsqAverageTrimThreshold=Mezní hodnota pro kurz BSQ +setting.preferences.avoidStandbyMode=Vyhněte se pohotovostnímu režimu +setting.preferences.autoConfirmXMR=Automatické potvrzení XMR +setting.preferences.autoConfirmEnabled=Povoleno +setting.preferences.autoConfirmRequiredConfirmations=Požadovaná potvrzení +setting.preferences.autoConfirmMaxTradeSize=Max. částka obchodu (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URL (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 +setting.preferences.prefCurrency=Preferovaná měna +setting.preferences.displayFiat=Zobrazit národní měny +setting.preferences.noFiat=Nejsou vybrány žádné národní měny +setting.preferences.cannotRemovePrefCurrency=Vybranou zobrazovanou měnu nelze odebrat. +setting.preferences.displayAltcoins=Zobrazit altcoiny +setting.preferences.noAltcoins=Nejsou vybrány žádné altcoiny +setting.preferences.addFiat=Přidejte národní měnu +setting.preferences.addAltcoin=Přidejte altcoin +setting.preferences.displayOptions=Zobrazit možnosti +setting.preferences.showOwnOffers=Zobrazit mé vlastní nabídky v seznamu nabídek +setting.preferences.useAnimations=Použít animace +setting.preferences.useDarkMode=Použít tmavý režim +setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/obchodů +setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované způsoby platby +setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API +setting.preferences.notifyOnPreRelease=Získávat oznámení o beta verzích +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 zprostředkování je řešeno v {0} a arbitráž v {1}. +setting.preferences.daoOptions=Možnosti DAO +setting.preferences.dao.resyncFromGenesis.label=Obnovit stav DAO z genesis tx +setting.preferences.dao.resyncFromResources.label=Obnovit stav DAO ze zdrojů +setting.preferences.dao.resyncFromResources.popup=Po restartu aplikace budou data správy sítě Bisq znovu načtena z počátečních uzlů a stav konsensu BSQ bude znovu vytvořen z nejnovějších zdrojů. +setting.preferences.dao.resyncFromGenesis.popup=Resynchronizace z genesis transakce může stát značné množství času a prostředků CPU. Opravdu to chcete udělat? Většinou je resynchronizace z nejnovějších zdrojových souborů dostatečná a mnohem rychlejší.\n\nPokud budete pokračovat, po restartu aplikace budou data správy sítě Bisq znovu načtena z počátečních uzlů a stav konsensu BSQ bude znovu vytvořen z genesis transakce. +setting.preferences.dao.resyncFromGenesis.resync=Resynchronizovat z genesis transakce a vypnout +setting.preferences.dao.isDaoFullNode=Spusťte Bisq jako full node DAO +setting.preferences.dao.rpcUser=Uživatelské jméno RPC +setting.preferences.dao.rpcPw=RPC heslo +setting.preferences.dao.blockNotifyPort=Blokovat oznamovací port +setting.preferences.dao.fullNodeInfo=Pro spuštění Bisq jako DAO full nodu musíte mít lokálně spuštěný Bitcoin Core a povoleno RPC. Všechny požadavky jsou dokumentovány v ''{0}''.\n\nPo změně režimu je třeba restartovat. +setting.preferences.dao.fullNodeInfo.ok=Otevřete stránku dokumentace +setting.preferences.dao.fullNodeInfo.cancel=Ne, zůstanu u režimu lite node +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.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.btcHeader=Bitcoinová síť +settings.net.p2pHeader=Síť Bisq +settings.net.onionAddressLabel=Moje onion adresa +settings.net.btcNodesLabel=Použijte vlastní Bitcoin Core node +settings.net.bitcoinPeersLabel=Připojené peer uzly +settings.net.useTorForBtcJLabel=Použít Tor pro Bitcoinovou síť +settings.net.bitcoinNodesLabel=Bitcoin Core nody, pro připojení +settings.net.useProvidedNodesRadio=Použijte nabízené Bitcoin Core nody +settings.net.usePublicNodesRadio=Použít veřejnou Bitcoinovou síť +settings.net.useCustomNodesRadio=Použijte vlastní Bitcoin Core node +settings.net.warn.usePublicNodes=Používáte-li veřejnou bitcoinovou síť, jste vystaveni závažnému problému s ochranou soukromí způsobenému porušením návrhu a implementace bloom filtru, který se používá pro peněženky SPV, jako je BitcoinJ (použitý v Bisq). Každý full node, ke kterému jste připojeni, mohl zjistit, že všechny vaše adresy peněženky patří jedné entitě.\n\nPřečtěte si více o podrobnostech na adrese: [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nOpravdu chcete použít veřejné nody? +settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené nody +settings.net.warn.usePublicNodes.usePublic=Ano, použít veřejnou síť +settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš bitcoinový node je důvěryhodný Bitcoin Core node!\n\nPřipojení k nodům, které nedodržují pravidla konsensu Bitcoin 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.invalidBtcConfig=Připojení k bitcoinové 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é bitcoinové uzly. Budete muset restartovat aplikaci. +settings.net.localhostBtcNodeInfo=Základní informace: Bisq při spuštění hledá místní Bitcoinový uzel. Pokud je nalezen, Bisq bude komunikovat se sítí Bitcoin výhradně skrze něj. +settings.net.p2PPeersLabel=Připojené uzly +settings.net.onionAddressColumn=Onion adresa +settings.net.creationDateColumn=Založeno +settings.net.connectionTypeColumn=Příchozí/Odchozí +settings.net.sentDataLabel=Statistiky odeslaných dat +settings.net.receivedDataLabel=Statistiky přijatých dat +settings.net.chainHeightLabel=Poslední výška bloku BTC +settings.net.roundTripTimeColumn=Roundtrip +settings.net.sentBytesColumn=Odesláno +settings.net.receivedBytesColumn=Přijato +settings.net.peerTypeColumn=Typ peer uzlu +settings.net.openTorSettingsButton=Otevřít nastavení Toru + +settings.net.versionColumn=Verze +settings.net.subVersionColumn=Subverze +settings.net.heightColumn=Výška + +settings.net.needRestart=Chcete-li použít tuto změnu, musíte restartovat aplikaci.\nChcete to udělat hned teď? +settings.net.notKnownYet=Není dosud známo... +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=Výška blockchainu - Bisq DAO: {0} | Bitcoin Peers: {1} +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.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.reSyncSPVChainLabel=Znovu synchronizovat SPV řetěz +settings.net.reSyncSPVChainButton=Odstraňte soubor SPV a znovu synchronizujte +settings.net.reSyncSPVSuccess=Opravdu chcete provést synchronizaci SPV? Pokud budete pokračovat, soubor řetězce SPV bude při příštím spuštění smazán.\n\nPo restartu může chvíli trvat, než se znovu provede synchronizuje se sítí a všechny transakce se zobrazí až po dokončení synchronizace.\n\nV závislosti na počtu transakcí a stáří vaší peněženky může resynchronizace trvat až několik hodin a spotřebuje 100% CPU. Nepřerušujte proces, jinak ho budete muset opakovat. +settings.net.reSyncSPVAfterRestart=Soubor řetězu SPV byl odstraněn. Prosím, buďte trpěliví. Resynchronizace se sítí může chvíli trvat. +settings.net.reSyncSPVAfterRestartCompleted=Resynchronizace je nyní dokončena. Restartujte aplikaci. +settings.net.reSyncSPVFailed=Nelze odstranit soubor řetězce SPV.\nChyba: {0} +setting.about.aboutBisq=O projektu Bisq +setting.about.about=Bisq je software s otevřeným zdrojovým kódem, který usnadňuje směnu bitcoinů 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 Bisq na naší webové stránce projektu. +setting.about.web=Webová stránka Bisq +setting.about.code=Zdrojový kód +setting.about.agpl=AGPL Licence +setting.about.support=Podpořte Bisq +setting.about.def=Bisq není společnost - je to projekt otevřený komunitě. Pokud se chcete zapojit nebo podpořit Bisq, postupujte podle níže uvedených odkazů. +setting.about.contribute=Přispět +setting.about.providers=Poskytovatelé dat +setting.about.apisWithFee=Bisq používá Bisq cenový index pro tržní ceny Fiat měn a Altcoinu a Bisq Mempool Nodes pro odhad poplatků za těžbu. +setting.about.apis=Bisq používá Bisq cenové indexy pro tržní ceny Fiat měn a Altcoinů. +setting.about.pricesProvided=Tržní ceny poskytované +setting.about.feeEstimation.label=Odhad poplatků za těžbu poskytl +setting.about.versionDetails=Podrobnosti o verzi +setting.about.version=Verze aplikace +setting.about.subsystems.label=Verze subsystémů +setting.about.subsystems.val=Verze sítě: {0}; Verze zpráv P2P: {1}; Verze lokální DB: {2}; Verze obchodního protokolu: {3} + +setting.about.shortcuts=Zkratky +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' nebo ''alt + {0}'' nebo ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Procházet hlavní nabídku +setting.about.shortcuts.menuNav.value=Pro pohyb v hlavním menu stiskněte: 'Ctrl' nebo 'alt' nebo 'cmd' s numerickou klávesou mezi '1-9' + +setting.about.shortcuts.close=Zavřít Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' nebo ''cmd + {0}'' nebo ''Ctrl + {1}'' nebo ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Zavřete vyskakovací nebo dialogové okno +setting.about.shortcuts.closePopup.value=Klávesa „ESCAPE“ + +setting.about.shortcuts.chatSendMsg=Odeslat obchodní soukromou zprávu +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' nebo ''alt + ENTER'' nebo ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Otevřít spor +setting.about.shortcuts.openDispute.value=Vyberte nevyřízený obchod a klikněte na: {0} + +setting.about.shortcuts.walletDetails=Otevřít okno s podrobností peněženky + +setting.about.shortcuts.openEmergencyBtcWalletTool=Otevřít nástroj nouzové peněženky pro BTC peněženku + +setting.about.shortcuts.openEmergencyBsqWalletTool=Otevřete nástroj nouzové peněženky pro BSQ peněženku + +setting.about.shortcuts.showTorLogs=Přepnout úroveň protokolu pro zprávy Tor mezi DEBUG a WARN + +setting.about.shortcuts.manualPayoutTxWindow=Otevřít okno pro manuální výběr z vkladu 2z2 Multisig tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Zveřejnit data správy DAO (návrhy, hlasy) + +setting.about.shortcuts.removeStuckTrade=Otevřít vyskakovací okno pro přesun neúspěšného obchodu zpět na kartu otevřených obchodů +setting.about.shortcuts.removeStuckTrade.value=Vyberte neúspěšný obchod a stiskněte: {0} + +setting.about.shortcuts.registerArbitrator=Registrovat rozhodce (pouze mediátor/rozhodce) +setting.about.shortcuts.registerArbitrator.value=Přejděte na účet a stiskněte: {0} + +setting.about.shortcuts.registerMediator=Registrovat mediátora (pouze mediátor/rozhodce) +setting.about.shortcuts.registerMediator.value=Přejděte na účet a stiskněte: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Otevřené okno pro podpis věku účtu (pouze starší rozhodci) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Přejděte ke starému zobrazení rozhodce a stiskněte: {0} + +setting.about.shortcuts.sendAlertMsg=Odeslat výstražnou nebo aktualizační zprávu (privilegovaná aktivita) + +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 BTC 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 Bisq 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á Bisq uzly průzkumníka spuštěné přispěvateli Bisq, 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 BTC 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 Bisq wiki: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +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 Bisq +account.info.msg=Zde můžete přidat obchodní účty pro národní měny & altcoiny a vytvořit zálohu dat vaší peněženky a účtu.\n\nPři prvním spuštění Bisq byla vytvořena nová bitcoinová peněženka.\n\nDůrazně doporučujeme zapsat si seed slova bitcoinový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 bitcoinů jsou spravovány v sekci \ "Finance \".\n\nOchrana osobních údajů a zabezpečení: protože Bisq 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 altcoinů a bitcoinu 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=Altcoinové účty +account.menu.password=Heslo peněženky +account.menu.seedWords=Seed peněženky +account.menu.walletInfo=Info o peněžence +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 BTC 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 Bisq 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 Bisq, a znemožnit tak provádění obchodů.\n\nNIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Bisq, 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 + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Veřejný klíč + +account.arbitratorRegistration.register=Registrovat +account.arbitratorRegistration.registration=Registrace {0} +account.arbitratorRegistration.revoke=Odvolat +account.arbitratorRegistration.info.msg=Upozorňujeme, že po odvolání musíte zůstat k dispozici 15 dní, protože mohou existovat obchody, pro které jste {0}. Max. povolené obchodní období je 8 dní a proces řešení sporu může trvat až 7 dní. +account.arbitratorRegistration.warn.min1Language=Musíte nastavit alespoň 1 jazyk.\nPřidali jsme vám výchozí jazyk. +account.arbitratorRegistration.removedSuccess=Úspěšně jste odstranili svou registraci ze sítě Bisq. +account.arbitratorRegistration.removedFailed=Registraci se nepodařilo odebrat. {0} +account.arbitratorRegistration.registerSuccess=Úspěšně jste se zaregistrovali do sítě Bisq. +account.arbitratorRegistration.registerFailed=Registraci se nepodařilo dokončit. {0} + +account.altcoin.yourAltcoinAccounts=Vaše altcoinové účty +account.altcoin.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.altcoin.popup.wallet.confirm=Rozumím a potvrzuji, že vím, jakou peněženku musím použít. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Obchodování s UPX na Bisq 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). +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Obchodování ARQ na Bisq 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). +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Obchodování s XMR na Bisq 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://bisq.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 Bisq 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://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Obchodování MSR na Bisq 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). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Obchodování BLUR na Bisq 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 Bisq. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Obchodování Solo na Bisq 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 Bisq. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Obchodování CASH2 na Bisq 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 Bisq. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Bisq 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 Bisq. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Obchodování Dragonglass na Bisq 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). +# suppress inspection "UnusedProperty" +account.altcoin.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. +# suppress inspection "UnusedProperty" +account.altcoin.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.altcoin.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\nBisq 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. +# suppress inspection "UnusedProperty" +account.altcoin.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. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin na Bisq 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 Bisq. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.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 Bisq, 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. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Obchodování s L-BTC na Bisq vyžaduje, abyste pochopili následující skutečnosti:\n\nKdyž přijímáte L-BTC za obchod na Bisq, nemůžete použít mobilní peněženku Green od Blockstreamu ani jinou custodial peněženku nebo peněženku na burze. L-BTC musíte přijmout pouze do peněženky Liquid Elements Core nebo do jiné L-BTC peněženky, která vám umožní získat slepý klíč pro vaši slepou adresu L-BTC.\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 Bisq nebo agentovi, který vrací peníze zaslat slepý klíč pro vaši L-BTC 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-BTC 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-BTC na Bisq. + +account.fiat.yourFiatAccounts=Vaše účty v národní měně + +account.backup.title=Zálohujte peněženku +account.backup.location=Umístění zálohy +account.backup.selectLocation=Zvolte umístění zálohy +account.backup.backupNow=Zálohujte nyní (záloha není šifrována!) +account.backup.appDir=Adresář dat aplikace +account.backup.openDirectory=Otevřít adresář +account.backup.openLogFile=Otevřít soubor log +account.backup.success=Záloha byla úspěšně uložena na:\n{0} +account.backup.directoryNotAccessible=Vybraný adresář není přístupný. {0} + +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 bitcoinů z vaší peněženky a při obnovení peněženky z seed slov. + +account.seed.backup.title=Zálohujte si seed slova peněženky +account.seed.info=Napište prosím seed slova peněženky a datum! Peněženku můžete kdykoli obnovit pomocí seed slov a datumu.\nStejná seed slova se používají pro peněženku BTC a BSQ.\n\nMěli byste si zapsat seed slova na list papíru. Neukládejte je do počítače.\n\nUpozorňujeme, že seed slova NEJSOU náhradou za zálohu.\nChcete-li obnovit stav a data aplikace, musíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\".\nImport seed slov se doporučuje pouze v naléhavých případech. Aplikace nebude funkční bez řádného zálohování databázových souborů a klíčů! +account.seed.backup.warning=Pamatujte, že seed slova NEJSOU náhradou za zálohu.\nChcete-li obnovit stav a data aplikace, musíte z obrazovky \"Účet/Záloha\" vytvořit zálohu celého adresáře aplikace.\nImport seed slov se doporučuje pouze v naléhavých případech. Bez řádného zálohování databázových souborů a klíčů nebude aplikace funkční!\n\nDalší informace najdete na wiki stránce [HYPERLINK:https://bisq.wiki/Backing_up_application_data]. +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.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 bitcoinovou 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.ok=Dobře, proveďte obnovu a vypněte Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Nastavení +account.notifications.download.label=Stáhnout mobilní aplikaci +account.notifications.waitingForWebCam=Čekání na webkameru... +account.notifications.webCamWindow.headline=Naskenujte QR kód z telefonu +account.notifications.webcam.label=Použijte webkameru +account.notifications.webcam.button=Naskenujte QR kód +account.notifications.noWebcam.button=Nemám webkameru +account.notifications.erase.label=Vymazat oznámení na telefonu +account.notifications.erase.title=Vymazat oznámení +account.notifications.email.label=Párovací token +account.notifications.email.prompt=Zadejte párovací token, který jste obdrželi e-mailem +account.notifications.settings.title=Nastavení +account.notifications.useSound.label=Přehrajte zvuk oznámení v telefonu +account.notifications.trade.label=Dostávat zprávy o obchodu +account.notifications.market.label=Dostávat upozornění na nabídky +account.notifications.price.label=Dostávat upozornění o cenách +account.notifications.priceAlert.title=Cenová upozornění +account.notifications.priceAlert.high.label=Upozorněte, pokud bude cena BTC nad +account.notifications.priceAlert.low.label=Upozorněte, pokud bude cena BTC 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 Bisq a začněte s platbou. +account.notifications.trade.message.msg.started=Kupující BTC 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 +account.notifications.offer.message.msg=Vaše nabídka s ID {0} byla přijata +account.notifications.dispute.message.title=Nová zpráva o sporu +account.notifications.dispute.message.msg=Obdrželi jste zprávu o sporu pro obchod s ID {0} + +account.notifications.marketAlert.title=Upozornění na nabídku +account.notifications.marketAlert.selectPaymentAccount=Nabídky odpovídající platebnímu účtu +account.notifications.marketAlert.offerType.label=Typ nabídky, o kterou mám zájem +account.notifications.marketAlert.offerType.buy=Nákupní nabídky (Chci prodat BTC) +account.notifications.marketAlert.offerType.sell=Prodejní nabídky (Chci si koupit BTC) +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 BTC, 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 +account.notifications.marketAlert.manageAlerts.title=Spravovat upozornění na nabídku +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Platební účet +account.notifications.marketAlert.manageAlerts.header.trigger=Limitní cena +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 Bisq 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.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 Bisq. +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. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Fakta & Čísla +dao.tab.bsqWallet=Peněženka BSQ +dao.tab.proposals=Vláda +dao.tab.bonding=Upisování +dao.tab.proofOfBurn=Poplatek za vedení aktiva/Důkaz spálení +dao.tab.monitor=Sledování sítě +dao.tab.news=Novinky + +dao.paidWithBsq=zaplacen BSQ +dao.availableBsqBalance=K dispozici pro výdaje (ověřené + nepotvrzené drobné výstupy) +dao.verifiedBsqBalance=Zůstatek všech ověřených UTXO +dao.unconfirmedChangeBalance=Zůstatek všech nepotvrzených drobných výstupů +dao.unverifiedBsqBalance=Zůstatek všech neověřených transakcí (čeká se na potvrzení bloku) +dao.lockedForVoteBalance=Použito pro hlasování +dao.lockedInBonds=Uzamčeno v úpisech +dao.availableNonBsqBalance=Dostupný zůstatek mimo BSQ (BTC) +dao.reputationBalance=Body zásluhy (nedají se utratit) + +dao.tx.published.success=Vaše transakce byla úspěšně zveřejněna. +dao.proposal.menuItem.make=Podat návrh +dao.proposal.menuItem.browse=Otevřené návrhy +dao.proposal.menuItem.vote=Hlasování o návrzích +dao.proposal.menuItem.result=Výsledky hlasování +dao.cycle.headline=Hlasovací cyklus +dao.cycle.overview.headline=Přehled hlasovacího cyklu +dao.cycle.currentPhase=Aktuální fáze +dao.cycle.currentBlockHeight=Aktuální výška bloku +dao.cycle.proposal=Fáze návrhu +dao.cycle.proposal.next=Další fáze návrhu +dao.cycle.blindVote=Fáze slepého hlasování +dao.cycle.voteReveal=Fáze odhalení hlasování +dao.cycle.voteResult=Výsledek hlasování +dao.cycle.phaseDuration={0} bloky (≈{1}); Bloky {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Blok {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Transakce odhalující hlasování zveřejněna +dao.voteReveal.txPublished=Vaše transakce odhalující hlasování s ID transakce {0} byla úspěšně zveřejněna.\n\nToto se provádí automaticky, pokud jste se zúčastnili hlasování DAO. + +dao.results.cycles.header=Cykly +dao.results.cycles.table.header.cycle=Cyklus +dao.results.cycles.table.header.numProposals=Návrhy +dao.results.cycles.table.header.voteWeight=Váha hlasování +dao.results.cycles.table.header.issuance=Emise + +dao.results.results.table.item.cycle=Cyklus {0} začal: {1} + +dao.results.proposals.header=Návrhy vybraného cyklu +dao.results.proposals.table.header.nameLink=Jméno/odkaz +dao.results.proposals.table.header.details=Detaily +dao.results.proposals.table.header.myVote=Můj hlas +dao.results.proposals.table.header.result=Výsledek hlasování +dao.results.proposals.table.header.threshold=Práh +dao.results.proposals.table.header.quorum=Kvórum + +dao.results.proposals.voting.detail.header=Výsledky hlasování pro vybraný návrh + +dao.results.exceptions=Výjimky výsledku hlasování + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Nedefinováno + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=Poplatek tvůrce BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=Poplatek příjemce BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Min. poplatek tvůrce BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Min. poplatek příjemce BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Poplatek tvůrce BTC +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Poplatek příjemce BTC +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Min. poplatek tvůrce BTC +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Min. poplatek příjemce BTC +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Poplatek za návrh v BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Hlasovací poplatek v BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Žádost o odměnu - min. částka BSQ +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Žádost o odměnu - max. částka BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Žádost o vyrovnání min. částka BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Žádost o vyrovnání max. částka BSQ + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Požadované kvórum v BSQ pro obecný návrh +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Požadované kvórum v BSQ pro žádost o odměnu +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Požadované kvórum v BSQ pro žádost o vyrovnání +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Požadované kvórum v BSQ pro změnu parametru +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Požadované kvórum v BSQ pro odebrání aktiva +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Požadované kvórum v BSQ pro žádost o konfiskaci +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Požadované kvórum v BSQ pro žádost o upsání + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Požadovaná prahová hodnota v % pro obecný návrh +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Požadovaná prahová hodnota v % pro žádost o odměnu +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Požadovaná prahová hodnota v % pro žádost o vyrovnání +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Požadovaná prahová hodnota v % pro změnu parametru +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Požadovaná prahová hodnota v % pro odebrání aktiva +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Požadovaná prahová hodnota v % pro žádost o konfiskaci +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Požadovaná prahová hodnota v % pro žádost o úpis + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=BTC adresa příjemce + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Poplatek za vedení aktiva za den +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Min. objem obchodu s aktivy + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Doba uzamčení pro alternativní výplaty obchodu tx +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Poplatek rozhodce v BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Max. obchodní limit v BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Jednotkový faktor úpisu v BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Emisní limit cyklu v BSQ + +dao.param.currentValue=Aktuální hodnota: {0} +dao.param.currentAndPastValue=Aktuální hodnota: {0} (Hodnota v okamžiku vytvoření návrhu: {1}) +dao.param.blocks={0} bloků + +dao.results.invalidVotes=V tomto hlasovacím cyklu jsme měli neplatné hlasy. To se může stát, pokud hlas nebyl v síti Bisq dobře distribuován.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Fáze návrhu +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Přestávka 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Fáze slepého hlasování +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Přestávka 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Fáze odhalení hlasování +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Přestávka 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Výsledná fáze + +dao.results.votes.table.header.stakeAndMerit=Váha hlasování +dao.results.votes.table.header.stake=Vklad +dao.results.votes.table.header.merit=Vyděláno +dao.results.votes.table.header.vote=Hlas + +dao.bond.menuItem.bondedRoles=Role s úpisy +dao.bond.menuItem.reputation=Vaše úpisy +dao.bond.menuItem.bonds=Všechny úpisy + +dao.bond.dashboard.bondsHeadline=Upsané BSQ +dao.bond.dashboard.lockupAmount=Zamknout prostředky +dao.bond.dashboard.unlockingAmount=Uvolnění prostředků (počkejte, dokud neuplyne doba uzamčení) + + +dao.bond.reputation.header=Zamknout úpis pro reputaci +dao.bond.reputation.table.header=Moje reputační úpisy +dao.bond.reputation.amount=Množství BSQ na uzamčení +dao.bond.reputation.time=Čas odemčení v blocích +dao.bond.reputation.salt=Salt +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Zamknout +dao.bond.reputation.lockup.headline=Potvrďte uzamčení transakce +dao.bond.reputation.lockup.details=Uzamčená částka: {0}\nČas odemknutí: {1} blok(ů) (≈ {2})\n\nPoplatek za těžbu: {3} ({4} Satoshis/vbyte)\nTransakční vsize: {5} Kb\n\nOpravdu chcete pokračovat? +dao.bond.reputation.unlock.headline=Potvrďte odemknutí transakce +dao.bond.reputation.unlock.details=Odemknout částku: {0}\nČas odemknutí: {1} blok(ů) (≈ {2})\n\nPoplatek za těžbu: {3} ({4} Satoshis/vbyte)\nTransakční vsize: {5} vKb\n\nOpravdu chcete pokračovat? + +dao.bond.allBonds.header=Všechny úpisy + +dao.bond.bondedReputation=Úpis reputace +dao.bond.bondedRoles=Role s úpisy + +dao.bond.details.header=Podrobnosti role +dao.bond.details.role=Role +dao.bond.details.requiredBond=Požadované BSQ úpisy +dao.bond.details.unlockTime=Čas odemčení v blocích +dao.bond.details.link=Odkaz na popis role +dao.bond.details.isSingleton=Může být přijato více držiteli rolí +dao.bond.details.blocks={0} bloků + +dao.bond.table.column.name=Jméno +dao.bond.table.column.link=Odkaz +dao.bond.table.column.bondType=Typ úpisu +dao.bond.table.column.details=Detaily +dao.bond.table.column.lockupTxId=Tx ID úpisu +dao.bond.table.column.bondState=Stav úpisu +dao.bond.table.column.lockTime=Čas odemknutí +dao.bond.table.column.lockupDate=Datum uzamčení + +dao.bond.table.button.lockup=Zamknout +dao.bond.table.button.unlock=Odemknout +dao.bond.table.button.revoke=Odvolat + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Zatím není úpis +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Uzamčení čeká na vyřízení +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Zamčený úpis +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Odemčení čeká na vyřízení +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Odemknutí tx potvrzeno +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Odblokování úpisů +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Odemčený úpis +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Úpis konfiskován + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Úpis +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Úpis reputace + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=GitHub admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Forum admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq správce +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-fork správce +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer správce +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Správce webu +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Operátor fóra +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Operátor seed nodu +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Operátor cenového nodu +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Operátor Bitcoinového nodu +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Operátor trhů +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Provozovatel průzkumníka +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Operátor přenosu mobilních oznámení +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Držitel domény +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediátor +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Rozhodce +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Majitel dárcovské adresy BTC + +dao.burnBsq.assetFee=Vedení aktiva +dao.burnBsq.menuItem.assetFee=Poplatek za vedení aktiva +dao.burnBsq.menuItem.proofOfBurn=Důkaz spálení +dao.burnBsq.header=Poplatek za vedení aktiva +dao.burnBsq.selectAsset=Vybrat aktivum +dao.burnBsq.fee=Poplatek +dao.burnBsq.trialPeriod=Zkušební doba +dao.burnBsq.payFee=Zaplatit poplatek +dao.burnBsq.allAssets=Všechna aktiva +dao.burnBsq.assets.nameAndCode=Jméno aktiva +dao.burnBsq.assets.state=Stav +dao.burnBsq.assets.tradeVolume=Objem obchodu +dao.burnBsq.assets.lookBackPeriod=Období ověření +dao.burnBsq.assets.trialFee=Poplatek za zkušební období +dao.burnBsq.assets.totalFee=Celkové zaplacené poplatky +dao.burnBsq.assets.days={0} dní +dao.burnBsq.assets.toFewDays=Poplatek za aktivum je příliš nízký. Min. počet dnů pro zkušební období je {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Ve zkušebním období +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Aktivně obchodováno +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Odstranění ze seznamu kvůli nečinnosti +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Odebráno hlasováním + +dao.proofOfBurn.header=Důkaz spálení +dao.proofOfBurn.amount=Množství +dao.proofOfBurn.preImage=Předloha +dao.proofOfBurn.burn=Spálit +dao.proofOfBurn.allTxs=Všechny transakce dokazující spálení +dao.proofOfBurn.myItems=Moje důkazy spálení +dao.proofOfBurn.date=Datum +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transakce +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Podepište zprávu klíčem z transakce dokazující spálení +dao.proofOfBurn.verify.window.title=Ověřte zprávu pomocí klíče z transakce dokazující spálení +dao.proofOfBurn.copySig=Zkopírujte podpis do schránky +dao.proofOfBurn.sign=Podepsat +dao.proofOfBurn.message=Zpráva +dao.proofOfBurn.sig=Podpis +dao.proofOfBurn.verify=Ověřit +dao.proofOfBurn.verificationResult.ok=Ověření proběhlo úspěšně +dao.proofOfBurn.verificationResult.failed=Ověření se nezdařilo + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Fáze návrhu +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Přestávka před fází slepého hlasování +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Fáze slepého hlasování +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Přestávka před fází odhalení hlasování +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Fáze odhalení hlasování +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Přestávka před výslednou fází +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Hlasujte ve výsledné fázi + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Fáze návrhu +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Slepé hlasování +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Odhalení hlasování +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Výsledek hlasování + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Žádost o odměnu +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Žádost o vyrovnání +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Žádost o úpis +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Návrh na odstranění aktiva +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Návrh na změnu parametru +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Obecný návrh +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Žádost o konfiskaci úpisu + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Žádost o odměnu +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Žádost o vyrovnání +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Úpis +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Odstranění altcoinu +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Změna parametru +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Obecný návrh +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Konfiskovat úpis + +dao.proposal.details=Detaily návrhu +dao.proposal.selectedProposal=Vybraný návrh +dao.proposal.active.header=Návrhy současného cyklu +dao.proposal.active.remove.confirm=Opravdu chcete tento návrh odebrat?\nJiž zaplacený poplatek za návrh bude ztracen. +dao.proposal.active.remove.doRemove=Ano, odeberte můj návrh +dao.proposal.active.remove.failed=Návrh nelze odebrat. +dao.proposal.myVote.title=Hlasování +dao.proposal.myVote.accept=Přijmout návrh +dao.proposal.myVote.reject=Odmítnout návrh +dao.proposal.myVote.removeMyVote=Ignorovat návrh +dao.proposal.myVote.merit=Hlasovací váha ze získaného BSQ +dao.proposal.myVote.stake=Hlasovací váha z vkladu +dao.proposal.myVote.revealTxId=Hlasování odhalí ID transakce +dao.proposal.myVote.stake.prompt=Max. dostupný vklad pro hlasování: {0} +dao.proposal.votes.header=Nastavte vklad pro hlasování a zveřejněte své hlasy +dao.proposal.myVote.button=Zveřejnit hlasy +dao.proposal.myVote.setStake.description=Po hlasování o všech návrzích musíte nastavit svůj vklad pro hlasování zamknutím BSQ. Čím více BSQ zamknete, tím větší váhu bude mít váš hlas.\n\nBSQ uzamčené pro hlasování bude znovu odemčeno během fáze odhalení hlasování. +dao.proposal.create.selectProposalType=Vyberte typ nabídky +dao.proposal.create.phase.inactive=Počkejte prosím do další fáze návrhu +dao.proposal.create.proposalType=Typ nabídky +dao.proposal.create.new=Vytvořte nový návrh +dao.proposal.create.button=Navrhněte +dao.proposal.create.publish=Zveřejnit návrh +dao.proposal.create.publishing=Probíhá zveřejnění nabídek... +dao.proposal=návrh +dao.proposal.display.type=Typ nabídky +dao.proposal.display.name=Přesné uživatelské jméno na GitHub +dao.proposal.display.link=Odkaz na podrobné informace +dao.proposal.display.link.prompt=Odkaz na návrh +dao.proposal.display.requestedBsq=Požadovaná částka v BSQ +dao.proposal.display.txId=ID transakce návrhu +dao.proposal.display.proposalFee=Poplatek za návrh +dao.proposal.display.myVote=Můj hlas +dao.proposal.display.voteResult=Souhrn výsledku hlasování +dao.proposal.display.bondedRoleComboBox.label=Typ úpisu +dao.proposal.display.requiredBondForRole.label=Požadovaný úpis +dao.proposal.display.option=Možnost + +dao.proposal.table.header.proposalType=Typ nabídky +dao.proposal.table.header.link=Odkaz +dao.proposal.table.header.myVote=Můj hlas +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Odstranit +dao.proposal.table.icon.tooltip.removeProposal=Odebrat můj návrh +dao.proposal.table.icon.tooltip.changeVote=Aktuální hlas: ''{0}''. Změnit hlas na: ''{1}'' + +dao.proposal.display.myVote.accepted=Přijato +dao.proposal.display.myVote.rejected=Odmítnuto +dao.proposal.display.myVote.ignored=Ignorováno +dao.proposal.display.myVote.unCounted=Hlasování nebylo zahrnuto do výsledku +dao.proposal.myVote.summary=Hlasováno: {0}; Hlasovací váha: {1} (získané: {2} + vklad: {3}) {4} +dao.proposal.myVote.invalid=Hlasování bylo neplatné + +dao.proposal.voteResult.success=Přijato +dao.proposal.voteResult.failed=Odmítnuto +dao.proposal.voteResult.summary=Výsledek: {0}; Prahová hodnota: {1} (požadováno > {2}); Kvórum: {3} (požadováno > {4}) + +dao.proposal.display.paramComboBox.label=Vyberte parametr, který chcete změnit +dao.proposal.display.paramValue=Hodnota parametru + +dao.proposal.display.confiscateBondComboBox.label=Zvolte úpis +dao.proposal.display.assetComboBox.label=Aktivum k odstranění + +dao.blindVote=slepý hlas + +dao.blindVote.startPublishing=Publikování transakce se slepým hlasováním ... +dao.blindVote.success=Vaše transakce se slepým hlasováním byla úspěšně zveřejněna.\n\nVezměte prosím na vědomí, že musíte být online ve fázi odhalení hlasování, aby vaše aplikace Bisq mohla zveřejnit transakci odhalení hlasování. Bez transakce odhalení hlasování by byl váš hlas neplatný! + +dao.wallet.menuItem.send=Odeslat +dao.wallet.menuItem.receive=Přijmout +dao.wallet.menuItem.transactions=Transakce + +dao.wallet.dashboard.myBalance=Můj zůstatek v peněžence + +dao.wallet.receive.fundYourWallet=Vaše přijímací BSQ adresa +dao.wallet.receive.bsqAddress=Adresa peněženky BSQ (čerstvá nepoužitá adresa) + +dao.wallet.send.sendFunds=Poslat finanční prostředky +dao.wallet.send.sendBtcFunds=Odeslat prostředky jiné než BSQ (BTC) +dao.wallet.send.amount=Částka v BSQ +dao.wallet.send.btcAmount=Částka v BTC (jiná než BSQ prostředky) +dao.wallet.send.setAmount=Nastavit částku k výběru (minimální částka je {0}) +dao.wallet.send.receiverAddress=BSQ adresa příjemce +dao.wallet.send.receiverBtcAddress=BTC adresa příjemce +dao.wallet.send.setDestinationAddress=Vyplňte svou cílovou adresu +dao.wallet.send.send=Pošlete BSQ prostředky +dao.wallet.send.inputControl=Vybrat vstupy +dao.wallet.send.sendBtc=Pošlete BTC prostředky +dao.wallet.send.sendFunds.headline=Potvrďte žádost o výběr +dao.wallet.send.sendFunds.details=Odesílání: {0}\nNa adresu pro příjem: {1}.\nPožadovaný poplatek za těžbu je: {2} ({3} satoshi/vbyte)\nVelikost transakce: {4} vKb\n\nPříjemce obdrží: {5}\n\nOpravdu chcete tuto částku vybrat? +dao.wallet.chainHeightSynced=Poslední ověřený blok: {0} +dao.wallet.chainHeightSyncing=Čekání na bloky... Ověřeno {0} bloků z {1} +dao.wallet.tx.type=Typ + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Nedefinováno +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Nerozpoznáno +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Neověřená transakce BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Neplatná transakce BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Genesis transakce +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Převod BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=Přijaté BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=Odeslané BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Obchodní poplatek +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Poplatek za žádost o odměnu +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Poplatek za žádost o vyrovnání +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Poplatek za návrh +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Poplatek za slepé hlasování +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Odhalení hlasování +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Zamknout úpis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Odemknout úpis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Poplatek za vedení aktiva +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Důkaz spálení +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Nepravidelný + +dao.tx.withdrawnFromWallet=BTC vybrané z peněženky +dao.tx.issuanceFromCompReq=Vydání odměny +dao.tx.issuanceFromCompReq.tooltip=Žádost o odměnu, která vedla k vydání nového BSQ.\nDatum vydání: {0} +dao.tx.issuanceFromReimbursement=Vydání vyrovnání +dao.tx.issuanceFromReimbursement.tooltip=Žádost o vyrovnání, která vedla k vydání nového BSQ.\nDatum vydání: {0} +dao.proposal.create.missingBsqFunds=Pro vytvoření návrhu nemáte dostatečné prostředky BSQ. Pokud máte nepotvrzenou transakci BSQ, musíte počkat na potvrzení na blockchainu, protože BSQ je validováno, pouze pokud je zahrnuto v bloku.\nChybí: {0} + +dao.proposal.create.missingBsqFundsForBond=Pro tuto roli nemáte dostatečné prostředky BSQ. Tento návrh můžete stále zveřejnit, ale pokud bude přijat, budete potřebovat celou částku BSQ potřebnou pro tuto roli.\nChybí: {0} + +dao.proposal.create.missingMinerFeeFunds=Nemáte dostatečné prostředky BTC pro vytvoření transakce návrhu. Všechny transakce BSQ vyžadují poplatek za těžbu v BTC.\nChybí: {0} + +dao.proposal.create.missingIssuanceFunds=Nemáte dostatečné prostředky BTC pro vytvoření transakce návrhu. Všechny transakce BSQ vyžadují poplatek za těžbu v BTC a emisní transakce také vyžadují BTC pro požadovanou částku BSQ ({0} Satoshi/BSQ).\nChybí: {1} + +dao.feeTx.confirm=Potvrďte {0} transakci +dao.feeTx.confirm.details={0} poplatek: {1}\nPoplatek za těžbu: {2} ({3} Satoshi/vbyte)\nTransakční vsize: {4} vKb\n\nOpravdu chcete publikovat transakci {5}? + +dao.feeTx.issuanceProposal.confirm.details={0} poplatek: {1}\nPro vydání BSQ je potřeba BTC: {2} ({3} Satoshi/BSQ)\nPoplatek za těžbu: {4} ({5} Satoshi/vbyte)\nTransakční vsize: {6} vKb\n\nPokud bude vaše žádost schválena, obdržíte požadovanou částku bez poplatku za návrh 2 BSQ.\n\nOpravdu chcete publikovat transakci {7}? + +dao.news.bisqDAO.title=BISQ DAO +dao.news.bisqDAO.description=Stejně jako je burza Bisq decentralizovaná a odolná vůči cenzuře, tak její model řízení - a Bisq DAO a BSQ token jsou nástroje, které to umožňují. +dao.news.bisqDAO.readMoreLink=Dozvědět se více o Bisq DAO + +dao.news.pastContribution.title=PŘISPĚLI JSTE V MINULOSTI? POŽÁDEJTE O BSQ +dao.news.pastContribution.description=Pokud jste přispěli do projektu Bisq, použijte prosím níže uvedenou adresu BSQ a požádejte o účast na distribuci prvních BSQ. +dao.news.pastContribution.yourAddress=Adresa vaší BSQ peněženky +dao.news.pastContribution.requestNow=Požádat hned + +dao.news.DAOOnTestnet.title=SPUSŤTE BISQ DAO NA NAŠEM TESTNETU +dao.news.DAOOnTestnet.description=Síť Bisq DAO ještě nebyla spuštěn, ale o Bisq DAO se můžete dozvědět jeho spuštěním na našem testnetu. +dao.news.DAOOnTestnet.firstSection.title=1. Přepněte do režimu DAO Testnet +dao.news.DAOOnTestnet.firstSection.content=Na obrazovce Nastavení přepněte na DAO Testnet. +dao.news.DAOOnTestnet.secondSection.title=2. Získejte některé BSQ +dao.news.DAOOnTestnet.secondSection.content=Vyžádejte si BSQ na Slacku nebo kupte BSQ na Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Zúčastněte se hlasovacího cyklu +dao.news.DAOOnTestnet.thirdSection.content=Předkládání návrhů a hlasování o návrzích na změnu různých aspektů Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Prozkoumejte BSQ Blok Explorer +dao.news.DAOOnTestnet.fourthSection.content=Protože BSQ je jen bitcoin, můžete vidět BSQ transakce na našem bitcoinovém blok exploreru. +dao.news.DAOOnTestnet.readMoreLink=Přečtěte si celou dokumentaci + +dao.monitor.daoState=Stav DAO +dao.monitor.proposals=Stav návrhů +dao.monitor.blindVotes=Stav slepých hlasů + +dao.monitor.table.peers=Peer uzly +dao.monitor.table.conflicts=Konflikty +dao.monitor.state=Stav +dao.monitor.requestAlHashes=Vyžádat si všechny hashe +dao.monitor.resync=Znovu synchronizovat stav DAO +dao.monitor.table.header.cycleBlockHeight=Cyklus / Výška bloku +dao.monitor.table.cycleBlockHeight=Cyklus {0} / blok {1} +dao.monitor.table.seedPeers=Seed node: {0} + +dao.monitor.daoState.headline=Stav DAO +dao.monitor.daoState.table.headline=Řetězec hashů stavu DAO +dao.monitor.daoState.table.blockHeight=Výška bloku +dao.monitor.daoState.table.hash=Hash stavu DAO +dao.monitor.daoState.table.prev=Předchozí hash +dao.monitor.daoState.conflictTable.headline=Hashe stavu DAO od partnerů v konfliktu +dao.monitor.daoState.utxoConflicts=Konflikt UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Výška bloku: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Součet všech UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Součet všech BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=Stav DAO není synchronizován se sítí. Po restartu se stav DAO znovu synchronizuje. + +dao.monitor.proposal.headline=Stav návrhů +dao.monitor.proposal.table.headline=Řetězec hashů stavu návrhu +dao.monitor.proposal.conflictTable.headline=Navrhované stavy hashů od partnerů v konfliktu + +dao.monitor.proposal.table.hash=Hash stavu návrhu +dao.monitor.proposal.table.prev=Předchozí hash +dao.monitor.proposal.table.numProposals=Počet návrhů + +dao.monitor.isInConflictWithSeedNode=Vaše lokální data nesouhlasí s alespoň jedním seed nodem. Synchronizujte znovu stav DAO. +dao.monitor.isInConflictWithNonSeedNode=Jeden z vašich peerů není v konsenzu se sítí, ale váš node je synchronizován se seed nody. +dao.monitor.daoStateInSync=Váš lokální node je v konsenzu se sítí + +dao.monitor.blindVote.headline=Stav slepých hlasů +dao.monitor.blindVote.table.headline=Řetězec hashů stavu slepého hlasování +dao.monitor.blindVote.conflictTable.headline=Hashe stavu slepého hlasování od partnerů v konfliktu +dao.monitor.blindVote.table.hash=Hash stavu slepého hlasování +dao.monitor.blindVote.table.prev=Předchozí hash +dao.monitor.blindVote.table.numBlindVotes=Počet slepých hlasování + +dao.factsAndFigures.menuItem.supply=Nabídka BSQ +dao.factsAndFigures.menuItem.transactions=Transakce BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Průměrná obchodní cena BSQ/BTC za 90 dní +dao.factsAndFigures.dashboard.avgPrice30=Průměrná obchodní cena BSQ/BTC za 30 dní +dao.factsAndFigures.dashboard.avgUSDPrice90=90denní objemově vážená průměrná cena BSQ/USD +dao.factsAndFigures.dashboard.avgUSDPrice30=30denní objemově vážená průměrná cena BSQ/USD +dao.factsAndFigures.dashboard.marketCap=Tržní kapitalizace (na základě průměrné ceny BSQ/USD za posledních 30 dní) +dao.factsAndFigures.dashboard.availableAmount=Celkem k dispozici BSQ +dao.factsAndFigures.dashboard.volumeUsd=Celkový objem obchodů v USD +dao.factsAndFigures.dashboard.volumeBtc=Celkový objem obchodů v BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Průměrný kurz BSQ/USD obchodů uzavřených ve zvoleném časovém intervalu +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Průměrný kurz BSQ/BTC obchodů uzavřených ve zvoleném časovém intervalu + +dao.factsAndFigures.supply.issuedVsBurnt=Vydaných BSQ vs. Spálených BSQ + +dao.factsAndFigures.supply.issued=Vydáno BSQ +dao.factsAndFigures.supply.compReq=Žádosti o odměnu +dao.factsAndFigures.supply.reimbursement=Žádosti o vyrovnání +dao.factsAndFigures.supply.genesisIssueAmount=BSQ vydané při první (genesis) transakci +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ vydáno na žádosti o odměnu +dao.factsAndFigures.supply.reimbursementAmount=BSQ vydáno na žádosti o vyrovnání +dao.factsAndFigures.supply.totalIssued=Celkem vydáno BSQ +dao.factsAndFigures.supply.totalBurned=Celkem spáleno BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=Spálených BSQ + +dao.factsAndFigures.supply.priceChat=BSQ cena +dao.factsAndFigures.supply.volumeChat=Objem obchodu +dao.factsAndFigures.supply.tradeVolumeInUsd=Objem obchodů v USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Objem obchodů v BTC +dao.factsAndFigures.supply.bsqUsdPrice=kurz BSQ/USD +dao.factsAndFigures.supply.bsqBtcPrice=kurz BSQ/BTC +dao.factsAndFigures.supply.btcUsdPrice=kurz BTC/USD + +dao.factsAndFigures.supply.locked=Globální stav uzamčených BSQ +dao.factsAndFigures.supply.totalLockedUpAmount=Zamčeno v úpisech +dao.factsAndFigures.supply.totalUnlockingAmount=Odemykání BSQ z úpisů +dao.factsAndFigures.supply.totalUnlockedAmount=Odemčené BSQ z úpisů +dao.factsAndFigures.supply.totalConfiscatedAmount=Konfiskované BSQ z úpisů +dao.factsAndFigures.supply.proofOfBurn=Důkaz spálení +dao.factsAndFigures.supply.bsqTradeFee=BSQ poplatky +dao.factsAndFigures.supply.btcTradeFee=BTC obchodní poplatky + +dao.factsAndFigures.transactions.genesis=Genesis transakce +dao.factsAndFigures.transactions.genesisBlockHeight=Výška počátečního (genesis) bloku +dao.factsAndFigures.transactions.genesisTxId=ID genesis transakce +dao.factsAndFigures.transactions.txDetails=Statistiky transakcí BSQ +dao.factsAndFigures.transactions.allTx=Počet všech transakcí BSQ +dao.factsAndFigures.transactions.utxo=Počet všech nevyčerpaných transakčních výstupů +dao.factsAndFigures.transactions.compensationIssuanceTx=Počet všech transakcí s vydáním odměn +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Počet všech transakcí s vydáním vyrovnání +dao.factsAndFigures.transactions.burntTx=Počet všech poplatků platebních transakcí +dao.factsAndFigures.transactions.invalidTx=Počet všech neplatných transakcí +dao.factsAndFigures.transactions.irregularTx=Počet všech nepravidelných transakcí + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Vyberte vstupy pro transakci +inputControlWindow.balanceLabel=Dostupný zůstatek + +contractWindow.title=Podrobnosti o sporu +contractWindow.dates=Datum nabídky / Datum obchodu +contractWindow.btcAddresses=Bitcoinová adresa kupujícího BTC / prodávajícího BTC +contractWindow.onions=Síťová adresa kupující BTC / prodávající BTC +contractWindow.accountAge=Stáří účtu BTC kupující / BTC prodejce +contractWindow.numDisputes=Počet sporů BTC kupující / BTC prodejce +contractWindow.contractHash=Hash kontraktu + +displayAlertMessageWindow.headline=Důležitá informace! +displayAlertMessageWindow.update.headline=Důležité informace o aktualizaci! +displayAlertMessageWindow.update.download=Stáhnout: +displayUpdateDownloadWindow.downloadedFiles=Soubory: +displayUpdateDownloadWindow.downloadingFile=Stahuji: {0} +displayUpdateDownloadWindow.verifiedSigs=Podpis ověřen pomocí klíčů: +displayUpdateDownloadWindow.status.downloading=Stahuji soubory... +displayUpdateDownloadWindow.status.verifying=Ověřování podpisu ... +displayUpdateDownloadWindow.button.label=Stáhněte si instalační program a ověřte podpis +displayUpdateDownloadWindow.button.downloadLater=Stáhnout později +displayUpdateDownloadWindow.button.ignoreDownload=Ignorovat tuto verzi +displayUpdateDownloadWindow.headline=K dispozici je nová aktualizace Bisq! +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://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\nStáhněte a ručně ověřte na adrese [HYPERLINK:https://bisq.network/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.openDir=Otevřít adresář ke stažení + +disputeSummaryWindow.title=Souhrn +disputeSummaryWindow.openDate=Datum otevření úkolu +disputeSummaryWindow.role=Role obchodníka +disputeSummaryWindow.payout=Výplata částky obchodu +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} dostane výplatu částky obchodu +disputeSummaryWindow.payout.getsAll=BTC {0} dostane maximální výplatu +disputeSummaryWindow.payout.custom=Vlastní výplata +disputeSummaryWindow.payoutAmount.buyer=Výše výplaty kupujícího +disputeSummaryWindow.payoutAmount.seller=Výše výplaty prodejce +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 + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Chyba +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Použitelnost +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Porušení protokolu +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Bez odpovědi +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Podvod +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Jiný +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banka +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Obchodování opcí +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Obchodník neodpovídá +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Špatný účet odesílatele +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Obchodní partner se opozdil +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Obchod je již vypořádán + +disputeSummaryWindow.summaryNotes=Souhrnné poznámky +disputeSummaryWindow.addSummaryNotes=Přidejte souhrnné poznámky +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 BTC: {6}\nVýplatní částka pro prodejce BTC: {7}\n\nDůvod sporu: {8}\n\nSouhrnné 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.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} ({4} satoshi/vbyte)\nTransakční vsize: {5} vKb\n\nOpravdu 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? + +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 Bisq, abychom mohli prozkoumat, co způsobilo problém. +emptyWalletWindow.balance=Váš zůstatek v peněžence +emptyWalletWindow.bsq.btcBalance=Zůstatek satoshi jiných než BSQ + +emptyWalletWindow.address=Vaše cílová adresa +emptyWalletWindow.button=Pošlete všechny prostředky +emptyWalletWindow.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud vyprázdníte peněženku.\nOpravdu chcete vyprázdnit peněženku? +emptyWalletWindow.openOffers.yes=Ano, jsem si jistý +emptyWalletWindow.sent.success=Zůstatek vaší peněženky byl úspěšně přenesen. + +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.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.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.btcNode=Filtrované Bitcoinové nody (adresy+porty oddělené čárkami) +filterWindow.preventPublicBtcNetwork=Zabraňte použití veřejné bitcoinové sítě +filterWindow.disableDao=Zakázat DAO +filterWindow.disableAutoConf=Zakázat automatické potvrzení +filterWindow.autoConfExplorers=Filtrované průzkumníky s automatickým potvrzením (adresy oddělené čárkami) +filterWindow.disableDaoBelowVersion=Min. verze nutná pro DAO +filterWindow.disableTradeBelowVersion=Min. verze nutná pro obchodování +filterWindow.add=Přidat filtr +filterWindow.remove=Zrušit filtr +filterWindow.btcFeeReceiverAddresses=Adresy příjemců poplatků BTC +filterWindow.disableApi=Deaktivovat API +filterWindow.disableMempoolValidation=Deaktivovat validaci mempoolu + +offerDetailsWindow.minBtcAmount=Min. částka BTC +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} bitcoin +offerDetailsWindow.confirm.taker=Potvrďte: Využít nabídku {0} bitcoin +offerDetailsWindow.creationDate=Datum vzniku +offerDetailsWindow.makersOnion=Onion adresa tvůrce + +qRCodeWindow.headline=QR Kód +qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Bisq 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.select=Vyberte vkladovou transakci + +sendAlertMessageWindow.headline=Odeslat globální oznámení +sendAlertMessageWindow.alertMsg=Výstražná zpráva +sendAlertMessageWindow.enterMsg=Zadejte zprávu +sendAlertMessageWindow.isSoftwareUpdate=Oznámení o nové verzi software +sendAlertMessageWindow.isUpdate=Plná verze +sendAlertMessageWindow.isPreRelease=Beta verze +sendAlertMessageWindow.version=Číslo nové verze +sendAlertMessageWindow.send=Odeslat oznámení +sendAlertMessageWindow.remove=Odebrat oznámení + +sendPrivateNotificationWindow.headline=Odeslat soukromou zprávu +sendPrivateNotificationWindow.privateNotification=Soukromé oznámení +sendPrivateNotificationWindow.enterNotification=Zadejte oznámení +sendPrivateNotificationWindow.send=Odeslat soukromé oznámení + +showWalletDataWindow.walletData=Data peněženky +showWalletDataWindow.includePrivKeys=Zahrnout soukromé klíče + +setXMRTxKeyWindow.headline=Prokázat odeslání XMR +setXMRTxKeyWindow.note=Přidání tx informací níže umožní automatické potvrzení pro rychlejší obchody. Zobrazit více: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=ID transakce (volitelné) +setXMRTxKeyWindow.txKey=Transakční klíč (volitelný) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Uživatelská dohoda +tacWindow.agree=Souhlasím +tacWindow.disagree=Nesouhlasím a odcházím +tacWindow.arbitrationSystem=Řešení sporů + +tradeDetailsWindow.headline=Obchod +tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce: +tradeDetailsWindow.tradeDate=Datum obchodu +tradeDetailsWindow.txFee=Poplatek za těžbu +tradeDetailsWindow.tradingPeersOnion=Onion adresa obchodního partnera +tradeDetailsWindow.tradingPeersPubKeyHash=Pubkey hash obchodních partnerů +tradeDetailsWindow.tradeState=Stav obchodu +tradeDetailsWindow.agentAddresses=Rozhodce/Mediátor +tradeDetailsWindow.detailData=Detailní data + +txDetailsWindow.headline=Detaily transakce +txDetailsWindow.btc.note=Poslali jste BTC. +txDetailsWindow.bsq.note=Poslali jste BSQ. BSQ je tzv. obarvený bitcoin, takže tato transakce bude viditelná v BSQ exploreru až poté, co bude potvrzena zařazením do bitcoin bloku. +txDetailsWindow.sentTo=Odesláno na +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Souhrn uzavřených obchodů +closedTradesSummaryWindow.totalAmount.title=Celkový objem obchodů +closedTradesSummaryWindow.totalAmount.value={0} ({1} podle aktuální tržní ceny) +closedTradesSummaryWindow.totalVolume.title=Celkový objem obchodovaný v {0} +closedTradesSummaryWindow.totalMinerFee.title=Suma poplatků za těžbu +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} z celkového objemu obchodů) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Suma obchodních poplatků v BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} z celkového objemu obchodů) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Suma obchodních poplatků v BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} z celkového objemu obchodů) + +walletPasswordWindow.headline=Pro odemknutí zadejte heslo + +torNetworkSettingWindow.header=Nastavení sítě Tor +torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) +torNetworkSettingWindow.providedBridges=Spojte se s poskytnutými mosty (bridges) +torNetworkSettingWindow.customBridges=Zadejte vlastní mosty (bridge) +torNetworkSettingWindow.transportType=Typ přepravy +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (doporučeno) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Zadejte jeden nebo více bridge relays (jeden na řádek) +torNetworkSettingWindow.enterBridgePrompt=typ addresa:port +torNetworkSettingWindow.restartInfo=Chcete-li použít změny, musíte restartovat aplikaci +torNetworkSettingWindow.openTorWebPage=Otevřít webovou stránku projektu Tor +torNetworkSettingWindow.deleteFiles.header=Problémy s připojením? +torNetworkSettingWindow.deleteFiles.info=Pokud máte při spuštění opakované problémy s připojením, může pomoci odstranění zastaralých souborů Tor. Chcete-li to provést, klikněte na tlačítko níže a poté restartujte aplikaci. +torNetworkSettingWindow.deleteFiles.button=Odstranit zastaralé soubory Tor a vypnout aplikaci +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. + +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 BTC. Pokud zvolíte BSQ, oceníte zlevněný obchodní poplatek. +feeOptionWindow.optionsLabel=Vyberte měnu pro platbu obchodního poplatku +feeOptionWindow.useBTC=Použít BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Oznámení +popup.headline.instruction=Upozornění: +popup.headline.attention=Pozor +popup.headline.backgroundInfo=Základní informace +popup.headline.feedback=Hotovo +popup.headline.confirmation=Potvrzení +popup.headline.information=Informace +popup.headline.warning=Varování +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 bisq.log stisknutím tlačítka „Otevřít log soubor“, uložením kopie a připojením ke zprávě o chybě. + +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. + +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 (Bisq používá Javu) upozornění na vyskakovací okno v systému MacOS („Bisq 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 „Bisq“.\n\nBisq 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 Bisq pro tento počítač.\nArchitektura vašeho počítače je: {0}.\nBinární kód Bisq, 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 Bisq.\nMůžete si jej stáhnout na adrese: [HYPERLINK:https://bisq.network/downloads].\n\nRestartujte aplikaci. +popup.warning.startupFailed.twoInstances=Bisq již běží. Nemůžete spustit dvě instance Bisq. +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.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 altcoinový úč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.notSufficientConnectionsToBtcNetwork=Musíte počkat, až budete mít alespoň {0} připojení k bitcoinové síti. +popup.warning.downloadNotComplete=Musíte počkat, až bude stahování chybějících bitcoinových bloků kompletní. +popup.warning.chainNotSynced=Výška blockchainu peněženky Bisq není správně synchronizována. Pokud jste aplikaci spustili nedávno, počkejte, dokud nebude zveřejněn jeden blok bitcoinů.\n\nVýšku blockchainu můžete zkontrolovat v Nastavení/Informace o síti. Pokud projde více než jeden blok a tento problém přetrvává, asi být zastaven, v takovém případě byste měli provést SPV resynchonizaci. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Opravdu chcete tuto nabídku odebrat?\nPokud tuto nabídku odstraníte, ztratí se poplatek tvůrce {0}. +popup.warning.tooLargePercentageValue=Nelze nastavit procento 100% nebo větší. +popup.warning.examplePercentageValue=Zadejte procento jako číslo \"5.4\" pro 5.4% +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.insufficientBtcFundsForBsqTx=Nemáte dostatečné prostředky BTC k zaplacení poplatku za těžbu za tuto transakci.\nFinancujte prosím svou BTC peněženku.\nChybějící prostředky: {0} +popup.warning.bsqChangeBelowDustException=Tato transakce vytváří výstup BSQ, který je pod limitem drobných (5,46 BSQ) a byl by bitcoinovou sítí odmítnut.\n\nMusíte buď poslat vyšší částku, abyste se vyhnuli drobným (např. přidáním drobné částky do vaší odeslané částky), nebo přidejte do své peněženky další prostředky BSQ, abyste se vyhnuli generování drobných.\n\nVýstup drobných {0}. +popup.warning.btcChangeBelowDustException=Tato transakce vytváří výstup, který je pod limitem drobných (546 satoshi) a byl by bitcoinovou sítí odmítnut.\n\nMusíte přidat vyšší množství drobných k vašemu odesílanému množství, abyste se vyhnuli vytváření drobných.\n\nVýstup drobných je {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=K provedení této transakce budete potřebovat více BSQ - posledních 5,46 BSQ ve vaší peněžence nelze použít k placení obchodních poplatků kvůli omezení prachových mincí v bitcoinovém protokolu.\n\nMůžete si buď koupit více BSQ, nebo zaplatit obchodní poplatky pomocí BTC.\n\nChybějící prostředky: {0} +popup.warning.noBsqFundsForBtcFeePayment=Peněženka BSQ nemá dostatečné prostředky na zaplacení obchodního poplatku v BSQ. +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.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. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=cenové relé +popup.warning.seed=seed +popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Bisq. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Bisq. +popup.warning.mandatoryUpdate.dao=Aktualizujte prosím na nejnovější verzi Bisq. Byla vydána povinná aktualizace, která zakazuje Bisq DAO a BSQ pro staré verze. Další informace naleznete na fóru Bisq. +popup.warning.disable.dao=Bisq DAO a BSQ jsou dočasně deaktivovány. Další informace naleznete na fóru Bisq. +popup.warning.noFilter="We did not receive a filter object from the seed nodes." Toto je neočekávaná situace. Prosím upozorněte vývojáře Bisq. +popup.warning.burnBTC=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 BTC k převodu. + +popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl bitcoinovou 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 Bisq Keybase týmu. + +popup.warning.trade.txRejected.tradeFee=obchodní poplatek +popup.warning.trade.txRejected.deposit=vklad +popup.warning.trade.txRejected=Bitcoinová 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 Bisq 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 Bisq 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 Bisq běžet, aby ji převzal jiný obchodník. Chcete-li zachovat své nabídky online, udržujte Bisq 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.confirm=Potvrzuji, že mohu provést vklad +popup.info.shutDownWithOpenOffers=Bisq se vypíná, ale existují otevřené nabídky.\n\nTyto nabídky nebudou dostupné v síti P2P, pokud bude Bisq vypnutý, ale budou znovu publikovány do sítě P2P při příštím spuštění Bisq.\n\nChcete-li zachovat své nabídky online, udržujte Bisq 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 Bisq na Qubes OS.\n\nUjistěte se, že je vaše Bisq qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade z verze {0} na verzi {1} není podporován. Použijte prosím nejnovější verzi Bisq. +popup.warn.daoRequiresRestart=Došlo k problému při synchronizaci stavu DAO. Pro nápravu prosím restartujte 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ší bitcoinové 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.bitcoinLocalhostNode.msg=Bisq detekoval Bitcoin Core node běžící na tomto systému (na localhostu).\n\nProsím ujistěte se, že:\n- tento node je plně synchronizován před spuštěním Bisq\n- prořezávání je vypnuto ('prune=0' v bitcoin.conf)\n- Bloomovy filtry jsou zapnuty ('peerbloomfilters=1' v bitcoin.conf) + +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.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ý. + +popup.accountSigning.selectAccounts.headline=Vyberte platební účty +popup.accountSigning.selectAccounts.description=Na základě způsobu platby a časového limitu budou pro podpis vybrány všechny platební účty, které jsou spojeny se sporem, ve kterém došlo k výplatě kupujícímu. +popup.accountSigning.selectAccounts.signAll=Podepište všechny platební metody +popup.accountSigning.selectAccounts.datePicker=Vyberte čas, do kterého budou účty podepsány + +popup.accountSigning.confirmSelectedAccounts.headline=Potvrďte vybrané platební účty +popup.accountSigning.confirmSelectedAccounts.description=Na základě vašeho zadání bude vybráno {0} platebních účtů. +popup.accountSigning.confirmSelectedAccounts.button=Potvrďte platební účty +popup.accountSigning.signAccounts.headline=Potvrďte podpis platebních účtů +popup.accountSigning.signAccounts.description=Na základě vašeho výběru budou podepsány platební účty {0}. +popup.accountSigning.signAccounts.button=Podepsat platební účty +popup.accountSigning.signAccounts.ECKey=Zadejte soukromý klíč rozhodce +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.bisq.network/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.singleAccountSelect.headline=Importujte svědka stáří účtu k podepsání +popup.accountSigning.confirmSingleAccount.headline=Potvrďte vybrané svědky o stáří účtu +popup.accountSigning.confirmSingleAccount.selectedHash=Hash vybraného svědka +popup.accountSigning.confirmSingleAccount.button=Podepsat svědka stáří účtu +popup.accountSigning.successSingleAccount.description=Svědek {0} byl podepsán +popup.accountSigning.successSingleAccount.success.headline=Úspěch + +popup.accountSigning.unsignedPubKeys.headline=Nepodepsané Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Podepsat Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys byly podepsány +popup.accountSigning.unsignedPubKeys.result.signed=Podepsané pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Nepodařilo se podepsat + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Oznámení o obchodu s ID {0} +notification.ticket.headline=Úkol na 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 BTC {0}. +notification.trade.confirmed=Váš obchod má alespoň jedno potvrzení blockchainu.\nPlatbu můžete začít hned teď. +notification.trade.paymentStarted=Kupující BTC zahájil platbu. +notification.trade.selectTrade=Vyberte obchod +notification.trade.peerOpenedDispute=Váš obchodní partner otevřel {0}. +notification.trade.disputeClosed={0} byl uzavřen. +notification.walletUpdate.headline=Aktualizace obchodní peněženky +notification.walletUpdate.msg=Vaše obchodní peněženka má dostatečné finanční prostředky.\nČástka: {0} +notification.takeOffer.walletUpdate.msg=Vaše obchodní peněženka již byla dostatečně financována z předchozího pokusu o nabídku.\nČástka: {0} +notification.tradeCompleted.headline=Obchod dokončen +notification.tradeCompleted.msg=Prostředky můžete nyní vybrat do své externí bitcoinové peněženky nebo je převést do peněženky Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Otevřít okno aplikace +systemTray.hide=Skrýt okno aplikace +systemTray.info=Informace o Bisq +systemTray.exit=Odejít +systemTray.tooltip=Bisq: Decentralizovaná směnárna bitcoinů + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Ujistěte se, že poplatek za těžbu používaný vaší externí peněženkou je alespoň {0} satoshi/vbyte. Jinak nemusí být obchodní transakce potvrzeny včas a obchod skončí sporem. + +guiUtil.accountExport.savedToPath=Obchodní účty uložené na:\n{0} +guiUtil.accountExport.noAccountSetup=Nemáte nastaveny obchodní účty pro export. +guiUtil.accountExport.selectPath=Vyberte cestu k {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Obchodní účet s ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Neobchodovali jsme obchodní účet s ID {0}, protože již existuje.\n +guiUtil.accountExport.exportFailed=Export do CSV selhal kvůli chybě.\nChyba = {0} +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.doOpen=Otevřít webovou stránku a znovu se neptat +guiUtil.openWebBrowser.copyUrl=Zkopírovat URL a zrušit +guiUtil.ofTradeAmount=obchodní částky +guiUtil.requiredMinimum=(požadované minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Vyberte měnu +list.currency.showAll=Zobrazit vše +list.currency.editList=Upravit seznam měn + +table.placeholder.noItems=Momentálně nejsou k dispozici žádné {0} +table.placeholder.noData=V současné době nejsou k dispozici žádné údaje +table.placeholder.processingData=Zpracovávají se data... + + +peerInfoIcon.tooltip.tradePeer=Obchodního partnera +peerInfoIcon.tooltip.maker=Tvůrčí +peerInfoIcon.tooltip.trade.traded={0} onion adresa: {1}\nUž jste s tímto partnerem obchodovali {2} x\n{3} +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. + +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} + +confidence.unknown=Neznámý stav transakce +confidence.seen=Viděno {0} partnery / 0 potvrzení +confidence.confirmed=Potvrzeno v {0} blocích +confidence.invalid=Transakce je neplatná + +peerInfo.title=Info o obchodním partnerovi +peerInfo.nrOfTrades=Počet dokončených obchodů +peerInfo.notTradedYet=Dosud jste s tímto uživatelem neobchodovali. +peerInfo.setTag=Nastavit poznámku pro tohoto uživatele +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í bitcoinovou peněženku +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í bitcoinové peněženky se nezdařilo. Možná nemáte žádnou nainstalovanou? + +peerInfoIcon.tooltip={0}\nŠtítek: {1} + +txIdTextField.copyIcon.tooltip=Zkopírujte ID transakce do schránky +txIdTextField.blockExplorerIcon.tooltip=Otevřete průzkumník blockchainu s tímto ID transakce +txIdTextField.missingTx.warning.tooltip=Chybí požadovaná transakce + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Účet\" +navigation.account.walletSeed=\"Účet/Seed peněženky\" +navigation.funds.availableForWithdrawal=\"Prostředky/Odeslat 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.settings.preferences=\"Nastavení/Preference\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Prostředky/Transakce\" +navigation.support=\"Podpora\" +navigation.dao.wallet.receive="DAO/Peněženka BSQ/Přijmout" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} částka{1} +formatter.makerTaker=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.fiat=Vytváříte nabídku: {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Vytváříte nabídku: {0} {1} ({2}te {3}) +formatter.asMaker={0} {1} jako tvůrce +formatter.asTaker={0} {1} jako příjemce + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (zastaralé) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=Rok +time.month=Měsíc +time.week=Týden +time.day=Den +time.hour=Hodina +time.minute10=10 minut +time.hours=hodiny +time.days=dny +time.1hour=1 hodina +time.1day=1 den +time.minute=minuta +time.second=sekunda +time.minutes=minuty +time.seconds=sekundy + + +password.enterPassword=Vložte heslo +password.confirmPassword=Potvrďte heslo +password.tooLong=Heslo musí mít méně než 500 znaků. +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 peněženky nelze nastavit. Možná jste importovali počáteční slova, která neodpovídají databázi peněženky. Kontaktujte vývojáře na Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +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.makeBackup=Provést zálohu + +seed.seedWords=Seed slova peněženky +seed.enterSeedWords=Vložte seed slova peněženky +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 bitcoinová 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é bitcoiny.\nV případě, že nemáte přístup ke svým bitcoinů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.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 bisq skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\nPeněženky BIP39 byly poprvé představeny v bisq 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.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? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Účet +payment.account.no=Číslo účtu +payment.account.name=Název účtu +payment.account.userName=Uživatelské jméno +payment.account.phoneNr=Telefonní číslo +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.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.bank.name=Jméno banky +payment.select.account=Vyberte typ účtu +payment.select.region=Vyberte region +payment.select.country=Vyberte zemi +payment.select.bank.country=Vyberte zemi původu banky +payment.foreign.currency=Opravdu chcete vybrat jinou měnu, než je výchozí měna země? +payment.restore.default=Ne, obnovit výchozí měnu +payment.email=E-mail +payment.country=Země +payment.extras=Zvláštní požadavky +payment.email.mobile=E-mail nebo mobilní číslo +payment.altcoin.address=Altcoin adresa +payment.altcoin.tradeInstantCheckbox=Obchodujte ihned s tímto altcoinem (do 1 hodiny) +payment.altcoin.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.altcoin=Altcoin +payment.select.altcoin=Vyberte nebo vyhledejte altcoin +payment.secret=Tajná otázka +payment.answer=Odpověď +payment.wallet=ID peněženky +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 +payment.moneyBeam.accountId=E-mail nebo číslo telefonu +payment.venmo.venmoUserName=Uživatelské jméno Venmo +payment.popmoney.accountId=E-mail nebo číslo telefonu +payment.promptPay.promptPayId=Občanské/daňové identifikační číslo nebo telefonní číslo +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.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ě +payment.accepted.banks=Akceptované banky (ID) +payment.mobile=Číslo mobilu +payment.postal.address=Poštovní adresa +payment.national.account.id.AR=Číslo CBU +shared.accountSigningState=Stav podpisu účtu + +#new +payment.altcoin.address.dyn={0} adresa +payment.altcoin.receiver.address=Altcoinová adresa příjemce +payment.accountNr=Číslo účtu +payment.emailOrMobile=E-mail nebo mobilní číslo +payment.useCustomAccountName=Použijte vlastní název účtu +payment.maxPeriod=Max. povolené obchodní období +payment.maxPeriodAndLimit=Max. doba trvání obchodu: {0} / Max. nákup: {1} / Max. prodej: {2} / Stáří účtu: {3} +payment.maxPeriodAndLimitCrypto=Max. doba trvání obchodu: {0} / Max. obchodní limit: {1} +payment.currencyWithSymbol=Měna: {0} +payment.nameOfAcceptedBank=Název akceptované banky +payment.addAcceptedBank=Přidat akceptovanou banku +payment.clearAcceptedBanks=Vymazat akceptované banky +payment.bank.nameOptional=Název banky (volitelné) +payment.bankCode=Kód banky +payment.bankId=ID Banky (BIC/SWIFT) +payment.bankIdOptional=ID Banky (BIC/SWIFT) (volitelné) +payment.branchNr=Číslo pobočky +payment.branchNrOptional=Číslo pobočky (volitelné) +payment.accountNrLabel=Číslo účtu (IBAN) +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 BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisqu, 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í BTC kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci BTC. 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í BTC zaslat prodejci BTC 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í BTC poslat prodejci BTC 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 BTC 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í BTC 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í Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Bisq 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://bisq.wiki/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.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.addUserNameInfo.headLine=Aktualizujte účet Revolut + +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.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 Bisq vyžaduje, abyste rozuměli následujícímu:\n\n- Kupující BTC musí před odesláním napsat jméno prodejce BTC 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í BTC musí odeslat USPMO prodejci BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Kontaktní informace +payment.cashByMail.contact.prompt=Obálka se jménem nebo pseudonymem by měla být adresována +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.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 BTC 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.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Banka +payment.japan.branch=Pobočka +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 BTC poslat kartu Amazon eGift přes svůj účet Amazon.\n\nBisq zobrazí e-mail nebo mobilní číslo prodejce BTC, 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://bisq.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). + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +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 +CASH_BY_MAIL=Odeslání hotovosti poštou +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Tváří v tvář (osobně) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australské PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Národní banky +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Stejná banka +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Konkrétní banky +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Vklad hotovosti +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=Hotovost poštou +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Okamžité platby +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoiny +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Instantní Altcoiny + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA okamžité +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoiny +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Instantní Altcoiny + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Prázdný vstup není povolen. +validation.NaN=Vstup není platné číslo. +validation.notAnInteger=Vstup není celočíselná hodnota. +validation.zero=Vstup 0 není povolen. +validation.negative=Záporná hodnota není povolena. +validation.fiat.toSmall=Vstup menší než minimální možné množství není povolen. +validation.fiat.toLarge=Vstup větší než maximální možné množství není povolen. +validation.btc.fraction=Zadání povede k hodnotě bitcoinu menší než 1 satoshi +validation.btc.toLarge=Vstup větší než {0} není povolen. +validation.btc.toSmall=Vstup menší než {0} není povolen. +validation.passwordTooShort=Zadané heslo je příliš krátké. Musí mít min. 8 znaků. +validation.passwordTooLong=Zadané heslo je příliš dlouhé. Nemůže být delší než 50 znaků. +validation.sortCodeNumber={0} se musí skládat z {1} čísel. +validation.sortCodeChars={0} musí obsahovat {1} znaků. +validation.bankIdNumber={0} se musí skládat z {1} čísel. +validation.accountNr=Číslo účtu se musí skládat z {0} čísel. +validation.accountNrChars=Číslo účtu musí obsahovat {0} znaků. +validation.btc.invalidAddress=Adresa není správná. Zkontrolujte formát adresy. +validation.integerOnly=Zadejte pouze celá čísla. +validation.inputError=Váš vstup způsobil chybu:\n{0} +validation.bsq.insufficientBalance=Váš dostupný zůstatek je {0}. +validation.btc.exceedsMaxTradeLimit=Váš obchodní limit je {0}. +validation.bsq.amountBelowMinAmount=Min. částka je {0} +validation.nationalAccountId={0} se musí skládat z {1} čísel. + +#new +validation.invalidInput=Neplatný vstup: {0} +validation.accountNrFormat=Číslo účtu musí být ve formátu: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Ověření adresy se nezdařilo, protože neodpovídá struktuře adresy {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=Adresa LTZ musí začínat na "L". Adresy začínající na "z" nejsou podporovány. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=Adresy ZEC musí začínat na "t". Adresy začínající na "z" nejsou podporovány. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Adresa není platná {0} adresa! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Nativní adresy segwit (ty začínající na 'lq') nejsou podporovány. +validation.bic.invalidLength=Délka vstupu musí být 8 nebo 11 +validation.bic.letters=Banka a kód země musí být písmena +validation.bic.invalidLocationCode=BIC obsahuje neplatný location kód +validation.bic.invalidBranchCode=BIC obsahuje neplatný kód pobočky +validation.bic.sepaRevolutBic=Účty Revolut Sepa nejsou podporovány. +validation.btc.invalidFormat=Neplatný formát bitcoinové adresy. +validation.bsq.invalidFormat=Neplatný formát BSQ adresy. +validation.email.invalidAddress=Neplatná adresa +validation.iban.invalidCountryCode=Kód země je neplatný +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.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 ' _ , . ? - +validation.interacETransfer.invalidAnswer=Musí to být jedno slovo a obsahovat pouze písmena, čísla a/nebo symbol - +validation.inputTooLarge=Vstup nesmí být větší než {0} +validation.inputTooSmall=Vstup musí být větší než {0} +validation.inputToBeAtLeast=Vstup musí být alespoň {0} +validation.amountBelowDust=Množství pod mezní hodnotou drobných (dust limit) {0} není povoleno. +validation.length=Délka musí být mezi {0} a {1} +validation.fixedLength=Délka musí být {0} +validation.pattern=Vstup musí být ve formátu: {0} +validation.noHexString=Vstup není ve formátu HEX. +validation.advancedCash.invalidFormat=Musí to být platný e-mail nebo ID peněženky ve formátu: X000000000000 +validation.invalidUrl=Toto není platná adresa URL +validation.mustBeDifferent=Váš vstup se musí lišit od aktuální hodnoty +validation.cannotBeChanged=Parametr nelze změnit +validation.numberFormatException=Výjimka formátu čísla {0} +validation.mustNotBeNegative=Vstup nesmí být záporný +validation.phone.missingCountryCode=K ověření telefonního čísla je potřeba dvoumístný kód země +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.invalidAddressList=Seznam platných adres musí být oddělený čárkami diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties new file mode 100644 index 0000000000..717d5402e5 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Weiterlesen +shared.openHelp=Hilfe öffnen +shared.warning=Warnung +shared.close=Schließen +shared.cancel=Abbrechen +shared.ok=OK +shared.yes=Ja +shared.no=Nein +shared.iUnderstand=Ich verstehe +shared.na=N/A +shared.shutDown=Herunterfahren +shared.reportBug=Fehler auf GitHub melden +shared.buyBitcoin=Bitcoin kaufen +shared.sellBitcoin=Bitcoin verkaufen +shared.buyCurrency={0} kaufen +shared.sellCurrency={0} verkaufen +shared.buyingBTCWith=kaufe BTC mit {0} +shared.sellingBTCFor=verkaufe BTC für {0} +shared.buyingCurrency=kaufe {0} (verkaufe BTC) +shared.sellingCurrency=verkaufe {0} (kaufe BTC) +shared.buy=kaufen +shared.sell=verkaufen +shared.buying=kaufe +shared.selling=verkaufe +shared.P2P=P2P +shared.oneOffer=Angebot +shared.multipleOffers=Angebote +shared.Offer=Angebot +shared.offerVolumeCode={0} Angebotsvolumen +shared.openOffers=offene Angebote +shared.trade=Handel +shared.trades=Trades +shared.openTrades=offene Trades +shared.dateTime=Datum/Zeit +shared.price=Preis +shared.priceWithCur=Preis in {0} +shared.priceInCurForCur=Preis in {0} für 1 {1} +shared.fixedPriceInCurForCur=Festpreis in {0} für 1 {1} +shared.amount=Betrag +shared.txFee=Transaktionsgebühr +shared.tradeFee=Handelsgebühr +shared.buyerSecurityDeposit=Käufer-Kaution +shared.sellerSecurityDeposit=Verkäufer-Kaution +shared.amountWithCur=Betrag in {0} +shared.volumeWithCur=Volumen in {0} +shared.currency=Währung +shared.market=Markt +shared.deviation=Abweichung +shared.paymentMethod=Zahlungsmethode +shared.tradeCurrency=Handelswährung +shared.offerType=Angebotstyp +shared.details=Details +shared.address=Adresse +shared.balanceWithCur=Guthaben in {0} +shared.utxo=Unverbrauchte Transaktionsausgabe +shared.txId=Transaktions-ID +shared.confirmations=Bestätigungen +shared.revert=Tx umkehren +shared.select=Auswählen +shared.usage=Nutzung +shared.state=Status +shared.tradeId=Handels-ID +shared.offerId=Angebots-ID +shared.bankName=Bankname +shared.acceptedBanks=Akzeptierte Banken +shared.amountMinMax=Betrag (min - max) +shared.amountHelp=Wurde für ein Angebot ein minimaler und maximaler Betrag gesetzt, können Sie jeden Betrag innerhalb dieses Bereiches handeln. +shared.remove=Entfernen +shared.goTo=Zu {0} gehen +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Angebot entfernen +shared.dontRemoveOffer=Angebot nicht entfernen +shared.editOffer=Angebot bearbeiten +shared.openLargeQRWindow=Großes QR-Code Fenster öffnen +shared.tradingAccount=Handelskonto +shared.faq=Zur FAQ Seite +shared.yesCancel=Ja, abbrechen +shared.nextStep=Nächster Schritt +shared.selectTradingAccount=Handelskonto auswählen +shared.fundFromSavingsWalletButton=Gelder aus Bisq-Wallet überweisen +shared.fundFromExternalWalletButton=Ihre externe Wallet zum Finanzieren öffnen +shared.openDefaultWalletFailed=Das Öffnen des Standardprogramms für Bitcoin-Wallets ist fehlgeschlagen. Sind Sie sicher, dass Sie eines installiert haben? +shared.belowInPercent=% unter dem Marktpreis +shared.aboveInPercent=% über dem Marktpreis +shared.enterPercentageValue=%-Wert eingeben +shared.OR=ODER +shared.notEnoughFunds=Für diese Transaktion haben Sie nicht genug Gelder in Ihrem Bisq-Wallet—{0} werden benötigt, aber nur {1} sind verfügbar.\n\nBitte fügen Sie Gelder aus einer externen Wallet hinzu, oder senden Sie Gelder an Ihr Bisq-Wallet unter Gelder > Gelder erhalten. +shared.waitingForFunds=Warte auf Gelder... +shared.depositTransactionId=Kautionstransaktions-ID +shared.TheBTCBuyer=Der BTC-Käufer +shared.You=Sie +shared.sendingConfirmation=Sende Bestätigung... +shared.sendingConfirmationAgain=Bitte senden Sie die Bestätigung erneut +shared.exportCSV=Als CSV exportieren +shared.exportJSON=Exportiere als JSON +shared.summary=Show summary +shared.noDateAvailable=Kein Datum verfügbar +shared.noDetailsAvailable=Keine Details vorhanden +shared.notUsedYet=Noch ungenutzt +shared.date=Datum +shared.sendFundsDetailsWithFee=Gesendet: {0}\nVon Adresse: {1}\nAn Empfangsadresse: {2}.\nBenötigte Mining-Gebühr ist: {3} ({4} satoshis/vbyte)\nTransaktionsgröße (vsize): {5} vKb\n\nDer Empfänger erhält: {6}\n\nSind Sie sicher, dass Sie diesen Betrag abheben wollen? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Diese Transaktion würde ein Wechselgeld erzeugen das unterhalb des Dust-Grenzwerts liegt (und daher von den Bitcoin-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 +shared.language=Sprache +shared.country=Land +shared.applyAndShutDown=Anwenden und herunterfahren +shared.selectPaymentMethod=Zahlungsmethode wählen +shared.accountNameAlreadyUsed=Der Name des Kontos wird bereits für ein existierendes Konto verwendet.\nBitte wählen Sie einen anderen Namen. +shared.askConfirmDeleteAccount=Möchten Sie das ausgewählte Konto wirklich löschen? +shared.cannotDeleteAccount=Sie können dieses Konto nicht löschen, da es in einem offenen Angebot oder Handel gebraucht wird. +shared.noAccountsSetupYet=Es wurden noch keine Konten eingerichtet +shared.manageAccounts=Konten verwalten +shared.addNewAccount=Neues Konto hinzufügen +shared.ExportAccounts=Konten exportieren +shared.importAccounts=Konten importieren +shared.createNewAccount=Neues Konto erstellen +shared.saveNewAccount=Neues Konto speichern +shared.selectedAccount=Konto auswählen +shared.deleteAccount=Konto löschen +shared.errorMessageInline=\nFehlermeldung: {0} +shared.errorMessage=Fehlermeldung +shared.information=Information +shared.name=Name +shared.id=ID +shared.dashboard=Übersicht +shared.accept=Annehmen +shared.balance=Guthaben +shared.save=Speichern +shared.onionAddress=Onion-Adresse +shared.supportTicket=Support-Ticket +shared.dispute=Konflikt +shared.mediationCase=Mediationsfall +shared.seller=Verkäufer +shared.buyer=Käufer +shared.allEuroCountries=Alle Euroländer +shared.acceptedTakerCountries=Akzeptierte Länder für Abnehmer +shared.tradePrice=Handelspreis +shared.tradeAmount=Handelsbetrag +shared.tradeVolume=Handelsvolumen +shared.invalidKey=Der eingegebene Schlüssel war nicht korrekt. +shared.enterPrivKey=Privaten Schlüssel zum Entsperren eingeben +shared.makerFeeTxId=Transaktions-ID der Erstellergebühr +shared.takerFeeTxId=Transaktions-ID der Abnehmergebühr +shared.payoutTxId=Transaktions-ID der Auszahlung +shared.contractAsJson=Vertrag im JSON-Format +shared.viewContractAsJson=Vertrag im JSON-Format ansehen +shared.contract.title=Vertrag für den Handel mit der ID: {0} +shared.paymentDetails=Zahlungsdetails des BTC-{0} +shared.securityDeposit=Kaution +shared.yourSecurityDeposit=Ihre Kaution +shared.contract=Vertrag +shared.messageArrived=Nachricht angekommen. +shared.messageStoredInMailbox=Nachricht in Postfach gespeichert. +shared.messageSendingFailed=Versenden der Nachricht fehlgeschlagen. Fehler: {0} +shared.unlock=Entsperren +shared.toReceive=erhalten +shared.toSpend=ausgeben +shared.btcAmount=BTC-Betrag +shared.yourLanguage=Ihre Sprachen +shared.addLanguage=Sprache hinzufügen +shared.total=Insgesamt +shared.totalsNeeded=Benötigte Gelder +shared.tradeWalletAddress=Adresse der Handels-Wallet +shared.tradeWalletBalance=Guthaben der Handels-Wallet +shared.makerTxFee=Ersteller: {0} +shared.takerTxFee=Abnehmer: {0} +shared.iConfirm=Ich bestätige +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Öffne {0} +shared.fiat=Fiat +shared.crypto=Crypto +shared.all=Alle +shared.edit=Bearbeiten +shared.advancedOptions=Erweiterte Optionen +shared.interval=Intervall +shared.actions=Aktionen +shared.buyerUpperCase=Käufer +shared.sellerUpperCase=Verkäufer +shared.new=NEU +shared.blindVoteTxId=Geheime Wahl-Transaktion ID +shared.proposal=Vorschlag +shared.votes=Stimmen +shared.learnMore=Mehr erfahren +shared.dismiss=Verwerfen +shared.selectedArbitrator=Gewählte Vermittler +shared.selectedMediator=Gewählter Vermittler +shared.selectedRefundAgent=Gewählter Vermittler +shared.mediator=Mediator +shared.arbitrator=Vermittler +shared.refundAgent=Vermittler +shared.refundAgentForSupportStaff=Rückerstattungsbeauftragten +shared.delayedPayoutTxId=Transaktions-ID der verzögerten Auszahlung +shared.delayedPayoutTxReceiverAddress=Verzögerte Auszahlungs-Transaktion gesendet an +shared.unconfirmedTransactionsLimitReached=Sie haben im Moment zu viele unbestätigte Transaktionen. Bitte versuchen Sie es später noch einmal. +shared.numItemsLabel=Anzahl der Einträge: {0} +shared.filter=Filter +shared.enabled=Aktiviert + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Markt +mainView.menu.buyBtc=BTC kaufen +mainView.menu.sellBtc=BTC verkaufen +mainView.menu.portfolio=Portfolio +mainView.menu.funds=Gelder +mainView.menu.support=Support +mainView.menu.settings=Einstellungen +mainView.menu.account=Konto +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Marktpreis von {0} +mainView.marketPrice.bisqInternalPrice=Preis des letzten Bisq-Handels +mainView.marketPrice.tooltip.bisqInternalPrice=Es ist kein Marktpreis von externen Marktpreis-Anbietern verfügbar.\nDer angezeigte Preis, ist der letzte Bisq-Handelspreis für diese Währung. +mainView.marketPrice.tooltip=Marktpreis bereitgestellt von {0}{1}\nLetzte Aktualisierung: {2}\nURL des Knoten-Anbieters: {3} +mainView.balance.available=Verfügbarer Betrag +mainView.balance.reserved=In Angeboten reserviert +mainView.balance.locked=In Trades gesperrt +mainView.balance.reserved.short=Reserviert +mainView.balance.locked.short=Gesperrt + +mainView.footer.usingTor=(über Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Aktuelle Gebühr: {0} sat/vB +mainView.footer.btcInfo.initializing=Verbindung mit Bitcoin-Netzwerk wird hergestellt +mainView.footer.bsqInfo.synchronizing=/ Synchronisiere DAO +mainView.footer.btcInfo.synchronizingWith=Synchronisierung mit {0} bei Block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synchronisierung mit {0} bei Block {1} +mainView.footer.btcInfo.connectingTo=Verbinde mit +mainView.footer.btcInfo.connectionFailed=Verbindung fehlgeschlagen zu +mainView.footer.p2pInfo=Bitcoin Netzwerk Peers: {0} / Bisq Netzwerk Peers: {1} +mainView.footer.daoFullNode=DAO Full Node + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Verbinde mit Tor-Netzwerk... +mainView.bootstrapState.torNodeCreated=(2/4) Tor-Knoten erstellt +mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service veröffentlicht +mainView.bootstrapState.initialDataReceived=(4/4) Anfangsdaten erhalten + +mainView.bootstrapWarning.noSeedNodesAvailable=Keine Seed-Knoten verfügbar +mainView.bootstrapWarning.noNodesAvailable=Keine Seed-Knoten und Peers verfügbar +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping zum Bisq-Netzwerk fehlgeschlagen + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Es sind keine Seed-Knoten oder bestehenden Peers verfügbar, um Daten anzufordern.\nÜberprüfen Sie bitte Ihre Internetverbindung oder versuchen Sie die Anwendung neu zu starten. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Verbinden mit Bisq-Netzwerk fehlgeschlagen (gemeldeter Fehler: {0}).\nBitte überprüfen Sie Ihre Internetverbindungen oder versuchen Sie die Anwendung neu zu starten. + +mainView.walletServiceErrorMsg.timeout=Verbindung mit Bitcoin-Netzwerk aufgrund einer Zeitüberschreitung fehlgeschlagen. +mainView.walletServiceErrorMsg.connectionError=Verbindung mit Bitcoin-Netzwerk aufgrund eines Fehlers fehlgeschlagen: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Eine Transaktion wurde aus dem Netzwerk abgelehnt.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Sie haben die Verbindung zu allen {0} Netzwerk-Peers verloren.\nMöglicherweise haben Sie Ihre Internetverbindung verloren oder Ihr Computer war im Standbymodus. +mainView.networkWarning.localhostBitcoinLost=Sie haben die Verbindung zum localhost Bitcoinknoten verloren.\nBitte starten Sie die Bisq Anwendung neu, um mit anderen Bitcoinknoten zu verbinden oder starten Sie den localhost Bitcoinknoten neu. +mainView.version.update=(Update verfügbar) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Angebotsbuch +market.tabs.spreadCurrency=Angebote nach Währung +market.tabs.spreadPayment=Angebote nach Zahlungsmethode +market.tabs.trades=Trades + +# OfferBookChartView +market.offerBook.buyAltcoin={0} kaufen ({1} verkaufen) +market.offerBook.sellAltcoin={0} verkaufen ({1} kaufen) +market.offerBook.buyWithFiat={0} kaufen +market.offerBook.sellWithFiat={0} verkaufen +market.offerBook.sellOffersHeaderLabel=Verkaufe {0} an +market.offerBook.buyOffersHeaderLabel=Kaufe {0} von +market.offerBook.buy=Ich möchte Bitcoins kaufen +market.offerBook.sell=Ich möchte Bitcoins verkaufen + +# SpreadView +market.spread.numberOfOffersColumn=Alle Angebote ({0}) +market.spread.numberOfBuyOffersColumn=BTC kaufen ({0}) +market.spread.numberOfSellOffersColumn=BTC verkaufen ({0}) +market.spread.totalAmountColumn=BTC insgesamt ({0}) +market.spread.spreadColumn=Verteilung +market.spread.expanded=Erweiterte Ansicht + +# TradesChartsView +market.trades.nrOfTrades=Trades: {0} +market.trades.tooltip.volumeBar=Volumen: {0} / {1}\nAnzahl der Trades: {2}\nDatum: {3} +market.trades.tooltip.candle.open=Eröffnung: +market.trades.tooltip.candle.close=Abschluss: +market.trades.tooltip.candle.high=Hoch: +market.trades.tooltip.candle.low=Niedrig: +market.trades.tooltip.candle.average=Durchschnitt: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=Datum: +market.trades.showVolumeInUSD=Volumen in USD anzeigen + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=Angebot erstellen +offerbook.takeOffer=Angebot annehmen +offerbook.takeOfferToBuy=Angebot annehmen {0} zu kaufen +offerbook.takeOfferToSell=Angebot annehmen {0} zu verkaufen +offerbook.trader=Händler +offerbook.offerersBankId=Bankkennung des Erstellers (BIC/SWIFT): {0} +offerbook.offerersBankName=Bankname des Erstellers: {0} +offerbook.offerersBankSeat=Banksitz-Land des Erstellers: {0} +offerbook.offerersAcceptedBankSeatsEuro=Als Banksitz akzeptierte Länder (Abnehmer): Alle Euroländer +offerbook.offerersAcceptedBankSeats=Als Banksitz akzeptierte Länder (Abnehmer):\n{0} +offerbook.availableOffers=Verfügbare Angebote +offerbook.filterByCurrency=Nach Währung filtern +offerbook.filterByPaymentMethod=Nach Zahlungsmethode filtern +offerbook.matchingOffers=Angebote die meinen Zahlungskonten entsprechen +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 +offerbook.timeSinceSigning.info.peer=von einem Handelspartner unterzeichnet, es muss noch %d Tage warten bis alle Beschränkungen aufgehoben werden +offerbook.timeSinceSigning.info.peerLimitLifted=von einem Partner unterzeichnet und Limits wurden aufgehoben +offerbook.timeSinceSigning.info.signer=vom Partner unterzeichnet und kann Partner-Konten unterzeichnen (Limits aufgehoben) +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.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 +offerbook.timeSinceSigning.notSigned.ageDays={0} Tage +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=Dieses Konto wurde noch nicht unterzeichnet. Es wurde vor {0} Tag(en) erstellt +shared.notSigned.noNeed=Konten dieses Typs benötigen keine Unterzeichnung +shared.notSigned.noNeedDays=Konten dieses Typs benötigen keine Unterzeichnung. Es wurde vor {0} Tag(en) erstellt +shared.notSigned.noNeedAlts=Altcoin Konten haben keine Merkmale wie Unterzeichnung oder Alter + +offerbook.nrOffers=Anzahl der Angebote: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Kaution BTC (%) +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.createOfferToBuy=Neues Angebot erstellen, um {0} zu kaufen +offerbook.createOfferToSell=Neues Angebot erstellen, um {0} zu verkaufen +offerbook.createOfferToBuy.withFiat=Neues Angebot erstellen, um {0} mit {1} zu kaufen +offerbook.createOfferToSell.forFiat=Neues Angebot erstellen, um {0} für {1} zu verkaufen +offerbook.createOfferToBuy.withCrypto=Angebot erstellen, um {0} zu verkaufen ({1} kaufen) +offerbook.createOfferToSell.forCrypto=Angebot erstellen, um {0} zu kaufen ({1} verkaufen) + +offerbook.takeOfferButton.tooltip=Angebot annehmen für {0} +offerbook.yesCreateOffer=Ja, Angebot erstellen +offerbook.setupNewAccount=Neues Handelskonto einrichten +offerbook.removeOffer.success=Das Entfernen des Angebots war erfolgreich. +offerbook.removeOffer.failed=Entfernen des Angebots ist fehlgeschlagen:\n{0} +offerbook.deactivateOffer.failed=Deaktivieren des Angebots fehlgeschlagen:\n{0} +offerbook.activateOffer.failed=Veröffentlichung des Angebots fehlgeschlagen:\n{0} +offerbook.withdrawFundsHint=Sie können die eingezahlten Gelder im {0}-Bildschirm abheben. + +offerbook.warning.noTradingAccountForCurrency.headline=Kein Zahlungskonto für die gewählte Währung +offerbook.warning.noTradingAccountForCurrency.msg=Sie haben kein Zahlungskonto für die gewählte Währung eingerichtet.\n\nWollen Sie stattdessen ein Handelsangebot für eine andere Währung erstellen? +offerbook.warning.noMatchingAccount.headline=Kein passendes Zahlungskonto. +offerbook.warning.noMatchingAccount.msg=Dieses Angebot verwendet eine Zahlungsmethode die Sie noch nicht eingerichtet haben.\n\nWollen Sie jetzt ein neues Zahlungskonto einrichten? + +offerbook.warning.counterpartyTradeRestrictions=Dieses Angebot kann aufgrund von Handelsbeschränkungen der Gegenpartei nicht angenommen werden + +offerbook.warning.newVersionAnnouncement=Mit dieser Version der Software können Handelspartner gegenseitig Zahlungskonten verifizieren und unterzeichnen, um ein Netzwerk vertrauenswürdiger Zahlungskonten aufzubauen.\n\nNach dem erfolgreichen Handel mit einem verifizierten Handelspartner, wird auch Ihr Zahlungskonto unterzeichnet und Ihre Handels-Beschränkungen werden nach einer gewissen Zeit aufgehoben (die Länge kann je nach Zahlungsmethode unterschiedlich sein).\n\nWeitere Informationen zur Unterzeichnung von Konten finden Sie hier in der Dokumentation: [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Der zulässige Trade-Betrag ist aufgrund von Sicherheitseinschränkungen, die auf den folgenden Kriterien basieren, auf {0} begrenzt:\n- Das Konto des Käufers wurde nicht von einem Vermittler oder einem Partner unterzeichnet\n- Die Zeit seit der Unterzeichnung des Kontos des Käufers beträgt nicht mindestens 30 Tage\n- Die Zahlungsmethode für dieses Angebot gilt als riskant für Bankrückbuchungen\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Der zulässige Trade-Betrag ist aufgrund von Sicherheitseinschränkungen, die auf den folgenden Kriterien basieren, auf {0} begrenzt:\n- Ihr Konto wurde nicht von einem Vermittler oder einem Partner unterzeichnet\n- Die Zeit seit der Unterzeichnung Ihres Kontos beträgt nicht mindestens 30 Tage\n- Die Zahlungsmethode für dieses Angebot gilt als riskant für Bankrückbuchungen\n\n{1} + +offerbook.warning.wrongTradeProtocol=Dieses Angebot benötigt eine andere Protokollversion, als die Version Ihrer Software.\n\nBitte überprüfen Sie, ob Sie die aktuellste Version installiert haben. Andernfalls hat der Nutzer, der das Angebot erstellt hat, eine ältere Version benutzt.\n\nNutzer können nicht mit inkompatiblen Protokollversionen handeln. +offerbook.warning.userIgnored=Sie haben die Onion-Adresse dieses Nutzers zu Ihrer Liste ignorierter Adressen hinzugefügt. +offerbook.warning.offerBlocked=Das Angebot wurde von den Bisq-Entwicklern blockiert.\nWahrscheinlich gibt es einen unbehobenen Bug, der Probleme beim Annehmen dieses Angebots verursacht. +offerbook.warning.currencyBanned=Die in diesem Handel verwendete Währung wurde von den Bisq-Entwicklern blockiert.\nBitte besuchen sie das Bisq-Forum für weitere Informationen. +offerbook.warning.paymentMethodBanned=Die in diesem Handel verwendete Zahlungsmethode wurde von den Bisq-Entwicklern blockiert.\nBitte besuchen sie das Bisq-Forum für weitere Informationen. +offerbook.warning.nodeBlocked=Die Onion-Adresse dieses Händlers wurde von den Bisq-Entwicklern blockiert.\nWahrscheinlich gibt es einen unbehobenen Bug, der Probleme beim Annehmen von Angeboten dieses Händlers verursacht. +offerbook.warning.requireUpdateToNewVersion=Ihre Bisq-Version ist nicht mehr zum Handeln geeignet.\nBitte updaten Sie Bisq auf die aktuellste Version unter [HYPERLINK:https://bisq.network/downloads]. +offerbook.warning.offerWasAlreadyUsedInTrade=Sie können dieses Angebot nicht annehmen, weil Sie das früher schon getan haben. Es kann sein, dass Ihr vorheriger Annahme-Versuch zu einem fehlgeschlagenen Handel geführt hat. + +offerbook.info.sellAtMarketPrice=Sie verkaufen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.buyAtMarketPrice=Sie kaufen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.sellBelowMarketPrice=Sie bekommen {0} weniger verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.buyAboveMarketPrice=Sie zahlen {0} mehr verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.sellAboveMarketPrice=Sie bekommen {0} mehr verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.buyBelowMarketPrice=Sie zahlen {0} weniger verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). +offerbook.info.buyAtFixedPrice=Sie kaufen zu diesem Festpreis. +offerbook.info.sellAtFixedPrice=Sie verkaufen zu diesem Festpreis. +offerbook.info.noArbitrationInUserLanguage=Im Konflikt ist zu beachten, dass die Vermittlung für dieses Angebot in {0} abgewickelt wird. Die Sprache ist derzeit auf {1} eingestellt. +offerbook.info.roundedFiatVolume=Der Betrag wurde gerundet, um die Privatsphäre Ihres Handels zu erhöhen. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Betrag in BTC eingeben +createOffer.price.prompt=Preis eingeben +createOffer.volume.prompt=Betrag in {0} eingeben +createOffer.amountPriceBox.amountDescription=Betrag in BTC zu {0} +createOffer.amountPriceBox.buy.volumeDescription=Auszugebender Betrag in {0} +createOffer.amountPriceBox.sell.volumeDescription=Zu erhaltender Betrag in {0} +createOffer.amountPriceBox.minAmountDescription=Minimaler Betrag in BTC +createOffer.securityDeposit.prompt=Kaution +createOffer.fundsBox.title=Ihr Angebot finanzieren +createOffer.fundsBox.offerFee=Handelsgebühr +createOffer.fundsBox.networkFee=Mining-Gebühr +createOffer.fundsBox.placeOfferSpinnerInfo=Das Angebot wird veröffentlicht ... +createOffer.fundsBox.paymentLabel=Bisq-Handel mit der ID {0} +createOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) +createOffer.fundsBox.fundsStructure.BSQ=({0} Kaution, {1} Mining-Gebühr) + {2} Handelsgebühr +createOffer.success.headline=Ihr Angebot wurde veröffentlicht +createOffer.success.info=Sie können Ihre offenen Angebote unter \"Portfolio/Meine offenen Angebote\" verwalten. +createOffer.info.sellAtMarketPrice=Sie verkaufen immer zum aktuellen Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.info.buyAtMarketPrice=Sie kaufen immer zum aktuellen Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.info.sellAboveMarketPrice=Sie erhalten immer {0}% mehr als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.info.buyBelowMarketPrice=Sie zahlen immer {0}% weniger als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.warning.sellBelowMarketPrice=Sie erhalten immer {0}% weniger als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.warning.buyAboveMarketPrice=Sie zahlen immer {0}% mehr als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. +createOffer.tradeFee.descriptionBTCOnly=Handelsgebühr +createOffer.tradeFee.descriptionBSQEnabled=Gebührenwährung festlegen + +createOffer.triggerPrice.prompt=Auslösepreis (optional) +createOffer.triggerPrice.label=Angebot bei einem Marktpreis von {0} deaktivieren +createOffer.triggerPrice.tooltip=Als Schutz vor drastischen Preisbewegungen können Sie einen Auslösepreis festlegen, der das Angebot deaktiviert, wenn der Marktpreis diesen Wert erreicht. +createOffer.triggerPrice.invalid.tooLow=Wert muss höher sein als {0} +createOffer.triggerPrice.invalid.tooHigh=Wert muss niedriger sein als {0} + +# new entries +createOffer.placeOfferButton=Überprüfung: Anbieten Bitcoins 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 Bisq-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. + +# 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. +createOffer.setAmountPrice=Betrag und Preis festlegen +createOffer.warnCancelOffer=Sie haben das Angebot bereits finanziert.\nWenn Sie jetzt abbrechen, werden Ihre Gelder in Ihre lokale Bisq-Wallet verschoben und können unter \"Gelder/Gelder senden\" abgehoben werden.\nSind Sie sicher, dass Sie abbrechen wollen? +createOffer.timeoutAtPublishing=Beim Veröffentlichen des Angebots ist eine Zeitüberschreitung aufgetreten. +createOffer.errorInfo=\n\nDie Erstellungsgebühr wurde schon gezahlt. Im schlimmsten Fall haben Sie diese Gebühr verloren.\nVersuchen Sie bitte die Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung um zu sehen, ob Sie das Problem beheben können. +createOffer.tooLowSecDeposit.warning=Sie haben die Kaution auf einen niedrigeren Wert als den empfohlenen Standardwert von {0} gesetzt.\nSind Sie sicher, dass Sie eine niedrigere Kaution nutzen wollen? +createOffer.tooLowSecDeposit.makerIsSeller=Dies gibt Ihnen weniger Schutz, sollte der Handelspartner nicht dem Handelsprotokoll folgen. +createOffer.tooLowSecDeposit.makerIsBuyer=Es gibt Ihrem Handelspartner weniger Schutz, dass Sie dem Handelsprotokoll folgen, da Sie so weniger Kaution riskieren. Andere Nutzer könnten andere Angebote Ihrem vorziehen. +createOffer.resetToDefault=Nein, den Standardwert wiederherstellen +createOffer.useLowerValue=Ja, meinen niedrigeren Wert nutzen +createOffer.priceOutSideOfDeviation=Der eingegebene Preis liegt außerhalb der maximal zulässigen Abweichung vom Marktpreis.\nDie maximale Abweichung ist {0} und kann in den Voreinstellungen angepasst werden. +createOffer.changePrice=Preis ändern +createOffer.tac=Mit der Erstellung dieses Angebots stimme ich zu, mit jedem Händler zu handeln, der die oben festgelegten Bedingungen erfüllt. +createOffer.currencyForFee=Handelsgebühr +createOffer.setDeposit=Kaution des Käufers festlegen (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Betrag in BTC eingeben +takeOffer.amountPriceBox.buy.amountDescription=Betrag in BTC zu verkaufen +takeOffer.amountPriceBox.sell.amountDescription=Betrag in BTC zu kaufen +takeOffer.amountPriceBox.priceDescription=Preis pro Bitcoin in {0} +takeOffer.amountPriceBox.amountRangeDescription=Mögliche Betragsspanne +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=Der eingegebene Betrag besitzt zu viele Nachkommastellen.\nDer Betrag wurde auf 4 Nachkommastellen angepasst. +takeOffer.validation.amountSmallerThanMinAmount=Der Betrag kann nicht kleiner als der im Angebot festgelegte minimale Betrag sein. +takeOffer.validation.amountLargerThanOfferAmount=Der eingegebene Betrag kann nicht größer als der im Angebot festgelegte Betrag sein. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Der eingegebene Betrag würde Staub als Wechselgeld für den BTC-Verkäufer erzeugen. +takeOffer.fundsBox.title=Ihren Handel finanzieren +takeOffer.fundsBox.isOfferAvailable=Verfügbarkeit des Angebots wird überprüft ... +takeOffer.fundsBox.tradeAmount=Zu verkaufender Betrag +takeOffer.fundsBox.offerFee=Handelsgebühr +takeOffer.fundsBox.networkFee=Gesamte Mining-Gebühr +takeOffer.fundsBox.takeOfferSpinnerInfo=Angebot wird angenommen ... +takeOffer.fundsBox.paymentLabel=Bisq-Handel mit der ID {0} +takeOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) +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} + +# new entries +takeOffer.takeOfferButton=Überprüfung: Angebot annehmen Bitcoins zu {0} +takeOffer.noPriceFeedAvailable=Sie können dieses Angebot nicht annehmen, da es auf einem Prozentsatz vom Marktpreis basiert, jedoch keiner verfügbar ist. +takeOffer.takeOfferFundWalletInfo.headline=Ihren Handel finanzieren +# suppress inspection "TrailingSpacesInProperty" +takeOffer.takeOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0}\n +takeOffer.takeOfferFundWalletInfo.msg=Sie müssen zum Annehmen dieses Angebots {0} einzahlen.\n\nDer Betrag ist die Summe aus:\n{1}- Ihre Kaution: {2}\n- Handelsgebühr: {3}\n- Gesamte Mining-Gebühr: {4}\n\nSie haben zwei Möglichkeiten Ihren Handel zu finanzieren:\n- Nutzen Sie Ihre Bisq-Wallet (bequem, aber Transaktionen können nach verfolgbar 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. +takeOffer.alreadyPaidInFunds=Wenn Sie bereits Gelder gezahlt haben, können Sie diese unter \"Gelder/Gelder senden\" abheben. +takeOffer.paymentInfo=Zahlungsinformationen +takeOffer.setAmountPrice=Betrag festlegen +takeOffer.alreadyFunded.askCancel=Sie haben das Angebot bereits finanziert.\nWenn Sie jetzt abbrechen, werden Ihre Gelder in Ihre lokale Bisq-Wallet verschoben und können unter \"Gelder/Gelder senden\" abgehoben werden.\nSind Sie sicher, dass Sie abbrechen wollen? +takeOffer.failed.offerNotAvailable=Die Annahme des Angebots ist fehlgeschlagen, da das Angebot nicht mehr verfügbar ist. Möglicherweise hat zwischenzeitlich ein anderer Händler das Angebot angenommen. +takeOffer.failed.offerTaken=Sie können dieses Angebot nicht annehmen, da es bereits von einem anderen Händler angenommen wurde. +takeOffer.failed.offerRemoved=Sie können dieses Angebot nicht annehmen, da es inzwischen entfernt wurde. +takeOffer.failed.offererNotOnline=Die Angebotsannahme ist fehlgeschlagen, da der Ersteller nicht mehr online ist. +takeOffer.failed.offererOffline=Sie können das Angebot nicht annehmen, da der Ersteller offline ist. +takeOffer.warning.connectionToPeerLost=Sie haben die Verbindung zum Ersteller verloren.\nEr ist möglicherweise offline gegangen oder hat die Verbindung zu Ihnen wegen zu vieler offener Verbindungen geschlossen.\n\nFalls Sie das Angebot noch im Angebotsbuch sehen, können Sie versuchen das Angebot erneut anzunehmen. + +takeOffer.error.noFundsLost=\n\nEs haben noch keine Gelder Ihre Wallet verlassen.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=.\n\n +takeOffer.error.depositPublished=\n\nDie Kautionstransaktion wurde schon veröffentlicht.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können.\nWenn das Problem weiter besteht, kontaktieren Sie bitte die Entwickler für Support. +takeOffer.error.payoutPublished=\n\nDie Auszahlungstransaktion wurde schon veröffentlicht.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können.\nWenn das Problem weiter besteht, kontaktieren Sie bitte die Entwickler für Support. +takeOffer.tac=Mit der Annahme dieses Angebots stimme ich den oben festgelegten Handelsbedingungen zu. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Triggerpreis +openOffer.triggerPrice=Auslösepreis {0} +openOffer.triggered=Das Angebot wurde deaktiviert, weil der Marktpreis Ihren Auslösepreis erreicht hat.\nBitte bearbeiten Sie das Angebot, um einen neuen Auslösepreis festzulegen. + +editOffer.setPrice=Preis festlegen +editOffer.confirmEdit=Bestätigen: Angebot bearbeiten +editOffer.publishOffer=Ihr Angebot wird veröffentlicht. +editOffer.failed=Bearbeiten des Angebots fehlgeschlagen:\n{0} +editOffer.success=Ihr Angebot wurde erfolgreich bearbeitet. +editOffer.invalidDeposit=Die Kaution des Käufers ist nicht in den, vom Bisq DAO definierten, Beschränkungen und können nicht mehr geändert werden. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Meine offenen Angebote +portfolio.tab.pendingTrades=Offene Trades +portfolio.tab.history=Verlauf +portfolio.tab.failed=Fehlgeschlagen +portfolio.tab.editOpenOffer=Angebot bearbeiten + +portfolio.closedTrades.deviation.help=Prozentuale Preisabweichung vom Markt + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Auf Blockchain-Bestätigung warten +portfolio.pending.step2_buyer.startPayment=Zahlung beginnen +portfolio.pending.step2_seller.waitPaymentStarted=Auf Zahlungsbeginn warten +portfolio.pending.step3_buyer.waitPaymentArrived=Auf Zahlungseingang warten +portfolio.pending.step3_seller.confirmPaymentReceived=Zahlungseingang bestätigen +portfolio.pending.step5.completed=Abgeschlossen + +portfolio.pending.step3_seller.autoConf.status.label=Status der automatischen Bestätigung +portfolio.pending.autoConf=Automatisch bestätigt +portfolio.pending.autoConf.blocks=XMR Bestätigungen: {0} / Benötigt: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaktionsschlüssel wiederverwendet. Bitte eröffnen Sie einen Konflikt / eine Auseinandersetzung. +portfolio.pending.autoConf.state.confirmations=XMR Bestätigungen: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaktion ist noch nicht im Mem-Pool sichtbar +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=ID / Schlüssel der Transaktion (noch) nicht validiert +portfolio.pending.autoConf.state.filterDisabledFeature=Von Entwicklern deaktiviert. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Automatische Bestätigung ist deaktiviert. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Handelsbetrag überschreitet das Limit für die automatische Bestätigung +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Handelspartner hat ungültige Daten angegeben. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Die Auszahlungstransaktion wurde bereits durchgeführt. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Ein Konflikt wurde eröffnet. Die automatische Bestätigung ist für diesen Handel deaktiviert. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Anfrage zum Nachweis der Transaktion gestartet +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Fortschrittsergebnisse: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Überprüfung aller Stationen war erfolgreich +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An einer Service-Abfrage ist ein Fehler aufgetreten. Eine Automatische Bestätigung ist nicht mehr möglich. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Ihr Handel hat mindestens eine Blockchain-Bestätigung erreicht.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Wichtig: Wenn Sie die Zahlung durchführen, lassen Sie das Feld \"Verwendungszweck\" leer. Geben Sie NICHT die Handels-ID oder einen anderen Text wie 'Bitcoin', 'BTC' oder 'Bisq' an. Sie können im Handels-Chat gerne besprechen ob ein alternativer \"Verwendungszweck\" für Sie beide zweckmäßig wäre. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees=Sollte Ihre Bank irgendwelche Gebühren für die Überweisung erheben, müssen Sie diese Gebühren bezahlen. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.altcoin=Bitte überweisen Sie von Ihrer externen {0}-Wallet\n{1} an den BTC-Verkäufer.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Bitte gehen Sie zu einer Bank und zahlen Sie {0} an den BTC-Verkäufer.\n\n +portfolio.pending.step2_buyer.cash.extra=WICHTIGE VORAUSSETZUNG:\nNachdem Sie die Zahlung getätigt haben, schreiben Sie auf die Quittung: NO REFUNDS.\nReißen Sie diese in zwei Teile und machen Sie ein Foto, das Sie an die E-Mail-Adresse des BTC-Verkäufers senden. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Bitte zahlen Sie {0} an den BTC-Verkäufer mit MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=WICHTIGE VORAUSSETZUNG: \nNachdem Sie die Zahlung getätigt haben, senden Sie die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail an den BTC-Verkäufer.\nDie Quittung muss den vollständigen Namen, das Land, Bundesland des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Bitte zahlen Sie {0} an den BTC-Verkäufer mit Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=WICHTIGE VORAUSSETZUNG: \nNachdem Sie die Zahlung getätigt haben, senden Sie die MTCN (Tracking-Nummer) und ein Foto der Quittung per E-Mail an den BTC-Verkäufer.\nDie Quittung muss den vollständigen Namen, die Stadt, das Land des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Bitte senden Sie {0} per \"US Postal Money Order\" an den BTC-Verkäufer.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Bitte schicken Sie {0} Bargeld per Post an den BTC 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 Bisq-Wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Bitte zahlen Sie {0} mit der gewählten Zahlungsmethode an den BTC Verkäufer. Sie finden die Konto Details des Verkäufers im nächsten Fenster.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Bitte kontaktieren Sie den BTC-Verkäufer, mit den bereitgestellten Daten und organisieren Sie ein Treffen um {0} zu zahlen.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Zahlung per {0} beginnen +portfolio.pending.step2_buyer.recipientsAccountData=Empfänger {0} +portfolio.pending.step2_buyer.amountToTransfer=Zu überweisender Betrag +portfolio.pending.step2_buyer.sellersAddress={0}-Adresse des Verkäufers +portfolio.pending.step2_buyer.buyerAccount=Ihr zu verwendendes Zahlungskonto +portfolio.pending.step2_buyer.paymentStarted=Zahlung begonnen +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=Sie haben Ihre {0} Zahlung noch nicht getätigt!\nBeachten Sie bitte, dass der Handel bis {1} abgeschlossen werden muss. +portfolio.pending.step2_buyer.openForDispute=Sie haben Ihre Zahlung noch nicht abgeschlossen!\nDie maximale Frist für den Handel ist abgelaufen, bitte wenden Sie sich an den Vermittler, um Hilfe zu erhalten. +portfolio.pending.step2_buyer.paperReceipt.headline=Haben Sie die Quittung an den BTC-Verkäufer gesendet? +portfolio.pending.step2_buyer.paperReceipt.msg=Erinnerung:\nSie müssen folgendes auf die Quittung schreiben: NO REFUNDS.\nZerreißen Sie diese dann in zwei Teile und machen Sie ein Foto, das Sie an die E-Mail-Adresse des BTC-Verkäufers senden. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Authorisierungs-Nummer und Quittung senden +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Sie müssen die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail an den BTC-Verkäufer senden.\nDie Quittung muss den vollständigen Namen, das Land, das Bundesland des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}.\n\nHaben Sie die Authorisierungs-Nummer und Vertragt an den Verkäufer gesendet? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCN und Quittung senden +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Sie müssen die MTCN (Tracking-Nummer) und ein Foto der Quittung per E-Mail an den BTC-Verkäufer senden.\nDie Quittung muss den vollständigen Namen, die Stadt, das Land des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}.\n\nHaben Sie die MTCN und Vertragt an den Verkäufer gesendet? +portfolio.pending.step2_buyer.halCashInfo.headline=HalCash Code senden +portfolio.pending.step2_buyer.halCashInfo.msg=Sie müssen eine SMS mit dem HalCash-Code sowie der Trade-ID ({0}) an den BTC-Verkäufer senden.\nDie Handynummer des Verkäufers lautet {1}.\n\nHaben Sie den Code an den Verkäufer gesendet? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Einige Banken könnten den Namen des Empfängers überprüfen. Faster Payments Konten, die in alten Bisq-Clients angelegt wurden, geben den Namen des Empfängers nicht an, also benutzen Sie bitte den Trade-Chat, um ihn zu erhalten (falls erforderlich). +portfolio.pending.step2_buyer.confirmStart.headline=Bestätigen Sie, dass Sie die Zahlung begonnen haben +portfolio.pending.step2_buyer.confirmStart.msg=Haben Sie die {0}-Zahlung an Ihren Handelspartner begonnen? +portfolio.pending.step2_buyer.confirmStart.yes=Ja, ich habe die Zahlung begonnen +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Sie haben keinen Zahlungsnachweis eingereicht. +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Sie haben die Transaktions-ID und den Transaktionsschlüssel nicht eingegeben.\n\nWenn Sie diese Informationen nicht zur Verfügung stellen, kann Ihr Handelspartner die automatische Bestätigung nicht nutzen, um die BTC freizugeben sobald die XMR erhalten wurden.\nAußerdem setzt Bisq voraus, dass der Sender der XMR Transaktion diese Informationen im Falle eines Konflikts dem Vermittler oder der Schiedsperson mitteilen kann.\nWeitere Informationen finden Sie im Bisq Wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Die Eingabe ist kein 32 byte Hexadezimalwert +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorieren und fortfahren +portfolio.pending.step2_seller.waitPayment.headline=Auf Zahlung warten +portfolio.pending.step2_seller.f2fInfo.headline=Kontaktinformation des Käufers +portfolio.pending.step2_seller.waitPayment.msg=Die Kautionstransaktion hat mindestens eine Blockchain-Bestätigung.\nSie müssen warten bis der BTC-Käufer die {0}-Zahlung beginnt. +portfolio.pending.step2_seller.warn=Der BTC-Käufer hat die {0}-Zahlung noch nicht getätigt.\nSie müssen warten bis die Zahlung begonnen wurde.\nWenn der Handel nicht bis {1} abgeschlossen wurde, wird der Vermittler diesen untersuchen. +portfolio.pending.step2_seller.openForDispute=Der BTC-Käufer hat seine Zahlung nicht begonnen!\nDie maximal zulässige Frist für den Handel ist abgelaufen.\nSie können länger warten und dem Handelspartner mehr Zeit geben oder den Vermittler um Hilfe bitten. +tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID ''{0}'' +tradeChat.openChat=Chat-Fenster öffnen +tradeChat.rules=Sie können mit Ihrem Trade-Partner kommunizieren, um mögliche Probleme mit diesem Trade zu lösen.\nEs ist nicht zwingend erforderlich, im Chat zu antworten.\nWenn ein Trader gegen eine der folgenden Regeln verstößt, eröffnen Sie einen Streitfall und melden Sie ihn dem Mediator oder Vermittler.\n\nChat-Regeln:\n\t● Senden Sie keine Links (Risiko von Malware). Sie können die Transaktions-ID und den Namen eines Block-Explorers senden.\n\t● Senden Sie keine Seed-Wörter, Private Keys, Passwörter oder andere sensible Informationen!\n\t● Traden Sie nicht außerhalb von Bisq (keine Sicherheit).\n\t● Beteiligen Sie sich nicht an Betrugsversuchen in Form von Social Engineering.\n\t● Wenn ein Partner nicht antwortet und es vorzieht, nicht über den Chat zu kommunizieren, respektieren Sie seine Entscheidung.\n\t● Beschränken Sie Ihre Kommunikation auf das Traden. Dieser Chat ist kein Messenger-Ersatz oder eine Trollbox.\n\t● Bleiben Sie im Gespräch freundlich und respektvoll. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +message.state.SENT=Nachricht gesendet +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Nachricht beim Peer angekommen +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Nachricht über die gesendete Zahlung wurde verschickt, aber vom Peer noch nicht erhalten +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Peer hat Nachrichtenerhalt bestätigt +# suppress inspection "UnusedProperty" +message.state.FAILED=Senden der Nachricht fehlgeschlagen + +portfolio.pending.step3_buyer.wait.headline=Auf Zahlungsbestätigung des BTC-Verkäufers warten +portfolio.pending.step3_buyer.wait.info=Auf Bestätigung des BTC-Verkäufers zum Erhalt der {0}-Zahlung warten. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Zahlungsbeginn-Nachricht-Status +portfolio.pending.step3_buyer.warn.part1a=in der {0}-Blockchain +portfolio.pending.step3_buyer.warn.part1b=bei Ihrem Zahlungsanbieter (z.B. Bank) +portfolio.pending.step3_buyer.warn.part2=Der BTC-Verkäufer hat Ihre Zahlung noch nicht bestätigt. Bitte überprüfen Sie {0}, ob der Zahlungsvorgang erfolgreich war. +portfolio.pending.step3_buyer.openForDispute=Der BTC-Verkäufer hat Ihre Zahlung nicht bestätigt! Die maximale Frist für den Handel ist abgelaufen. Sie können länger warten und dem Trading-Partner mehr Zeit geben oder den Vermittler um Hilfe bitten. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Ihr Handelspartner hat bestätigt, die {0}-Zahlung begonnen zu haben.\n\n +portfolio.pending.step3_seller.altcoin.explorer=in ihrem bevorzugten {0} Blockchain Explorer +portfolio.pending.step3_seller.altcoin.wallet=in ihrer {0} Wallet +portfolio.pending.step3_seller.altcoin={0}Bitte überprüfen Sie mit Ihrem bevorzugten {1}-Blockchain-Explorer, ob die Transaktion zu Ihrer Empfangsadresse\n{2}\nschon genug Blockchain-Bestätigungen hat.\nDer Zahlungsbetrag muss {3} sein\n\nSie können Ihre {4}-Adresse vom Hauptbildschirm kopieren und woanders einfügen, nachdem dieser Dialog geschlossen wurde. +portfolio.pending.step3_seller.postal={0}Bitte überprüfen Sie, ob Sie {1} per \"US Postal Money Order\" vom BTC-Käufer erhalten haben. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Bitte überprüfen Sie, ob Sie {1} als \"Bargeld per Post\" vom BTC-Käufer erhalten haben. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Ihr Handelspartner hat den Beginn der {0}-Zahlung bestätigt.\n\nBitte gehen Sie auf Ihre Online-Banking-Website und überprüfen Sie, ob Sie {1} vom BTC-Käufer erhalten haben. +portfolio.pending.step3_seller.cash=Da die Zahlung per Cash Deposit ausgeführt wurde, muss der BTC-Käufer \"NO REFUND\" auf die Quittung schreiben, diese in 2 Teile reißen und Ihnen ein Foto per E-Mail schicken.\n\nUm die Gefahr einer Rückbuchung zu vermeiden bestätigen Sie nur, wenn Sie die E-Mail erhalten haben und Sie sicher sind, dass die Quittung gültig ist.\nWenn Sie nicht sicher sind, {0} +portfolio.pending.step3_seller.moneyGram=Der Käufer muss Ihnen die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail zusenden.\nDie Quittung muss deutlich Ihren vollständigen Namen, Ihr Land, Ihr Bundesland und den Betrag enthalten. Bitte überprüfen Sie Ihre E-Mail, wenn Sie die Authorisierungs-Nummer erhalten haben.\n\nNach dem Schließen dieses Pop-ups sehen Sie den Namen und die Adresse des BTC-Käufers, um das Geld von MoneyGram abzuholen.\n\nBestätigen Sie den Erhalt erst, nachdem Sie das Geld erfolgreich abgeholt haben! +portfolio.pending.step3_seller.westernUnion=Der Käufer muss Ihnen die MTCN (Sendungsnummer) und ein Foto der Quittung per E-Mail zusenden.\nDie Quittung muss deutlich Ihren vollständigen Namen, Ihre Stadt, Ihr Land und den Betrag enthalten. Bitte überprüfen Sie Ihre E-Mail, wenn Sie die MTCN erhalten haben.\n\nNach dem Schließen dieses Pop-ups sehen Sie den Namen und die Adresse des BTC-Käufers, um das Geld von Western Union abzuholen.\n\nBestätigen Sie den Erhalt erst, nachdem Sie das Geld erfolgreich abgeholt haben! +portfolio.pending.step3_seller.halCash=Der Käufer muss Ihnen den HalCash-Code als SMS zusenden. Außerdem erhalten Sie eine Nachricht von HalCash mit den erforderlichen Informationen, um EUR an einem HalCash-fähigen Geldautomaten abzuheben.\n\nNachdem Sie das Geld am Geldautomaten abgeholt haben, bestätigen Sie bitte hier den Zahlungseingang! +portfolio.pending.step3_seller.amazonGiftCard=Der Käufer hat Ihnen eine Amazon eGift Geschenkkarte per E-Mail oder per Textnachricht auf Ihr Handy geschickt. Bitte lösen Sie die Amazon eGift Geschenkkarte jetzt in Ihrem Amazon-Konto ein und bestätigen Sie nach der erfolgreichen Annahme den Zahlungseingang. + +portfolio.pending.step3_seller.bankCheck=\n\nBitte überprüfen Sie auch, ob der Name des im Trading-Vertrag angegebenen Absenders mit dem Namen auf Ihrem Kontoauszug übereinstimmt:\nName des Absenders, pro Trade-Vertrag: {0}\n\nWenn die Namen nicht genau gleich sind, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=bestätigen Sie den Zahlungseingang nicht. Eröffnen Sie stattdessen einen Konflikt, indem Sie \"alt + o\" oder \"option + o\" drücken.\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Zahlungserhalt bestätigen +portfolio.pending.step3_seller.amountToReceive=Zu erhaltender Betrag +portfolio.pending.step3_seller.yourAddress=Ihre {0}-Adresse +portfolio.pending.step3_seller.buyersAddress={0}-Adresse des Käufers +portfolio.pending.step3_seller.yourAccount=Ihr Handelskonto +portfolio.pending.step3_seller.xmrTxHash=Transaktions-ID +portfolio.pending.step3_seller.xmrTxKey=Transaktions-Schlüssel +portfolio.pending.step3_seller.buyersAccount=Käufer Konto-Informationen +portfolio.pending.step3_seller.confirmReceipt=Zahlungserhalt bestätigen +portfolio.pending.step3_seller.buyerStartedPayment=Der BTC-Käufer hat die {0}-Zahlung begonnen.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Überprüfen Sie Ihre Altcoin-Wallet oder Ihren Block-Explorer auf Blockchain-Bestätigungen und bestätigen Sie die Zahlung, wenn ausreichend viele Blockchain-Bestätigungen angezeigt werden. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Prüfen Sie Ihr Handelskonto (z.B. Bankkonto) und bestätigen Sie, wenn Sie die Zahlung erhalten haben. +portfolio.pending.step3_seller.warn.part1a=in der {0}-Blockchain +portfolio.pending.step3_seller.warn.part1b=bei Ihrem Zahlungsanbieter (z.B. Bank) +portfolio.pending.step3_seller.warn.part2=Sie haben den Eingang der Zahlung noch nicht bestätigt. Bitte überprüfen Sie {0} ob Sie die Zahlung erhalten haben. +portfolio.pending.step3_seller.openForDispute=Sie haben den Eingang der Zahlung nicht bestätigt!\nDie maximale Frist für den Handel ist abgelaufen.\nBitte bestätigen Sie oder bitten Sie den Vermittler um Unterstützung. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Ist die {0}-Zahlung Ihres Handelspartners eingegangen?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Bitte überprüfen Sie auch, ob der Name des im Trade-Vertrag angegebenen Absenders mit dem Namen auf Ihrem Kontoauszug übereinstimmt:\nName des Absenders, pro Trade-Vertrag: {0}\n\nWenn die Namen nicht genau gleich sind, bestätigen Sie den Zahlungseingang nicht. Eröffnen Sie stattdessen einen Konflikt, indem Sie \"alt + o\" oder \"option + o\" drücken.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Bitte beachten Sie, dass, sobald Sie den Erhalt bestätigt haben, der gesperrte Trade-Betrag an den BTC-Käufer freigegeben wird und die Kaution zurückerstattet wird.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Bestätigen Sie, die Zahlung erhalten zu haben +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Ja, ich habe die Zahlung erhalten +portfolio.pending.step3_seller.onPaymentReceived.signer=WICHTIG: Mit der Bestätigung des Zahlungseingangs verifizieren Sie auch das Konto der Gegenpartei und unterzeichnen es entsprechend. Da das Konto der Gegenpartei noch nicht unterzeichnet ist, sollten Sie die Bestätigung der Zahlung so lange wie möglich hinauszögern, um das Risiko einer Rückbelastung zu reduzieren. + +portfolio.pending.step5_buyer.groupTitle=Zusammenfassung des abgeschlossenen Handels +portfolio.pending.step5_buyer.tradeFee=Handelsgebühr +portfolio.pending.step5_buyer.makersMiningFee=Mining-Gebühr +portfolio.pending.step5_buyer.takersMiningFee=Gesamte Mining-Gebühr +portfolio.pending.step5_buyer.refunded=Rückerstattete Kaution +portfolio.pending.step5_buyer.withdrawBTC=Ihre Bitcoins abheben +portfolio.pending.step5_buyer.amount=Abzuhebender Betrag +portfolio.pending.step5_buyer.withdrawToAddress=An diese Adresse abheben +portfolio.pending.step5_buyer.moveToBisqWallet=Gelder in der Bisq Wallet aufbewahren +portfolio.pending.step5_buyer.withdrawExternal=An externe Wallet abheben +portfolio.pending.step5_buyer.alreadyWithdrawn=Ihre Gelder wurden bereits abgehoben.\nBitte überprüfen Sie den Transaktionsverlauf. +portfolio.pending.step5_buyer.confirmWithdrawal=Anfrage zum Abheben bestätigen +portfolio.pending.step5_buyer.amountTooLow=Der zu überweisende Betrag ist kleiner als die Transaktionsgebühr und der minimale Tx-Wert (Staub). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Abheben abgeschlossen +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Ihre abgeschlossenen Trades sind unter \"Portfolio/Verlauf\" gespeichert.\nSie können all Ihre Bitcoin-Transaktionen unter \"Gelder/Transaktionen\" einsehen +portfolio.pending.step5_buyer.bought=Sie haben gekauft +portfolio.pending.step5_buyer.paid=Sie haben gezahlt + +portfolio.pending.step5_seller.sold=Sie haben verkauft +portfolio.pending.step5_seller.received=Sie haben erhalten + +tradeFeedbackWindow.title=Glückwunsch zum Abschluss ihres Handels. +tradeFeedbackWindow.msg.part1=Wir würden gerne von Ihren Erfahrungen mit Bisq hören. Dies hilft uns die Software zu verbessern und etwaige Stolpersteine zu beseitigen. Um uns ihr Feedback mitzuteilen, füllen Sie bitte diese kurze Umfrage aus (keine Registrierung benötigt): +tradeFeedbackWindow.msg.part2=Sollten Sie Fragen oder Probleme haben, kontaktieren Sie andere Nutzer und Mitwirkende im Bisq-Forum auf: +tradeFeedbackWindow.msg.part3=Vielen Dank, dass Sie Bisq benutzen! + +portfolio.pending.role=Meine Rolle +portfolio.pending.tradeInformation=Handelsinformationen +portfolio.pending.remainingTime=Verbleibende Zeit +portfolio.pending.remainingTimeDetail={0} (bis {1}) +portfolio.pending.tradePeriodInfo=Die Handelsdauer beginnt mit der ersten Blockchain-Bestätigung. Abhängig von der Zahlungart, wird eine maximale Handesldauer gesetzt. +portfolio.pending.tradePeriodWarning=Wird die Dauer überschritten, können beide Händler einen Konflikt öffnen. +portfolio.pending.tradeNotCompleted=Maximale Handelsdauer wurde überschritten (bis {0}) +portfolio.pending.tradeProcess=Handelsprozess +portfolio.pending.openAgainDispute.msg=Wenn Sie sich nicht sicher sind, ob die Nachricht an den Vermittler oder die Schiedsperson angekommen ist (z. B., wenn Sie nach einem Tag noch keine Antwort erhalten haben), können Sie mit Cmd/Strg+o einen weiteren Konfliktfall eröffnen. Sie können auch im Bisq Forum nach Hilfe fragen [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Konflikt erneut öffnen +portfolio.pending.openSupportTicket.headline=Support-Ticket öffnen +portfolio.pending.openSupportTicket.msg=Bitte verwenden Sie diese Funktion nur in Notfällen, wenn Sie keinen \"Open support\" oder \"Open dispute\" Button sehen.\n\nWenn Sie ein Support-Ticket öffnen, wird der Trade unterbrochen und von einem Mediator oder Vermittler bearbeitet. + +portfolio.pending.timeLockNotOver=Sie müssen ≈{0} ({1} weitere Blöcke) warten, bevor Sie einen Vermittlungskonflikt eröffnen können. +portfolio.pending.error.depositTxNull=Die Einzahlungstransaktion ist null. Sie können einen Streitfall nicht ohne eine gültige Einzahlungstransaktion eröffnen. Bitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\n\nFür weitere Hilfe wenden Sie sich bitte an den Bisq-Support-Kanal des Bisq Keybase Teams. +portfolio.pending.mediationResult.error.depositTxNull=Die Einzahlungstransaktion ist ungültig. Sie können den Handel zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=Die verzögerte Auszahlungstransaktion ist ungültig. Sie können den Handel zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.error.depositTxNotConfirmed=Die Einzahlungstransaktion ist nicht bestätigt. Sie können einen Streitfall nicht ohne eine bestätigte Einzahlungstransaktion eröffnen. Bitte warten Sie, bis diese bestätigt ist, oder gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\n\nFür weitere Hilfe wenden Sie sich bitte an den Bisq-Support-Kanal des Bisq Keybase Teams. + +portfolio.pending.support.headline.getHelp=Brauchen Sie Hilfe? +portfolio.pending.support.text.getHelp=Wenn Sie irgendwelche Probleme haben, können Sie versuchen, den Trade-Partner im Trade-Chat zu kontaktieren oder die Bisq-Community unter https://bisq.community zu fragen. Wenn Ihr Problem immer noch nicht gelöst ist, können Sie weitere Hilfe von einem Mediator anfordern. +portfolio.pending.support.button.getHelp=Trader Chat öffnen +portfolio.pending.support.headline.halfPeriodOver=Zahlung überprüfen +portfolio.pending.support.headline.periodOver=Die Handelsdauer ist abgelaufen + +portfolio.pending.mediationRequested=Mediation beantragt +portfolio.pending.refundRequested=Rückerstattung beantragt +portfolio.pending.openSupport=Support-Ticket öffnen +portfolio.pending.supportTicketOpened=Support-Ticket geöffnet +portfolio.pending.communicateWithArbitrator=Bitte setzen Sie sich im \"Support\"-Bildschirm mit dem Vermittler in Verbindung. +portfolio.pending.communicateWithMediator=Bitte kommunizieren Sie im \"Support\" Bildschirm mit dem Mediator. +portfolio.pending.disputeOpenedMyUser=Sie haben bereits einen Konflikt geöffnet.\n{0} +portfolio.pending.disputeOpenedByPeer=Ihr Handelspartner hat einen Konflikt geöffnet\n{0} +portfolio.pending.noReceiverAddressDefined=Keine Empfangsadresse festgelegt + +portfolio.pending.mediationResult.headline=Vorgeschlagene Auszahlung aus der Mediation +portfolio.pending.mediationResult.info.noneAccepted=Schließen Sie den Trade ab, indem Sie den Vorschlag des Mediators für die Trade-Auszahlung annehmen. +portfolio.pending.mediationResult.info.selfAccepted=Sie haben den Vorschlag des Mediators angenommen. Warten Sie darauf, dass auch der Partner akzeptiert. +portfolio.pending.mediationResult.info.peerAccepted=Ihr Trade-Partner hat den Vorschlag des Mediators angenommen. Akzeptieren Sie ihn auch? +portfolio.pending.mediationResult.button=Lösungsvorschlag ansehen +portfolio.pending.mediationResult.popup.headline=Mediationsergebnis für Trade mit ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Ihr Trade-Partner hat den Vorschlag des Mediators akzeptiert für Trade {0} +portfolio.pending.mediationResult.popup.info=Der Vermittler hat folgende Auszahlung vorgeschlagen: \nSie erhalten: {0}\nIhr Handelspartner erhält: {1}\n\nSie können die vorgeschlagene Auszahlung akzeptieren oder ablehnen.\n\nAkzeptieren Sie, unterzeichnen Sie die vorgeschlagene Transaktion. Wenn Ihr Handelspartner auch akzeptiert und unterzeichnet, wird die Auszahlung getätigt und der Handel abgeschlossen.\n\nWenn einer oder beide den Vorschlag ablehnen, müssen Sie bis {2} (block {3}) warten, um eine zweite Konfliktrunde mit einer Schiedsperson zu starten, die den Handel erneut untersuchen wird und je nach eigenem Ergebnis eine Auszahlung veranlassen wird.\n\nDie Schiedsperson kann eine kleine Gebühr für ihre Arbeit berechnen (maximale Gebühr: Sicherheitskaution des Händlers). Im Idealfall akzeptieren beide Händler den Vorschlag des Vermittlers — eine Schiedsperson hinzuzuziehen ist nur für außergewöhnliche Fälle vorgesehen. Ein solcher Fall wäre, wenn ein Händler sich sicher ist, dass der Auszahlungsvorschlag nicht fair ist, oder der Handelspartner nicht antwortet.\n\nWeitere Informationen über das Schlichtungssystem finden Sie unter [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Sie haben die vom Vermittler vorgeschlagene Auszahlung akzeptiert, aber es scheint so, als hätte Ihr Handelspartner sie noch nicht akzeptiert.\n\nSobald die Sperre bei {0} (block {1})) aufgehoben ist, können Sie eine zweite Runde des Konflikts eröffnen. Eine Schiedsperson wird dann den Konflikt erneut untersuchen und je nach eigenem Ergebnis eine Auszahlung veranlassen.\n\nHier können Sie mehr Informationen über das Schiedsverfahren finden:\n[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Ablehnen und Vermittler hinzuziehen +portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzeptiert + +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-(Fiat-) oder Altcoin-Zahlungen an den BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Handel zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.failedTrade.warningIcon.tooltip=Klicken Sie hier um herauszufinden welche Probleme beim Handel aufgetreten sind. +portfolio.failed.revertToPending.popup=Wollen Sie diesen Handel zu den offenen Händeln verschieben? +portfolio.failed.revertToPending=Handel zu den offenen Händeln verschieben + +portfolio.closed.completed=Abgeschlossen +portfolio.closed.ticketClosed=Vermittelt +portfolio.closed.mediationTicketClosed=Mediiert +portfolio.closed.canceled=Abgebrochen +portfolio.failed.Failed=Fehlgeschlagen +portfolio.failed.unfail=Bevor Sie fortfahren, stellen Sie sicher, dass Sie ein Backup Ihres Datenverzeichnisses haben!\nWollen Sie diesen Trade wieder in offene Trades verschieben?\nDies ist eine Möglichkeit, Gelder freizugeben, die in einem gescheiterten Trade stecken geblieben sind. +portfolio.failed.cantUnfail=Dieser Trade kann im Moment nicht wieder in offene Trades verschoben werden. \nVersuchen Sie es nach Abschluss des/der Trades erneut {0} +portfolio.failed.depositTxNull=Der Trade kann nicht als offener Trade zurückgeändert werden. Einzahlungstransaktion ist ungültig. +portfolio.failed.delayedPayoutTxNull=Der Trade kann nicht als offener Trade zurückgeändert werden. Verzögerte Auszahlungstransaktion ist ungültig. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Gelder erhalten +funds.tab.withdrawal=Gelder senden +funds.tab.reserved=Reservierte Gelder +funds.tab.locked=Gesperrte Gelder +funds.tab.transactions=Transaktionen + +funds.deposit.unused=Ungenutzt +funds.deposit.usedInTx=In {0} Transaktion(en) genutzt +funds.deposit.fundBisqWallet=Bisq-Wallet finanzieren +funds.deposit.noAddresses=Es wurden noch keine Kautionsadressen generiert +funds.deposit.fundWallet=Ihre Wallet finanzieren +funds.deposit.withdrawFromWallet=Gelder aus Wallet übertragen +funds.deposit.amount=Betrag in BTC (optional) +funds.deposit.generateAddress=Neue Adresse generieren +funds.deposit.generateAddressSegwit=Native segwit Format (Bech32) +funds.deposit.selectUnused=Bitte wählen Sie eine ungenutzte Adresse aus der Tabelle oben, anstatt eine neue zu generieren. + +funds.withdrawal.arbitrationFee=Vermittlergebühr +funds.withdrawal.inputs=Eingaben Auswahl +funds.withdrawal.useAllInputs=Alle verfügbaren Eingaben benutzen +funds.withdrawal.useCustomInputs=Spezifische Eingaben benutzen +funds.withdrawal.receiverAmount=Empfängers Betrag +funds.withdrawal.senderAmount=Senders Betrag +funds.withdrawal.feeExcluded=Betrag ohne Mining-Gebühr +funds.withdrawal.feeIncluded=Betrag beinhaltet Mining-Gebühr +funds.withdrawal.fromLabel=Von Adresse abheben +funds.withdrawal.toLabel=An diese Adresse abheben +funds.withdrawal.memoLabel=Notiz für Abhebung +funds.withdrawal.memo=Optional ausgefüllte Notiz +funds.withdrawal.withdrawButton=Auswahl abheben +funds.withdrawal.noFundsAvailable=Keine Gelder zum Abheben verfügbar +funds.withdrawal.confirmWithdrawalRequest=Anfrage zum Abheben bestätigen +funds.withdrawal.withdrawMultipleAddresses=Von mehreren Adressen abheben ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Von mehreren Adressen abheben:\n{0} +funds.withdrawal.notEnoughFunds=Sie haben nicht genug Geld in Ihrer Wallet. +funds.withdrawal.selectAddress=Wählen Sie eine Quelladresse aus der Tabelle +funds.withdrawal.setAmount=Legen Sie den abzuhebenen Betrag fest +funds.withdrawal.fillDestAddress=Geben Sie Ihre Zieladresse an +funds.withdrawal.warn.noSourceAddressSelected=Sie müssen eine Quelladresse aus der Tabelle oben wählen. +funds.withdrawal.warn.amountExceeds=Ihre Gelder in den ausgewählten Adressen reichen nicht aus.\nWählen Sie mehrere Adressen aus der Tabelle oben, oder ändern Sie die Gebühren-Schalter, um die Mining-Gebühr zu beinhalten. + +funds.reserved.noFunds=Es sind keine Gelder in offenen Angeboten reserviert +funds.reserved.reserved=In lokaler Wallet für das Angebot mit dieser ID reserviert: {0} + +funds.locked.noFunds=Es sind keine Gelder in Trades gesperrt +funds.locked.locked=Für den Handel mit dieser ID in MultiSig eingesperrt: {0} + +funds.tx.direction.sentTo=Gesendet nach: +funds.tx.direction.receivedWith=Erhalten mit: +funds.tx.direction.genesisTx=Aus Ursprungs-Tx: +funds.tx.txFeePaymentForBsqTx=Mining-Gebühr für BSQ-Tx +funds.tx.createOfferFee=Ersteller- und Tx-Gebühr: {0} +funds.tx.takeOfferFee=Abnehmer- und Tx-Gebühr: {0} +funds.tx.multiSigDeposit=MultiSig-Kaution: {0} +funds.tx.multiSigPayout=MultiSig-Auszahlung: {0} +funds.tx.disputePayout=Konfliktauszahlung: {0} +funds.tx.disputeLost=Verlorener Konflikt: {0} +funds.tx.collateralForRefund=Sicherheiten für die Rückerstattung: {0} +funds.tx.timeLockedPayoutTx=Zeitgesperrte Auszahlung tx: {0} +funds.tx.refund=Erstattung aus dem Vermittlungsverfahren: {0} +funds.tx.unknown=Unbekannter Grund: {0} +funds.tx.noFundsFromDispute=Keine Rückzahlung vom Konflikt +funds.tx.receivedFunds=Gelder erhalten +funds.tx.withdrawnFromWallet=Von Wallet abgehoben +funds.tx.withdrawnFromBSQWallet=BTC von BSQ Wallet abgehoben +funds.tx.memo=Notiz +funds.tx.noTxAvailable=Keine Transaktionen verfügbar +funds.tx.revert=Umkehren +funds.tx.txSent=Transaktion erfolgreich zu einer neuen Adresse in der lokalen Bisq-Wallet gesendet. +funds.tx.direction.self=An Sie selbst senden +funds.tx.daoTxFee=Mining-Gebühr für BSQ-Tx +funds.tx.reimbursementRequestTxFee=Rückerstattungsantrag +funds.tx.compensationRequestTxFee=Entlohnungsanfrage +funds.tx.dustAttackTx=Staub erhalten +funds.tx.dustAttackTx.popup=Diese Transaktion sendet einen sehr kleinen BTC Betrag an Ihre Wallet und kann von Chainanalyse Unternehmen genutzt werden um ihre Wallet zu spionieren.\n\nWenn Sie den Transaktionsausgabe in einer Ausgabe nutzen, wird es lernen, dass Sie wahrscheinlich auch Besitzer der anderen Adressen sind (coin merge),\n\nUm Ihre Privatsphäre zu schützen, wir die Bisqwallet Staubausgaben für Ausgaben und bei der Anzeige der Guthabens ignorieren. Sie können den Grenzwert, ab wann ein Wert als Staub angesehen wird in den Einstellungen ändern. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Vermittlung +support.tab.legacyArbitration.support=Legacy-Vermittlung +support.tab.ArbitratorsSupportTickets={0} Tickets +support.filter=Konflikte durchsuchen +support.filter.prompt=Tragen sie Handel ID, Datum, Onion Adresse oder Kontodaten + +support.sigCheck.button=Signatur überprüfen +support.sigCheck.popup.info=Im Falle eines Vergütungsantrags an die DAO müssen Sie eine Zusammenfassung des Vermittlungs- und Schiedsverfahrens in Ihren Vergütungsantrags auf Github einfügen. Um diese Angabe überprüfbar zu machen, kann jeder Nutzer mit diesem Tool überprüfen, ob die Signatur des Vermittlers oder der Schiedsperson mit der Zusammenfassung übereinstimmt. +support.sigCheck.popup.header=Signatur des Konfliktergebnisses überprüfen +support.sigCheck.popup.msg.label=Zusammenfassende Nachricht +support.sigCheck.popup.msg.prompt=Zusammenfassende Angaben aus dem Konflikt kopieren und einfügen +support.sigCheck.popup.result=Ergebnis der Überprüfung +support.sigCheck.popup.success=Unterzeichnung ist gültig +support.sigCheck.popup.failed=Unterzeichnung der Signatur ist fehlgeschlagen +support.sigCheck.popup.invalidFormat=Nachricht ist nicht im erwarteten Format. Copy & paste Konflikt-Zusammenfassung. + +support.reOpenByTrader.prompt=Sind Sie sicher, dass Sie den Konflikt wiedereröffnen möchten? +support.reOpenButton.label=Wiedereröffnen +support.sendNotificationButton.label=Private Benachrichtigung +support.reportButton.label=Melden +support.fullReportButton.label=Alle Konflikte +support.noTickets=Keine offenen Tickets vorhanden +support.sendingMessage=Nachricht wird gesendet... +support.receiverNotOnline=Empfänger ist nicht online. Nachricht wird in der Mailbox gespeichert. +support.sendMessageError=Senden der Nachricht fehlgeschlagen. Fehler: {0} +support.receiverNotKnown=Empfänger unbekannt +support.wrongVersion=Das Angebot im Konflikt wurde mit einer älteren Bisq-Version erstellt.\nSie können den Konflikt nicht mir Ihrer Version der Anwendung schließen.\n\nNutzen Sie bitte eine ältere Version mit der Protokollversion {0} +support.openFile=Anzufügende Datei öffnen (max. Dateigröße: {0} kb) +support.attachmentTooLarge=Die Gesamtgröße Ihres Anhangs ist {0} kb und überschreitet die maximal erlaubte Nachrichtengröße von {1} kB. +support.maxSize=Die maximal erlaubte Dateigröße ist {0} kB. +support.attachment=Anhang +support.tooManyAttachments=Sie können nicht mehr als 3 Anhänge mit einer Nachricht senden. +support.save=Datei auf Festplatte speichern +support.messages=Nachrichten +support.input.prompt=Nachricht eingeben... +support.send=Senden +support.addAttachments=Anhang anfügen +support.closeTicket=Ticket schließen +support.attachments=Anhänge: +support.savedInMailbox=Die Nachricht wurde im Postfach des Empfängers gespeichert +support.arrived=Die Nachricht ist beim Empfänger angekommen +support.acknowledged=Nachrichtenankunft vom Empfänger bestätigt +support.error=Empfänger konnte die Nachricht nicht verarbeiten. Fehler: {0} +support.buyerAddress=BTC-Adresse des Käufers +support.sellerAddress=BTC-Adresse des Verkäufers +support.role=Rolle +support.agent=Support-Mitarbeiter +support.state=Status +support.chat=Chat +support.closed=Geschlossen +support.open=Offen +support.process=Process +support.buyerOfferer=BTC-Käufer/Ersteller +support.sellerOfferer=BTC-Verkäufer/Ersteller +support.buyerTaker=BTC-Käufer/Abnehmer +support.sellerTaker=BTC-Verkäufer/Abnehmer + +support.backgroundInfo=Bisq ist kein Unternehmen, daher behandelt es Konflikte unterschiedlich.\n\nTrader können innerhalb der Anwendung über einen sicheren Chat auf dem Bildschirm für offene Trades kommunizieren, um zu versuchen, Konflikte selbst zu lösen. Wenn das nicht ausreicht, kann ein Mediator einschreiten und helfen. Der Mediator wird die Situation bewerten und eine Auszahlung von Trade Funds vorschlagen. Wenn beide Trader diesen Vorschlag annehmen, ist die Auszahlungstransaktion abgeschlossen und der Trade geschlossen. Wenn ein oder beide Trader mit der vom Mediator vorgeschlagenen Auszahlung nicht einverstanden sind, können sie ein Vermittlungsverfahren beantragen, bei dem der Vermittler die Situation neu bewertet und, falls gerechtfertigt, dem Trader persönlich eine Rückerstattung leistet und die Rückerstattung dieser Zahlung vom Bisq DAO verlangt. +support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der BTC-Käufer sind: Haben Sie die Fiat- oder Altcoin-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der BTC-Verkäufer sind: Haben Sie die Fiat- oder Altcoin-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Bisq verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}'' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} +support.systemMsg=Systemnachricht: {0} +support.youOpenedTicket=Sie haben eine Anfrage auf Support geöffnet.\n\n{0}\n\nBisq-Version: {1} +support.youOpenedDispute=Sie haben eine Anfrage für einen Konflikt geöffnet.\n\n{0}\n\nBisq-version: {1} +support.youOpenedDisputeForMediation=Sie haben um Mediation gebeten.\n\n{0}\n\nBisq-Version: {1} +support.peerOpenedTicket=Ihr Trading-Partner hat aufgrund technischer Probleme Unterstützung angefordert.\n\n{0}\n\nBisq-Version: {1} +support.peerOpenedDispute=Ihr Trading-Partner hat einen Konflikt eröffnet.\n\n{0}\n\nBisq-Version: {1} +support.peerOpenedDisputeForMediation=Ihr Trading-Partner hat eine Mediation beantragt.\n\n{0}\n\nBisq-Version: {1} +support.mediatorsDisputeSummary=Systemnachricht: Konflikt-Zusammenfassung des Mediators:\n{0} +support.mediatorsAddress=Node-Adresse des Mediators: {0} +support.warning.disputesWithInvalidDonationAddress=Die verzögerte Auszahlungstransaktion hat eine ungültige Empfängeradresse verwendet. Sie stimmt mit keinem der DAO-Parameter für die gültigen Spendenadressen überein.\n\nDies könnte ein Betrugsversuch sein. Bitte informieren Sie die Entwickler über diesen Vorfall und schließen Sie den Fall nicht ab, bevor die Situation geklärt ist!\n\nIn dem Konflikt verwendete Adresse: {0}\n\nAlle DAO-Param-Spendenadressen: {1}\n\nHandels-ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nWollen Sie den Konflikt trotzdem schließen? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nSie müssen nicht auszahlen. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Voreinstellungen +settings.tab.network=Netzwerk-Info +settings.tab.about=Über + +setting.preferences.general=Allgemeine Voreinstellungen +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Max. Abweichung vom Marktpreis +setting.preferences.bsqAverageTrimThreshold=Auslöser-Schwellenwert für BSQ-Rate +setting.preferences.avoidStandbyMode=Standby Modus verhindern +setting.preferences.autoConfirmXMR=XMR automatische Bestätigung +setting.preferences.autoConfirmEnabled=Aktiviert +setting.preferences.autoConfirmRequiredConfirmations=Benötigte Bestätigungen +setting.preferences.autoConfirmMaxTradeSize=Max. Trade-Höhe (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (verwendet Tor, außer localhost, LAN IP Adresse und *.local hostnames) +setting.preferences.deviationToLarge=Werte größer als {0}% sind nicht erlaubt. +setting.preferences.txFee=Auszahlungstransaktionsgebühr (satoshis/vbyte) +setting.preferences.useCustomValue=Spezifischen Wert nutzen +setting.preferences.txFeeMin=Die Transaktionsgebühr muss mindestens {0} satoshis/vbyte betragen +setting.preferences.txFeeTooLarge=Ihre Eingabe ist höher als jeder sinnvolle Wert (>5000 satoshis/vbyte). Transaktionsgebühren sind normalerweise zwischen 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Ignorierte Peers [Onion Adresse:Port] +setting.preferences.ignoreDustThreshold=Min. nicht-dust Ausgabewert +setting.preferences.currenciesInList=Währungen in Liste der Marktpreise +setting.preferences.prefCurrency=Bevorzugte Währung +setting.preferences.displayFiat=Nationale Währungen anzeigen +setting.preferences.noFiat=Es wurden keine nationalen Währungen ausgewählt +setting.preferences.cannotRemovePrefCurrency=Sie können Ihre ausgewählte bevorzugte Anzeigewährung nicht entfernen +setting.preferences.displayAltcoins=Altcoins anzeigen +setting.preferences.noAltcoins=Es sind keine Altcoins ausgewählt +setting.preferences.addFiat=Nationale Währung hinzufügen +setting.preferences.addAltcoin=Altcoin hinzufügen +setting.preferences.displayOptions=Darstellungsoptionen +setting.preferences.showOwnOffers=Eigenen Angebote im Angebotsbuch zeigen +setting.preferences.useAnimations=Animationen abspielen +setting.preferences.useDarkMode=Nacht-Modus benutzen +setting.preferences.sortWithNumOffers=Marktlisten nach Anzahl der Angebote/Trades sortieren +setting.preferences.onlyShowPaymentMethodsFromAccount=Nicht unterstützte Zahlungsmethoden ausblenden +setting.preferences.denyApiTaker=Taker die das API nutzen vermeiden +setting.preferences.notifyOnPreRelease=Pre-Release Benachrichtungen erhalten +setting.preferences.resetAllFlags=Alle \"Nicht erneut anzeigen\"-Häkchen zurücksetzen +settings.preferences.languageChange=Um den Sprachwechsel auf alle Bildschirme anzuwenden ist ein Neustart nötig. +settings.preferences.supportLanguageWarning=Wenn es zu einem Streitfall kommen sollte, beachten Sie bitte, dass die Mediation in {0} und das Vermittlungsverfahren in {1} geregelt wird. +setting.preferences.daoOptions=DAO-Optionen +setting.preferences.dao.resyncFromGenesis.label=DAO-Zustand von der Genesis-Tx wiederherstellen +setting.preferences.dao.resyncFromResources.label=DAO-Zustand aus Ressourcen wiederherstellen +setting.preferences.dao.resyncFromResources.popup=Nach einem Neustart der Anwendung werden die Bisq-Netzwerk-Governance-Daten von den Seed-Nodes neu geladen, und der BSQ-Konsensstatus wird aus den neuesten Ressourcendateien neu aufgebaut. +setting.preferences.dao.resyncFromGenesis.popup=Eine Resync von der Genesis-Transaktion kann erhebliche Zeit und CPU-Ressourcen in Anspruch nehmen. Sind Sie sicher, dass Sie das tun wollen? Meistens ist ein Resync von den neuesten Ressourcendateien ausreichend und viel schneller.\n\nWenn Sie fortfahren, werden nach einem Neustart der Anwendung die Bisq-Netzwerk-Governance-Daten von den Seed-Nodes neu geladen und der BSQ-Konsensstatus wird aus der Genesis-Transaktion neu aufgebaut. +setting.preferences.dao.resyncFromGenesis.resync=Resync von Genesis ausführen und beenden +setting.preferences.dao.isDaoFullNode=Bisq als DAO Full Node betreiben +setting.preferences.dao.rpcUser=RPC Benutzername +setting.preferences.dao.rpcPw=RPC Passwort +setting.preferences.dao.blockNotifyPort=Blockbenachrichtigung-Port +setting.preferences.dao.fullNodeInfo=Um Bisq als DAO Fullnode laufen zu lassen, müssen Sie Bitcoin Core lokal laufen und RPC eingeschaltet haben. Alle Bedingungen sind in "{0}" dokumentiert.\n\nNach dem Ändern des Modus müssen Sie neu starten. +setting.preferences.dao.fullNodeInfo.ok=Dokumentationsseite öffnen +setting.preferences.dao.fullNodeInfo.cancel=Nein, ich möchte weiterhin den Lite Node Modus verwenden +settings.preferences.editCustomExplorer.headline=Explorer-Einstellungen +settings.preferences.editCustomExplorer.description=Wählen Sie auf der linken Liste einen Explorer des Systems aus, und/oder passen Sie ihn an Ihre Vorlieben an. +settings.preferences.editCustomExplorer.available=Verfügbare Explorer +settings.preferences.editCustomExplorer.chosen=Wählen Sie die Explorer-Einstellungen aus +settings.preferences.editCustomExplorer.name=Name +settings.preferences.editCustomExplorer.txUrl=Transaktions-URL +settings.preferences.editCustomExplorer.addressUrl=Adress-URL + +settings.net.btcHeader=Bitcoin-Netzwerk +settings.net.p2pHeader=Bisq-Netzwerk +settings.net.onionAddressLabel=Meine Onion-Adresse +settings.net.btcNodesLabel=Spezifische Bitcoin-Core-Knoten verwenden +settings.net.bitcoinPeersLabel=Verbundene Peers +settings.net.useTorForBtcJLabel=Tor für das Bitcoin-Netzwerk verwenden +settings.net.bitcoinNodesLabel=Mit Bitcoin-Core-Knoten verbinden +settings.net.useProvidedNodesRadio=Bereitgestellte Bitcoin-Core-Knoten verwenden +settings.net.usePublicNodesRadio=Öffentliches Bitcoin-Netzwerk benutzen +settings.net.useCustomNodesRadio=Spezifische Bitcoin-Core-Knoten verwenden +settings.net.warn.usePublicNodes=Wenn Sie das öffentliche Bitcoin Netzwerk verwenden, sind Sie den Privatsphäre-Probleme die durch das defekte Bloom Filter Design, das von SPV wallets wie BitcoinJ (verwendet von Bisq) verwendet wird, ausgesetzt. Jede Full Node mit der Sie sich verbinden könnte dadurch alle Adressen, die zu einer Entität gehören, herausfinden.\n\nWeitere Informationen finden Sie unter [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nSind Sie sicher, dass Sie die öffentlichen Nodes verwenden wollen? +settings.net.warn.usePublicNodes.useProvided=Nein, bereitgestellte Knoten verwenden +settings.net.warn.usePublicNodes.usePublic=Ja, öffentliches Netzwerk verwenden +settings.net.warn.useCustomNodes.B2XWarning=Bitte stellen Sie sicher, dass Sie sich mit einem vertrauenswürdigen Bitcoin-Core-Knoten verbinden!\n\nWenn Sie sich mit Knoten verbinden, die gegen die Bitcoin Core Konsensus-Regeln verstoßen, kann es zu Problemen in Ihrer Wallet und im Verlauf des Handelsprozesses kommen.\n\nBenutzer die sich zu oben genannten Knoten verbinden, sind für den verursachten Schaden verantwortlich. Dadurch entstandene Konflikte werden zugunsten des anderen Teilnehmers entschieden. Benutzer die unsere Warnungen und Sicherheitsmechanismen ignorieren wird keine technische Unterstützung geleistet! +settings.net.warn.invalidBtcConfig=Die Verbindung zum Bitcoin-Netzwerk ist fehlgeschlagen, weil Ihre Konfiguration ungültig ist.\n\nIhre Konfiguration wurde zurückgesetzt, um stattdessen die bereitgestellten Bitcoin-Nodes zu verwenden. Sie müssen die Anwendung neu starten. +settings.net.localhostBtcNodeInfo=Hintergrundinformationen: Bisq sucht beim Start nach einem lokalen Bitcoin-Node. Wird dieser gefunden, kommuniziert Bisq ausschließlich über diesen mit dem Bitcoin-Netzwerk. +settings.net.p2PPeersLabel=Verbundene Peers +settings.net.onionAddressColumn=Onion-Adresse +settings.net.creationDateColumn=Eingerichtet +settings.net.connectionTypeColumn=Ein/Aus +settings.net.sentDataLabel=Daten-Statistiken senden +settings.net.receivedDataLabel=Daten-Statistiken empfangen +settings.net.chainHeightLabel=Letzte BTC Blockzeit +settings.net.roundTripTimeColumn=Umlaufzeit +settings.net.sentBytesColumn=Gesendet +settings.net.receivedBytesColumn=Erhalten +settings.net.peerTypeColumn=Peer-Typ +settings.net.openTorSettingsButton=Tor-Netzwerkeinstellungen öffnen + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Höhe + +settings.net.needRestart=Sie müssen die Anwendung neustarten, um die Änderungen anzuwenden.\nMöchten Sie jetzt neustarten? +settings.net.notKnownYet=Noch nicht bekannt... +settings.net.sentData=Gesendete Daten: {0}, {1} Nachrichten, {2} Nachrichten/Sekunde +settings.net.receivedData=Empfangene Daten: {0}, {1} Nachrichten, {2} Nachrichten/Sekunde +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[IP Adresse:Port | Hostname:Port | Onion-Adresse:Port] (Komma getrennt). Port kann weggelassen werden, wenn Standard genutzt wird (8333). +settings.net.seedNode=Seed-Knoten +settings.net.directPeer=Peer (direkt) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Peer +settings.net.inbound=eingehend +settings.net.outbound=ausgehend +settings.net.reSyncSPVChainLabel=SPV-Kette neu synchronisieren +settings.net.reSyncSPVChainButton=SPV-Datei löschen und neu synchronisieren +settings.net.reSyncSPVSuccess=Sind Sie sicher, dass Sie den SPV Resync starten möchten? Wenn Sie fortfahren, wird die SPV chain beim nächsten Start gelöscht.\n\nNach dem Restart kann der Resync des Netzwerks etwas Zeit in Anspruch nehmen und Sie werden die Transaktionen erst sehen wenn der Resync vollständig durchgeführt wurde.\n\nAbhängig von der Anzahl an Transaktionen und dem Alter Ihrer Wallet kann der Resync mehrere Stunden dauern und 100% der CPU Power beanspruchen. Unterbrechen Sie den Resync nicht, ansonsten müssen Sie ihn wiederholen. +settings.net.reSyncSPVAfterRestart=Die SPV-Kettendatei wurde gelöscht. Haben Sie bitte Geduld, es kann eine Weile dauern mit dem Netzwerk neu zu synchronisieren. +settings.net.reSyncSPVAfterRestartCompleted=Die erneute Synchronisation ist jetzt abgeschlossen. Bitte starten Sie die Anwendung neu. +settings.net.reSyncSPVFailed=Konnte SPV-Kettendatei nicht löschen.\nFehler: {0} +setting.about.aboutBisq=Über Bisq +setting.about.about=Bisq ist Open-Source Software, die den Tausch von Bitcoin mit nationaler Währung (und anderen Kryptowährungen), durch ein dezentralisiertes Peer-zu-Peer Netzwerk auf eine Weise ermöglicht, die Ihre Privatsphäre stark beschützt. Lernen Sie auf unserer Projektwebseite mehr über Bisq. +setting.about.web=Bisq-Website +setting.about.code=Quellcode +setting.about.agpl=AGPL-Lizenz +setting.about.support=Bisq unterstützen +setting.about.def=Bisq ist keine Firma, sondern ein Gemeinschaftsprojekt, das offen für Mitwirken ist. Wenn Sie an Bisq mitwirken oder das Projekt unterstützen wollen, folgen Sie bitte den unten stehenden Links. +setting.about.contribute=Mitwirken +setting.about.providers=Datenanbieter +setting.about.apisWithFee=Bisq verwendet die Bisq Preis Indizes für Fiat und Altcoin Preise und die Bisq Mempool Nodes für die Abschätzung der Mining-Gebühr. +setting.about.apis=Bisq verwendet den Bisq Price Index für Fiat und Altcoin Preise. +setting.about.pricesProvided=Marktpreise zur Verfügung gestellt von +setting.about.feeEstimation.label=Geschätzte Mining-Gebühr bereitgestellt von +setting.about.versionDetails=Versionsdetails +setting.about.version=Anwendungsversion +setting.about.subsystems.label=Version des Teilsystems +setting.about.subsystems.val=Netzwerkversion: {0}; P2P-Nachrichtenversion: {1}; Lokale DB-Version: {2}; Version des Handelsprotokolls: {3} + +setting.about.shortcuts=Shortcuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Strg + {0}'' oder ''Alt + {0}'' oder ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Hauptmenü navigieren +setting.about.shortcuts.menuNav.value=Um durch das Hauptmenü zu navigieren, drücken Sie: 'Strg' oder 'Alt' oder 'cmd' mit einer numerischen Taste zwischen '1-9' + +setting.about.shortcuts.close=Bisq beenden +setting.about.shortcuts.close.value=''Strg + {0}'' oder ''cmd + {0}'' bzw. ''Strg + {1}'' oder ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Popup- oder Dialogfenster schließen +setting.about.shortcuts.closePopup.value='ESCAPE' Taste + +setting.about.shortcuts.chatSendMsg=Trader eine Chat-Nachricht senden +setting.about.shortcuts.chatSendMsg.value=''Strg + ENTER'' oder ''Alt + ENTER'' oder ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Streitfall eröffnen +setting.about.shortcuts.openDispute.value=Wählen Sie den ausstehenden Trade und klicken Sie auf: {0} + +setting.about.shortcuts.walletDetails=Öffnen Sie das Fenster für Wallet-Details + +setting.about.shortcuts.openEmergencyBtcWalletTool=Öffnen Sie das Notfallwerkzeug für die BTC-Wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Öffnen Sie das Notfallwerkzeug für die BSQ-Wallet + +setting.about.shortcuts.showTorLogs=Umschalten des Log-Levels für Tor-Meldungen zwischen DEBUG und WARN + +setting.about.shortcuts.manualPayoutTxWindow=Fenster öffnen für manuelle Auszahlung einer 2von2 Multisig Einzahlung tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Neu veröffentlichen von DAO Governance-Daten (Vorschläge, Abstimmungen) + +setting.about.shortcuts.removeStuckTrade=Popup öffnen um fehlgeschlagenen Trade wieder zu den offenen Trades zu verschieben +setting.about.shortcuts.removeStuckTrade.value=Fehlgeschlagenen Trade auswählen und drücken: {0} + +setting.about.shortcuts.registerArbitrator=Vermittler registrieren (nur Mediator/Vermittler) +setting.about.shortcuts.registerArbitrator.value=Navigieren Sie zu Konto und drücken Sie: {0} + +setting.about.shortcuts.registerMediator=Mediator registrieren (nur Mediator/Vermittler) +setting.about.shortcuts.registerMediator.value=Navigieren Sie zu Konto und drücken Sie: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Öffnen Sie das Fenster für Kontoalter-Unterzeichnung (nur Legacy-Vermittler) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigieren Sie zur Legacy-Vermittler-Ansicht und drücken Sie: {0} + +setting.about.shortcuts.sendAlertMsg=Warnung oder Update per Nachricht senden (privilegierte Aktivität) + +setting.about.shortcuts.sendFilter=Filter einstellen (privilegierte Aktivität) + +setting.about.shortcuts.sendPrivateNotification=Private Benachrichtigung an Peer senden (privilegierte Aktivität) +setting.about.shortcuts.sendPrivateNotification.value=Öffnen Sie die Partner-Infos am Avatar und klicken Sie: {0} + +setting.info.headline=Neues automatisches Bestätigungs-Feature für XMR +setting.info.msg=Wenn Sie BTC für XMR verkaufen, können Sie die automatische Bestätigung aktivieren um nachzuprüfen ob die korrekte Menge an Ihr Wallet gesendet wurde. So kann Bisq den Trade automatisch abschließen und Trades dadurch für alle schneller machen.\n\nDie automatische Bestätigung überprüft die XMR Transaktion über mindestens 2 XMR Explorer Nodes mit dem privaten Transaktionsschlüssel den der Sender zur Verfügung gestellt hat. Bisq verwendet standardmäßig Explorer Nodes die von Bisq Contributors betrieben werden aber wir empfehlen, dass Sie für ein Maximum an Sicherheit und Privatsphäre Ihre eigene XMR Explorer Node betreiben.\n\nFür automatische Bestätigungen, können Sie die max. Höhe an BTC pro Trade und die Anzahl der benötigten Bestätigungen in den Einstellungen festlegen.\n\nFinden Sie weitere Informationen (und eine Anleitung wie Sie Ihre eigene Explorer Node aufsetzen) im Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator-Registrierung +account.tab.refundAgentRegistration=Registrierung des Rückerstattungsbeauftragten +account.tab.signing=Unterzeichnung +account.info.headline=Willkommen in Ihrem Bisq-Konto +account.info.msg=Hier können Sie Trading-Konten für nationale Währungen und Altcoins hinzufügen und Backups für Ihre Wallets & Kontodaten erstellen.\n\nEine leere Bitcoin-Wallet wurde erstellt, als Sie das erste Mal Bisq gestartet haben.\n\nWir empfehlen, dass Sie Ihre Bitcoin-Wallet-Seed-Wörter aufschreiben (siehe Tab oben) und sich überlegen ein Passwort hinzuzufügen, bevor Sie einzahlen. Bitcoin-Einzahlungen und Auszahlungen werden unter \"Gelder\" verwaltet.\n\nHinweis zu Privatsphäre & Sicherheit: da Bisq eine dezentralisierte Börse ist, bedeutet dies, dass all Ihre Daten auf ihrem Computer bleiben. Es gibt keine Server und wir haben keinen Zugriff auf Ihre persönlichen Informationen, Ihre Gelder oder selbst Ihre IP-Adresse. Daten wie Bankkontonummern, Altcoin- & Bitcoinadressen, etc werden nur mit Ihrem Trading-Partner geteilt, um Trades abzuschließen, die Sie initiiert haben (im Falle eines Konflikts wird der Vermittler die selben Daten sehen wie Ihr Trading-Partner). + +account.menu.paymentAccount=Nationale Währungskonten +account.menu.altCoinsAccountView=Altcoin-Konten +account.menu.password=Wallet-Passwort +account.menu.seedWords=Wallet-Seed +account.menu.walletInfo=Wallet Info +account.menu.backup=Backup +account.menu.notifications=Benachrichtigungen + +account.menu.walletInfo.balance.headLine=Wallet-Guthaben +account.menu.walletInfo.balance.info=Hier wird das Wallet-Guthaben einschließlich unbestätigter Transaktionen angezeigt.\nFür BTC sollte das unten angezeigte Wallet-Guthaben mit der Summe der oben rechts in diesem Fenster angezeigten "Verfügbaren" und "Reservierten" Guthaben übereinstimmen. +account.menu.walletInfo.xpub.headLine=Watch Keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} Wallet +account.menu.walletInfo.path.headLine=HD Keychain Pfade +account.menu.walletInfo.path.info=Wenn Sie Seed Wörter in eine andere Wallet (wie Electrum) importieren, müssen Sie den Pfad angeben. Dies sollte nur in Notfällen gemacht werden, wenn Sie den Zugriff auf die Bisq-Wallet und das Data-Verzeichnis verloren haben.\nDenken Sie daran, dass die Ausgabe von Geldern aus einer Nicht-Bisq-Wallet die internen Bisq-Datenstrukturen, die mit den Wallet-Daten verbunden sind, durcheinander bringen kann, was zu fehlgeschlagenen Trades führen kann.\n\nSenden Sie NIEMALS BSQ von einer Nicht-Bisq-Wallet, da dies wahrscheinlich zu einer ungültigen BSQ-Transaktion und dem Verlust Ihrer BSQ führen wird. + +account.menu.walletInfo.openDetails=Wallet-Rohdaten und Private Schlüssel anzeigen + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Öffentlicher Schlüssel + +account.arbitratorRegistration.register=Registrieren +account.arbitratorRegistration.registration={0} Registrierung +account.arbitratorRegistration.revoke=Widerrufen +account.arbitratorRegistration.info.msg=Beachten Sie bitte, dass Sie nach dem Widerrufen für 15 Tage verfügbar bleiben müssen, da es Trades geben kann, die Sie als {0} nutzen. Die maximal erlaubte Trade-Dauer ist 8 Tage und der Konfliktprozess kann bis zu 7 Tage dauern. +account.arbitratorRegistration.warn.min1Language=Sie müssen wenigstens 1 Sprache festlegen.\nWir haben Ihre Standardsprache für Sie hinzugefügt. +account.arbitratorRegistration.removedSuccess=Sie haben Ihre Registrierung erfolgreich aus dem Bisq-Netzwerk entfernt. +account.arbitratorRegistration.removedFailed=Die Registrierung konnte nicht entfernt werden.{0} +account.arbitratorRegistration.registerSuccess=Sie haben sich erfolgreich im Bisq-Netzwerk registriert. +account.arbitratorRegistration.registerFailed=Die Registrierung konnte nicht abgeschlossen werden.{0} + +account.altcoin.yourAltcoinAccounts=Ihre Altcoin-Konten +account.altcoin.popup.wallet.msg=Bitte stellen Sie sicher, dass Sie die Anforderungen für die Verwendung von {0} Wallets wie auf der {1} Webseite beschrieben erfüllen.\nDie Verwendung von Wallets von zentralisierten Börsen, bei denen (a) Sie Ihre Keys nicht kontrollieren oder (b) die keine kompatible Wallet-Software verwenden, ist riskant: Es kann zum Verlust der gehandelten Gelder führen!\nDer Mediator oder Vermittler ist kein {2} Spezialist und kann in solchen Fällen nicht helfen. +account.altcoin.popup.wallet.confirm=Ich verstehe und bestätige, dass ich weiß, welche Wallet ich benutzen muss. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Der Handel mit UPX auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür das Senden von UPX müssen Sie entweder das offizielle uPlexa GUI-Wallet oder das uPlexa CLI-Wallet mit aktiviertem store-tx-info Flag verwenden (Standard in neuen Versionen). Bitte stellen Sie sicher, dass Sie auf den tx key zugreifen können, da dies bei einem Konfliktfall erforderlich wäre.\nuplexa-wallet-cli (verwenden Sie den Befehl get_tx_key)\nuplexa-wallet-gui (gehen Sie zum History Tab und klicken Sie auf (P) für den Zahlungsnachweis)\n\nBei normalen Blockexplorern ist der Transfer nicht verifizierbar.\n\nSie müssen dem Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Der tx Private Key\n- Der Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der UPX-Sender ist dafür verantwortlich, im Konfliktfall dem Vermittler die Verifizierung des UPX-Transfers nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, besuchen Sie den uPlexa discord channel (https://discord.gg/vhdNSrV) oder den uPlexa Telegram Chat (https://t.me/uplexaOfficial), um weitere Informationen zu erhalten. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Der Handel mit ARQ auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür den Versand von ARQ müssen Sie entweder das offizielle ArQmA GUI-Wallet oder das ArQmA CLI-Wallet mit aktiviertem store-tx-info Flag verwenden (Standard in neuen Versionen). Bitte stellen Sie sicher, dass Sie auf den tx Key zugreifen können, da dies im Falle eines Konfliktes erforderlich wäre.\narqma-wallet-cli (verwenden Sie den Befehl get_tx_key)\narqma-wallet-gui (gehen Sie zur History Tab und klicken Sie auf (P) für den Zahlungsnachweis)\n\nBei normalen Blockexplorern ist der Transfer nicht verifizierbar.\n\nSie müssen dem Mediator oder Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Der tx Private Key\n- Der Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der ARQ-Sender ist im Fall eines Konflikts dafür verantwortlich, die Verifizierung des ARQ-Transfers dem Mediator oder Vermittler nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, besuchen Sie den ArQmA Discord Channel (https://discord.gg/s9BQpJT) oder das ArQmA Forum (https://labs.arqma.com), um weitere Informationen zu erhalten. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Der Handel mit XMR auf Bisq setzt voraus, dass Sie die folgende Anforderung verstehen.\n\nWenn Sie XMR verkaufen, müssen Sie in der Lage sein, einem Vermittler oder einer Schiedsperson im Falle eines Konflikts die folgenden Informationen zur Verfügung zu stellen:\n- den Transaktionsschlüssel (Tx Key, Tx Secret Key oder Tx Private Key)\n- die Transaktions-ID (Tx ID oder Tx Hash)\n- die Zieladresse (Empfängeradresse)\n\nIm Wiki finden Sie Einzelheiten darüber wo Sie diese Informationen in den populärsten Monero-Wallets finden [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nWerden die erforderlichen Transaktionsdaten nicht zur Verfügung gestellt, führt dies dazu, dass der Konflikt zu Ihrem Nachteil entschieden wird.\n\nBeachten Sie auch, dass Bisq jetzt eine automatische Bestätigung für XMR-Transaktionen anbietet, um Transaktionen schneller zu machen, aber Sie müssen dies in den Einstellungen aktivieren.\n\nWeitere Informationen über die automatische Bestätigungsfunktion finden Sie im Wiki: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Der Handel mit MSR auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür den Versand von MSR müssen Sie entweder das offizielle Masari GUI Wallet, das Masari CLI Wallet mit dem aktivierten store-tx-info Flag (standardmäßig aktiviert) oder das Masari Web Wallet (https://wallet.getmasari.org) verwenden. Bitte stellen Sie sicher, dass Sie auf den tx Key zugreifen können, da dies im Falle eines Konfliktes erforderlich wäre.\nmasari-wallet-cli (verwenden Sie den Befehl get_tx_key)\nmasari-wallet-gui (gehen Sie zur History Tab und klicken Sie auf (P) für den Zahlungsnachweis).\n\nMasari Web Wallet (gehen Sie zum Konto -> Transaktionshistorie und lassen Sie Details zu Ihrer gesendeten Transaktion anzeigen)\n\nDie Verifizierung kann im Wallet durchgeführt werden.\nmasari-wallet-cli : mit dem Befehl (check_tx_key).\nmasari-wallet-gui : auf der Seite Advanced > Prove/Check.\nDie Verifizierung kann im Block-Explorer durchgeführt werden. \nÖffnen Sie den Block-Explorer (https://explorer.getmasari.org), verwenden Sie die Suchleiste, um Ihren Transaktionshash zu finden.\nSobald die Transaktion gefunden wurde, scrollen Sie nach unten zum Bereich 'Prove Sending' und geben Sie bei Bedarf Details ein.\nSie müssen dem Mediator oder Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Den tx Private Key\n- Den Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der MSR-Sender ist im Fall eines Konflikts dafür verantwortlich, die Verifizierung des MSR-Transfers dem Mediator oder Vermittler nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, fragen Sie um Hilfe auf der offiziellen Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Der Handel mit BLUR auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm BLUR zu senden, müssen Sie die Blur Network CLI oder GUI Wallet verwenden. \n\nWenn Sie die CLI-Wallet verwenden, wird nach dem Senden eines Transfers ein Transaktionshash (tx ID) angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden des Transfers müssen Sie den Private Key der Transaktion mit dem Befehl 'get_tx_key' ermitteln. Wenn Sie diesen Schritt nicht ausführen, können Sie den Key möglicherweise später nicht mehr abrufen. \n\nWenn Sie das Blur Network GUI Wallet verwenden, können Sie problemlos den Private Key der Transaktion und die Transaktion-ID im "History" Tab finden. Suchen Sie sofort nach dem Absenden die Transaktion, die von Interesse ist. Klicken Sie auf das Symbol "?" in der unteren rechten Ecke des Feldes, das die Transaktion enthält. Sie müssen diese Informationen speichern. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1.) die Transaktions-ID, 2.) den Private Key der Transaktion und 3.) die Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den BLUR-Transfer mit dem Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, verlieren Sie den Konfliktfall. In allen Konfliktfällen trägt der BLUR-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Bisq. Als Erstes suchen Sie Hilfe im Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Der Handel mit Solo auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm Solo zu senden, müssen Sie das Solo Network CLI Wallet verwenden. \n\nWenn Sie das CLI-Wallet verwenden, wird nach dem Senden eines Transfers ein Transaktionshash (tx ID) angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden des Transfers müssen Sie den Private Key der Transaktion mit dem Befehl 'get_tx_key' ermitteln. Wenn Sie diesen Schritt nicht ausführen, können Sie den Key möglicherweise später nicht mehr abrufen.\n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1.) die Transaktion-ID, 2.) den Private Key der Transaktion und 3.) die Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den Solo-Transfer mit dem Solo Block Explorer, indem er nach der Transaktion sucht und dann die Funktion "Senden nachweisen" (https://explorer.minesolo.com/) verwendet.\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, verlieren Sie den Konfliktfall. In allen Konfliktfällen trägt der Solo-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Bisq. Suchen Sie zuerst Hilfe im Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Der Handel mit CASH2 auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm CASH2 zu versenden, müssen Sie die Cash2 Wallet Version 3 oder höher verwenden. \n\nNachdem eine Transaktion gesendet wurde, wird die Transaktions-ID angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden der Transaktion müssen Sie den Befehl 'getTxKey' in simplewallet verwenden, um den Secret Key der Transaktion abzurufen. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) die Transaktions-ID, 2) den Secret Key der Transaktion und 3) die Cash2-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den CASH2-Transfer mit dem Cash2 Block Explorer (https://blocks.cash2.org).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der CASH2-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Bisq, sondern suchen Sie zunächst Hilfe im Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Der Handel mit Qwertycoin auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm QWC zu versenden, müssen Sie die offizielle QWC Wallet Version 5.1.3 oder höher verwenden. \n\nNachdem eine Transaktion gesendet wurde, wird die Transaktions-ID angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden der Transaktion müssen Sie den Befehl 'get_Tx_Key' in simplewallet verwenden, um den Secret Key der Transaktion abzurufen. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) die Transaktions-ID, 2) den Secret Key der Transaktion und 3) die QWC-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den QWC-Transfer mit dem QWC Block Explorer (https://explorer.qwertycoin.org).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der QWC-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Bisq, sondern suchen Sie zunächst Hilfe im QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Der Handel mit Dragonglass auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nAufgrund der Privatsphäre, die Dragonglass bietet, ist eine Transaktion auf der Public Blockchain nicht verifizierbar. Bei Bedarf können Sie Ihre Zahlung durch die Verwendung Ihres TXN-Private-Key nachweisen.\nDer TXN-Private Key ist ein einmaliger Schlüssel, der automatisch für jede Transaktion generiert wird, auf die Sie nur über Ihre DRGL-Wallet zugreifen können - entweder über die DRGL-Wallet GUI (im Transaktionsdetaildialog) oder über die Dragonglass CLI simplewallet (mit dem Befehl "get_tx_key").\n\nDRGL-Version "Oathkeeper" und höher sind für beide ERFORDERLICH.\n\nIm Konfliktfall müssen Sie dem Mediator oder Vermittler die folgenden Daten zur Verfügung stellen:\n- Den TXN-Private-Key\n- Den Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nDie Verifizierung der Zahlung kann mit den oben genannten Daten als Eingabe unter (http://drgl.info/#check_txn) erfolgen.\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Klärungsfall. Der Dragonglass-Sender ist für die Verifizierung des DRGL-Transfers gegenüber dem Mediator oder Vermittler im Konfliktfall verantwortlich. Die Verwendung einer PaymentID ist nicht erforderlich.\n\nWenn Sie sich über irgendeinen Teil dieses Prozesses unsicher sind, besuchen Sie Dragonglass auf Discord (http://discord.drgl.info) um Hilfe zu erhalten. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Bei der Verwendung von Zcash können Sie nur die transparenten Adressen (beginnend mit t) verwenden, nicht die z-Adressen (privat), da der Mediator oder Vermittler nicht in der Lage wäre, die Transaktion mit z-Adressen zu verifizieren. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Bei der Verwendung von Zcoin können Sie nur die transparenten (rückverfolgbaren) Adressen verwenden, nicht die nicht rückverfolgbaren Adressen, da der Mediator oder Vermittler nicht in der Lage wäre, die Transaktion mit nicht rückverfolgbaren Adressen in einem Block-Explorer zu verifizieren. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN benötigt einen interaktiven Prozess zwischen Sender und Empfänger, um die Transaktion zu erstellen. Stellen Sie sicher, den Anweisungen der GRIN Projekt Webseite zu folgen, um zuverlässig GRIN zu senden und empfangen (der Empfänger muss oinline oder wenigstens während eines gewissen Zeitfensters). \n\nBisq unterstützt nur das Grinbox (Wallet713) Wallet URL Format. \n\nDer GRIN Sender muss beweisen können, die GRIN erfolgreich gesendet zu haben. Wenn die Wallet dies nicht kann, wird ein potentieller Konflikt zugunsten des GRIN Empfängers entschieden. Bitte stellen Sie sicher, dass Sie die letzte Grinbox Software nutzen, die den Transaktionsbeweis unterstützt, und Sie den Prozess verstehen, wie GRIN gesendet und empfangen wird, sowie wie man den Beweis erstellt. \n\nBeachten Sie https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only für weitere Informationen über das Grinbox proof tool. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM benötigt einen interaktiven Prozess zwischen Sender und Empfänger, um die Transaktion zu erstellen.\n\nStellen Sie sicher, den Anweisungen der BEAM Projekt Webseite zu folgen, um zuverlässig BEAM zu senden und empfangen (der Empfänger muss oinline oder wenigstens während eines gewissen Zeitfensters). \n\nDer BEAM Sender muss beweisen können, die BEAM erfolgreich gesendet zu haben. Bitte stellen Sie sicher, dass Sie Wallet Software verwenden, die solche Beweise erstellen kann. Falls Die Wallet den Beweis nicht erstellen kann, wird ein potentieller Konflikt zugunsten des BEAM Empfängers entschieden. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Der Handel mit ParsiCoin auf Bisq setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm PARS zu versenden, müssen Sie die offizielle ParsiCoin Wallet Version 3.0.0 oder höher verwenden. \n\nSie können Ihren Transaktionshash und Transaktionsschlüssel im Bereich Transaktionen in Ihrer GUI-Wallet (ParsiPay) überprüfen. Sie müssen mit der rechten Maustaste auf die Transaktion und dann auf Details anzeigen klicken.\n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) den Transaktionshash, 2) den Transaktionsschlüssel und 3) die PARS-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den PARS-Transfer mit dem ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der ParsiCoin-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Bisq, sondern suchen Sie zunächst Hilfe im ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=Um "Burnt Blackcoins" zu handeln, müssen Sie folgendes wissen:\n\nBurnt Blackcoins können nicht ausgegeben werden. Um sie auf Bisq zu handeln, müssen die Ausgabeskripte in der Form vorliegen: OP_RETURN OP_PUSHDATA, gefolgt von zugehörigen Datenbytes, die nach der Hex-Codierung Adressen darstellen. Beispielsweise haben Burnt Blackcoins mit der Adresse 666f6f ("foo" in UTF-8) das folgende Skript:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nUm Burnt Blackcoins zu erstellen, kann man den in einigen Wallets verfügbaren RPC-Befehl "burn" verwenden.\n\nFür mögliche Anwendungsfälle kann man einen Blick auf https://ibo.laboratorium.ee werfen.\n\nDa Burnt Blackcoins nicht ausgegeben werden können, können sie nicht wieder verkauft werden. "Verkaufen" von Burnt Blackcoins bedeutet, gewöhnliche Blackcoins zu verbrennen (mit zugehörigen Daten entsprechend der Zieladresse).\n\nIm Konfliktfall hat der BLK-Verkäufer den Transaktionshash zur Verfügung zu stellen. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Das Trading mit L-BTC auf Bisq setzt voraus, dass Sie Folgendes verstehen:\n\nWenn Sie L-BTC für einen Trade auf Bisq erhalten, können Sie nicht die mobile Blockstream Green Wallet App oder eine Custodial/Exchange Wallet verwenden. Sie dürfen L-BTC nur in der Liquid Elements Core Wallet oder eine andere L-BTC-Wallet erhalten, die es Ihnen ermöglicht, den Blinding Key für Ihre verdeckte L-BTC-Adresse zu erhalten.\n\nFalls eine Mediation erforderlich ist oder ein Trade-Konflikt entsteht, müssen Sie den Blinding Key für Ihre L-BTC-Empfangsadresse dem Bisq-Mediator oder dem Refund Agent mitteilen, damit dieser die Details Ihrer vertraulichen Transaktion auf seinem eigenen Elements Core Full Node überprüfen kann.\n\nWenn Sie dem Mediator oder Refund Agent die erforderlichen Informationen nicht zur Verfügung stellen, führt dies dazu, dass Sie den Streitfall verlieren. In allen Streitfällen trägt der L-BTC-Empfänger 100 % der Verantwortung für die Bereitstellung kryptographischer Beweise an den Mediator oder den Refund Agent.\n\nWenn Sie diese Anforderungen nicht verstehen, sollten Sie nicht mit L-BTC auf Bisq traden. + +account.fiat.yourFiatAccounts=Ihre Nationalen Währungskonten + +account.backup.title=Backup der Wallet erstellen +account.backup.location=Speicherort des Backups +account.backup.selectLocation=Speicherort des Backups wählen +account.backup.backupNow=Backup jetzt erstellen (Backup ist nicht verschlüsselt!) +account.backup.appDir=Verzeichnis der Anwendungsdaten +account.backup.openDirectory=Verzeichnis öffnen +account.backup.openLogFile=Protokolldatei öffnen +account.backup.success=Backup erfolgreich gespeichert in:\n{0} +account.backup.directoryNotAccessible=Der ausgewählt Ordner ist nicht erreichbar. {0} + +account.password.removePw.button=Passwort entfernen +account.password.removePw.headline=Passwortschutz Ihrer Wallet entfernen +account.password.setPw.button=Passwort festlegen +account.password.setPw.headline=Passwortschutz Ihrer Wallet einrichten +account.password.info=Mit Passwortschutz müssen Sie Ihr Passwort eingeben, sowohl wenn Sie Bitcoins aus Ihrer Wallet abheben, wenn Sie Ihre Wallet einsehen oder aus den Seed-Wörtern wiederherstellen wollen, als auch beim Start der Anwendung. + +account.seed.backup.title=Backup der Seed-Wörter Ihrer Wallet erstellen +account.seed.info=Bitte schreiben Sie die sowohl Seed-Wörter als auch das Datum auf! Mit diesen Seed-Wörtern und dem Datum können Sie Ihre Wallet jederzeit wiederherstellen.\nDie Seed-Wörter werden für die BTC- und BSQ-Wallet genutzt.\n\nSchreiben Sie die Seed-Wörter auf ein Blatt Papier schreiben und speichern Sie sie nicht auf Ihrem Computer.\n\nBitte beachten Sie, dass die Seed-Wörter KEIN Ersatz für ein Backup sind.\nSie müssen ein Backup des gesamten Anwendungsverzeichnisses unter \"Konto/Backup\" erstellen, um den ursprünglichen Zustand der Anwendung wiederherstellen zu können.\nDas Importieren der Seed-Wörter wird nur für Notfälle empfohlen. Die Anwendung wird ohne richtiges Backup der Datenbankdateien und Schlüssel nicht funktionieren! +account.seed.backup.warning=Bitte beachten Sie, dass die Seed Wörter kein Ersatz für ein Backup sind.\nSie müssen ein Backup vom gesamten Anwendungs-Verzeichnis vom \"Account/Backup\" Pfad machen um den Status und die Dateien der Anwendung wiederherzustellen. \nDie Seed Wörter zu importieren wird nur für Notfälle empfohlen. Die Anwendung wird ohne ordnungsgemäßes Backup der Dateien und Schlüssel nicht funktionieren!\n\nWeitere Informationen finden Sie auf der Wiki-Seite [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Sie haben kein Wallet-Passwort festgelegt, was das Anzeigen der Seed-Wörter schützen würde.\n\nMöchten Sie die Seed-Wörter jetzt anzeigen? +account.seed.warn.noPw.yes=Ja, und nicht erneut fragen +account.seed.enterPw=Geben Sie Ihr Passwort ein um die Seed-Wörter zu sehen +account.seed.restore.info=Bitte erstellen Sie vor dem Wiederherstellen durch Keimwörter ein Backup. Beachten Sie auch, dass Wallet-Wiederherstellung nur für Notfälle ist und Probleme mit der internen Wallet-Datenbank verursachen kann.\nEs ist kein Weg ein Backup anzuwenden! Bitte nutzen Sie ein Backup des Anwendungsdatenordner um eine vorherigen Zustand wiederherzustellen. \n\nNach der Wiederherstellung wird die Anwendung herunterfahren. Nachdem Sie die Anwendung wieder gestartet haben, wird sie wieder mit dem Bitcoin-Netzwerk synchronisieren. Dies kann lange dauern und die CPU stark beanspruchen, vor allem, wenn die Wallet alt und viele Transaktionen hatte. Bitte unterbreche Sie diesen Prozess nicht, sonst müssen Sie vielleicht die SPV Kettendatei löschen und den Wiederherstellungsprozess wiederholen. +account.seed.restore.ok=Ok, mache die Wiederherstellung und fahre Bisq herunter + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Konfiguration +account.notifications.download.label=Mobile App herunterladen +account.notifications.waitingForWebCam=Warte auf Webcam... +account.notifications.webCamWindow.headline=Scanne QR-Code vom Smartphone +account.notifications.webcam.label=Nutze Webcam +account.notifications.webcam.button=QR-Code scannen +account.notifications.noWebcam.button=Ich habe keine Webcam +account.notifications.erase.label=Benachrichtigungen von Smartphone löschen +account.notifications.erase.title=Benachrichtigungen leeren +account.notifications.email.label=Kopplungs-Token +account.notifications.email.prompt=Geben Sie den Kopplungs-Token ein, den Sie per E-Mail erhalten haben +account.notifications.settings.title=Einstellungen +account.notifications.useSound.label=Benachrichtigungs-Ton am Smartphone abspielen +account.notifications.trade.label=Erhalte Nachrichten zu Händel +account.notifications.market.label=Erhalte Benachrichtigungen zu Angeboten +account.notifications.price.label=Erhalte Preisbenachrichtigungen +account.notifications.priceAlert.title=Preisalarme: +account.notifications.priceAlert.high.label=Benachrichtigen, wenn BTC-Preis über +account.notifications.priceAlert.low.label=Benachrichtigen, wenn BTC-Preis unter +account.notifications.priceAlert.setButton=Preisalarm setzen +account.notifications.priceAlert.removeButton=Preisalarm entfernen +account.notifications.trade.message.title=Handelsstatus verändert +account.notifications.trade.message.msg.conf=Die Kaution-Transaktion für den Handel mit ID {0} wurde bestätigt. Bitte öffnen Sie Ihre Bisq Anwendung und starten die Zahlung. +account.notifications.trade.message.msg.started=Der BTC-Käufer hat die Zahlung für den Handel mit ID {0} begonnen. +account.notifications.trade.message.msg.completed=Der Handel mit ID {0} ist abgeschlossen. +account.notifications.offer.message.title=Ihr Angebot wurde angenommen +account.notifications.offer.message.msg=Ihr Angebot mit ID {0} wurde angenommen +account.notifications.dispute.message.title=Neue Konflikt-Nachricht +account.notifications.dispute.message.msg=Sie haben eine Konflikt-Nachricht für den Handel mit ID {0} erhalten + +account.notifications.marketAlert.title=Angebotsalarme +account.notifications.marketAlert.selectPaymentAccount=Angebote, die dem Zahlungskonto entsprechen +account.notifications.marketAlert.offerType.label=Angebotstyp, an dem ich interessiert bin +account.notifications.marketAlert.offerType.buy=Kauf-Angebote (Ich möchte BTC verkaufen) +account.notifications.marketAlert.offerType.sell=Verkauf-Angebote (Ich möchte BTC kaufen) +account.notifications.marketAlert.trigger=Angebot Preisdistanz (%) +account.notifications.marketAlert.trigger.info=Mit gesetzter Preisdistanz, werden Sie nur einen Alarm erhalten, wenn ein Angebot veröffentlicht wird, das die Bedingungen erfüllt (oder übertrifft). Beispiel: Sie möchten BTC verkaufen, aber Sie werden nur 2% über dem momentanen Marktpreis verkaufen. Dieses Feld auf 2% setzen stellt sicher, dass Sie nur nur Alarme für Angebote erhalten, die 2% (oder mehr) über dem momentanen Marktpreis liegen. +account.notifications.marketAlert.trigger.prompt=Prozentualer Abstand zum Marktpreis (z.B. 2.50%, -0.50%, etc) +account.notifications.marketAlert.addButton=Angebotsalarme hinzufügen +account.notifications.marketAlert.manageAlertsButton=Angebotsalarme verwalten +account.notifications.marketAlert.manageAlerts.title=Angebotsalarme verwalten +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Zahlungskonto +account.notifications.marketAlert.manageAlerts.header.trigger=Triggerpreis +account.notifications.marketAlert.manageAlerts.header.offerType=Angebotstyp +account.notifications.marketAlert.message.title=Angebotsalarm +account.notifications.marketAlert.message.msg.below=unter +account.notifications.marketAlert.message.msg.above=über +account.notifications.marketAlert.message.msg=Eine neue "{0} {1}" Angebote mit {2} ({3} {4} Marktpreis) und Zahlungsmethode "{5}" wurde zum Bisq Angebotsbuch.\nAngebot ID: {6}. +account.notifications.priceAlert.message.title=Preisalarm für {0} +account.notifications.priceAlert.message.msg=Ihr Preisalarm wurde ausgelöst. Der momentane {0} Preis ist {1} {2} +account.notifications.noWebCamFound.warning=Keine Webcam gefunden.\n\nBitte nutzen Sie die E-Mail Option um den Token und Schlüssel von Ihrem Smartphone zur Bisq-Anwendung zu senden. +account.notifications.priceAlert.warning.highPriceTooLow=Der hohe Preis muss größer als der tiefe Preis sein. +account.notifications.priceAlert.warning.lowerPriceTooHigh=Der tiefe Preis muss kleiner als der hohe Preis sein. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Zahlen, Daten & Fakten +dao.tab.bsqWallet=BSQ-Wallet +dao.tab.proposals=Führung der DAO +dao.tab.bonding=Kopplung +dao.tab.proofOfBurn=Asset-Listungsgebühr/Nachweis der Verbrennung +dao.tab.monitor=Netzwerkmonitor +dao.tab.news=Neuigkeiten + +dao.paidWithBsq=bezahlt mit BSQ +dao.availableBsqBalance=Zum Ausgeben verfügbar (bestätigte und unbestätigte Restbeträge) +dao.verifiedBsqBalance=Guthaben aller verifizierter UTXOs +dao.unconfirmedChangeBalance=Guthaben aller unbestätigter Restbeträge +dao.unverifiedBsqBalance=Guthaben aller unbestätigten Transaktionen (warte auf Block-Bestätigung) +dao.lockedForVoteBalance=Zum wählen genutzt +dao.lockedInBonds=In Kopplungen gesperrt +dao.availableNonBsqBalance=Verfügbares nicht-BSQ-Guthaben (BTC) +dao.reputationBalance=Merit-Wert (nicht ausgabefähig) + +dao.tx.published.success=Ihre Transaktion wurde erfolgreich veröffentlicht. +dao.proposal.menuItem.make=Antrag erstellen +dao.proposal.menuItem.browse=Offene Vorschläge +dao.proposal.menuItem.vote=Für Vorschläge stimmen +dao.proposal.menuItem.result=Wahlergebnisse +dao.cycle.headline=Wahlzyklus +dao.cycle.overview.headline=Wahlzyklus-Übersicht +dao.cycle.currentPhase=Aktuelle Phase +dao.cycle.currentBlockHeight=Momentane Blockhöhe +dao.cycle.proposal=Vorschlag-Phase +dao.cycle.proposal.next=Nächste Vorschlag-Phase +dao.cycle.blindVote=Geheime Wahl-Phase +dao.cycle.voteReveal=Wahloffenbarung-Phase +dao.cycle.voteResult=Wahlergebnis +dao.cycle.phaseDuration={0} Blöcke (≈{1}); Block {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Block {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Wahloffenbarung Transaktion veröffentlicht +dao.voteReveal.txPublished=Ihre Wahloffenbarung-Transaktion mit ID {0} wurde erfolgreich veröffentlicht.\n\nDies geschieht automatisch durch die Software, wenn Sie an einer DAO Wahl teilgenommen haben. + +dao.results.cycles.header=Zyklen +dao.results.cycles.table.header.cycle=Zyklus +dao.results.cycles.table.header.numProposals=Vorschläge +dao.results.cycles.table.header.voteWeight=Stimm-Gewicht +dao.results.cycles.table.header.issuance=Ausgabe + +dao.results.results.table.item.cycle=Zyklus {0} gestartet: {1} + +dao.results.proposals.header=Vorschläge des gewählten Zyklus +dao.results.proposals.table.header.nameLink=Name/Link +dao.results.proposals.table.header.details=Details +dao.results.proposals.table.header.myVote=Meine Stimme +dao.results.proposals.table.header.result=Wahlergebnis +dao.results.proposals.table.header.threshold=Schwellenwert +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Wahlergebnisse für gewählten Vorschlag + +dao.results.exceptions=Wahlergebnisseausnahme(n) + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Undefiniert + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQ Erstellungsgebühr +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQ Abnehmergebühr +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Min. BSQ Erstellergebühr +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Min. BSQ Abnehmergebühr +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTC Erstellungsgebühr +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTC Abnehmergebühr +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Min. BTC Erstellergebühr +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Min. BTC Abnehmergebühr +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Gebühr für Antrag +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Gebühr für Wahl in BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Min. BSQ-Betrag für Entlohnungsantrag +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Max. BSQ-Betrag für Entlohnungsantrag +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Min. BSQ Betrag für Rückerstattungsantrag +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Max. BSQ Betrag für Rückerstattungsantrag + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Benötigtes Quorum in BSQ für allgemeinen Antrag +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Benötigtes Quorum in BSQ für Entlohnungsantrag +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Benötigtes Quorum für Rückerstattungsantrag +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Benötigtes Quorum in BSQ um einen Parameter zu ändern +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Benötigtes Quorum in BSQ um ein Gut zu entfernen +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Benötigtes Quorum für Konfiszierungsantrag +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Benötigtes Quorum in BSQ für Antrag einer Rolle mit Pfand + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Benötigter Schwellwert in % für allgemeinen Antrag +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Benötigter Schwellwert in % für Entlohnungsantrag +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Benötigter Schwellwert in % für Entschädigungsantrag +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Benötigter Schwellwert in % um einen Parameter zu ändern +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Benötigter Schwellwert in % um ein Gut zu entfernen +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Benötigter Schwellwert in % für Konfiszierungsantrag +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Benötigter Schwellwert in % für Antrag einer Rolle mit Pfand + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Adresse des BTC Empfängers + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Gut Listungsgebühr pro Tag +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Min. Handelsvolumen für Güter + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Sperrzeit für alternative Handelsauszahlung Tx +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Vermittlergebühr in BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Max. Handels-Limit in BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Gebundene Rolle Einheitsfaktor in BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Ausgabelimit pro Zyklus in BSQ + +dao.param.currentValue=Aktueller Wert: {0} +dao.param.currentAndPastValue=Aktueller Wert: {0} (Wert, als der Vorschlag gemacht wurde: {1}) +dao.param.blocks={0} Blöcke + +dao.results.invalidVotes=Wir hatten in diesem Abstimmungszyklus ungültige Stimmen. Das kann passieren, wenn eine Abstimmung im Bisq-Netzwerk nicht gut verteilt wurde.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Vorschlag-Phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Pause 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Geheime Wahl-Phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Pause 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Wahloffenbarung-Phase +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Pause 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Ergebnis-Phase + +dao.results.votes.table.header.stakeAndMerit=Wahl-Gewicht +dao.results.votes.table.header.stake=Einsatz +dao.results.votes.table.header.merit=Verdient +dao.results.votes.table.header.vote=Wahl + +dao.bond.menuItem.bondedRoles=Gekoppelte Rollen +dao.bond.menuItem.reputation=Gekoppeltes Ansehen +dao.bond.menuItem.bonds=Pfänder + +dao.bond.dashboard.bondsHeadline=Gekoppelte BSQ +dao.bond.dashboard.lockupAmount=Gesperrte Gelder +dao.bond.dashboard.unlockingAmount=Entsperre Gelder (Warten Sie bis die Sperrzeit vorbei ist) + + +dao.bond.reputation.header=Ein Pfand für Reputation hinterlegen +dao.bond.reputation.table.header=Meine Pfänder für Reputation +dao.bond.reputation.amount=Betrag von BSQ zu sperren +dao.bond.reputation.time=Entsperrzeit in Blöcken +dao.bond.reputation.salt=Salt +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Sperren +dao.bond.reputation.lockup.headline=Sperrung-Transaktion bestätigen +dao.bond.reputation.lockup.details=Gesperrter Betrag: {0}\nEntsperrzeit: {1} Block(Blöcke) (≈{2})\n\nMining-Gebühr: {3} ({4} satoshis/vbyte)\nTransaktionsgröße (vsize): {5} Kb\n\nSind Sie sicher, dass Sie fortfahren möchten?? +dao.bond.reputation.unlock.headline=Entsperrung-Transaktion bestätigen +dao.bond.reputation.unlock.details=Entsperrter Betrag: {0}\nEntsperrzeit: {1} Block(Blöcke) (≈{2})\n\nMining-Gebühr: {3} ({4} satoshis/vbyte)\nTransaktionsgröße (vsize): {5} Kb\n\nSind Sie sicher, dass Sie fortfahren möchten? + +dao.bond.allBonds.header=Alle Pfänder + +dao.bond.bondedReputation=Bepfändete Reputation +dao.bond.bondedRoles=Gekoppelte Rollen + +dao.bond.details.header=Rollendetails +dao.bond.details.role=Rolle +dao.bond.details.requiredBond=Benötigt BSQ Kopplung +dao.bond.details.unlockTime=Entsperrzeit in Blöcken +dao.bond.details.link=Link zur Rollenbeschreibung +dao.bond.details.isSingleton=Kann von mehreren Rollenhalter genommen werden +dao.bond.details.blocks={0} Blöcke + +dao.bond.table.column.name=Name +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Pfand-Typ +dao.bond.table.column.details=Details +dao.bond.table.column.lockupTxId=Sperrung Tx ID +dao.bond.table.column.bondState=Kopplungsstatus +dao.bond.table.column.lockTime=Entsperrzeit +dao.bond.table.column.lockupDate=Zeitpunkt der Sperrung + +dao.bond.table.button.lockup=Sperren +dao.bond.table.button.unlock=Entsperren +dao.bond.table.button.revoke=Widerrufen + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Noch nicht gekoppelt +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Sperrung ausstehend +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Kopplung gesperrt +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Entsperrung ausstehend +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Entsperrungs-Tx bestätigt +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Entsperre Kopplung +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Kopplung entsperrt +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Pfand konfisziert + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Gekoppelte Rolle +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Gekoppeltes Ansehen + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=GitHub Administrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Forum Administrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter Administrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase Admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube Administrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq Betreuer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-Fork Betreuer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer Betreuer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Betreiber der Webseite +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Betreiber des Forums +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Seed-Knoten Betreiber +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Preis-Node Betreiber +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin-Node Betreiber +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Betreiber der Handelsstatistik Webseite +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer Operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Mobile Benachrichtigung Weiterleitung Betreiber +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Domainnamen Inhaber +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS Administrator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Vermittler +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC Spendenadresse Besitzer + +dao.burnBsq.assetFee=Gut Listung +dao.burnBsq.menuItem.assetFee=Gut Listungsgebühr +dao.burnBsq.menuItem.proofOfBurn=Nachweis der Verbrennung +dao.burnBsq.header=Asset-Listungsgebühr +dao.burnBsq.selectAsset=Gut auswählen +dao.burnBsq.fee=Gebühr +dao.burnBsq.trialPeriod=Probezeit +dao.burnBsq.payFee=Gebühr bezahlen +dao.burnBsq.allAssets=Alle Güter +dao.burnBsq.assets.nameAndCode=Gutsname +dao.burnBsq.assets.state=Status +dao.burnBsq.assets.tradeVolume=Handelsvolumen +dao.burnBsq.assets.lookBackPeriod=Überprüfungsphase +dao.burnBsq.assets.trialFee=Gebühr für Probezeit +dao.burnBsq.assets.totalFee=Insgesamt gezahlte Gebühren +dao.burnBsq.assets.days={0} Tage +dao.burnBsq.assets.toFewDays=Die Gutsgebühr ist zu niedrig. Die min. Anzahl Tage für die Probezeit sind {0} Tage. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Probezeit läuft +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Aktiv gehandelt +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Aufgrund von Inaktivität aus der Liste entfernt +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Per Wahl entfernt + +dao.proofOfBurn.header=Nachweis der Verbrennung +dao.proofOfBurn.amount=Betrag +dao.proofOfBurn.preImage=Vorabbild +dao.proofOfBurn.burn=Verbrennen +dao.proofOfBurn.allTxs=Alle Transaktionen zum Nachweis der Verbrennung +dao.proofOfBurn.myItems=Meine Transaktionen zum Nachweis der Verbrennung +dao.proofOfBurn.date=Datum +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transaktionen +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Unterzeichnen einer Nachricht mit Schlüssel vom Nachweis der Verbrennung +dao.proofOfBurn.verify.window.title=Verifizieren einer Nachricht mit Schlüssel vom Nachweis der Verbrennung +dao.proofOfBurn.copySig=Signatur in Zwischenablage kopieren +dao.proofOfBurn.sign=Unterzeichnen +dao.proofOfBurn.message=Nachricht +dao.proofOfBurn.sig=Signatur +dao.proofOfBurn.verify=Bestätigen +dao.proofOfBurn.verificationResult.ok=Überprüfung erfolgreich +dao.proofOfBurn.verificationResult.failed=Überprüfung fehlgeschlagen + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Vorschlag-Phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Pause vor geheime Wahl-Phase +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Geheime Wahl-Phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Pause vor Wahloffenbarung-Phase +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Wahloffenbarung-Phase +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Pause vor Ergebnis-Phase +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Wahlergebnisse-Phase + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Vorschlag-Phase +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Geheime Wahl +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Wahl offenbaren +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Wahlergebnis + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Entlohnungsanfrage +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Rückerstattungsantrag +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Vorschlag für Gekoppelte Rolle +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Vorschlag ein Gut zu entfernen +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Vorschlag einen Parameter zu ändern +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Allgemeiner Vorschlag +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Vorschlag eine Kopplung zu konfiszieren + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Entlohnungsanfrage +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Rückerstattungsantrag +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Gekoppelte Rolle +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Einen Altcoin entfernen +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Einen Parameter ändern +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Allgemeiner Vorschlag +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Konfisziere eine Kopplung + +dao.proposal.details=Vorschlagdetails +dao.proposal.selectedProposal=Ausgewählte Anträge +dao.proposal.active.header=Vorschläge des momentanen Zyklus +dao.proposal.active.remove.confirm=Sind Sie sicher, dass Sie diesen Antrag entfernen wollen?\nDie Erstellergebühr geht verloren, wenn Sie den Antrag entfernen. +dao.proposal.active.remove.doRemove=Ja, Antrag entfernen +dao.proposal.active.remove.failed=Konnte Vorschlag nicht entfernen +dao.proposal.myVote.title=Wahl +dao.proposal.myVote.accept=Vorschlag annehmen +dao.proposal.myVote.reject=Vorschlag ablehnen +dao.proposal.myVote.removeMyVote=Vorschlag ignorieren +dao.proposal.myVote.merit=Wahlgewicht durch verdiente BSQ +dao.proposal.myVote.stake=Wahlgewicht vom Einsatz +dao.proposal.myVote.revealTxId=Wahloffenbarung Transaktion ID +dao.proposal.myVote.stake.prompt=Max. verfügbarer Einsatz zum wählen: {0} +dao.proposal.votes.header=Einsatz für Wahl festlegen und Ihre Wahlen veröffentlichen +dao.proposal.myVote.button=Wahlen veröffentlichen +dao.proposal.myVote.setStake.description=Nach dem Sie für alle Vorschläge gewählt haben, müssen Sie Ihren Einsatz festlegen, indem Sie BSQ sperren. Desto mehr BSQ Sie sperren, desto mehr Gewicht hat Ihre Wahl.\n\nGesperrte BSQ wird während der Wahl Veröffentlichungsphase entsperrt. +dao.proposal.create.selectProposalType=Vorschlagtyp auswählen +dao.proposal.create.phase.inactive=Bitte warten sie auf die nächste Vorschlags-Phase +dao.proposal.create.proposalType=Vorschlagtype +dao.proposal.create.new=Neuen Antrag erstellen +dao.proposal.create.button=Antrag erstellen +dao.proposal.create.publish=Vorschlag veröffentlichen +dao.proposal.create.publishing=Vorschlag wird veröffentlicht ... +dao.proposal=Vorschlag +dao.proposal.display.type=Vorschlagtyp +dao.proposal.display.name=Exakter GitHub-Benutzername  +dao.proposal.display.link=Link zu detaillierten Infos +dao.proposal.display.link.prompt=Link zu Vorschlag +dao.proposal.display.requestedBsq=Gelder in BSQ anfordern +dao.proposal.display.txId=Antrag Transaktion-ID: +dao.proposal.display.proposalFee=Vorschlag-Gebühr +dao.proposal.display.myVote=Meine Wahl +dao.proposal.display.voteResult=Zusammenfassung des Wahlergebnisses +dao.proposal.display.bondedRoleComboBox.label=Typ der Rolle mit Pfand +dao.proposal.display.requiredBondForRole.label=Benötigter Pfand für Rolle +dao.proposal.display.option=Option + +dao.proposal.table.header.proposalType=Vorschlagtyp +dao.proposal.table.header.link=Link +dao.proposal.table.header.myVote=Meine Wahl +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Entfernen +dao.proposal.table.icon.tooltip.removeProposal=Antrag entfernen +dao.proposal.table.icon.tooltip.changeVote=Aktuelles Votum: ''{0}''. Votum abändern zu: ''{1}'' + +dao.proposal.display.myVote.accepted=Angenommen +dao.proposal.display.myVote.rejected=Abgelehnt +dao.proposal.display.myVote.ignored=Ignoriert +dao.proposal.display.myVote.unCounted=Stimme wurde nicht in das Ergebnis einbezogen +dao.proposal.myVote.summary=Gewählt: {0}; Stimmengewicht: {1} (verdient: {2} + Einsatz: {3}) {4} +dao.proposal.myVote.invalid=Wahl war ungültig + +dao.proposal.voteResult.success=Angenommen +dao.proposal.voteResult.failed=Abgelehnt +dao.proposal.voteResult.summary=Ergebnis: {0}; Schwellwert: {1} (benötigt > {2}); Quorum: {3} (benötigt > {4}) + +dao.proposal.display.paramComboBox.label=Zu ändernden Parameter auswählen +dao.proposal.display.paramValue=Wert des Parameter + +dao.proposal.display.confiscateBondComboBox.label=Kopplung wählen +dao.proposal.display.assetComboBox.label=Zu entfernendes Gut + +dao.blindVote=Geheime Wahl + +dao.blindVote.startPublishing=Veröffentliche geheimen Wahl Transaktion... +dao.blindVote.success=Die Transaktion ihrer geheimen Wahl wurde erfolgreich veröffentlicht.\n\nDamit ihre Stimme auch gezählt wird, müssen sie im Zeitraum der Stimmoffenbahrungs-Phase in Bisq online sein. Wenn die Stimmoffenbahrungs-Transaktion nicht veröffentlicht wird, ist ihre Stimme nicht gültig! + +dao.wallet.menuItem.send=Senden +dao.wallet.menuItem.receive=Empfangen +dao.wallet.menuItem.transactions=Transaktionen + +dao.wallet.dashboard.myBalance=Mein Guthaben der Handels-Wallet + +dao.wallet.receive.fundYourWallet=Ihre BSQ Empfangs-Adresse +dao.wallet.receive.bsqAddress=Adresse der BSQ-Wallet (Neue ungebrauchte Adresse) + +dao.wallet.send.sendFunds=Gelder senden +dao.wallet.send.sendBtcFunds=Sende nicht-BSQ-Gelder (BTC) +dao.wallet.send.amount=Betrag in BSQ +dao.wallet.send.btcAmount=Betrag in BTC (nicht-BSQ-Gelder) +dao.wallet.send.setAmount=Betrag zum Abheben festlegen (Min­dest­be­trag ist {0}) +dao.wallet.send.receiverAddress=Adresse des BSQ Empfängers +dao.wallet.send.receiverBtcAddress=Adresse des BTC Empfängers +dao.wallet.send.setDestinationAddress=Tragen Sie Ihre Zieladresse ein +dao.wallet.send.send=BSQ-Gelder senden +dao.wallet.send.inputControl=Inputs auswählen +dao.wallet.send.sendBtc=BTC-Gelder senden +dao.wallet.send.sendFunds.headline=Abhebeanfrage bestätigen +dao.wallet.send.sendFunds.details=Senden: {0}\nEmpfangsadresse: {1}.\nBenötigte Mining-Gebühr beträgt: {2} ({3} satoshis/vbyte)\nTransaktion vsize: {4} vKb\n\nDer empfänger wird erhalten: {5}\n\nSind Sie sich sicher die Menge abzuheben? +dao.wallet.chainHeightSynced=Synchronisiert bis Block: {0} +dao.wallet.chainHeightSyncing=Erwarte Blöcke... {0} von {1} Blöcken verifiziert +dao.wallet.tx.type=Typ + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Undefiniert +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Nicht erkannt +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Unbestätigte BSQ-Transaktion +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Ungültige BSQ-Transaktion +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Ursprungstransaktion +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=BSQ überweisen +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ erhalten +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ senden +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Handelsgebühr +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Gebühr für Entlohnungsanfrage +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Gebühr für Rückerstattungsantrag +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Gebühr für Vorschlag +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Gebühr für geheime Wahl +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Wahl offenbaren +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Sperre Kopplung +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Entsperre Kopplung +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Listungsgebühr für Gut +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Nachweis der Verbrennung +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregulär + +dao.tx.withdrawnFromWallet=BTC von Wallet abgehoben +dao.tx.issuanceFromCompReq=Entlohnungsanfrage/ausgabe +dao.tx.issuanceFromCompReq.tooltip=Entlohnungsanfrage, die zur Ausgabe neuere BSQ führte.\nAusgabedatum: {0} +dao.tx.issuanceFromReimbursement=Rückerstattungsantrag/Ausgabe +dao.tx.issuanceFromReimbursement.tooltip=Rückerstattungsanfrage, die zur Ausgabe neuer BSQ führte.\nAusgabedatum: {0} +dao.proposal.create.missingBsqFunds=Sie haben nicht ausreichend BSQ um diesen Vorschlag zu erstellen. Falls sie nicht bestätigte BSQ-Transaktionen offen haben, müssen sie auf die Blockchain-Bestätigung warten. BSQ kann nur validiert werden wenn es in einen Block aufgenommen wurde.\nEs fehlen: {0} + +dao.proposal.create.missingBsqFundsForBond=Du hast nicht ausreichend BSQ für diese Rolle. Du kannst den Vorschlag zwar veröffentlichen, benötigst jedoch den vollen BSQ Betrag für diese Rolle, falls dein Vorschlag akzeptiert wird.\nEs fehlen: {0} + +dao.proposal.create.missingMinerFeeFunds=Du hast nicht ausreichend BTC, um die Vorschlags-Transaktion zu erstellen. Jede BSQ-Transaktion benötigt eine Mining-Gebühr in BTC.\nEs fehlen: {0} + +dao.proposal.create.missingIssuanceFunds=Sie haben nicht ausreichend BTC, um die Vorschlags-Transaktion zu erstellen. Jede BSQ-Transaktion benötigt eine Mining-Gebühr in BTC, Ausgabetransaktionen brauchen auch BTC für den angefragten BSQ Betrag ({0} Satoshis/BSQ).\nEs fehlen: {1} + +dao.feeTx.confirm=Bestätige {0} Transaktion +dao.feeTx.confirm.details={0} Gebühr: {1} \nMining-Gebühr: {2} ({3} Satoshis/vByte)\nTransaktionsgröße: {4} Kb\n\nSind Sie sicher, dass Sie die {5} Transaktion senden wollen? + +dao.feeTx.issuanceProposal.confirm.details={0} Gebühr: {1}\nBenötigte BTC für die BSQ Ausgabe: {2} ({3} Satoshis/BSQ)\nMining-Gebühr: {4} ({5} Satoshis/Byte)\nTransaktionsgröße: {6} Kb\n\nFalls Ihre Anfrage angenommen wird, erhalten Sie den angefragten Betrag minus die 2 BSQ Antragsgebühr.\n\nSind Sie sicher, dass Sie die {7} Transaktion veröffentlichen wollen? + +dao.news.bisqDAO.title=DER BISQ DAO +dao.news.bisqDAO.description=Genauso wie der Bisq Handelsplatz dezentral und resistent gegen Zensur ist, ist auch die Führung der DAO - die Bisq DAO und der BSQ Token machen es möglich. +dao.news.bisqDAO.readMoreLink=Erfahren Sie mehr über den Bisq DAO + +dao.news.pastContribution.title=BEREITS ZU BISQ BEIGETRAGEN? BSQ ANFRAGEN +dao.news.pastContribution.description=Falls sie in der Vergangenheit zu Bisq beigetragen haben, verwenden sie die darunterstehende BSQ Adresse um einen Antrag zur Aufnahme in die BSQ Genesis Transaktion zu erstellen. +dao.news.pastContribution.yourAddress=Ihre BSQ-Wallets-Adresse +dao.news.pastContribution.requestNow=Jetzt anfordern + +dao.news.DAOOnTestnet.title=DEN BISQ DAO AUF UNSEREM TESTNETZWERK LAUFEN LASSEN +dao.news.DAOOnTestnet.description=Die Bisq DAO wurde auf Mainnet noch nicht veröffentlicht, jedoch können sie die Bisq DAO jetzt schon in unserem Testnet ausprobieren. +dao.news.DAOOnTestnet.firstSection.title=1. Nach DAO-Testnetzmodus wechseln +dao.news.DAOOnTestnet.firstSection.content=Vom Einstellungsmenü ins DAO-Testnetzwerk wechseln. +dao.news.DAOOnTestnet.secondSection.title=2. Einige BSQ erwerben +dao.news.DAOOnTestnet.secondSection.content=Fragen sie einfach auf Slack nach BSQ oder kaufen sie direkt BSQ in Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Beim Wahl-Zyklus teilhaben +dao.news.DAOOnTestnet.thirdSection.content=Erstellen sie Vorschläge/Anträge und stimmen sie über bestehende ab, um unterschiedliche Aspekte von Bisq zu verändern. +dao.news.DAOOnTestnet.fourthSection.title=4. Einen BSQ-Block-Forscher erkunden +dao.news.DAOOnTestnet.fourthSection.content=Da es sich bei BSQ um Bitcoin handelt, können sie die BSQ-Transaktionen in ihrem Bitcoin Block Explorer einsehen. +dao.news.DAOOnTestnet.readMoreLink=Die volle Dokumentation lesen. + +dao.monitor.daoState=DAO Status +dao.monitor.proposals=Vorschlag Status +dao.monitor.blindVotes=Geheime Wahl-Status + +dao.monitor.table.peers=Peers +dao.monitor.table.conflicts=Konflikte +dao.monitor.state=Status +dao.monitor.requestAlHashes=Alle Hashs anfragen +dao.monitor.resync=DAO Status neu syncronisieren +dao.monitor.table.header.cycleBlockHeight=Zyklus / Blockhöhe +dao.monitor.table.cycleBlockHeight=Zyklus {0} / Block {1} +dao.monitor.table.seedPeers=Seed-Knoten: {0} + +dao.monitor.daoState.headline=DAO Status +dao.monitor.daoState.table.headline=Kette von DAO Status Hashs +dao.monitor.daoState.table.blockHeight=Blockhöhe +dao.monitor.daoState.table.hash=Hash des DAO Status +dao.monitor.daoState.table.prev=Vorheriger Hash +dao.monitor.daoState.conflictTable.headline=DAO Status Hashs der Peers im Konflikt +dao.monitor.daoState.utxoConflicts=UTXO Konflikte +dao.monitor.daoState.utxoConflicts.blockHeight=Blockhöhe: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Summe aller UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Summe aller BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=Der DAO Status ist nicht mit dem Netzwerk synchronisiert. Nach dem Neustart wird der DAO Status neu synchronisiert. + +dao.monitor.proposal.headline=Vorschlag Status +dao.monitor.proposal.table.headline=Kette von Vorschlag Status Hashs +dao.monitor.proposal.conflictTable.headline=Vorschlag Status Hashs der Peers im Konflikt + +dao.monitor.proposal.table.hash=Hash des Vorschlag Status +dao.monitor.proposal.table.prev=Vorheriger Hash +dao.monitor.proposal.table.numProposals=Keine Vorschläge + +dao.monitor.isInConflictWithSeedNode=Deine lokalen Daten stimmen mit mindestens einem Seed-Knoten nicht überein. Bitte synchronisiere den DAO-Status neu. +dao.monitor.isInConflictWithNonSeedNode=Einer deiner Peers is nicht im Konsens mit dem Netzwerk, aber dein Knoten ist synchron mit den Seed-Knoten. +dao.monitor.daoStateInSync=Dein lokaler Knoten ist in Konsens mit dem Netzwerk + +dao.monitor.blindVote.headline=Geheime Wahl-Status +dao.monitor.blindVote.table.headline=Kette von geheime Wahl Status Hashs +dao.monitor.blindVote.conflictTable.headline=Geheime Wahl Status Hashs der Peers im Konflikt +dao.monitor.blindVote.table.hash=Hash des geheimen Wahl Status +dao.monitor.blindVote.table.prev=Vorheriger Hash +dao.monitor.blindVote.table.numBlindVotes=Anzahl geheimer Wahlen + +dao.factsAndFigures.menuItem.supply=Angebot an BSQ +dao.factsAndFigures.menuItem.transactions=BSQ Transaktionen + +dao.factsAndFigures.dashboard.avgPrice90=90 Tage durchschnittlicher BSQ/BTC-Handelspreis +dao.factsAndFigures.dashboard.avgPrice30=30 Tage durchschnittlicher BSQ/BTC-Handelspreis +dao.factsAndFigures.dashboard.avgUSDPrice90=90 Tage volumengewichteter durchschnittlicher BSQ/USD Preis +dao.factsAndFigures.dashboard.avgUSDPrice30=30 Tage volumengewichteter durchschnittlicher BSQ/USD Preis +dao.factsAndFigures.dashboard.marketCap=Marktkapitalisierung (basierend auf 30-tägigem Durchschnittspreis von BSQ/USD) +dao.factsAndFigures.dashboard.availableAmount=Insgesamt verfügbare BSQ +dao.factsAndFigures.dashboard.volumeUsd=Gesamtes Handelsvolumen in USD +dao.factsAndFigures.dashboard.volumeBtc=Gesamtes Handelsvolumen in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Durchschnittlicher BSQ/USD Handelspreis einer ausgewählten Zeitperiode im Chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Durchschnittlicher BSQ/BTC Handelspreis einer ausgewählten Zeitperiode im Chart + +dao.factsAndFigures.supply.issuedVsBurnt=Ausgestellte BSQ v. verbrannte BSQ + +dao.factsAndFigures.supply.issued=Ausgestellte BSQ +dao.factsAndFigures.supply.compReq=Entlohnungsanfragen +dao.factsAndFigures.supply.reimbursement=Rückerstattungsantrag +dao.factsAndFigures.supply.genesisIssueAmount=Ausgestellte BSQ in Genesis-Transaktion +dao.factsAndFigures.supply.compRequestIssueAmount=Ausgestellte BSQ für Entlohnungsanträge +dao.factsAndFigures.supply.reimbursementAmount=Ausgestellte BSQ für Rückerstattungsanträge +dao.factsAndFigures.supply.totalIssued=Gesamt ausgegebene BSQ +dao.factsAndFigures.supply.totalBurned=Gesamt verbrannte BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=Verbrannte BSQ (Gebühren und Proof-of-Burn) + +dao.factsAndFigures.supply.priceChat=BSQ Preis +dao.factsAndFigures.supply.volumeChat=Handelsvolumen +dao.factsAndFigures.supply.tradeVolumeInUsd=Handelsvolumen in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Handelsvolumen in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD Preis +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC Preis +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD Preis + +dao.factsAndFigures.supply.locked=Globaler Zustand der gesperrten BSQ +dao.factsAndFigures.supply.totalLockedUpAmount=In Pfänden gesperrt +dao.factsAndFigures.supply.totalUnlockingAmount=BSQ von Pfand auslösen +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ von Pfand ausgelöst +dao.factsAndFigures.supply.totalConfiscatedAmount=Konfiszierte BSQ von Pfänden +dao.factsAndFigures.supply.proofOfBurn=Nachweis der Verbrennung +dao.factsAndFigures.supply.bsqTradeFee=BSQ Handelsgebühren +dao.factsAndFigures.supply.btcTradeFee=BTC Handelsgebühren + +dao.factsAndFigures.transactions.genesis=Ursprungstransaktion +dao.factsAndFigures.transactions.genesisBlockHeight=Genesisblock-Höhe +dao.factsAndFigures.transactions.genesisTxId=Genesis-Transaktions-ID +dao.factsAndFigures.transactions.txDetails=BSQ Transationsstatistiken +dao.factsAndFigures.transactions.allTx=Anzahl aller BSQ-Transaktionen +dao.factsAndFigures.transactions.utxo=Anzahl aller nicht ausgegebenen Transaktionsausgängen +dao.factsAndFigures.transactions.compensationIssuanceTx=Anzahl aller Transaktionen von Entlohnungsanfragen +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Anzahl aller Transaktionen von Entschädigungsanfragen +dao.factsAndFigures.transactions.burntTx=Anzahl aller Transaktionen von bezahlten Gebühren +dao.factsAndFigures.transactions.invalidTx=Anzahl aller ungültigen Transaktionen +dao.factsAndFigures.transactions.irregularTx=Anzahl aller irregulären Transaktionen + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Inputs für die Transaktion auswählen +inputControlWindow.balanceLabel=Verfügbarer Betrag + +contractWindow.title=Konfliktdetails +contractWindow.dates=Angebotsdatum / Handelsdatum +contractWindow.btcAddresses=Bitcoinadresse BTC-Käufer / BTC-Verkäufer +contractWindow.onions=Netzwerkadresse BTC-Käufer / BTC-Verkäufer +contractWindow.accountAge=Kontoalter BTC Käufer / BTC Verkäufer +contractWindow.numDisputes=Anzahl Konflikte BTC-Käufer / BTC-Verkäufer +contractWindow.contractHash=Vertrags-Hash + +displayAlertMessageWindow.headline=Wichtige Informationen! +displayAlertMessageWindow.update.headline=Wichtige Update Informationen! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=Dateien: +displayUpdateDownloadWindow.downloadingFile=Lade herunter: {0} +displayUpdateDownloadWindow.verifiedSigs=Signatur mit Schlüsseln überprüft: +displayUpdateDownloadWindow.status.downloading=Lade Dateien herunter... +displayUpdateDownloadWindow.status.verifying=Überprüfe Signatur... +displayUpdateDownloadWindow.button.label=Installer herunterladen und Signatur überprüfen +displayUpdateDownloadWindow.button.downloadLater=Später herunterladen +displayUpdateDownloadWindow.button.ignoreDownload=Diese Version ignorieren +displayUpdateDownloadWindow.headline=Ein neues Bisq-Update ist verfügbar! +displayUpdateDownloadWindow.download.failed.headline=Download fehlgeschlagen +displayUpdateDownloadWindow.download.failed=Download fehlgeschlagen.\nBitte downloaden Sie und verifizieren Sie manuell unter [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unfähig den richtigen Installer zu bestimmen. Bitte downloaden und verifizieren Sie manuell unter [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verifikation fehlgeschlagen.\nBitte downloaden Sie und verifizieren Sie manuell unter [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=Die neue Version wurde erfolgreich heruntergeladen und die Signatur überprüft.\n\nBitte öffnen Sie das Downloadverzeichnis, schließen die Anwendung und installieren die neue Version. +displayUpdateDownloadWindow.download.openDir=Downloadverzeichnis öffnen + +disputeSummaryWindow.title=Zusammenfassung +disputeSummaryWindow.openDate=Erstellungsdatum des Tickets +disputeSummaryWindow.role=Rolle des Händlers +disputeSummaryWindow.payout=Auszahlung des Handelsbetrags +disputeSummaryWindow.payout.getsTradeAmount=Der BTC-{0} erhält die Auszahlung des Handelsbetrags +disputeSummaryWindow.payout.getsAll=Menge in BTC zu {0} +disputeSummaryWindow.payout.custom=Spezifische Auszahlung +disputeSummaryWindow.payoutAmount.buyer=Auszahlungsbetrag des Käufers +disputeSummaryWindow.payoutAmount.seller=Auszahlungsbetrag des Verkäufers +disputeSummaryWindow.payoutAmount.invert=Verlierer als Veröffentlicher nutzen +disputeSummaryWindow.reason=Grund des Konflikts +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Fehler +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Benutzerfreundlichkeit +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protokollverletzung +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Keine Antwort +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Betrug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Andere +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Bank +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Options-Handel +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader antwortet nicht +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Falsches Sender-Konto +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer war zu spät +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade wurde bereits festgelegt. + +disputeSummaryWindow.summaryNotes=Zusammenfassende Anmerkungen +disputeSummaryWindow.addSummaryNotes=Zusammenfassende Anmerkungen hinzufügen +disputeSummaryWindow.close.button=Ticket schließen + +# 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 geschlossen am {0}\n{1}Node Adresse: {2}\n\nZusammenfassung:\nTrade ID: {3}\nWährung: {4}\nTrade-Betrag: {5}\nAuszahlungsbetrag für den BTC Käufer: {6}\nAuszahlungsbetrag für den BTC Verkäufer: {7}\n\nGrund für den Konflikt: {8}\n\nWeitere Hinweise:\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=\nNächster Schritt:\nTrade öffnen und Vorschlag des Mediators akzeptieren oder ablehnen. +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNächster Schritt:\nSie müssen nichts weiteres machen. Wenn der Arbitrator in Ihrem Vorteil entscheidet, sehen Sie eine "Rückerstattung des Arbitratos"-Transaktion in Ihren Funds/Transaktionen +disputeSummaryWindow.close.closePeer=Sie müssen auch das Ticket des Handelspartners schließen! +disputeSummaryWindow.close.txDetails.headline=Rückerstattungstransaktion veröffentlichen +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Käufer erhält {0} an Adresse: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Verkäufer erhält {0} an Adresse: {1}\n +disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3} ({4} Satoshis/Byte)\nTransaktionsgröße: {5} Kb\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten? + +disputeSummaryWindow.close.noPayout.headline=Ohne Auszahlung schließen +disputeSummaryWindow.close.noPayout.text=Wollen Sie schließen ohne eine Auszahlung zu tätigen? + +emptyWalletWindow.headline={0} Notfall-Wallets-Werkzeug +emptyWalletWindow.info=Bitte nur in Notfällen nutzen, wenn Sie vom UI aus nicht auf Ihre Gelder zugreifen können.\n\nBeachten Sie bitte, dass alle offenen Angebote geschlossen werden, wenn Sie dieses Werkzeug verwenden.\n\nErstellen Sie ein Backup Ihres Dateiverzeichnisses, bevor Sie dieses Werkzeug verwenden. Dies können Sie unter \"Konto/Backup\" tun.\n\nBitte melden Sie uns das Problem und erstellen Sie einen Fehlerbericht auf GitHub oder im Bisq-Forum, damit wir feststellen können, was das Problem verursacht hat. +emptyWalletWindow.balance=Ihr verfügbares Wallets-Guthaben +emptyWalletWindow.bsq.btcBalance=Guthaben von nicht-BSQ Satoshis + +emptyWalletWindow.address=Ihre Zieladresse +emptyWalletWindow.button=Alle Gelder senden +emptyWalletWindow.openOffers.warn=Sie haben offene Angebote, die entfernt werden, wenn Sie die Wallet leeren.\nSind Sie sicher, dass Sie Ihre Wallet leeren wollen? +emptyWalletWindow.openOffers.yes=Ja, ich bin sicher. +emptyWalletWindow.sent.success=Das Guthaben Ihrer Wallet wurde erfolgreich überwiesen. + +enterPrivKeyWindow.headline=Private Key für die Registrierung eingeben + +filterWindow.headline=Filterliste bearbeiten +filterWindow.offers=Herausgefilterte Angebote (durch Kommas getrennt) +filterWindow.onions=Vom Handel ausgeschlossene Adressen (Komma getr.) +filterWindow.bannedFromNetwork=Vom Netzwerk ausgeschlossene Adressen (Komma getr.) +filterWindow.accounts=Herausgefilterte Handelskonten Daten:\nFormat: Komma getrennte Liste von [Zahlungsmethoden ID | Datenfeld | Wert] +filterWindow.bannedCurrencies=Herausgefilterte Währungscodes (durch Kommas getrennt) +filterWindow.bannedPaymentMethods=Herausgefilterte Zahlungsmethoden-IDs (durch Kommas getrennt) +filterWindow.bannedAccountWitnessSignerPubKeys=Gefilterte Pub Keys von unterzeichnenden Kontozeugen (durch Komma getrennte Hex der Pub Keys) +filterWindow.bannedPrivilegedDevPubKeys=Gefilterte privilegierte Dev Pub Keys (durch Komma getrennte Hex der Pub Keys) +filterWindow.arbitrators=Gefilterte Vermittler (mit Komma getr. Onion-Adressen) +filterWindow.mediators=Gefilterte Mediatoren (mit Komma getr. Onion-Adressen) +filterWindow.refundAgents=Gefilterte Rückerstattungsagenten (mit Komma getr. Onion-Adressen) +filterWindow.seedNode=Gefilterte Seed-Knoten (Komma getr. Onion-Adressen) +filterWindow.priceRelayNode=Gefilterte Preisrelais Knoten (Komma getr. Onion-Adressen) +filterWindow.btcNode=Gefilterte Bitcoinknoten (Komma getr. Adresse + Port) +filterWindow.preventPublicBtcNetwork=Nutzung des öffentlichen Bitcoin-Netzwerks verhindern +filterWindow.disableDao=DAO deaktivieren +filterWindow.disableAutoConf=Automatische Bestätigung deaktivieren +filterWindow.autoConfExplorers=Gefilterter Explorer mit Auto-Bestätigung (Adressen mit Komma separiert) +filterWindow.disableDaoBelowVersion=Min. für DAO erforderliche Version +filterWindow.disableTradeBelowVersion=Min. zum Handeln erforderliche Version +filterWindow.add=Filter hinzufügen +filterWindow.remove=Filter entfernen +filterWindow.btcFeeReceiverAddresses=BTC Gebühr Empfänger-Adressen +filterWindow.disableApi=API deaktivieren +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Min. BTC-Betrag +offerDetailsWindow.min=(min. {0}) +offerDetailsWindow.distance=(Abstand zum Marktpreis: {0}) +offerDetailsWindow.myTradingAccount=Mein Handelskonto +offerDetailsWindow.offererBankId=(Erstellers Bank ID/BIC/SWIFT) +offerDetailsWindow.offerersBankName=(Bankname des Erstellers) +offerDetailsWindow.bankId=Bankkennung (z.B. BIC oder SWIFT) +offerDetailsWindow.countryBank=Land der Bank des Erstellers +offerDetailsWindow.commitment=Verpflichtung +offerDetailsWindow.agree=Ich stimme zu +offerDetailsWindow.tac=Geschäftsbedingungen +offerDetailsWindow.confirm.maker=Bestätigen: Anbieten Bitcoin zu {0} +offerDetailsWindow.confirm.taker=Bestätigen: Angebot annehmen Bitcoin zu {0} +offerDetailsWindow.creationDate=Erstellungsdatum +offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Bisq Wallet von Ihrem externen Wallet aufzuladen. +qRCodeWindow.request=Zahlungsanfrage:\n{0} + +selectDepositTxWindow.headline=Kautionstransaktion für Konflikt auswählen +selectDepositTxWindow.msg=Die Kautionstransaktion wurde nicht im Handel gespeichert.\nBitte wählen Sie die existierenden MultiSig-Transaktionen aus Ihrer Wallet, die die Kautionstransaktion für den fehlgeschlagenen Handel war.\n\nSie können die korrekte Transaktion finden, indem Sie das Handelsdetail-Fenster öffnen (klicken Sie auf die Handels-ID in der Liste) und dem Transaktions-Output der Handelsgebührenzahlung zur nächsten Transaktion folgen, wo Sie die MultiSig-Kautionstransaktion sehen (die Adresse beginnt mit einer 3). Diese Transaktions-ID sollte in der dargestellten Liste auftauchen. Sobald Sie die korrekte Transaktion gefunden haben, wählen Sie diese Transaktion hier aus und fahren fort.\n\nEntschuldigen Sie die Unannehmlichkeiten, aber dieser Fehler sollte sehr selten auftreten und wir werden in Zukunft versuchen bessere Wege zu finden, ihn zu lösen. +selectDepositTxWindow.select=Kautionstransaktion auswählen + +sendAlertMessageWindow.headline=Globale Benachrichtigung senden +sendAlertMessageWindow.alertMsg=Warnmeldung +sendAlertMessageWindow.enterMsg=Nachricht eingeben +sendAlertMessageWindow.isSoftwareUpdate=Software Download Benachrichtigung +sendAlertMessageWindow.isUpdate=Ist voller Release +sendAlertMessageWindow.isPreRelease=Ist Pre-Release +sendAlertMessageWindow.version=Neue Versionsnummer +sendAlertMessageWindow.send=Benachrichtigung senden +sendAlertMessageWindow.remove=Benachrichtigung entfernen + +sendPrivateNotificationWindow.headline=Private Nachricht senden +sendPrivateNotificationWindow.privateNotification=Private Benachrichtigung +sendPrivateNotificationWindow.enterNotification=Benachrichtigung eingeben +sendPrivateNotificationWindow.send=Private Benachrichtigung senden + +showWalletDataWindow.walletData=Wallet-Daten +showWalletDataWindow.includePrivKeys=Private Schlüssel einbeziehen + +setXMRTxKeyWindow.headline=Senden der XMR beweisen +setXMRTxKeyWindow.note=Hinzufügen der Transaktionsinfo unten aktiviert die automatische Bestätigung für schneller Trades. Mehr lesen: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=ID der Transaktion (optional) +setXMRTxKeyWindow.txKey=Transaktionsschlüssel (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Nutzervereinbarung +tacWindow.agree=Ich stimme zu +tacWindow.disagree=Ich stimme nicht zu und beende +tacWindow.arbitrationSystem=Streitbeilegung + +tradeDetailsWindow.headline=Handel +tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung: +tradeDetailsWindow.tradeDate=Handelsdatum +tradeDetailsWindow.txFee=Mining-Gebühr +tradeDetailsWindow.tradingPeersOnion=Onion-Adresse des Handelspartners +tradeDetailsWindow.tradingPeersPubKeyHash=Trading Peers Pubkey Hash +tradeDetailsWindow.tradeState=Handelsstatus +tradeDetailsWindow.agentAddresses=Vermittler/Mediator +tradeDetailsWindow.detailData=Detaillierte Daten + +txDetailsWindow.headline=Transaktionsdetails +txDetailsWindow.btc.note=Sie haben BTC gesendet. +txDetailsWindow.bsq.note=Sie haben BSQ-Gelder gesendet. BSQ sind "Colored Bitcoin", daher wird die Transaktion nicht in einem BSQ-Explorer angezeigt, bis sie in einem Bitcoin-Block bestätigt wurde. +txDetailsWindow.sentTo=Gesendet an +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Passwort zum Entsperren eingeben + +torNetworkSettingWindow.header=Tor-Netzwerkeinstellungen +torNetworkSettingWindow.noBridges=Keine Bridges verwenden +torNetworkSettingWindow.providedBridges=Mit bereitgestellten Bridges verbinden +torNetworkSettingWindow.customBridges=Benutzerdefinierte Bridges eingeben +torNetworkSettingWindow.transportType=Transport-Typ +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (empfohlen) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Geben Sie ein oder mehrere Bridge-Relays ein (eine pro Zeile) +torNetworkSettingWindow.enterBridgePrompt=Adresse:Port eingeben +torNetworkSettingWindow.restartInfo=Sie müssen neu starten, um die Änderungen anzuwenden +torNetworkSettingWindow.openTorWebPage=Tor-Projekt-Webseite öffnen +torNetworkSettingWindow.deleteFiles.header=Verbindungsprobleme? +torNetworkSettingWindow.deleteFiles.info=Wenn beim Start häufig Probleme mit der Verbindung auftreten, kann das Löschen veralteter Tor-Dateien hilfreich sein. Dazu klicken Sie auf die Schaltfläche unten und starten danach neu. +torNetworkSettingWindow.deleteFiles.button=Lösche veraltete Tor-Dateien und fahre herunter +torNetworkSettingWindow.deleteFiles.progress=Tor wird herunterfahren +torNetworkSettingWindow.deleteFiles.success=Veraltete Tor-Dateien erfolgreich gelöscht. Bitte neu starten. +torNetworkSettingWindow.bridges.header=Ist Tor blockiert? +torNetworkSettingWindow.bridges.info=Falls Tor von Ihrem Provider oder in Ihrem Land blockiert wird, können Sie versuchen Tor-Bridges zu nutzen.\nBesuchen Sie die Tor-Webseite unter: https://bridges.torproject.org/bridges um mehr über Bridges und pluggable transposrts zu lernen. + +feeOptionWindow.headline=Währung für Handelsgebührzahlung auswählen +feeOptionWindow.info=Sie können wählen, die Gebühr in BSQ oder BTC zu zahlen. Wählen Sie BSQ, erhalten Sie eine vergünstigte Handelsgebühr. +feeOptionWindow.optionsLabel=Währung für Handelsgebührzahlung auswählen +feeOptionWindow.useBTC=BTC nutzen +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Benachrichtigung +popup.headline.instruction=Bitte beachten Sie: +popup.headline.attention=Achtung +popup.headline.backgroundInfo=Hintergrundinformation +popup.headline.feedback=Abgeschlossen +popup.headline.confirmation=Bestätigung +popup.headline.information=Information +popup.headline.warning=Warnung +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 bisq.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} + +error.spvFileCorrupted=Beim Einlesen der SPV-Kettendatei ist ein Fehler aufgetreten.\nDie SPV-Kettendatei ist möglicherweise beschädigt.\n\nFehlermeldung: {0}\n\nMöchten Sie diese löschen und neu synchronisieren? +error.deleteAddressEntryListFailed=Konnte AddressEntryList-Datei nicht löschen.\nFehler: {0} +error.closedTradeWithUnconfirmedDepositTx=Die Einzahlungstransaktion des geschlossenen Trades mit der Trade ID {0} ist noch immer unbestätigt.\n\nBitte führen Sie eine SPV-Resynchronisation unter \" Einstellungen/Netzwerkinformationen\" durch, um zu sehen, ob die Transaktion gültig ist. +error.closedTradeWithNoDepositTx=Die Einzahlungstransaktion des geschlossenen Trades mit der Trade-ID {0} ist null.\n\nBitte starten Sie die Anwendung neu, um die Liste der geschlossenen Trades zu bereinigen. + +popup.warning.walletNotInitialized=Die Wallet ist noch nicht initialisiert +popup.warning.osxKeyLoggerWarning=Aufgrund strengerer Sicherheitsmaßnahmen ab MacOS 10.14 führt der Start einer Java-Anwendung (Bisq verwendet Java) zu einer Popup-Warnung in MacOS ("Bisq möchte Tastenanschläge von einer Anwendung empfangen").\n\nUm dieses Problem zu vermeiden, öffnen Sie bitte Ihre 'macOS-Einstellungen' und gehen Sie zu 'Sicherheit & Datenschutz' -> 'Datenschutz' -> 'Eingabe-Überwachung' und entfernen Sie 'Bisq' aus der Liste auf der rechten Seite.\n\nBisq wird auf eine neuere Java-Version upgraden, um dieses Problem zu vermeiden, sobald die technischen Einschränkungen (Java-Packager für die benötigte Java-Version wird noch nicht ausgeliefert) behoben sind. +popup.warning.wrongVersion=Sie verwenden vermutlich die falsche Bisq-Version für diesen Computer.\nDie Architektur Ihres Computers ist: {0}.\nDie installierten Bisq-Binärdateien sind: {1}.\nBitte fahren Sie Bisq herunter und installieren die korrekte Version ({2}). +popup.warning.incompatibleDB=Wir haben inkompatible Datenbankdateien entdeckt!\n\nDiese Datenbankdatei(en) ist (sind) nicht kompatibel mit unserer aktuellen Code-Basis:\n{0}\n\nWir haben ein Backup der beschädigten Datei(en) erstellt und die Standardwerte auf eine neue Datenbankversion angewendet.\n\nDas Backup befindet sich unter:\n{1}/db/backup_of_corrupted_data.\n\nBitte prüfen Sie, ob Sie die neueste Version von Bisq installiert haben.\nSie können sie herunterladen unter: [HYPERLINK:https://bisq.network/downloads].\n\nBitte starten Sie die Anwendung neu. +popup.warning.startupFailed.twoInstances=Bisq läuft bereits. Sie können nicht zwei Instanzen von Bisq laufen lassen. +popup.warning.tradePeriod.halfReached=Ihr Trade mit der ID {0} hat die Hälfte der maximal erlaubten Trade-Periode erreicht und ist immer noch nicht abgeschlossen.\n\nDie Trade-Periode endet am {1}\n\nBitte überprüfen Sie den Status Ihres Trades unter \"Portfolio/Offene Trades\" für weitere Informationen. +popup.warning.tradePeriod.ended=Ihr Trade mit der ID {0} hat die maximal zulässige Trade-Periode erreicht und ist nicht abgeschlossen.\n\nDie Trade-Periode endete am {1}.\n\nBitte überprüfen Sie Ihren Trade unter \"Portfolio/Offene Trades\", um den Mediator zu kontaktieren. +popup.warning.noTradingAccountSetup.headline=Sie haben kein Handelskonto eingerichtet +popup.warning.noTradingAccountSetup.msg=Sie müssen ein nationales Währung- oder Altcoin-Konto einrichten, bevor Sie ein Angebot erstellen können.\nMöchten Sie ein Konto einrichten? +popup.warning.noArbitratorsAvailable=Momentan sind keine Vermittler verfügbar. +popup.warning.noMediatorsAvailable=Es sind keine Mediatoren verfügbar. +popup.warning.notFullyConnected=Sie müssen warten, bis Sie vollständig mit dem Netzwerk verbunden sind.\nDas kann bis ungefähr 2 Minuten nach dem Start dauern. +popup.warning.notSufficientConnectionsToBtcNetwork=Sie müssen warten, bis Sie wenigstens {0} Verbindungen zum Bitcoinnetzwerk haben. +popup.warning.downloadNotComplete=Sie müssen warten bis der Download der fehlenden Bitcoinblöcke abgeschlossen ist. +popup.warning.chainNotSynced=Die Blockchain Größe der Bisq Wallet ist nicht korrekt synchronisiert. Wenn Sie kürzlich die Applikation geöffnet haben, warten Sie bitte bis ein Bitcoin Block veröffentlicht wurde.\n\nSie können die Blockchain Größe unter Einstellungen/Netzwerkinformationen finden. Wenn mehr als ein Block veröffentlicht wird und das Problem weiterhin bestehen sollte, wird es eventuell abgewürgt werden. Dann sollten Sie einen SPV Resync durchführen. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Sind Sie sicher, dass Sie das Angebot entfernen wollen?\nDie Erstellergebühr von {0} geht verloren, wenn Sie des Angebot entfernen. +popup.warning.tooLargePercentageValue=Es kann kein Prozentsatz von 100% oder mehr verwendet werden. +popup.warning.examplePercentageValue=Bitte geben sei einen Prozentsatz wie folgt ein \"5.4\" für 5.4% +popup.warning.noPriceFeedAvailable=Es ist kein Marktpreis für diese Währung verfügbar. Sie können keinen auf Prozent basierenden Preis verwenden.\nBitte wählen Sie den Festpreis. +popup.warning.sendMsgFailed=Das Senden der Nachricht an Ihren Handelspartner ist fehlgeschlagen.\nVersuchen Sie es bitte erneut und falls es weiter fehlschlägt, erstellen Sie bitte einen Fehlerbericht. +popup.warning.insufficientBtcFundsForBsqTx=Sie haben nicht genügend BTC-Gelder, um die Mining-Gebühr für diese Transaktion zu bezahlen.\nBitte finanzieren Sie Ihre BTC-Wallet.\nFehlende Gelder: {0} +popup.warning.bsqChangeBelowDustException=Diese Transaktion erzeugt eine BSQ-Wechselgeld-Ausgabe, die unter dem Dust-Limit (5.46 BSQ) liegt und vom Bitcoin-Netzwerk abgelehnt werden würde.\n\nSie müssen entweder einen höheren Betrag senden, um die Wechselgeld-Ausgabe zu vermeiden (z.B. indem Sie den Dust-Betrag zu Ihrem Sende-Betrag hinzufügen) oder mehr BSQ-Guthaben zu Ihrer Wallet hinzufügen, damit Sie vermeiden, eine Dust-Ausgabe zu generieren.\n\nDie Dust-Ausgabe ist {0}. +popup.warning.btcChangeBelowDustException=Diese Transaktion erzeugt eine Wechselgeld-Ausgabe, die unter dem Dust-Limit (546 Satoshi) liegt und vom Bitcoin-Netzwerk abgelehnt würde.\n\nSie müssen den Dust-Betrag zu Ihrem Sende-Betrag hinzufügen, um zu vermeiden, dass eine Dust-Ausgabe generiert wird.\n\nDie Dust-Ausgabe ist {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=Sie benötigen mehr BSQ um diese Transaktion durchzuführen - die letzten 5.46 BSQ in Ihrer Wallet können aufgrund der Dust-Limits im Bitcoin Protokoll nicht für die Trading-Gebühren verwendet werden.\n\nSie können entweder mehr BSQ kaufen oder die Trading-Gebühren in BTC bezahlen.\n\nFehlende Funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Ihre BSQ-Wallet hat keine ausreichenden Gelder, um die Handels-Gebühr in BSQ zu bezahlen. +popup.warning.messageTooLong=Ihre Nachricht überschreitet die maximal erlaubte Größe. Sende Sie diese in mehreren Teilen oder laden Sie sie in einen Dienst wie https://pastebin.com hoch. +popup.warning.lockedUpFunds=Sie haben gesperrtes Guthaben aus einem gescheiterten Trade.\nGesperrtes Guthaben: {0} \nEinzahlungs-Tx-Adresse: {1}\nTrade ID: {2}.\n\nBitte öffnen Sie ein Support-Ticket, indem Sie den Trade im Bildschirm "Offene Trades" auswählen und auf \"alt + o\" oder \"option + o\" drücken. + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignorieren und fortfahren + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=Preisrelais +popup.warning.seed=Seed +popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Bisq-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Bisq-Forum für weitere Informationen. +popup.warning.mandatoryUpdate.dao=Bitte aktualisieren Sie auf die neueste Bisq-Version. Ein obligatorisches Update wurde veröffentlicht, das die Bisq DAO und BSQ für alte Versionen deaktiviert. Bitte besuchen Sie das Bisq-Forum für weitere Informationen. +popup.warning.disable.dao=Der Bisq DAO und BSQ sind temporär deaktiviert. Bitte besuchen sie das Bisq-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 Bisq Entwickler. +popup.warning.burnBTC=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 BTC zum übertragen angesammelt haben. + +popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Bitcoin-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 Bisq-Support-Kanal des Bisq Keybase Teams. + +popup.warning.trade.txRejected.tradeFee=Trade-Gebühr +popup.warning.trade.txRejected.deposit=Kaution +popup.warning.trade.txRejected=Die {0} Transaktion für den Trade mit der ID {1} wurde vom Bitcoin-Netzwerk abgelehnt.\nTransaktions-ID={2}}\nDer Trade wurde in gescheiterte Trades verschoben.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie einen SPV Resync durch.\nFür weitere Hilfe wenden Sie sich bitte an den Bisq-Support-Kanal des Bisq Keybase Teams. + +popup.warning.openOfferWithInvalidMakerFeeTx=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} ist ungültig.\nTransaktions-ID={1}.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Bisq-Support-Kanal des Bisq Keybase Teams. + +popup.info.securityDepositInfo=Um sicherzustellen, dass beide Händler dem Handelsprotokoll folgen, müssen diese eine Kaution zahlen.\n\nDie Kaution bleibt in Ihrer lokalen Wallet, bis das Angebot von einem anderen Händler angenommen wurde.\nSie wird Ihnen zurückerstattet, nachdem der Handel erfolgreich abgeschlossen wurde.\n\nBitte beachten Sie, dass Sie die Anwendung laufen lassen müssen, wenn Sie ein offenes Angebot haben.\nWenn ein anderer Händler Ihr Angebot annehmen möchte ist es notwendig, dass Ihre Anwendung online ist und reagieren kann.\nStellen Sie sicher, dass Sie den Ruhezustand deaktiviert haben, da dieser Ihren Client vom Netzwerk trennen würde (Der Ruhezustand des Monitors ist kein Problem). + +popup.info.cashDepositInfo=Stellen Sie sicher, dass eine Bank-Filiale in Ihrer Nähe befindet, um die Bargeld Kaution zu zahlen.\nDie Bankkennung (BIC/SWIFT) der Bank des Verkäufers ist: {0}. +popup.info.cashDepositInfo.confirm=Ich bestätige, dass ich die Kaution zahlen kann +popup.info.shutDownWithOpenOffers=Bisq wird heruntergefahren, aber Sie haben offene Angebote verfügbar.\n\nDiese Angebote sind nach dem Herunterfahren nicht mehr verfügbar und werden erneut im P2P-Netzwerk veröffentlicht wenn Sie das nächste Mal Bisq starten.\n\nLassen Sie Bisq weiter laufen und stellen Sie sicher, dass Ihr Computer online bleibt, um Ihre Angebote verfügbar zu halten (z.B.: verhindern Sie den Standby-Modus... der Standby-Modus des Monitors stellt kein Problem dar). +popup.info.qubesOSSetupInfo=Es scheint so als ob Sie Bisq auf Qubes OS laufen haben.\n\nBitte stellen Sie sicher, dass Bisq qube nach unserem Setup Guide eingerichtet wurde: [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade von Version {0} auf Version {1} wird nicht unterstützt. Bitte nutzen Sie die aktuelle Bisq Version. +popup.warn.daoRequiresRestart=Es gab ein Problem mit der Synchronisierung des DAO-Zustands. Sie müssen die Anwendung neu starten, um das Problem zu beheben. + +popup.privateNotification.headline=Wichtige private Benachrichtigung! + +popup.securityRecommendation.headline=Wichtige Sicherheitsempfehlung +popup.securityRecommendation.msg=Wir würden Sie gerne daran erinnern, sich zu überlegen, den Passwortschutz Ihrer Wallet zu verwenden, falls Sie diesen noch nicht aktiviert haben.\n\nEs wird außerdem dringend empfohlen, dass Sie die Wallet-Seed-Wörter aufschreiben. Diese Seed-Wörter sind wie ein Master-Passwort zum Wiederherstellen ihrer Bitcoin-Wallet.\nIm \"Wallet-Seed\"-Abschnitt finden Sie weitere Informationen.\n\nZusätzlich sollten Sie ein Backup des ganzen Anwendungsdatenordners im \"Backup\"-Abschnitt erstellen. + +popup.bitcoinLocalhostNode.msg=Bisq hat einen Bitcoin Core Node entdeckt, der auf diesem Rechner (auf localhost) läuft.\n\nBitte stellen Sie sicher dass:\n- der Node vollständig synchronisiert ist, bevor Sie Bisq starten\n- Pruning deaktiviert ist ('prune=0' in bitcoin.conf)\n- Bloom-Filter aktiviert sind ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Anwendung wird heruntergefahren +popup.shutDownInProgress.msg=Das Herunterfahren der Anwendung kann einige Sekunden dauern.\nBitte unterbrechen Sie diesen Vorgang nicht. + +popup.attention.forTradeWithId=Der Handel mit der ID {0} benötigt Ihre Aufmerksamkeit +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 bringt eine gravierende Änderung der Trading Regeln zum Thema \"Zahlungsgrund\" bei Banküberweisungen mit sich. Bitte lassen Sie dieses Feld immer leer -- fügen Sie NICHT MEHR die Trade-ID als \"Zahlungsgrund\" an. + +popup.info.multiplePaymentAccounts.headline=Mehrere Zahlungskonten verfügbar +popup.info.multiplePaymentAccounts.msg=Für dieses Angebot stehen Ihnen mehrere Zahlungskonten zur Verfügung. Bitte stellen Sie sicher, dass Sie das richtige ausgewählt haben. + +popup.accountSigning.selectAccounts.headline=Zahlungskonten auswählen +popup.accountSigning.selectAccounts.description=Basierend auf der Zahlungsmethode und dem Zeitpunkt werden alle Zahlungskonten, die mit einem Konfliktfall verbunden sind, bei dem eine Auszahlung an den Käufer erfolgt ist, zur Unterzeichnung ausgewählt. +popup.accountSigning.selectAccounts.signAll=Alle Zahlungsmethoden unterzeichnen +popup.accountSigning.selectAccounts.datePicker=Zeitpunkt wählen, bis zu dem die Konten unterzeichnet werden sollen + +popup.accountSigning.confirmSelectedAccounts.headline=Ausgewählte Zahlungskonten bestätigen +popup.accountSigning.confirmSelectedAccounts.description=Basierend auf Ihren Eingaben werden {0} Zahlungskonten ausgewählt. +popup.accountSigning.confirmSelectedAccounts.button=Zahlungskonten bestätigen +popup.accountSigning.signAccounts.headline=Unterzeichnung der Zahlungskonten bestätigen +popup.accountSigning.signAccounts.description=Basierend auf Ihrer Auswahl werden {0} Zahlungskonten unterzeichnet. +popup.accountSigning.signAccounts.button=Zahlungskonten unterzeichnen +popup.accountSigning.signAccounts.ECKey=Privaten Vermittler-Schlüssel eingeben +popup.accountSigning.signAccounts.ECKey.error=Ungültiger Vermittler ECKey + +popup.accountSigning.success.headline=Glückwunsch +popup.accountSigning.success.description=Alle {0} Zahlungskonten wurden erfolgreich unterzeichnet! +popup.accountSigning.generalInformation=Den Unterzeichnungsstand all Ihrer Konten finden Sie im Abschnitt Konto.\n\nFür weitere Informationen besuchen Sie bitte [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Eines Ihrer Zahlungskonten wurde von einem Vermittler verifiziert und unterzeichnet. Wenn Sie mit diesem Konto traden, wird das Konto Ihres Trade-Partners nach einem erfolgreichen Trade automatisch unterzeichnet.\n\n{0} +popup.accountSigning.signedByPeer=Eines Ihrer Zahlungskonten wurde von einem Trade-Partner verifiziert und unterzeichnet. Ihr anfängliches Trade-Limit wird aufgehoben und Sie können in {0} Tagen andere Konten unterzeichnen.\n\n{1} +popup.accountSigning.peerLimitLifted=Das anfängliche Limit für eines Ihrer Konten wurde aufgehoben.\n\n{0} +popup.accountSigning.peerSigner=Eines Ihrer Konten ist reif genug, um andere Zahlungskonten zu unterzeichnen, und das anfängliche Limit für eines Ihrer Konten wurde aufgehoben.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Importiere unsigniertes Zeugnis für Kontoalter +popup.accountSigning.confirmSingleAccount.headline=Ausgewähltes Zeugnis für Kontoalter bestätigen +popup.accountSigning.confirmSingleAccount.selectedHash=Ausgewählter Zeugnis-Hash +popup.accountSigning.confirmSingleAccount.button=Zeugnis für Kontoalter signieren +popup.accountSigning.successSingleAccount.description=Zeugnis {0} wurde signiert +popup.accountSigning.successSingleAccount.success.headline=Erfolg + +popup.accountSigning.unsignedPubKeys.headline=Nicht unterzeichnete Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Pubkeys unterzeichnen +popup.accountSigning.unsignedPubKeys.signed=Pubkeys wurden unterzeichnet +popup.accountSigning.unsignedPubKeys.result.signed=Unterzeichnete Pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Unterzeichnung fehlgeschlagen + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Benachrichtigung zum Handel mit der ID {0} +notification.ticket.headline=Support-Ticket für den Handel mit der ID {0} +notification.trade.completed=Ihr Handel ist jetzt abgeschlossen und Sie können Ihre Gelder abheben. +notification.trade.accepted=Ihr Angebot wurde von einem BTC-{0} angenommen. +notification.trade.confirmed=Ihr Handel hat wenigstens eine Blockchain-Bestätigung.\nSie können die Zahlung nun beginnen. +notification.trade.paymentStarted=Der BTC-Käufer hat die Zahlung begonnen. +notification.trade.selectTrade=Handel wählen +notification.trade.peerOpenedDispute=Ihr Handelspartner hat ein/einen {0} geöffnet. +notification.trade.disputeClosed=Der/Das {0} wurde geschlossen. +notification.walletUpdate.headline=Update der Handels-Wallets +notification.walletUpdate.msg=Ihre Handels-Wallet ist ausreichend finanziert.\nBetrag: {0} +notification.takeOffer.walletUpdate.msg=Ihre Handels-Wallet wurde bereits durch eine früher versuchte Angebotsannahme ausreichend finanziert.\nBetrag: {0} +notification.tradeCompleted.headline=Handel abgeschlossen +notification.tradeCompleted.msg=Sie können Ihre Gelder jetzt auf eine externe Bitcoin-Wallet abheben oder an die Bisq-Wallet überweisen. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Anwendungsfenster anzeigen +systemTray.hide=Anwendungsfenster verstecken +systemTray.info=Informationen zu Bisq +systemTray.exit=Beenden +systemTray.tooltip=Bisq: Ein dezentrales Bitcoin-Tauschbörsen-Netzwerk + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Bitte stellen Sie sicher, dass die Mining-Gebühr für Ihre externe Wallet mindestens {0} satoshis/byte ist. Ansonsten wird die Transaktion des Trades nicht rechtzeitig bestätigt und der Trade wird in einem Konflikt enden. + +guiUtil.accountExport.savedToPath=Handelskonten in Verzeichnis gespeichert:\n{0} +guiUtil.accountExport.noAccountSetup=Sie haben kein Handelskonto zum Exportieren eingerichtet. +guiUtil.accountExport.selectPath=Verzeichnis auswählen zum {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Handelskonto mit der ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Wir haben das Handelskonto mit der Kennung {0} nicht importiert, da es schon existiert.\n +guiUtil.accountExport.exportFailed=Export in CSV ist aufgrund eines Fehlers fehlgeschlagen.\nFehler = {0} +guiUtil.accountExport.selectExportPath=Exportverzeichnis auswählen +guiUtil.accountImport.imported=Handelskonto aus Verzeichnis importiert:\n{0}\n\nImportierte Konten:\n{1} +guiUtil.accountImport.noAccountsFound=Keine exportierten Handelskonten im Verzeichnis gefunden: {0}.\nDateiname ist {1}." +guiUtil.openWebBrowser.warning=Es wird eine Website im Webbrowser Ihres Betriebssystems geöffnet.\nMöchten Sie die Website jetzt öffnen?\n\nWenn Sie nicht den \"Torbrowser\" als Standardbrowser verwenden, werden Sie sich über das Clearnet mit der Website verbinden.\n\nURL:\"{0}\" +guiUtil.openWebBrowser.doOpen=Website öffnen und nicht erneut fragen +guiUtil.openWebBrowser.copyUrl=URL kopieren und abbrechen +guiUtil.ofTradeAmount=vom Handelsbetrag +guiUtil.requiredMinimum=(erforderliches Minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Währung wählen +list.currency.showAll=Alle anzeigen +list.currency.editList=Währungsliste bearbeiten + +table.placeholder.noItems=Momentan sind keine {0} verfügbar +table.placeholder.noData=Momentan sind keine Daten verfügbar +table.placeholder.processingData=Datenverarbeitung... + + +peerInfoIcon.tooltip.tradePeer=Handelspartners +peerInfoIcon.tooltip.maker=Erstellers +peerInfoIcon.tooltip.trade.traded={0} Onion-Adresse: {1}\nSie haben schon {2} Mal(e) mit diesem Partner gehandelt.\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} Onion-Adresse: {1}\nSie haben noch nicht mit diesem Partner gehandelt.\n{2} +peerInfoIcon.tooltip.age=Zahlungskonto vor {0} erstellt. +peerInfoIcon.tooltip.unknownAge=Alter des Zahlungskontos unbekannt. + +tooltip.openPopupForDetails=Dialogfenster für Details öffnen +tooltip.invalidTradeState.warning=Dieser Trade hat einen ungültigen Status. Öffnen Sie das Detail-Fenster für weitere Informationen. +tooltip.openBlockchainForAddress=Externen Blockchain-Explorer für Adresse öffnen: {0} +tooltip.openBlockchainForTx=Externen Blockchain-Explorer für Transaktion öffnen: {0} + +confidence.unknown=Unbekannter Transaktionsstatus +confidence.seen=Von {0} Peer(s) gesehen / 0 Bestätigungen +confidence.confirmed=In {0} Blöcken(Block) bestätigt +confidence.invalid=Die Transaktion ist ungültig + +peerInfo.title=Peer-Infos +peerInfo.nrOfTrades=Anzahl abgeschlossener Trades +peerInfo.notTradedYet=Sie haben noch nicht mit diesem Nutzer gehandelt. +peerInfo.setTag=Markierung für diesen Peer setzen +peerInfo.age.noRisk=Alter des Zahlungskontos +peerInfo.age.chargeBackRisk=Zeit seit der Unterzeichnung +peerInfo.unknownAge=Alter unbekannt + +addressTextField.openWallet=Ihre Standard-Bitcoin-Wallet öffnen +addressTextField.copyToClipboard=Adresse in Zwischenablage kopieren +addressTextField.addressCopiedToClipboard=Die Adresse wurde in die Zwischenablage kopiert +addressTextField.openWallet.failed=Öffnen einer Bitcoin-Wallet-Standardanwendung ist fehlgeschlagen. Haben Sie möglicherweise keine installiert? + +peerInfoIcon.tooltip={0}\nMarkierung: {1} + +txIdTextField.copyIcon.tooltip=Transaktions-ID in Zwischenablage kopieren +txIdTextField.blockExplorerIcon.tooltip=Blockchain Explorer mit dieser Transaktions-ID öffnen +txIdTextField.missingTx.warning.tooltip=Erforderliche Transaktion fehlt + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Konto\" +navigation.account.walletSeed=\"Konto/Wallet-Seed\" +navigation.funds.availableForWithdrawal=\"Funds/Funds senden\" +navigation.portfolio.myOpenOffers=\"Portfolio/Meine offenen Angebote\" +navigation.portfolio.pending=\"Portfolio/Offene Trades\" +navigation.portfolio.closedTrades=\"Portfolio/Verlauf\" +navigation.funds.depositFunds=\"Gelder/Gelder erhalten\" +navigation.settings.preferences=\"Einstellungen/Voreinstellungen\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Gelder/Transaktionen\" +navigation.support=\"Support\" +navigation.dao.wallet.receive=\"DAO/BSQ-Wallet/Erhalten\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} Betrag{1} +formatter.makerTaker=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.fiat=Sie erstellen ein Angebot um {1} zu {0} +formatter.youAreCreatingAnOffer.altcoin=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) +formatter.asMaker={0} {1} als Ersteller +formatter.asTaker={0} {1} als Abnehmer + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin-Hauptnetzwerk +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin-Testnetzwerk +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin-Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin-DAO-Testnetzwerk (veraltet) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=Jahr +time.month=Monat +time.week=Woche +time.day=Tag +time.hour=Stunde +time.minute10=10 Minuten +time.hours=Stunden +time.days=Tage +time.1hour=1 Stunde +time.1day=1 Tag +time.minute=Minute +time.second=Sekunde +time.minutes=Minuten +time.seconds=Sekunden + + +password.enterPassword=Passwort eingeben +password.confirmPassword=Passwort bestätigen +password.tooLong=Das Passwort muss aus weniger als 500 Zeichen bestehen. +password.deriveKey=Schlüssel aus Passwort ableiten +password.walletDecrypted=Die Wallet wurde erfolgreich entschlüsselt und der Passwortschutz entfernt. +password.wrongPw=Sie haben das falsche Passwort eingegeben.\n\nVersuchen Sie bitte Ihr Passwort erneut einzugeben, wobei Sie dies vorsichtig auf Tipp- und Rechtschreibfehler überprüfen sollten. +password.walletEncrypted=Die Wallet wurde erfolgreich verschlüsselt und der Passwortschutz aktiviert. +password.walletEncryptionFailed=Wallet Passwort konnte nicht eingerichtet werden. Sie haben vielleicht Seed-Wörter importiert, die nicht mit der Wallet-Datenbank übereinstimmen. Bitte kontaktieren Sie die Entwickler auf Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=Die 2 eingegebenen Passwörter stimmen nicht überein. +password.forgotPassword=Passwort vergessen? +password.backupReminder=Beachten Sie, dass wenn Sie ein Passwort setzen, alle automatisch erstellten Backups der unverschlüsselten Wallet gelöscht werden.\n\nEs wird dringend empfohlen ein Backup des Anwendungsverzeichnisses zu erstellen und die Seed-Wörter aufzuschreiben, bevor Sie ein Passwort erstellen! +password.backupWasDone=Ich habe bereits ein Backup erstellt +password.setPassword=Passwort hinzufügen (Ich habe schon ein Backup erstellt) +password.makeBackup=Backup erstellen + +seed.seedWords=Seed-Wörter der Wallet +seed.enterSeedWords=Seed-Wörter der Wallet eingeben +seed.date=Wallets-Datum +seed.restore.title=Wallets aus Seed-Wörtern wiederherstellen +seed.restore=Wallets wiederherstellen +seed.creationDate=Erstellungsdatum +seed.warn.walletNotEmpty.msg=Ihre Bitcoin-Wallet ist nicht leer.\n\nSie müssen diese Wallet leeren, bevor Sie versuchen, eine ältere Wallet wiederherzustellen, da das Verwechseln von Wallets zu ungültigen Backups führen kann.\n\nBitte schließen Sie Ihre laufenden Trades ab, schließen Sie Ihre offenen Angebote und gehen Sie auf \"Gelder\", um Ihre Bitcoins zu versenden.\nSollten Sie nicht auf Ihre Bitcoins zugreifen können, können Sie das Notfallwerkzeug nutzen, um Ihre Wallet zu leeren.\nUm das Notfallwerkzeug zu öffnen, drücken Sie \"alt + e\" oder \"cmd/Strg + e\". +seed.warn.walletNotEmpty.restore=Ich möchte trotzdem wiederherstellen +seed.warn.walletNotEmpty.emptyWallet=Ich werde meine Wallets erst leeren +seed.warn.notEncryptedAnymore=Ihre Wallets sind verschlüsselt.\n\nNach einer Wiederherstellung werden die Wallets nicht mehr verschlüsselt sein und Sie werden ein neues Passwort festlegen müssen.\n\nMöchten Sie fortfahren? +seed.warn.walletDateEmpty=Da Sie kein Wallet-Datum angegeben haben, muss bisq die Blockchain ab 09. 10. 2013 (seit diesem Datum existiert BIP39) scannen.\n\nBIP39-Wallets wurden in bisq erstmals am 28.06.2017 (Release v0.5) eingeführt. Wenn Sie dieses Datum nehmen, können Sie etwas Zeit sparen.\n\nIdealerweise sollten Sie das Datum angeben, an dem Ihr Wallet Seed erstellt wurde.\n\n\nSind Sie sicher, dass Sie ohne Angabe eines Wallet-Datums fortfahren wollen? +seed.restore.success=Wallets mit den neuen Seed-Wörtern erfolgreich wiederhergestellt.\n\nSie müssen die Anwendung herunterfahren und neu starten. +seed.restore.error=Beim Wiederherstellen der Wallets mit den Seed-Wörtern ist ein Fehler aufgetreten.{0} +seed.restore.openOffers.warn=Sie haben noch offene Angebote die entfernt werden wenn Sie Ihre Seed Wörter wiederherstellen.\nSind Sie sicher, dass Sie fortfahren möchten? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Konto +payment.account.no=Kontonummer +payment.account.name=Kontoname +payment.account.userName=Benutzername +payment.account.phoneNr=Telefonnummer +payment.account.owner=Vollständiger Name des Kontoinhabers +payment.account.fullName=Vollständiger Name (vor, zweit, nach) +payment.account.state=Bundesland/Landkreis/Region +payment.account.city=Stadt +payment.bank.country=Land der Bank +payment.account.name.email=Vollständiger Name / E-Mail des Kontoinhabers +payment.account.name.emailAndHolderId=Vollständiger Name / E-Mail / {0} des Kontoinhabers +payment.bank.name=Bankname +payment.select.account=Kontotyp wählen +payment.select.region=Region wählen +payment.select.country=Land wählen +payment.select.bank.country=Land der Bank wählen +payment.foreign.currency=Sind Sie sicher, dass Sie eine Währung wählen wollen, die nicht der Standardwährung des Landes entspricht? +payment.restore.default=Nein, Standardwährung wiederherstellen +payment.email=E-Mail +payment.country=Land +payment.extras=Besondere Erfordernisse +payment.email.mobile=E-Mail oder Telefonnummer +payment.altcoin.address=Altcoin-Adresse +payment.altcoin.tradeInstantCheckbox=Handeln sie schnell (innerhalb 1 Stunde) mit diesem Altcoin +payment.altcoin.tradeInstant.popup=Für "Schnelles Handeln" müssen beide Handelspartner online sein, um den Handel innerhalb 1 Stunde abschließen zu können.\n\nFalls sie offene Angebote haben jedoch nicht verfügbar sind, deaktivieren sie bitte diese Angebote unter 'Portfolio'. +payment.altcoin=Altcoin +payment.select.altcoin=Altcoin wählen oder suchen +payment.secret=Geheimfrage +payment.answer=Antwort +payment.wallet=Wallets-ID +payment.amazon.site=Kaufe Geschenkkarte auf +payment.ask=Im Trader Chat fragen +payment.uphold.accountId=Nutzername oder Email oder Telefonnr. +payment.moneyBeam.accountId=E-Mail oder Telefonnummer +payment.venmo.venmoUserName=Venmo Nutzername +payment.popmoney.accountId=E-Mail oder Telefonnummer +payment.promptPay.promptPayId=Personalausweis/Steuernummer oder Telefonnr. +payment.supportedCurrencies=Unterstützte Währungen +payment.supportedCurrenciesForReceiver=Währungen für den Geldeingang +payment.limitations=Einschränkungen +payment.salt=Salt für Überprüfung des Kontoalters +payment.error.noHexSalt=Der Salt muss im HEX-Format sein.\nEs wird empfohlen das Salt-Feld zu bearbeiten, wenn Sie den Salt von einem alten Konto übertragen, um das Alter Ihres Kontos zu erhalten. Das Alter des Kontos wird durch den Konto-Salt und die Kontodaten (z.B. IBAN) verifiziert. +payment.accept.euro=Trades aus diesen Euroländern akzeptieren +payment.accept.nonEuro=Trades aus diesen Nicht-Euroländern akzeptieren +payment.accepted.countries=Akzeptierte Länder +payment.accepted.banks=Akzeptierte Banken (ID) +payment.mobile=Mobil-Tel.-Nr. +payment.postal.address=Postanschrift +payment.national.account.id.AR=CBU Nummer +shared.accountSigningState=Konto-Unterzeichnungsstatus + +#new +payment.altcoin.address.dyn={0} Adresse +payment.altcoin.receiver.address=Altcoin Adresse des Empfängers +payment.accountNr=Kontonummer +payment.emailOrMobile=E-Mail oder Telefonnummer +payment.useCustomAccountName=Spezifischen Kontonamen nutzen +payment.maxPeriod=Max. erlaubte Handelsdauer +payment.maxPeriodAndLimit=Max. Trade-Dauer : {0} / Max. Kaufen: {1} / Max. Verkauf: {2} / Kontoalter: {3} +payment.maxPeriodAndLimitCrypto=Max. Handelsdauer: {0} / Max. Handelsgrenze: {1} +payment.currencyWithSymbol=Währung: {0} +payment.nameOfAcceptedBank=Name der akzeptierten Bank +payment.addAcceptedBank=Akzeptierte Bank hinzufügen +payment.clearAcceptedBanks=Akzeptierte Banken entfernen +payment.bank.nameOptional=Bankname (optional) +payment.bankCode=Bankleitzahl +payment.bankId=Bankkennung (BIC/SWIFT) +payment.bankIdOptional=Bankkennung (BIC/SWIFT) (optional) +payment.branchNr=Filialnummer +payment.branchNrOptional=Filialnummer (optional) +payment.accountNrLabel=Kontonummer (IBAN) +payment.accountType=Kontotyp +payment.checking=Überprüfe +payment.savings=Ersparnisse +payment.personalId=Personalausweis +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq-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 Bisq 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 Bisq 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 BTC Käufer die MoneyGram Zulassungsnummer und ein Foto der Quittung per E-Mail an den BTC-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 BTC Käufer die MTCN (Tracking-Nummer) Foto der Quittung per E-Mail an den BTC-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 BTC-Käufer dem BTC-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 BTC-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 BTC-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 Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut benötigt den "Benutzernamen" als Account ID und nicht die Telefonnummer oder E-Mail, wie es in der Vergangenheit war. +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.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 + +payment.usPostalMoneyOrder.info=Der Handel auf Bisq unter Verwendung von US Postal Money Orders (USPMO) setzt voraus, dass Sie Folgendes verstehen:\n\n- Der BTC-Käufer muss den Namen des BTC-Verkäufers sowohl in das Feld des Zahlers als auch in das Feld des Zahlungsempfängers eintragen und vor dem Versand ein hochauflösendes Foto des USPMO und des Umschlags mit dem Tracking-Nachweis machen.\n- BTC-Käufer müssen den USPMO mit Zustellbenachrichtigung an den BTC-Verkäufer schicken.\n\nFür den Fall, dass eine Mediation erforderlich ist oder es zu einem Handelskonflikt kommt, müssen Sie die Fotos zusammen mit der USPMO-Seriennummer, der Nummer des Postamtes und dem Dollarbetrag an den Bisq-Vermittler oder Rückerstattungsbeauftragten schicken, damit dieser die Angaben auf der Website der US-Post überprüfen kann.\n\nWenn Sie dem Vermittler oder der Schiedsperson die erforderlichen Informationen nicht zur Verfügung stellen, führt dies dazu, dass der Konflikt zu Ihrem Nachteil entschieden wird.\n\nIn allen Konfliktfällen trägt der USPMO-Absender 100% der Verantwortung für die Bereitstellung von Beweisen/Nachweisen für den Vermittler oder die Schiedsperson.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie bitte nicht auf Bisq unter Verwendung von USPMO. + +payment.cashByMail.info=Beim Bargeld per Post Handel auf Bisq, müssen Sie folgendes verstehen: \n\n● Der BTC Käufer sollte das Bargeld in einen manipulationssicheren Geldbeutel verpacken.\n● Der BTC Käufer sollte den Verpackungsprozess des Bargeldes filmen oder mit hochauflösenden Fotos dokumentieren. Die Adresse & Tracking Nummer sollen auf der Packung bereits angebracht sein.\n● Der BTC Käufer sollte das Bargeld mit Versandbestätigung und ausreichender Versicherung an den Verkäufer senden.\n● Der BTC Verkäufer sollte die Öffnung des Bargeld-Paketes so filmen, dass die Tracking Nummer des Senders im Video sichtbar ist.\n● Der Ersteller des Angebots muss spezielle Bedingungen oder Abmachungen im 'Zusätzliche Informationen'-Feld des Zahlungskontos eintragen.\n● Der Annehmer des Angebots akzeptiert die Bedingungen und Abmachungen des Ersteller des Angebots durch die Annahme des Angebots.\n\nBargeld per Post Trades benötigen die Ehrlichkeit und das Vertrauen beider Peers.\n\n● Bargeld per Post Trades haben weniger verifizierbare Handlungen als andere FIAT Trades. Das macht die Abwicklung von Konflikten viel schwerer.\nCBM trades put the onus to act honestly squarely on both peers.\n● Versuchen Sie den Konflikt mit dem Handelspartner direkt über den Trader Chat zu lösen. Das ist der vielversprechendste Weg Konflikte bei solchen Trades zu lösen.\n● Mediatoren können den Fall untersuchen und einen Vorschlag machen aber es ist nicht garantiert, dass sie wirklich helfen können.\n● Wenn ein Mediator eingeschalten wurde und beide Peers den Vorschlag des Mediators ablehnen, werden die Gelder beider Peers zu einer Bisq Spendenadresse [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction] gesendet und der Trade ist somit abgeschlossen.\n● Wenn ein Trader den Vorschlag des Mediators ablehnt und eine Arbitration eröffnet, kann es zu einem Verlust der Trading- und der Deposit-Funds kommen.\n● Arbitratoren werden ihre Entscheidungen basierend auf den zur Verfügung gestellten Beweisen treffen. Deshalb sollten Sie die Abläufe von oben befolgen und dokumentieren um im Falle eines Konflikts Beweise zu haben. Bei Bargeld per Post Trades ist die Entscheidung des Arbitrators entscheidend.\n● Rückerstattungsanfragen, von verlorenen Funds durch einen Bargeld per Post Trade, über die DAO werden nicht berücksichtigt.\n\nUm sicherzustellen, dass Sie die Anforderungen bei Bargeld per Post Trades verstanden haben lesen Sie: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\nWenn Sie diese Anforderungen nicht verstehen, nutzen Sie die Bisq-Zahlungsmethode Bargeld per Post nicht. + +payment.cashByMail.contact=Kontaktinformationen +payment.cashByMail.contact.prompt=Name oder Pseudonym Umschlag sollten adressiert werden an +payment.f2f.contact=Kontaktinformationen +payment.f2f.contact.prompt=Wie möchten Sie vom Trading-Peer kontaktiert werden? (E-Mail Adresse, Telefonnummer,...) +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.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 Bitcoin 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.bisq.network/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.japan.bank=Bank +payment.japan.branch=Filiale +payment.japan.account=Konto +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 BTC-Verkäufer senden. \n\nBisq zeigt die E-Mail-Adresse oder Telefonnummer des BTC-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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Inlandsüberweisung +SAME_BANK=Überweisung mit derselben Bank +SPECIFIC_BANKS=Überweisungen mit bestimmten Banken +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=Cash Deposit +CASH_BY_MAIL=Bargeld per Post +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Angesicht zu Angesicht (persönlich) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australische PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Inlandsbanken +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Gleiche Bank +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Spezifische Banken +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Cash Deposit +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=BargeldPerPost +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=A2A +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Echtzeitzahlungen +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon Gift-Karte +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins schnell + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Echtzeit +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon Gift-Karte +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins schnell + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Eine leere Eingabe ist nicht erlaubt. +validation.NaN=Die Eingabe ist keine gültige Zahl. +validation.notAnInteger=Eingabe ist keine ganze Zahl. +validation.zero=Die Eingabe von 0 ist nicht erlaubt. +validation.negative=Ein negativer Wert ist nicht erlaubt. +validation.fiat.toSmall=Eingaben kleiner als der minimal mögliche Betrag sind nicht erlaubt. +validation.fiat.toLarge=Eingaben größer als der maximal mögliche Betrag sind nicht erlaubt. +validation.btc.fraction=Input wird einem Bitcoin Wert von weniger als 1 satoshi entsprechen +validation.btc.toLarge=Eingaben größer als {0} sind nicht erlaubt. +validation.btc.toSmall=Eingabe kleiner als {0} ist nicht erlaubt. +validation.passwordTooShort=Das Passwort das Sie eingegeben haben ist zu kurz. Es muss mindestens 8 Zeichen enthalten. +validation.passwordTooLong=Das eingegebene Passwort ist zu lang. Es darf nicht aus mehr als 50 Zeichen bestehen. +validation.sortCodeNumber={0} muss aus {1} Zahlen bestehen. +validation.sortCodeChars={0} muss aus {1} Zeichen bestehen. +validation.bankIdNumber={0} muss aus {1} Zahlen bestehen. +validation.accountNr=Die Kontonummer muss aus {0} Zahlen bestehen. +validation.accountNrChars=Die Kontonummer muss aus {0} Zeichen bestehen. +validation.btc.invalidAddress=Die Adresse ist nicht korrekt. Bitte überprüfen Sie das Adressformat. +validation.integerOnly=Bitte nur ganze Zahlen eingeben. +validation.inputError=Ihre Eingabe hat einen Fehler verursacht:\n{0} +validation.bsq.insufficientBalance=Ihr verfügbares Guthaben ist {0}. +validation.btc.exceedsMaxTradeLimit=Ihr Handelslimit ist {0}. +validation.bsq.amountBelowMinAmount=Min. Betrag ist {0} +validation.nationalAccountId={0} muss aus {1} Zahlen bestehen. + +#new +validation.invalidInput=Ungültige Eingabe: {0} +validation.accountNrFormat=Die Kontonummer muss folgendes Format haben: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Die Adressvalidierung ist fehlgeschlagen, da diese nicht mit der Struktur einer {0}-Adresse übereinstimmt. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=Die LTZ Adresse muss mit L beginnen. Adressen die mit z beginnen werden nicht unterstützt. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC Adressen müssen mit t beginnen. Adressen die mit z beginnen werden nicht unterstützt. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Die Adresse ist keine gültige {0}-Adresse! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native Segwit-Adressen (die mit 'lq' beginnen) werden nicht unterstützt. +validation.bic.invalidLength=Eingabelänge muss 8 oder 11 betragen +validation.bic.letters=Bank- und Ländercode müssen aus Buchstaben bestehen +validation.bic.invalidLocationCode=Der BIC enthält einen ungültigen Standort-Code +validation.bic.invalidBranchCode=Der BIC enthält eine ungültige Filialennummer +validation.bic.sepaRevolutBic=Revolut SEPA Konten werden nicht unterstüzt. +validation.btc.invalidFormat=Ungültiges Bitcoin Adressformat. +validation.bsq.invalidFormat=Ungültiges BSQ Adressformat. +validation.email.invalidAddress=Ungültige Adresse +validation.iban.invalidCountryCode=Der Ländercode ist ungültig +validation.iban.checkSumNotNumeric=Die Prüfsumme muss numerisch sein +validation.iban.nonNumericChars=Nicht-alphanumerisches Zeichen entdeckt +validation.iban.checkSumInvalid=Die IBAN-Prüfsumme ist ungültig +validation.iban.invalidLength=Die Zahl muss zwischen 15 und 34 Zeichen lang sein. +validation.interacETransfer.invalidAreaCode=Nicht-kanadische Postleitzahl +validation.interacETransfer.invalidPhone=Bitte geben Sie eine gültige 11-stellige Telefonnummer (ex:1-123-456-7890) oder eine E-Mail Adresse an +validation.interacETransfer.invalidQuestion=Nur Buchstaben, Zahlen, Leerzeichen und/oder die Symbole _ , . ? - sind erlaubt +validation.interacETransfer.invalidAnswer=Muss ein Wort mit Buchstaben, Zahlen und/oder dem Symbol - sein +validation.inputTooLarge=Eingabe darf nicht größer als {0} sein +validation.inputTooSmall=Eingabe muss größer als {0} sein +validation.inputToBeAtLeast=Eingabe muss mindestens {0} sein +validation.amountBelowDust=Menge unter dem Dust Limit von {0} Satoshi ist nicht erlaubt. +validation.length=Die Länge muss zwischen {0} und {1} sein +validation.fixedLength=Länge muss {0} betragen +validation.pattern=Die Eingabe muss im Format {0} sein +validation.noHexString=Die Eingabe ist nicht im HEX-Format. +validation.advancedCash.invalidFormat=Gültige E-Mail-Adresse oder Wallets ID vom Format "X000000000000" benötigt +validation.invalidUrl=Dies ist keine gültige URL +validation.mustBeDifferent=Ihre Eingabe muss vom aktuellen Wert abweichen +validation.cannotBeChanged=Parameter kann nicht geändert werden +validation.numberFormatException=Zahlenformat Ausnahme {0} +validation.mustNotBeNegative=Eingabe darf nicht negativ sein +validation.phone.missingCountryCode=Es wird eine zweistellige Ländervorwahl benötigt, um die Telefonnummer zu bestätigen +validation.phone.invalidCharacters=Telefonnummer {0} enthält ungültige Zeichen +validation.phone.insufficientDigits=Das ist keine gültige Telefonnummer. Sie habe nicht genügend Stellen angegeben. +validation.phone.tooManyDigits=Es sind zu viele Ziffern in {0} um eine gültige Telefonnummer zu sein. +validation.phone.invalidDialingCode=Die Ländervorwahl in der Nummer {0} ist für das Land {1} ungültig. Die richtige Vorwahl ist {2}. +validation.invalidAddressList=Muss eine kommagetrennte Liste der gültigen Adressen sein diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties new file mode 100644 index 0000000000..110870e948 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Leer más +shared.openHelp=Abrir ayuda +shared.warning=Advertencia +shared.close=Cerrar +shared.cancel=Cancelar +shared.ok=Ok +shared.yes=Sí +shared.no=No +shared.iUnderstand=Entendido +shared.na=No disponible +shared.shutDown=Apagar +shared.reportBug=Reportar error de software en Github +shared.buyBitcoin=Comprar bitcoin +shared.sellBitcoin=Vender bitcoin +shared.buyCurrency=Comprar {0} +shared.sellCurrency=Vender {0} +shared.buyingBTCWith=Comprando BTC con {0} +shared.sellingBTCFor=Vendiendo BTC por {0} +shared.buyingCurrency=comprando {0} (Vendiendo BTC) +shared.sellingCurrency=Vendiendo {0} (comprando BTC) +shared.buy=comprar +shared.sell=vender +shared.buying=comprando +shared.selling=vendiendo +shared.P2P=P2P +shared.oneOffer=oferta +shared.multipleOffers=ofertas +shared.Offer=Oferta +shared.offerVolumeCode={0} Volumen de oferta +shared.openOffers=ofertas abiertas +shared.trade=intercambio +shared.trades=intercambios +shared.openTrades=intercambios abiertos +shared.dateTime=Fecha/Hora +shared.price=Precio +shared.priceWithCur=Precio en {0} +shared.priceInCurForCur=precio en {0} por 1 {1} +shared.fixedPriceInCurForCur=Precio fijo en {0} por 1 {1} +shared.amount=Cantidad +shared.txFee=Tasa de transacción +shared.tradeFee=Tasa de intercambio +shared.buyerSecurityDeposit=Depósito de comprador +shared.sellerSecurityDeposit=Depósito de vendedor +shared.amountWithCur=Cantidad en {0} +shared.volumeWithCur=Volumen en {0} +shared.currency=Moneda +shared.market=Mercado +shared.deviation=Desviación +shared.paymentMethod=Método de pago +shared.tradeCurrency=Moneda de intercambio +shared.offerType=Tipo de oferta +shared.details=Detalles +shared.address=Dirección +shared.balanceWithCur=Saldo en {0} +shared.utxo=Output de transacción no gastado +shared.txId=ID de la transacción +shared.confirmations=Confirmaciones +shared.revert=Revertir Tx +shared.select=Seleccionar +shared.usage=Uso +shared.state=Estado +shared.tradeId=ID de intercambio +shared.offerId=ID de oferta +shared.bankName=Nombre del banco +shared.acceptedBanks=Bancos aceptados +shared.amountMinMax=Cantidad (min-max) +shared.amountHelp=Si una oferta tiene una cantidad mínima y máxima establecida, entonces puede intercambiar cualquier cantidad dentro de este rango. +shared.remove=Eliminar +shared.goTo=Ir a {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Eliminar oferta +shared.dontRemoveOffer=No eliminar oferta +shared.editOffer=Editar oferta +shared.openLargeQRWindow=Abrir código QR en ventana grande +shared.tradingAccount=Cuenta de intercambio +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 Bisq +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 +shared.aboveInPercent=% por encima del precio de mercado +shared.enterPercentageValue=Introduzca valor % +shared.OR=ó +shared.notEnoughFunds=No tiene suficientes fondos en su monedero bisq para esta transacción. Necesita {0} pero solo tiene {1} disponibles.\n\nPor favor deposite desde un monedero externo o agregue fondos a su monedero Bisq en Fondos > Recibir Fondos. +shared.waitingForFunds=Esperando fondos... +shared.depositTransactionId=ID de transacción del depósito +shared.TheBTCBuyer=El comprador de BTC +shared.You=Usted +shared.sendingConfirmation=Enviando confirmación... +shared.sendingConfirmationAgain=Por favor envíe confirmación de nuevo +shared.exportCSV=Exportar a csv +shared.exportJSON=Exportar a JSON +shared.summary=Mostrar resumen +shared.noDateAvailable=Sin fecha disponible +shared.noDetailsAvailable=Sin detalles disponibles +shared.notUsedYet=Sin usar aún +shared.date=Fecha +shared.sendFundsDetailsWithFee=Enviando: {0}\nDesde la dirección: {1}\nA la dirección receptora: {2}.\nLa comisión requerida de transacción es: {3} ({4} Satoshis/vbyte)\nTamaño de la transacción: {5} vKb\n\nEl receptor recibirá: {6}\n\nSeguro que quiere retirar esta cantidad? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq 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 Bitcoin). 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 +shared.language=Idioma +shared.country=País +shared.applyAndShutDown=Aplicar y cerrar +shared.selectPaymentMethod=Seleccionar método de pago +shared.accountNameAlreadyUsed=Ese nombre de cuenta ya está en uso para otra cuenta guardada.\nPor favor use otro nombre. +shared.askConfirmDeleteAccount=¿Realmente quiere borrar la cuenta seleccionada? +shared.cannotDeleteAccount=No puede borrar esta cuenta porque está siendo usada en una oferta abierta (o en un intercambio abierto). +shared.noAccountsSetupYet=Aún no hay cuentas configuradas. +shared.manageAccounts=Gestionar cuentas +shared.addNewAccount=Añadir una nueva cuenta +shared.ExportAccounts=Exportar cuentas +shared.importAccounts=Importar cuentas +shared.createNewAccount=Crear nueva cuenta +shared.saveNewAccount=Guardar nueva cuenta +shared.selectedAccount=Cuenta seleccionada +shared.deleteAccount=Borrar cuenta +shared.errorMessageInline=\nMensaje de error: {0} +shared.errorMessage=Mensaje de error +shared.information=Información +shared.name=Nombre +shared.id=ID +shared.dashboard=Panel de control +shared.accept=Aceptar +shared.balance=Saldo +shared.save=Guardar +shared.onionAddress=Dirección onion +shared.supportTicket=Ticket de soporte +shared.dispute=Disputa +shared.mediationCase=caso de mediación +shared.seller=vendedor +shared.buyer=comprador +shared.allEuroCountries=Todos los países Euro +shared.acceptedTakerCountries=Países aceptados como tomador +shared.tradePrice=Precio de intercambio +shared.tradeAmount=Cantidad de intercambio +shared.tradeVolume=Volumen de intercambio +shared.invalidKey=La clave que ha introducido no es correcta. +shared.enterPrivKey=Introducir clave privada para desbloquear +shared.makerFeeTxId=ID de transacción de comisión del creador +shared.takerFeeTxId=ID de transacción de comisión del tomador +shared.payoutTxId=ID de transacción de pago +shared.contractAsJson=Contrato en formato JSON +shared.viewContractAsJson=Ver contrato en formato JSON +shared.contract.title=Contrato de intercambio con ID: {0} +shared.paymentDetails=Detalles de pago BTC {0} +shared.securityDeposit=Depósito de seguridad +shared.yourSecurityDeposit=Su depósito de seguridad +shared.contract=Contrato +shared.messageArrived=Mensaje recibido. +shared.messageStoredInMailbox=Mensaje almacenado en buzón. +shared.messageSendingFailed=Envío de mensaje fallido. Error: {0} +shared.unlock=Desbloquear +shared.toReceive=a recibir +shared.toSpend=a gastar +shared.btcAmount=Cantidad BTC +shared.yourLanguage=Sus idiomas +shared.addLanguage=Añadir idioma +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.makerTxFee=Creador: {0} +shared.takerTxFee=Tomador: {0} +shared.iConfirm=Confirmo +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Abrir {0} +shared.fiat=Fiat +shared.crypto=Cripto +shared.all=Todos +shared.edit=Editar +shared.advancedOptions=Opciones avanzadas +shared.interval=Intervalo +shared.actions=Acciones +shared.buyerUpperCase=Comprador +shared.sellerUpperCase=Vendedor +shared.new=NUEVO +shared.blindVoteTxId=ID de la transacción de voto secreto +shared.proposal=Propuesta +shared.votes=Votos +shared.learnMore=Aprender más +shared.dismiss=Descartar +shared.selectedArbitrator=Árbitro seleccionado +shared.selectedMediator=Mediador seleccionado +shared.selectedRefundAgent=Árbitro seleccionado +shared.mediator=Mediador +shared.arbitrator=Árbitro +shared.refundAgent=Árbitro +shared.refundAgentForSupportStaff=Agente de devolución de fondos +shared.delayedPayoutTxId=ID de transacción del pago demorado +shared.delayedPayoutTxReceiverAddress=Transacción de pago demorado enviada a +shared.unconfirmedTransactionsLimitReached=Tiene demasiadas transacciones no confirmadas en este momento. Por favor, inténtelo de nuevo más tarde. +shared.numItemsLabel=Número de entradas: {0} +shared.filter=Filtro +shared.enabled=Habilitado + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Mercado +mainView.menu.buyBtc=Comprar BTC +mainView.menu.sellBtc=Vender BTC +mainView.menu.portfolio=Portafolio +mainView.menu.funds=Fondos +mainView.menu.support=Soporte +mainView.menu.settings=Configuración +mainView.menu.account=Cuenta +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Precio de mercado por {0} +mainView.marketPrice.bisqInternalPrice=Precio del último intercambio en Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=No existe un precio de mercado disponible proveniente de fuentes externas.\nEl precio mostrado es el último precio de intercambio en Bisq para esa moneda. +mainView.marketPrice.tooltip=Precio de mercado ofrecido por {0}{1}\nÚltima actualización: {2}\nURL del nodo proveedor: {3} +mainView.balance.available=Saldo disponible +mainView.balance.reserved=Reservado en ofertas +mainView.balance.locked=Bloqueado en intercambios +mainView.balance.reserved.short=Reservado +mainView.balance.locked.short=Bloqueado + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/Tasas actuales: {0} sat/vB +mainView.footer.btcInfo.initializing=Conectando a la red Bitcoin +mainView.footer.bsqInfo.synchronizing=/ Sincronizando DAO +mainView.footer.btcInfo.synchronizingWith=Sincronizando con {0} en el bloque: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Sincronizado con {0} en el bloque {1} +mainView.footer.btcInfo.connectingTo=Conectando a +mainView.footer.btcInfo.connectionFailed=Conexión fallida a +mainView.footer.p2pInfo=Pares de Bitcoin: {0} / Pares de la red de Bisq: {1} +mainView.footer.daoFullNode=Nodo completo DAO + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando a la red Tor... +mainView.bootstrapState.torNodeCreated=(2/4) Nodo Tor creado +mainView.bootstrapState.hiddenServicePublished=(3/4) Servicio oculto publicado +mainView.bootstrapState.initialDataReceived=(4/4) Datos iniciales recibidos + +mainView.bootstrapWarning.noSeedNodesAvailable=No hay nodos de siembra disponibles +mainView.bootstrapWarning.noNodesAvailable=No hay nodos de sembrado y pares disponibles +mainView.bootstrapWarning.bootstrappingToP2PFailed=Fallo al conectarse a la red Bisq en el arranque + +mainView.p2pNetworkWarnMsg.noNodesAvailable=No hay nodos de sembrado o puntos de red persistentes para los datos requeridos.\nPor favor, compruebe su conexión a Internet o intente reiniciar la aplicación. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Fallo conectándose a la red Bisq (error reportado: {0}).\nPor favor, compruebe su conexión a internet o pruebe reiniciando la aplicación. + +mainView.walletServiceErrorMsg.timeout=Error al conectar a la red Bitcoin en el límite de tiempo establecido. +mainView.walletServiceErrorMsg.connectionError=La conexión a la red Bitcoin falló por un error: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Se rechazó una transacción desde la red.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Perdió la conexión a todos los {0} usuarios de red.\nTal vez se ha interrumpido su conexión a Internet o su computadora estaba en modo suspendido. +mainView.networkWarning.localhostBitcoinLost=Perdió la conexión al nodo Bitcoin localhost.\nPor favor reinicie la aplicación Bisq para conectarse a otros nodos Bitcoin o reinice el nodo Bitcoin localhost. +mainView.version.update=(Actualización disponible) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Libro de ofertas +market.tabs.spreadCurrency=Ofertas según moneda +market.tabs.spreadPayment=Ofertas según método de pago +market.tabs.trades=Intercambios + +# OfferBookChartView +market.offerBook.buyAltcoin=Comprar {0} (vender {1}) +market.offerBook.sellAltcoin=Vender {0} (comprar {1}) +market.offerBook.buyWithFiat=Comprar {0} +market.offerBook.sellWithFiat=Vender {0} +market.offerBook.sellOffersHeaderLabel=Vender {0} a +market.offerBook.buyOffersHeaderLabel=Comprar {0} de +market.offerBook.buy=Quiero comprar bitcoin +market.offerBook.sell=Quiero vender bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Todas las ofertas ({0}) +market.spread.numberOfBuyOffersColumn=Comprar BTC ({0}) +market.spread.numberOfSellOffersColumn=Vender BTC ({0}) +market.spread.totalAmountColumn=Total BTC ({0}) +market.spread.spreadColumn=Diferencial +market.spread.expanded=Vista expandida + +# TradesChartsView +market.trades.nrOfTrades=Intercambios: {0} +market.trades.tooltip.volumeBar=Volumen: {0} / {1}\nNúmero de intercambios: {2}\nFecha: {3} +market.trades.tooltip.candle.open=Apertura: +market.trades.tooltip.candle.close=Cierre: +market.trades.tooltip.candle.high=Máximo: +market.trades.tooltip.candle.low=Mínimo: +market.trades.tooltip.candle.average=Media: +market.trades.tooltip.candle.median=Mediana: +market.trades.tooltip.candle.date=Fecha: +market.trades.showVolumeInUSD=Mostrar volumen en USD + +#################################################################### +# OfferView +#################################################################### + +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.trader=Trader +offerbook.offerersBankId=ID del banco del creador (BIC/SWIFT): {0} +offerbook.offerersBankName=Nombre del banco del creador: {0} +offerbook.offerersBankSeat=País de establecimiento del banco del creador: {0} +offerbook.offerersAcceptedBankSeatsEuro=País de establecimiento del banco aceptado (tomador): Todos los países Euro +offerbook.offerersAcceptedBankSeats=Países de sede de banco aceptados (tomador):\n {0} +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.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 +offerbook.timeSinceSigning.info.peer=firmado por un par, esperando %d días para aumentar límites +offerbook.timeSinceSigning.info.peerLimitLifted=firmador por un par y los límites se elevaron +offerbook.timeSinceSigning.info.signer=firmado por un par y puede firmar cuentas de pares (límites elevados) +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.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 +offerbook.timeSinceSigning.notSigned.ageDays={0} días +offerbook.timeSinceSigning.notSigned.noNeed=No disponible +shared.notSigned=Esta cuenta no ha sido firmada aún y fue creada hace {0} días +shared.notSigned.noNeed=Este tipo de cuenta no necesita firmado +shared.notSigned.noNeedDays=Este tipo de cuenta no necesita firmado y fue creada hace {0} días +shared.notSigned.noNeedAlts=Las cuentas de altcoin no necesitan firmado o edad + +offerbook.nrOffers=Número de ofertas: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Depósito en BTC (%) +offerbook.deposit.help=Depósito pagado por cada comerciante para garantizar el intercambio. Será devuelto al acabar el intercambio. + +offerbook.createOfferToBuy=Crear nueva oferta para comprar {0} +offerbook.createOfferToSell=Crear nueva oferta para vender {0} +offerbook.createOfferToBuy.withFiat=Crear nueva oferta para comprar {0} con {1} +offerbook.createOfferToSell.forFiat=Crear nueva oferta para vender {0} por {1} +offerbook.createOfferToBuy.withCrypto=Crear nueva oferta para vender {0} (comprar {1}) +offerbook.createOfferToSell.forCrypto=Crear nueva oferta para comprar {0} (vender {1}) + +offerbook.takeOfferButton.tooltip=Tomar oferta {0} +offerbook.yesCreateOffer=Sí, crear oferta +offerbook.setupNewAccount=Configurar una nueva cuenta de intercambio +offerbook.removeOffer.success=Oferta eliminada con éxito. +offerbook.removeOffer.failed=Fallo en la eliminación de oferta:\n{0} +offerbook.deactivateOffer.failed=Error desactivando la oferta:\n{0} +offerbook.activateOffer.failed=Fallo en la publicación de la oferta:\n{0} +offerbook.withdrawFundsHint=Puede retirar los fondos pagados desde la pantalla {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=No hay cuenta de intercambio para la moneda seleccionada +offerbook.warning.noTradingAccountForCurrency.msg=No tiene una cuenta de pago para la moneda seleccionada.\n¿Desea crear una oferta con otra moneda en su lugar? +offerbook.warning.noMatchingAccount.headline=No La cuenta de pago no concuerda. +offerbook.warning.noMatchingAccount.msg=Esta oferta usa un método de pago que no tiene configurado.\n\n¿Quiere configurar un nuevo método de pago ahora? + +offerbook.warning.counterpartyTradeRestrictions=Esta oferta no puede tomarse debido a restricciones de intercambio de la contraparte + +offerbook.warning.newVersionAnnouncement=Con esta versión de software, los pares de intercambio pueden verificar y firmar entre sí sus cuentas de pago para crear una red de cuentas de pago de confianza.\n\nDespués de intercambiar con éxito con un par con una cuenta de pago verificada, su cuenta de pago será firmada y los límites de intercambio se elevarán después de un cierto intervalo de tiempo (la duración de este intervalo depende del método de verificación).\n\nPara más información acerca del firmado de cuentas, por favor vea la documentación en [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=El monto de intercambio permitido está limitado a {0} debido a restricciones de seguridad basadas en los siguientes criterios:\n- La cuenta del comprador no ha sido firmada por un árbitro o par\n- El tiempo desde el firmado de la cuenta del comprador no es de al menos 30 días.\n- el método de pago para esta oferta se considera riesgoso para devoluciones de cargo\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=El monto de intercambio permitido está limitado a {0} debido a restricciones de seguridad basadas en los siguientes criterios:\n- Su cuenta de pago no ha sido firmada por un árbitro o par\n- El tiempo desde el firmado de su cuenta no es de al menos 30 días\n- El método de pago para esta oferta se considera riesgoso para devoluciones de cargo\n\n{1} + +offerbook.warning.wrongTradeProtocol=Esta oferta requiere un protocolo de intercambio diferente al utilizado en su versión del software.\n\nPor favor, compruebe que tiene instalada la última versión del software, o de otra forma el usuario que creó la oferta ha utilizado una versión más antigua que la suya.\n\nLos usuarios no pueden realizar transacciones con una versión de protocolo de intercambio incompatible. +offerbook.warning.userIgnored=Ha añadido esta dirección onion a la lista de ignorados. +offerbook.warning.offerBlocked=Esta oferta ha sido bloqueada por los desarrolladores de Bisq.\nProbablemente existe un error de software desatendido que causa problemas al tomar esta oferta. +offerbook.warning.currencyBanned=La moneda utilizada en esta oferta fue bloqueada por los desarrolladores de Bisq.\nPor favor visite el Forum de Bisq para más información. +offerbook.warning.paymentMethodBanned=El método de pago utilizado en esta oferta fue bloqueado por los desarrolladores de Bisq.\nPor favor visite el Forum Bisq para más información. +offerbook.warning.nodeBlocked=La dirección onion de este comerciante ha sido bloqueada por los desarrolladores de Bisq.\nProbablemente existe un error de software desatendido que causa problemas al tomar ofertas de este comerciante. +offerbook.warning.requireUpdateToNewVersion=Su versión de Bisq ya no es compatible para realizar intercambios.\nPor favor actualice a la última versión de Bisq en [HYPERLINK:https://bisq.network/downloads]. +offerbook.warning.offerWasAlreadyUsedInTrade=No puede aceptar esta oferta porque ya lo hizo antes. Podría ser que su intento anterior de aceptar esta oferta haya terminado como un intercambio fallido. + +offerbook.info.sellAtMarketPrice=Venderá a precio de mercado (actualizado cada minuto). +offerbook.info.buyAtMarketPrice=Comprará a precio de mercado (actualizado cada minuto). +offerbook.info.sellBelowMarketPrice=Recibirá {0} menos que el precio de mercado actual (actualizado cada minuto). +offerbook.info.buyAboveMarketPrice=Pagará {0} más que el precio de mercado actual (actualizado cada minuto). +offerbook.info.sellAboveMarketPrice=Recibirá {0} más que el precio de mercado actual (actualizado cada minuto). +offerbook.info.buyBelowMarketPrice=Pagará {0} menos que el precio de mercado actual (actualizado cada minuto) +offerbook.info.buyAtFixedPrice=Comprará a este precio fijo. +offerbook.info.sellAtFixedPrice=Venderá a este precio fijo. +offerbook.info.noArbitrationInUserLanguage=En caso de disputa, tenga en cuenta que el arbitraje para esta oferta se manejará en {0}. El idioma actualmente está configurado en {1}. +offerbook.info.roundedFiatVolume=La cantidad se redondeó para incrementar la privacidad de su intercambio. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Introducir cantidad en BTC +createOffer.price.prompt=Introducir precio +createOffer.volume.prompt=Introducir cantidad en {0} +createOffer.amountPriceBox.amountDescription=Cantidad de BTC a {0} +createOffer.amountPriceBox.buy.volumeDescription=Cantidad a gastar en {0} +createOffer.amountPriceBox.sell.volumeDescription=Cantidad a recibir en {0}. +createOffer.amountPriceBox.minAmountDescription=Cantidad mínima de BTC +createOffer.securityDeposit.prompt=Depósito de seguridad +createOffer.fundsBox.title=Dote de fondos su oferta. +createOffer.fundsBox.offerFee=Comisión de transacción +createOffer.fundsBox.networkFee=Comisión de minado +createOffer.fundsBox.placeOfferSpinnerInfo=Publicación de oferta en curso... +createOffer.fundsBox.paymentLabel=Intercambio Bisq con ID {0} +createOffer.fundsBox.fundsStructure=({0} depósito de seguridad, {1} comisión de transacción, {2} comisión de minado) +createOffer.fundsBox.fundsStructure.BSQ=({0} depósito seguridad, {1} comisión de minado) + {2} comisión de intercambio +createOffer.success.headline=Su oferta ha sido publicada. +createOffer.success.info=Puede gestionar sus ofertas abiertas en \"Portafolio/Mis ofertas abiertas\". +createOffer.info.sellAtMarketPrice=Siempre venderá a precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.info.buyAtMarketPrice=Siempre comprará a precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.info.sellAboveMarketPrice=Siempre tendrá {0}% más que el precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.info.buyBelowMarketPrice=Siempre pagará {0}% menos que el precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.warning.sellBelowMarketPrice=Siempre tendrá {0}% menos que el precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.warning.buyAboveMarketPrice=Siempre pagará {0}% más que el precio de mercado ya que el precio de su oferta será actualizado continuamente. +createOffer.tradeFee.descriptionBTCOnly=Comisión de transacción +createOffer.tradeFee.descriptionBSQEnabled=Seleccionar moneda de comisión de intercambio + +createOffer.triggerPrice.prompt=Establecer precio de ejecución opcional +createOffer.triggerPrice.label=Desactivar oferta si el precio de mercado es {0} +createOffer.triggerPrice.tooltip=Como protección contra movimientos drásticos de precio puede establecer un precio de ejecución que desactive la oferta si el precio de mercado alcanza ese valor. +createOffer.triggerPrice.invalid.tooLow=El valor debe ser superior a {0} +createOffer.triggerPrice.invalid.tooHigh=El valor debe ser inferior a {0} + +# new entries +createOffer.placeOfferButton=Revisar: Poner oferta para {0} bitcoin +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 Bisq (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. + +# 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. +createOffer.setAmountPrice=Establezca cantidad y precio +createOffer.warnCancelOffer=Ya ha destinado fondos para esa oferta.\nSi cancela ahora, sus fondos serán transferidos a su cartera Bisq local y estarán disponibles para retirar en la pantalla \"Fondos/Enviar fondos\".\n¿Está seguro que quiere cancelar? +createOffer.timeoutAtPublishing=Error. Fuera de tiempo en la publicación de la oferta. +createOffer.errorInfo=\n\nLa tasa del creador ya se ha pagado. En el peor caso ha perdido esa tasa. Lo sentimos, pero tenga en cuenta que es una cantidad pequeña.\nPor favor pruebe a reiniciar su aplicación y compruebe la conexión a la red para ver si puede resolver el asunto. +createOffer.tooLowSecDeposit.warning=Ha configurado el depósito de seguridad en un valor más bajo que el recomendado de forma predeterminada, que es {0}.\n¿Está seguro que quiere usar un valor de depósito de seguridad más bajo? +createOffer.tooLowSecDeposit.makerIsSeller=Le da menos protección en caso de que el participante en la transacción no siga el protocolo de intercambio. +createOffer.tooLowSecDeposit.makerIsBuyer=Esto resulta en menos protección para el otro participante en la transacción si usted continua el protocolo de intercambio, pues tiene menos depósitos en riesgo. Otros usuarios podrían preferir tomar otras ofertas en vez de la suya. +createOffer.resetToDefault=No, restablecer al valor por defecto +createOffer.useLowerValue=Sí, usar mi valor más bajo +createOffer.priceOutSideOfDeviation=El precio que ha introducido está fuera de la máxima desviación permitida en relación al precio de mercado.\nLa desviación máxima permitida es {0} y puede ajustarse en las preferencias. +createOffer.changePrice=Cambiar precio +createOffer.tac=Al colocar esta oferta estoy de acuerdo en comerciar con cualquier comerciante que cumpla con las condiciones definidas anteriormente. +createOffer.currencyForFee=Tasa de transacción +createOffer.setDeposit=Establecer depósito de seguridad para el comprador (%) +createOffer.setDepositAsBuyer=Establecer mi depósito de seguridad como comprador (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Introducir la cantidad en BTC +takeOffer.amountPriceBox.buy.amountDescription=Cantidad de BTC a vender +takeOffer.amountPriceBox.sell.amountDescription=Cantidad de BTC a comprar +takeOffer.amountPriceBox.priceDescription=Precio por bitcoin en {0} +takeOffer.amountPriceBox.amountRangeDescription=Rango de cantidad posible. +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=La cantidad introducida excede el número de decimales permitidos.\nLa cantidad ha sido ajustada a 4 decimales. +takeOffer.validation.amountSmallerThanMinAmount=La cantidad no puede ser menor que el mínimo definido en la oferta. +takeOffer.validation.amountLargerThanOfferAmount=La cantidad introducida no puede ser mayor que el máximo definido en la oferta. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=La cantidad introducida crearía polvo (dust change) para el vendedor de bitcoin. +takeOffer.fundsBox.title=Dote de fondos su intercambio. +takeOffer.fundsBox.isOfferAvailable=Comprobar si la oferta está disponible... +takeOffer.fundsBox.tradeAmount=Cantidad a vender +takeOffer.fundsBox.offerFee=Comisión de transacción +takeOffer.fundsBox.networkFee=Comisiones de minado totales +takeOffer.fundsBox.takeOfferSpinnerInfo=Aceptación de oferta en espera... +takeOffer.fundsBox.paymentLabel=Intercambio Bisq con ID {0} +takeOffer.fundsBox.fundsStructure=({0} depósito de seguridad {1} tasa de intercambio, {2} tarifa de minado) +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} + +# new entries +takeOffer.takeOfferButton=Revisión: Tomar oferta a {0} bitcoin +takeOffer.noPriceFeedAvailable=No puede tomar esta oferta porque utiliza un precio porcentual basado en el precio de mercado y no hay fuentes de precio disponibles. +takeOffer.takeOfferFundWalletInfo.headline=Dotar de fondos su intercambio +# suppress inspection "TrailingSpacesInProperty" +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 Bisq (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.alreadyPaidInFunds=Si ya ha depositado puede retirarlo en la pantalla \"Fondos/Disponible para retirar\". +takeOffer.paymentInfo=Información de pago +takeOffer.setAmountPrice=Establecer cantidad +takeOffer.alreadyFunded.askCancel=Ya ha destinado fondos para esta oferta.\nSi cancela ahora, sus fondos serán transferidos a su cartera Bisq local y estarán disponibles para retirar en la pantalla \"Fondos/Enviar fondos\".\n¿Está seguro que quiere cancelar? +takeOffer.failed.offerNotAvailable=Falló la solicitud de toma de oferta porque la oferta ya no está disponible. Tal vez otro comerciante la haya tomado en su lugar. +takeOffer.failed.offerTaken=No puede tomar la oferta porque la oferta fue tomada por otro comerciante. +takeOffer.failed.offerRemoved=No puede tomar esta oferta porque la oferta ha sido eliminada. +takeOffer.failed.offererNotOnline=La solicitud de toma de oferta falló porque el creador no se encuentra online. +takeOffer.failed.offererOffline=No puede tomar la oferta porque el tomador está offline. +takeOffer.warning.connectionToPeerLost=Ha perdido conexión con el creador.\nPuede haberse desconectado o haber cortado la conexión hacia usted debido a que existan demasiadas conexiones abiertas.\n\nSi aún puede ver la oferta en el libro de ofertas puede intentar tomarla de nuevo. + +takeOffer.error.noFundsLost=\n\nNingún importe de su cartera ha sido deducido aún.\nPor favor intente reiniciar su aplicación y compruebe la conexión a la red para ver si puede resolver el problema. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=.\n\n +takeOffer.error.depositPublished=\n\nLa transacción de depósito ya se ha publicado.\nPor favor intente reiniciar su aplicación y compruebe su conexión a la red para ver si puede resolver el problema.\nSi el problema persiste, por favor contacte a los desarrolladores para solicitar asistencia. +takeOffer.error.payoutPublished=\n\nLa transacción de pago ya se ha publicado.\nPor favor intente reiniciar su aplicación y compruebe su conexión a la red para ver si puede resolver el problema.\nSi el problema persiste, por favor contacte a los desarrolladores para solicitar asistencia. +takeOffer.tac=Al tomar esta oferta, afirmo estar de acuerdo con las condiciones de intercambio definidas anteriormente en esta pantalla. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Precio de ejecución +openOffer.triggerPrice=Precio de ejecución {0} +openOffer.triggered=La oferta ha sido desactivada porque el precio de mercado alcanzó su precio de disparo.\nPor favor edite la oferta para definir un nuevo precio de disparo. + +editOffer.setPrice=Establecer precio +editOffer.confirmEdit=Confirmar: Editar oferta +editOffer.publishOffer=Publicando su oferta. +editOffer.failed=Fallo en la edición de oferta:\n{0} +editOffer.success=Su oferta ha sido editada con éxito. +editOffer.invalidDeposit=El depósito de seguridad del comprador no está dentro de los límites definidos por la DAO Bisq y ya no puede ser ser editado. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Mis ofertas abiertas +portfolio.tab.pendingTrades=Intercambios abiertos +portfolio.tab.history=Historial +portfolio.tab.failed=Fallidas +portfolio.tab.editOpenOffer=Editar oferta + +portfolio.closedTrades.deviation.help=Desviación porcentual de precio de mercado + +portfolio.pending.invalidTx=Hay un problema con una transacción inválida o no encontrada.\n\nPor faovr NO envíe el pago de fiat o altcoins.\n\nAbra un ticket de soporte para obtener asistencia de un mediador.\n\nMensaje de error: {0} + +portfolio.pending.step1.waitForConf=Esperar a la confirmación en la cadena de bloques +portfolio.pending.step2_buyer.startPayment=Comenzar pago +portfolio.pending.step2_seller.waitPaymentStarted=Esperar hasta que el pago se haya iniciado +portfolio.pending.step3_buyer.waitPaymentArrived=Esperar hasta que el pago haya llegado +portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar recepción de pago +portfolio.pending.step5.completed=Completado + +portfolio.pending.step3_seller.autoConf.status.label=Estado de autoconfirmación +portfolio.pending.autoConf=Auto-confirmado +portfolio.pending.autoConf.blocks=Confirmaciones XMR: {0} / Requerido: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Clave de transacción re-utilizada. Por favor, abra una disputa. +portfolio.pending.autoConf.state.confirmations=Confirmaciones XMR: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transacción no vista aún en la mempool +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=ID de transacción no válida / clave de transacción +portfolio.pending.autoConf.state.filterDisabledFeature=Deshabilitado por los desarrolladores + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=La función de autoconfirmación está deshabilitada. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=La cantidad de intercambio excede el límite. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=El par entregó datos inválidos. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=La transacción de pago ya fue publicada. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=La disputa se abrió. La autoconfirmación se ha desactivado para este intercambio. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=La solicitud de prueba de transacción comenzó +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Resultados de éxito: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Prueba con éxito en todos los servicios +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=Ocurrió un error en el servicio solicitado. No es posible la autoconfirmación. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Su intercambio tiene al menos una confirmación en la cadena de bloques.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Importante: al hacer un pago, deje el campo \"motivo de pago\" vacío. NO PONGA la ID de intercambio o algún otro texto como 'bitcoin', 'BTC', o 'Bisq'. Los comerciantes pueden convenir en el chat de intercambio un \"motivo de pago\" alternativo. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees=Si su banco carga alguna tasa por hacer la transferencia, es responsable de pagar esas tasas. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.altcoin=Por favor transfiera fondos desde su cartera externa {0}\n{1} al vendedor de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Por favor vaya al banco y pague {0} al vendedor de BTC.\n\n +portfolio.pending.step2_buyer.cash.extra=REQUERIMIENTO IMPORTANTE:\nDespués de haber hecho el pago escribe en el recibo de papel: NO REFUNDS\nLuego divídalo en 2 partes, haga una foto y envíela a la dirección de correo electrónico del vendedor de BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Por favor pague {0} al vendedor de BTC utilizando MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=REQUERIMIENTO IMPORTANTE:\nDespués que usted haya realizado el pago, envíe el número de autorización y una foto del recibo al vendedor de BTC por correo electrónico.\nEl recibo debe mostrar claramente el monto, asi como el nombre completo, país y demarcación (departamento,estado, etc.) del vendedor. El correo electrónico del vendedor es: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Por favor pague {0} al vendedor de BTC usando Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=REQUERIMIENTO IMPORTANTE:\nDespués de haber realizado el pago envíe el MTCN (número de seguimiento) y una foto de el recibo por email a el vendedor de BTC.\nEl recibo debe mostrar claramente el nombre completo del emisor, la ciudad, país y la cantidad. El email del vendedor es: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Por favor envíe {0} mediante \"US Postal Money Order\" a el vendedor de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=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 Bisq [HYPERLINK:https://bisq.wiki/Cash_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 BTC. Encontrará los detalles de la cuenta del vendedor en la siguiente pantalla.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Por favor contacte al vendedor de BTC con el contacto proporcionado y acuerden un encuentro para pagar {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Comenzar pago utilizando {0} +portfolio.pending.step2_buyer.recipientsAccountData=Receptores {0} +portfolio.pending.step2_buyer.amountToTransfer=Cantidad a transferir +portfolio.pending.step2_buyer.sellersAddress=Dirección {0} del vendedor +portfolio.pending.step2_buyer.buyerAccount=Su cuenta de pago para ser usada +portfolio.pending.step2_buyer.paymentStarted=Pago iniciado +portfolio.pending.step2_buyer.fillInBsqWallet=Pagar desde el monedero BSQ +portfolio.pending.step2_buyer.warn=¡Todavía no ha realizado su pago {0}!\nPor favor, tenga en cuenta que el pago tiene que completarse antes de {1}. +portfolio.pending.step2_buyer.openForDispute=¡No ha completado su pago!\nEl periodo máximo para el intercambio ha concluido. Por favor, contacte con el mediador para abrir una disputa. +portfolio.pending.step2_buyer.paperReceipt.headline=¿Ha enviado el recibo a el vendedor de BTC? +portfolio.pending.step2_buyer.paperReceipt.msg=Recuerde:\nTiene que escribir en el recibo de papel: NO REFUNDS.\nLuego divídalo en 2 partes, haga una foto y envíela a la dirección de e-mail del vendedor de BTC. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar número de autorización y recibo +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Debe enviar el número de autorización y una foto del recibo por correo electrónico al vendedor de BTC.\nEl recibo debe mostrar claramente el monto, así como el nombre completo, país y demarcación (departamento,estado, etc.) del vendedor. El correo electrónico del vendedor es: {0}.\n\n¿Envió usted el número de autorización y el contrato al vendedor? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN y recibo +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Necesita enviar el MTCN (número de seguimiento) y una foto de el recibo por email a el vendedor de BTC\nEl recibo debe mostrar claramente el nombre completo del emisor, la ciudad, el país y la cantidad. El email del vendedor es: {0}\n\n¿Envió el MTCN y el contrato al vendedor? +portfolio.pending.step2_buyer.halCashInfo.headline=Enviar código HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Necesita enviar un mensaje de texto con el código HalCash\nEl móvil del vendedor es {1}.\n\n¿Envió el código al vendedor? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Algunos bancos pueden verificar el nombre del receptor. Las cuentas Faster Payments creadas en antiguos clientes de Bisq no proporcionan el nombre completo del receptor, así que por favor, utilice el chat del intercambio para obtenerlo (si es necesario). +portfolio.pending.step2_buyer.confirmStart.headline=Confirme que ha comenzado el pago. +portfolio.pending.step2_buyer.confirmStart.msg=¿Ha iniciado el pago de {0} a su par de intercambio? +portfolio.pending.step2_buyer.confirmStart.yes=Sí, lo he iniciado. +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=No ha entregado una prueba de pago válida. +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=No ha introducido la transacción de ID y la clave de transacción.\n\nAl no proveer esta información el par no puede usar la autoconfirmación para liberar los BTC en cuanto los XMR se han recibido.\nAdemás de esto, Bisq requiere que el emisor de la transacción XMR sea capaz de entregar esta información al mediador o árbitro en caso de disputa.\nVea más detalles en la wiki de Bisq: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=El valor introducido no es un valor hexadecimal de 32 bytes +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorar y continuar de todos modos +portfolio.pending.step2_seller.waitPayment.headline=Esperar al pago. +portfolio.pending.step2_seller.f2fInfo.headline=Información de contacto del comprador +portfolio.pending.step2_seller.waitPayment.msg=La transacción del depósito tiene al menos una confirmación en la cadena de bloques.\nTiene que esperar hasta que el comprador de BTC comience el pago de {0}. +portfolio.pending.step2_seller.warn=El comprador de BTC aún no ha realizado el pago de {0}.\nNecesita esperar hasta que el pago comience.\nSi el intercambio aún no se ha completado el {1} el árbitro procederá a investigar. +portfolio.pending.step2_seller.openForDispute=El comprador de BTC no ha comenzado su pago!\nEl periodo máximo permitido ha finalizado.\nPuede esperar más y dar más tiempo a la otra parte o contactar con el mediador para abrir una disputa. +tradeChat.chatWindowTitle=Ventana de chat para intercambio con ID "{0}" +tradeChat.openChat=Abrir ventana de chat +tradeChat.rules=Puede comunicarse con su par de intercambio para resolver posibles problemas con este intercambio.\nNo es obligatorio responder en el chat.\nSi un comerciante viola alguna de las reglas de abajo, abra una disputa y repórtelo al mediador o árbitro.\n\nReglas del chat:\n\t● No enviar ningún enlace (riesgo de malware). Puedes enviar el ID de la transacción y el nombre de un explorador de bloques.\n\t● ¡No enviar las palabras semilla, llaves privadas, contraseñas u otra información sensible!\n\t● No alentar a intercambiar fuera de Bisq (sin seguridad).\n\t● No se enfrente a ningún intento de estafa de ingeniería social.\n\t● Si un par no responde y prefiere no comunicarse, respete su decisión.\n\t● Limite el tema de conversación al intercambio. Este chat no es un sustituto del messenger o troll-box.\n\t● Mantenga la conversación amigable y respetuosa. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +message.state.SENT=Mensaje enviado +# suppress inspection "UnusedProperty" +message.state.ARRIVED=El mensaje llegó al usuario de red +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Mensaje de pago enviado, pero aún no recibido por el par. +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=El usuario de red confirmó la recepción del mensaje +# suppress inspection "UnusedProperty" +message.state.FAILED=El envío del mensaje falló + +portfolio.pending.step3_buyer.wait.headline=Espere a la confirmación de pago del vendedor de BTC. +portfolio.pending.step3_buyer.wait.info=Esperando a la confirmación del recibo de pago de {0} por parte del vendedor de BTC. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Estado del mensaje de pago iniciado +portfolio.pending.step3_buyer.warn.part1a=en la cadena de bloques {0} +portfolio.pending.step3_buyer.warn.part1b=en su proveedor de pago (v.g. banco) +portfolio.pending.step3_buyer.warn.part2=El vendedor de BTC aún no ha confirmado su pago. Por favor, compruebe {0} si el envío del pago fue correcto. +portfolio.pending.step3_buyer.openForDispute=¡El vendedor de BTC aún no ha confirmado su pago! El periodo máximo para el intercambio ha concluido. Puede esperar y dar más tiempo a la otra parte o solicitar asistencia del mediador. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=La otra parte del intercambio confirma haber iniciado el pago de {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=en su explorador de cadena de bloques {0} favorito +portfolio.pending.step3_seller.altcoin.wallet=en su cartera {0} +portfolio.pending.step3_seller.altcoin={0}Por favor compruebe {1} si la transacción a su dirección de recepción\n{2}\ntiene suficientes confirmaciones en la cadena de bloques.\nLa cantidad a pagar tiene que ser {3}\n\nPuede copiar y pegar su dirección {4} desde la pantalla principal después de cerrar este popup. +portfolio.pending.step3_seller.postal={0}Por favor compruebe si ha recibido {1} con \"US Postal Money Order\" enviados por el comprador BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Por favor compruebe si ha recibido {1} con \"Efectivo por Correo\" enviados por el comprador BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Su par de intercambio confirma que ha iniciado el pago de {0}.\n\nPor favor vaya a la página web de su banco y compruebe si ha recibido {1} del comprador de BTC. +portfolio.pending.step3_seller.cash=Debido a que el pago se hecho vía depósito en efectivo el comprador de BTC tiene que escribir \"SIN REEMBOLSO\" en el recibo de papel, dividirlo en 2 partes y enviarte una foto por e-mail.\n\nPara impedir el riesgo de reembolso, solo confirme si ha recibido el e-mail y si está seguro de que el recibo es válido.\nSi no está seguro, {0} +portfolio.pending.step3_seller.moneyGram=El comprador tiene que enviarle el número de autorización y una foto del recibo por correo electrónico.\n\nEl recibo debe mostrar claramente el monto, asi como su nombre completo, país y demarcación (departamento,estado, etc.). Por favor revise su correo electrónico si recibió el número de autorización.\n\nDespués de cerrar esa ventana emergente (popup), verá el nombre y la dirección del comprador de BTC para retirar el dinero de MoneyGram.\n\n¡Solo confirme el recibo de transacción después de haber obtenido el dinero con éxito! +portfolio.pending.step3_seller.westernUnion=El comprador tiene que enviarle el MTCN (número de seguimiento) y una foto de el recibo por email.\nEl recibo debe mostrar claramente su nombre completo, ciudad, país y la cantidad. Por favor compruebe su email si ha recibido el MTCN.\n\nDespués de cerrar ese popup verá el nombre del comprador de BTC y la dirección para recoger el dinero de Western Union.\n\nSolo confirme el recibo después de haber recogido satisfactoriamente el dinero! +portfolio.pending.step3_seller.halCash=El comprador tiene que enviarle el código HalCash como un mensaje de texto. Junto a esto recibirá un mensaje desde HalCash con la información requerida para retirar los EUR de un cajero que soporte HalCash.\n\nDespués de retirar el dinero del cajero confirme aquí la recepción del pago! +portfolio.pending.step3_seller.amazonGiftCard=El comprador le ha enviado una Tarjeta Amazon eGift por email o mensaje de texto al teléfono móvil. Por favor canjee ahora la Tarjeta Amazon eGift en su cuenta Amazon y una vez aceptado confirme el recibo del pago. + +portfolio.pending.step3_seller.bankCheck=\n\nPor favor verifique también que el nombre y el emisor especificado en el contrato de intercambio se corresponde con el nombre que aparece en su declaración bancaria:\nNombre del emisor, para el contrato de intercambio: {0}\n\nSi los nombres no son exactamente los mismos, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=no confirme el recibo de pago. En su lugar, abra una disputa pulsando \"alt + o\" o \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recibo de pago +portfolio.pending.step3_seller.amountToReceive=Cantidad a recibir +portfolio.pending.step3_seller.yourAddress=Su dirección {0} +portfolio.pending.step3_seller.buyersAddress=Dirección {0} del comprador +portfolio.pending.step3_seller.yourAccount=Su cuenta de intercambio +portfolio.pending.step3_seller.xmrTxHash=ID de la transacción +portfolio.pending.step3_seller.xmrTxKey=Clave de transacción +portfolio.pending.step3_seller.buyersAccount=Datos de cuenta del comprador +portfolio.pending.step3_seller.confirmReceipt=Confirmar recibo de pago +portfolio.pending.step3_seller.buyerStartedPayment=El comprador de BTC ha iniciado el pago de {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Compruebe las confirmaciones en la cadena de bloques en su monedero de altcoin o explorador de bloques y confirme el pago cuando tenga suficientes confirmaciones. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Compruebe su cuenta de intercambio (v.g. cuenta bancaria) y confirme cuando haya recibido el pago. +portfolio.pending.step3_seller.warn.part1a=en la cadena de bloques {0} +portfolio.pending.step3_seller.warn.part1b=en su proveedor de pago (v.g. banco) +portfolio.pending.step3_seller.warn.part2=Todavía no ha confirmado el recibo del pago. Por favor, compruebe {0} si ha recibido el pago. +portfolio.pending.step3_seller.openForDispute=No ha confirmado la recepción del pago.\nEl periodo máximo para el intercambio ha concluido.\nPor favor confirme o solicite asistencia del mediador. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=¿Ha recibido el pago de {0} de su par de intercambio?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Por favor verifique también que el nombre del emisor especificado en el contrato de intercambio concuerda con el nombre que aparece en su declaración bancaria:\nNombre del emisor, para el contrato de intercambio: {0}\n\nSi los nombres no son exactamente los mismos, no confirme el recibo de pago. En su lugar, abra una disputa pulsando \"alt + o\" o \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Por favor tenga en cuenta, que tan pronto como haya confirmado el recibo, la cantidad de intercambio bloqueada será librerada al comprador de BTC y el depósito de seguridad será devuelto. +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirme que ha recibido el pago +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sí, he recibido el pago +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Confirmando el recibo de pago, está también verificando la cuenta de la contraparte y firmándola en consecuencia. Como la cuenta de la contraparte no ha sido firmada aún, debería retrasar la confirmación de pago tanto como sea posible para reducir el riesgo de devolución de cargo. + +portfolio.pending.step5_buyer.groupTitle=Resumen de el intercambio completado +portfolio.pending.step5_buyer.tradeFee=Comisión de transacción +portfolio.pending.step5_buyer.makersMiningFee=Comisión de minado +portfolio.pending.step5_buyer.takersMiningFee=Comisiones de minado totales +portfolio.pending.step5_buyer.refunded=Depósito de seguridad devuelto +portfolio.pending.step5_buyer.withdrawBTC=Retirar bitcoins +portfolio.pending.step5_buyer.amount=Cantidad a retirar +portfolio.pending.step5_buyer.withdrawToAddress=Retirar a la dirección +portfolio.pending.step5_buyer.moveToBisqWallet=Mantener fondos en el monedero de Bisq +portfolio.pending.step5_buyer.withdrawExternal=Retirar al monedero externo +portfolio.pending.step5_buyer.alreadyWithdrawn=Sus fondos ya han sido retirados.\nPor favor, compruebe el historial de transacciones. +portfolio.pending.step5_buyer.confirmWithdrawal=Confirme la petición de retiro +portfolio.pending.step5_buyer.amountTooLow=La cantidad a transferir es inferior a la tasa de transacción y el mínimo valor de transacción posible (polvo - dust). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retiro completado +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Sus intercambios completados están almacenados en \"Portfolio/Historial\".\nPuede revisar todas las transacciones de bitcoin en \"Fondos/Transacciones\" +portfolio.pending.step5_buyer.bought=Ha comprado +portfolio.pending.step5_buyer.paid=Ha pagado + +portfolio.pending.step5_seller.sold=Ha vendido +portfolio.pending.step5_seller.received=Ha recibido + +tradeFeedbackWindow.title=Felicitaciones por completar su intercambio +tradeFeedbackWindow.msg.part1=Nos encantaría saber su opinión acerca de su experiencia. Nos ayudará a mejorar el software y refinar sus características. Si desea enviar sus comentarios, por favor complete esta breve encuesta (sin registro requerido) en: +tradeFeedbackWindow.msg.part2=Si tiene alguna pregunta o experimenta algún problema, por favor póngase en contacto con otros usuarios y colaboradores a través del foro Bisq en: +tradeFeedbackWindow.msg.part3=¡Gracias por usar Bisq! + +portfolio.pending.role=Mi rol +portfolio.pending.tradeInformation=Información de intercambio +portfolio.pending.remainingTime=Tiempo requerido +portfolio.pending.remainingTimeDetail={0} (hasta {1}) +portfolio.pending.tradePeriodInfo=Después de la primera confirmación en la cadena de bloques, el periodo de intercambio comienza. Según el método de pago utilizado, se aplicará un periodo de intercambio máximo permitido. +portfolio.pending.tradePeriodWarning=Si el periodo se excede ambos comerciantes pueden abrir una disputa. +portfolio.pending.tradeNotCompleted=Intercambio no completado a tiempo(hasta {0}) +portfolio.pending.tradeProcess=Proceso de intercambio +portfolio.pending.openAgainDispute.msg=Si no está seguro de que el mensaje al mediador o árbitro llegó (Ej. si no ha tenido respuesta después de 1 día), siéntase libre de abrir una disputa de nuevo con Cmd/Ctrl+o. También puede pedir ayuda adicional en el forum de Bisq en [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Abrir disputa de nuevo +portfolio.pending.openSupportTicket.headline=Abrir ticket de soporte +portfolio.pending.openSupportTicket.msg=Por favor use esta función solo en caso de emergencia si no se muestra el botón \"Abrir soporte\" o \"Abrir disputa\".\n\nCuando abra un ticket de soporte el intercambio se interrumpirá y será manejado por un mediador o un árbitro. + +portfolio.pending.timeLockNotOver=Tiene hasta ≈{0} ({1} bloques más) antes de que pueda abrir una disputa de arbitraje. +portfolio.pending.error.depositTxNull=La transacción de depósito es inválida. No puede abrir una disputa sin una transacción de depósito válida. Por favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\n\nPara obtener ayuda contacte con el equipo de soporte en el canal Bisq de Keybase. +portfolio.pending.mediationResult.error.depositTxNull=La transacción de depósito es nula. Puede mover la transacción a operaciones fallidas. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=La transacción de pago demorado es nula. Puede mover la transacción a operaciones fallidas. +portfolio.pending.error.depositTxNotConfirmed=El depósito de transacción no se ha confirmado. No puede abrir una disputa de arbitraje con una transacción de depósito no confirmada. Por favor espere a que se confirme o vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\n\nPara más ayuda por favor contacte con el equipo de soporte en el canal Bisq de Keybase. + +portfolio.pending.support.headline.getHelp=¿Necesita ayuda? +portfolio.pending.support.text.getHelp=Si tiene algún problema puede intentar contactar al par de intercambio en el chat o preguntar en la la comunidad Bisq en https://bisq.comunnity. Si su problema no se resuelve, puede abrir una disputa con un mediador. +portfolio.pending.support.button.getHelp=Abrir chat de intercambio +portfolio.pending.support.headline.halfPeriodOver=Comprobar pago +portfolio.pending.support.headline.periodOver=El periodo de intercambio se acabó + +portfolio.pending.mediationRequested=Mediación solicitada +portfolio.pending.refundRequested=Devolución de fondos solicitada +portfolio.pending.openSupport=Abrir ticket de soporte +portfolio.pending.supportTicketOpened=Ticket de soporte abierto +portfolio.pending.communicateWithArbitrator=Por favor, comuníquese en la pantalla de \"Soporte\" con el árbitro. +portfolio.pending.communicateWithMediator=Por favor, comuníquese en la pantalla \"Soporte\" con el mediador. +portfolio.pending.disputeOpenedMyUser=Ya ha abierto una disputa.\n{0} +portfolio.pending.disputeOpenedByPeer=Su pareja de intercambio ha abierto una disputa\n{0} +portfolio.pending.noReceiverAddressDefined=No se ha definido la dirección del receptor. + +portfolio.pending.mediationResult.headline=Pago sugerido por la mediación +portfolio.pending.mediationResult.info.noneAccepted=Complete el intercambio aceptando la sugerencia del mediador para el pago de la transacción. +portfolio.pending.mediationResult.info.selfAccepted=Ha aceptado la sugerencia del mediador. Esperando a que el par también la acepte. +portfolio.pending.mediationResult.info.peerAccepted=El par de intercambio ha aceptador la sugerencia del mediador. ¿Usted también la acepta? +portfolio.pending.mediationResult.button=Ver resolución propuesta +portfolio.pending.mediationResult.popup.headline=Resultado de mediación para el intercambio con ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=El par de intercambio ha aceptado la sugerencia del mediador para el intercmabio {0} +portfolio.pending.mediationResult.popup.info=El mediador ha sugerido el siguiente pago:\nUsted recibe: {0}\nEl par de intercambio recibe: {1}\n\nUsted puede aceptar o rechazar esta sugerencia de pago.\n\nAceptándola, usted firma el pago propuesto. Si su par de intercambio también acepta y firma, el pago se completará y el intercambio se cerrará.\n\nSi una o ambas partes rechaza la sugerencia, tendrá que esperar hasta {2} (bloque {3}) para abrir una segunda ronda de disputa con un árbitro que investigará el caso de nuevo y realizará el pago de acuerdo a sus hallazgos.\n\nEl árbitro puede cobrar una tasa pequeña (tasa máxima: el depósito de seguridad del comerciante) como compensación por su trabajo. Que las dos partes estén de acuerdo es el buen camino, ya que requerir arbitraje se reserva para circunstancias excepcionales, como que un comerciante esté seguro de que el mediador hizo una sugerencia de pago injusta (o que la otra parte no responda).\n\nMás detalles acerca del nuevo modelo de arbitraje:\n[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Ha aceptado el pago sugerido por el mediador, pero parece que su par de intercambio no lo ha aceptado.\n\nUna vez que finaliza el tiempo de bloqueo en el {0} (bloque {1}), puede abrir una segunda ronda de disputa con un árbitro que investigará el caso nuevamente y realizará un pago en función de sus hallazgos.\n\nPuede encontrar más detalles sobre el modelo de arbitraje en:\n[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Rechazar y solicitar arbitraje +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 fiat o altcoin al vendedor de BTC, 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.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 Bisq.\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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Mover intercambio a intercambios fallidos +portfolio.pending.failedTrade.warningIcon.tooltip=Clique para mostrar los detalles sobre los problemas en este intercambio +portfolio.failed.revertToPending.popup=¿Quiere mover este intercambio a intercambios abiertos? +portfolio.failed.revertToPending=Mueva intercambio a intercambios abiertos + +portfolio.closed.completed=Completado +portfolio.closed.ticketClosed=Arbitrada +portfolio.closed.mediationTicketClosed=Mediada +portfolio.closed.canceled=Cancelado +portfolio.failed.Failed=Fallado +portfolio.failed.unfail=Antes de continuar, ¡asegúrese de tener un respaldo de su directorio de datos!\n¿Desea mover este intercambio de nuevo a intercambios abiertos?\nEsta es una forma de desbloquear los fondos retenidos en los intercambios fallidos. +portfolio.failed.cantUnfail=Este intercambio no puede ser movido de nuevo a intercambios abiertos en este momento.\nIntente de nuevo después de completar el/los intercambios/s {0} +portfolio.failed.depositTxNull=El intercambio no se puede revertir a intercambios abiertos. La transacción de depósito es nula. +portfolio.failed.delayedPayoutTxNull=El intercambio no se puede revertir a uno abierto. La transacción del pago demorado es nula. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Recibir fondos +funds.tab.withdrawal=Enviar fondos +funds.tab.reserved=Fondos reservados +funds.tab.locked=Fondos bloqueados +funds.tab.transactions=Transacciones + +funds.deposit.unused=Sin usar +funds.deposit.usedInTx=Usadas en {0} transacciones +funds.deposit.fundBisqWallet=Fondear monedero Bisq +funds.deposit.noAddresses=Aún no se ha generado la dirección de depósito +funds.deposit.fundWallet=Dotar de fondos su monedero +funds.deposit.withdrawFromWallet=Enviar fondos desde monedero +funds.deposit.amount=Cantidad en BTC (opcional) +funds.deposit.generateAddress=Generar una nueva dirección +funds.deposit.generateAddressSegwit=Formato de segwit nativo (Bech32) +funds.deposit.selectUnused=Por favor seleccione una dirección no utilizada de la tabla de arriba en vez de generar una nueva. + +funds.withdrawal.arbitrationFee=Tasa de arbitraje +funds.withdrawal.inputs=Selección de entradas +funds.withdrawal.useAllInputs=Usar todos los entradas disponibles +funds.withdrawal.useCustomInputs=Usar entradas personalizados +funds.withdrawal.receiverAmount=Cantidad del receptor +funds.withdrawal.senderAmount=Cantidad del emisor +funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado +funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado +funds.withdrawal.fromLabel=Retirar desde la dirección +funds.withdrawal.toLabel=Retirar a la dirección +funds.withdrawal.memoLabel=Nota de retiro +funds.withdrawal.memo=Rellenar opcionalmente la nota +funds.withdrawal.withdrawButton=Retiro seleccionado +funds.withdrawal.noFundsAvailable=No hay fondos disponibles para el retiro +funds.withdrawal.confirmWithdrawalRequest=Confirme la petición de retiro +funds.withdrawal.withdrawMultipleAddresses=Retirar desde múltiples direcciones ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Retirar desde múltiples direcciones:\n{0} +funds.withdrawal.notEnoughFunds=No tiene suficientes fondos en su cartera. +funds.withdrawal.selectAddress=Seleccione una dirección de fuente de la tabla +funds.withdrawal.setAmount=Introduzca la cantidad a retirar +funds.withdrawal.fillDestAddress=Introduzca su dirección de destino +funds.withdrawal.warn.noSourceAddressSelected=Necesita seleccionar una fuente de direcciones en la tabla superior. +funds.withdrawal.warn.amountExceeds=No tiene suficientes fondos disponibles en la dirección seleccionada.\nConsidere seleccionar múltiples direcciones en la tabla superior o cambien el selector de comisión para incluir una comisión de minería. + +funds.reserved.noFunds=No hay fondos reservados en las ofertas abiertas +funds.reserved.reserved=Reservados en el monedero local para la oferta con ID: {0} + +funds.locked.noFunds=No hay fondos bloqueados en intercambios +funds.locked.locked=Bloqueado en Multifirma para el intercambio con ID: {0} + +funds.tx.direction.sentTo=Enviado a: +funds.tx.direction.receivedWith=Recibido con: +funds.tx.direction.genesisTx=Desde la transacción Génesis: +funds.tx.txFeePaymentForBsqTx=Comisión de minería para la tx BSQ +funds.tx.createOfferFee=Creador y comisión de transacción: {0} +funds.tx.takeOfferFee=Tomador y comisión de transacción: {0} +funds.tx.multiSigDeposit=Depósito Multifirma: {0} +funds.tx.multiSigPayout=Pago Multifirma: {0} +funds.tx.disputePayout=Pago disputa:{0} +funds.tx.disputeLost=Caso de pérdida de disputa: {0} +funds.tx.collateralForRefund=Colateral para devolución de fondos: {0} +funds.tx.timeLockedPayoutTx=Pago de transacción bloqueada en tiempo: {0} +funds.tx.refund=Devolución de fondos de arbitraje: {0} +funds.tx.unknown=Razón desconocida: {0} +funds.tx.noFundsFromDispute=Sin devolución de disputa +funds.tx.receivedFunds=Fondos recibidos +funds.tx.withdrawnFromWallet=Retirar desde el monedero +funds.tx.withdrawnFromBSQWallet=BTC retirados desde el monedero BSQ +funds.tx.memo=Nota +funds.tx.noTxAvailable=Sin transacciones disponibles +funds.tx.revert=Revertir +funds.tx.txSent=Transacción enviada exitosamente a una nueva dirección en la billetera Bisq local. +funds.tx.direction.self=Enviado a usted mismo +funds.tx.daoTxFee=Comisión de minería para la tx BSQ +funds.tx.reimbursementRequestTxFee=Solicitud de reembolso +funds.tx.compensationRequestTxFee=Solicitud de compensación +funds.tx.dustAttackTx=Dust recibido +funds.tx.dustAttackTx.popup=Esta transacción está enviando una cantidad de BTC muy pequeña a su monedero y puede ser un intento de compañías de análisis de cadenas para espiar su monedero.\n\nSi usa este output para gastar en una transacción, conocerán que probablemente usted sea el propietario de sus otras direcciones (fusión de monedas).\n\nPara proteger su privacidad el monedero Bisq ignora estos outputs para propósitos de gasto y en el balance mostrado. Puede establecer el umbral en el que un output es considerado dust en ajustes. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediación +support.tab.arbitration.support=Arbitraje +support.tab.legacyArbitration.support=Legado de arbitraje +support.tab.ArbitratorsSupportTickets=Tickets de {0} +support.filter=Buscar disputas +support.filter.prompt=Introduzca ID de transacción, fecha, dirección onion o datos de cuenta. + +support.sigCheck.button=Comprobar firma +support.sigCheck.popup.info=En caso de una solicitud de reembolso a la DAO, debe pegar el mensaje de resumen del proceso de mediación y arbitraje en su solicitud de reembolso en Github. Para que esta declaración sea verificable, cualquier usuario puede verificar con esta herramienta si la firma del mediador o árbitro coincide con el resumen del mensaje. +support.sigCheck.popup.header=Verificar firma del resultado de la disputa +support.sigCheck.popup.msg.label=Mensaje de resumen +support.sigCheck.popup.msg.prompt=Copie y pegue el mensaje de resumen de la disputa +support.sigCheck.popup.result=Resultado de la validación +support.sigCheck.popup.success=La firma es válida +support.sigCheck.popup.failed=La verificación de la firma ha fallado +support.sigCheck.popup.invalidFormat=El mensaje no tiene el formato esperado. Copie y pegue el resumen del mensaje desde la disputa. + +support.reOpenByTrader.prompt=¿Está seguro de que quiere reabrir la disputa? +support.reOpenButton.label=Volver a abrir +support.sendNotificationButton.label=Notificación privada +support.reportButton.label=Reportar +support.fullReportButton.label=Todas las disputas +support.noTickets=No hay tickets abiertos +support.sendingMessage=Enviando mensaje... +support.receiverNotOnline=El receptor no está conectado. El mensaje se ha guardado en su bandeja de entrada. +support.sendMessageError=El envío del mensaje no tuvo éxito. Error: {0} +support.receiverNotKnown=Receptor desconocido +support.wrongVersion=La oferta en esta disputa ha sido creada con una versión antigua de Bisq.\nNo puede cerrar esta disputa con su versión de la aplicación.\n\nPor favor, utilice una versión anterior con la versión de protocolo {0} +support.openFile=Abrir archivo a adjuntar (tamaño máximo del archivo: {0} kb) +support.attachmentTooLarge=El tamaño total de sus adjuntos es {0} kb y excede el máximo permitido por mensaje de {1} kB. +support.maxSize=El tamaño máximo permitido del archivo es {0} kB. +support.attachment=Adjuntado +support.tooManyAttachments=No puede enviar más de 3 adjuntos en un mensaje. +support.save=Guardar archivo al disco +support.messages=Mensajes +support.input.prompt=Introduzca mensaje... +support.send=Enviar +support.addAttachments=Añadir adjuntos +support.closeTicket=Cerrar ticket +support.attachments=Adjuntos: +support.savedInMailbox=Mensaje guardado en la bandeja de entrada del receptor +support.arrived=El mensaje ha llegado al receptor +support.acknowledged=El arribo del mensaje fue confirmado por el receptor +support.error=El receptor no pudo procesar el mensaje. Error: {0} +support.buyerAddress=Dirección del comprador de BTC +support.sellerAddress=Dirección del vendedor de BTC +support.role=Rol +support.agent=Agente de soporte +support.state=Estado +support.chat=Chat +support.closed=Cerrado +support.open=Abierto +support.process=Proceso +support.buyerOfferer=comprador/creador BTC +support.sellerOfferer=vendedor/creador BTC +support.buyerTaker=comprador/Tomador BTC +support.sellerTaker=vendedor/Tomador BTC + +support.backgroundInfo=Bisq no es una compañía, por ello maneja las disputas de una forma diferente.\n\nLos compradores y vendedores pueden comunicarse a través de la aplicación por un chat seguro en la pantalla de intercambios abiertos para intentar resolver una disputa por su cuenta. Si eso no es suficiente, un mediador puede intervenir para ayudar. El mediador evaluará la situación y dará una recomendación para el pago de los fondos de la transacción. Si ambos aceptan esta sugerencia, la transacción del pago se completa y el intercambio se cierra. Si uno o ambos no están de acuerdo con el pago recomendado por el mediador, pueden solicitar arbitraje. El árbitro re-evaluará la situación y, si es necesario, hará el pago personalmente y solicitará un reembolso de este pago a la DAO de Bisq. +support.initialInfo=Por favor, introduzca una descripción de su problema en el campo de texto de abajo. Añada tanta información como sea posible para agilizar la resolución de la disputa.\n\nEsta es una lista de la información que usted debe proveer:\n\t● Si es el comprador de BTC: ¿Hizo la transferencia Fiat o Altcoin? Si es así, ¿Pulsó el botón 'pago iniciado' en la aplicación?\n\t● Si es el vendedor de BTC: ¿Recibió el pago Fiat o Altcoin? Si es así, ¿Pulsó el botón 'pago recibido' en la aplicación?\n\t● ¿Qué versión de Bisq está usando?\n\t● ¿Qué sistema operativo está usando?\n\t● Si tiene problemas con transacciones fallidas, por favor considere cambiar a un nuevo directorio de datos.\n\tA veces el directorio de datos se corrompe y causa errores extraños.\n\tVer: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPor favor, familiarícese con las reglas básicas del proceso de disputa:\n\t● Tiene que responder a los requerimientos de {0} en 2 días.\n\t● Los mediadores responden en 2 días. Los árbitros responden en 5 días laborables.\n\t● El periodo máximo para una disputa es de 14 días.\n\t● Tiene que cooperar con {1} y proveer la información necesaria que soliciten.\n\t● Aceptó la reglas esbozadas en el documento de disputa en el acuerdo de usuario cuando inició por primera ver la aplicación.\n\nPuede leer más sobre el proceso de disputa en: {2} +support.systemMsg=Mensaje de sistema: {0} +support.youOpenedTicket=Ha abierto una solicitud de soporte.\n\n{0}\n\nVersión Bisq: {1} +support.youOpenedDispute=Ha abierto una solicitud de disputa.\n\n{0}\n\nVersión Bisq: {1} +support.youOpenedDisputeForMediation=Ha solicitado mediación\n\n{0}\n\nVersión Bisq: {1} +support.peerOpenedTicket=Su par de intercambio ha solicitado soporte debido a problemas técnicos\n\n{0}\n\nVersión Bisq: {1} +support.peerOpenedDispute=Su pareja de intercambio ha solicitado una disputa.\n\n{0}\n\nVersión Bisq: {1} +support.peerOpenedDisputeForMediation=Su par de intercambio ha solicitado mediación.\n\n{0}\n\nVersión Bisq: {1} +support.mediatorsDisputeSummary=Mensaje de sistema: Resumen de la disputa del mediador: {0} +support.mediatorsAddress=Dirección del nodo del mediador: {0} +support.warning.disputesWithInvalidDonationAddress=La transacción de pago demorado utilizó una dirección de receptor inválida. No coincide con ninguno de los valores de parámetro de la DAO para las direcciones de donación válidas.\n\nEsto podría ser un intento de estafa. Informe a los desarrolladores sobre ese incidente y no cierre ese caso antes de que se resuelva la situación.\n\nDirección utilizada en la disputa: {0}\n\nTodas las direcciones de donación en los parámetros de la DAO: {1}\n\nIdentificación comercial: {2} {3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\n¿Aún quiere cerrar la disputa? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nUsted no debería realizar el pago. +support.warning.traderCloseOwnDisputeWarning=Los comerciantes puden cerrar por sí mismos sus tickets de soporte cuando el intercambio se haya pagado. +support.info.disputeReOpened=El ticket de disputa se ha reabierto. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preferencias +settings.tab.network=Información de red +settings.tab.about=Acerca de + +setting.preferences.general=Preferencias generales +setting.preferences.explorer=Explorador Bitcoin +setting.preferences.explorer.bsq=Explorador Bisq +setting.preferences.deviation=Desviación máxima del precio de mercado +setting.preferences.bsqAverageTrimThreshold=Umbral de valores atípicos de la tasa de BSQ +setting.preferences.avoidStandbyMode=Evitar modo en espera +setting.preferences.autoConfirmXMR=Autoconfirmación XMR +setting.preferences.autoConfirmEnabled=Habilitado +setting.preferences.autoConfirmRequiredConfirmations=Confirmaciones requeridas +setting.preferences.autoConfirmMaxTradeSize=Cantidad máxima de intecambio (BTC) +setting.preferences.autoConfirmServiceAddresses=Explorador de URLs Monero (usa Tor, excepto para localhost, direcciones LAN IP, y hostnames *.local) +setting.preferences.deviationToLarge=No se permiten valores superiores a {0}% +setting.preferences.txFee=Tasa de transacción de retiro (satoshis/vbyte) +setting.preferences.useCustomValue=Usar valor personalizado +setting.preferences.txFeeMin=La tasa de transacción debe ser al menos de {0} sat/vbyte +setting.preferences.txFeeTooLarge=El valor introducido está muy por encima de lo razonable (>5000 satoshis/vbyte). La tasa de transacción normalmente está en el rango de 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Pares ignorados [dirección onion:puerto] +setting.preferences.ignoreDustThreshold=Valor mínimo de output que no sea dust +setting.preferences.currenciesInList=Monedas en lista para precio de mercado +setting.preferences.prefCurrency=Moneda preferida +setting.preferences.displayFiat=Mostrar monedas nacionales +setting.preferences.noFiat=No hay monedas nacionales seleccionadas +setting.preferences.cannotRemovePrefCurrency=No puede eliminar su moneda preferida para mostrar seleccionada +setting.preferences.displayAltcoins=Mostrar altcoins +setting.preferences.noAltcoins=No hay altcoins seleccionadas +setting.preferences.addFiat=Añadir moneda nacional +setting.preferences.addAltcoin=Añadir altcoin +setting.preferences.displayOptions=Mostrar opciones +setting.preferences.showOwnOffers=Mostrar mis propias ofertas en el libro de ofertas +setting.preferences.useAnimations=Usar animaciones +setting.preferences.useDarkMode=Usar modo oscuro +setting.preferences.sortWithNumOffers=Ordenar listas de mercado por número de ofertas/intercambios +setting.preferences.onlyShowPaymentMethodsFromAccount=Ocultar métodos de pago no soportados +setting.preferences.denyApiTaker=Denegar tomadores usando la misma API +setting.preferences.notifyOnPreRelease=Recibir notificaciones de pre-lanzamiento +setting.preferences.resetAllFlags=Restablecer todas las casillas \"No mostrar de nuevo\" +settings.preferences.languageChange=Para aplicar un cambio de idioma en todas las pantallas, se precisa reiniciar. +settings.preferences.supportLanguageWarning=En caso de disputa, tenga en cuenta que la mediación se maneja en {0} y el arbitraje en {1}. +setting.preferences.daoOptions=Opciones de DAO +setting.preferences.dao.resyncFromGenesis.label=Reconstruir estado de la DAO desde la tx génesis +setting.preferences.dao.resyncFromResources.label=Reconstruir el estado de la DAO desde recursos +setting.preferences.dao.resyncFromResources.popup=Después de un reinicio de la aplicación, los datos de gobernanza en la red Bisq se volverán a cargar desde los nodos semilla y el estado de consenso en BSQ se reconstruirá a partir de los últimos archivos de recursos. +setting.preferences.dao.resyncFromGenesis.popup=Una resincronización desde la transacción génesis puede llevar mucho tiempo y recursos de CPU. ¿Está seguro de que quiere hacer eso? Generalmente una resincronización de los últimos archivos de recursos es suficiente y mucho más rápida\n\nSi continúa, después de reiniciar la aplicación, los datos de gobernanza de la red Bisq se volverán a cargar desde los nodos semilla y el estado de consenso BSQ se reconstruirá a partir de la transacción génesis. +setting.preferences.dao.resyncFromGenesis.resync=Resincronizar desde la transacción génesis y cerrar la aplicación +setting.preferences.dao.isDaoFullNode=Ejecutar Bisq como nodo completo de la DAO +setting.preferences.dao.rpcUser=nombre de usuario RPC +setting.preferences.dao.rpcPw=contraseña RPC +setting.preferences.dao.blockNotifyPort=Puerto de notificación de bloque +setting.preferences.dao.fullNodeInfo=Para ejecutar Bisq como un nodo completo de la DAO necesita estar ejecutando localmente Bitcoin Core con RPC activado. Todos los requisitos están documentados en ''{0}''.\n\nDespués de cambiar el modo, necesita reiniciar. +setting.preferences.dao.fullNodeInfo.ok=Abrir página de documentos +setting.preferences.dao.fullNodeInfo.cancel=No, me quedo con el modo ligero +settings.preferences.editCustomExplorer.headline=Configuraciones de explorador +settings.preferences.editCustomExplorer.description=Elija un explorador definido por el sistema de la lista de la izquierda, y/o personalícelo para ajustarse a sus preferencias. +settings.preferences.editCustomExplorer.available=Exploradores disponibles +settings.preferences.editCustomExplorer.chosen=Configuración de exploradores elegidos +settings.preferences.editCustomExplorer.name=Nombre +settings.preferences.editCustomExplorer.txUrl=URL de transacción +settings.preferences.editCustomExplorer.addressUrl=URL de la dirección + +settings.net.btcHeader=Red Bitcoin +settings.net.p2pHeader=Red Bisq +settings.net.onionAddressLabel=Mi dirección onion +settings.net.btcNodesLabel=Utilizar nodos Bitcoin Core personalizados +settings.net.bitcoinPeersLabel=Pares conectados +settings.net.useTorForBtcJLabel=Usar Tor para la red Bitcoin +settings.net.bitcoinNodesLabel=Nodos Bitcoin Core para conectarse +settings.net.useProvidedNodesRadio=Utilizar nodos Bitcoin Core proporcionados +settings.net.usePublicNodesRadio=Utilizar red pública Bitcoin +settings.net.useCustomNodesRadio=Utilizar nodos Bitcoin Core personalizados +settings.net.warn.usePublicNodes=Si usa la red pública de Bitcoin está expuesto a problemas de privacidad causados por el fallo en el diseño y la implementación del filtro bloom que se utiliza para carteras SPV como BitcoinJ (usado en Bisq). Cualquier nodo completo al que esté conectado podría conocer que todas las direcciones del monedero pertenecen a una entidad.\n\nPor favor, lea más sobre los detalles en: [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\n¿Está seguro de que quiere utilizar los nodos públicos? +settings.net.warn.usePublicNodes.useProvided=No, utilizar nodos proporcionados +settings.net.warn.usePublicNodes.usePublic=Sí, utilizar la red pública +settings.net.warn.useCustomNodes.B2XWarning=¡Por favor, asegúrese de que su nodo Bitcoin es un nodo de confianza Bitcoin Core!\n\nConectar a nodos que no siguen las reglas de consenso puede causar perjuicios a su cartera y causar problemas en el proceso de intercambio.\n\nLos usuarios que se conecten a los nodos que violan las reglas de consenso son responsables de cualquier daño que estos creen. Las disputas causadas por ello se decidirán en favor del otro participante. No se dará soporte técnico a usuarios que ignoren esta advertencia y los mecanismos de protección! +settings.net.warn.invalidBtcConfig=La conexión a la red Bitcoin falló debido a que su configuración es inválida.\n\nSu configuración se ha reestablecido para usar los nodos Bitcoin proporcionados. Necesitará reiniciar la aplicación. +settings.net.localhostBtcNodeInfo=Información complementaria: Bisq busca un nodo local Bitcoin al inicio. Si lo encuentra, Bisq se comunicará a la red Bitcoin exclusivamente a través de él. +settings.net.p2PPeersLabel=Pares conectados +settings.net.onionAddressColumn=Dirección onion +settings.net.creationDateColumn=Establecido +settings.net.connectionTypeColumn=Dentro/Fuera +settings.net.sentDataLabel=Estadísticas de datos enviados +settings.net.receivedDataLabel=Estadísticas de datos recibidos +settings.net.chainHeightLabel=Altura del último bloque BTC +settings.net.roundTripTimeColumn=Tiempo de ida y vuelta +settings.net.sentBytesColumn=Enviado +settings.net.receivedBytesColumn=Recibido +settings.net.peerTypeColumn=Tipo de par +settings.net.openTorSettingsButton=Abrir configuración Tor + +settings.net.versionColumn=Versión +settings.net.subVersionColumn=Sub versión +settings.net.heightColumn=Altura + +settings.net.needRestart=Necesita reiniciar la aplicación para aplicar ese cambio.\n¿Quiere hacerlo ahora? +settings.net.notKnownYet=Aún no conocido... +settings.net.sentData=Datos enviados: {0}, mensajes {1}, mensajes {2} mensajes por segundo +settings.net.receivedData=Datos recibidos: {0}, mensajes {1}, mensajes por segundo {2} +settings.net.chainHeight=Altura de cadena de la DAO Bisq: {0} | Altura de la cadena de pares Bitcoin: {1} +settings.net.ips=[Dirección IP:puerto | host:puerto | dirección onion:puerto] (separado por coma). El puerto puede ser omitido si se utiliza el predeterminado (8333). +settings.net.seedNode=Nodo semilla +settings.net.directPeer=Par (directo) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Par +settings.net.inbound=entrante +settings.net.outbound=saliente +settings.net.reSyncSPVChainLabel=Resincronizar cadena SPV +settings.net.reSyncSPVChainButton=Borrar archivo SPV y resincronizar +settings.net.reSyncSPVSuccess=Está seguro de quere hacer una resincronización SPV? Si procede, la cadena SPV se borrará al siguiente inicio.\n\nDespués de reiniciar puede llevarle un rato resincronizar con la red y solo verá las transacciones una vez se haya completado la resincronización.\n\nDependiendo del número de transacciones y la edad de su monedero, la resincronización puede llevarle hasta algunas horas y consumir el 100% de su CPU. No interrumpa el proceso o tendrá que repetirlo. +settings.net.reSyncSPVAfterRestart=La cadena SPV ha sido borrada. Por favor, sea paciente. Puede llevar un tiempo resincronizar con la red. +settings.net.reSyncSPVAfterRestartCompleted=La resincronización se ha completado. Por favor, reinicie la aplicación. +settings.net.reSyncSPVFailed=No se pudo borrar el archivo de cadena SPV\nError: {0} +setting.about.aboutBisq=Acerca de Bisq +setting.about.about=Bisq es un software de código abierto que facilita el intercambio de bitcoin por monedas nacionales (y otras criptomonedas) a través de una red descentralizada peer-to-peer de modo que se proteja fuertemente la privacidad del usuario. Aprenda más acerca de Bisq en la página web del proyecto. +setting.about.web=Página web de Bisq +setting.about.code=código fuente +setting.about.agpl=Licencia AGPL +setting.about.support=Apoye a Bisq +setting.about.def=Bisq no es una compañía - es un proyecto abierto a la comunidad. Si quiere participar o ayudar a Bisq por favor siga los enlaces de abajo. +setting.about.contribute=Contribuir +setting.about.providers=Proveedores de datos +setting.about.apisWithFee=Bisq usa Índices de Precios Bisq para los precios de mercado de fiat y altcoin, y los Nodos de Mempool de Bisq para la estimación de tasas de minado. +setting.about.apis=Bisq usa los Índices de Precios Bisq para los precios de mercado de fiat y altcoin. +setting.about.pricesProvided=Precios de mercado proporcionados por: +setting.about.feeEstimation.label=Estimación de comisión de minería proporcionada por: +setting.about.versionDetails=Detalles de la versión +setting.about.version=Versión de la aplicación: +setting.about.subsystems.label=Versión de subsistemas: +setting.about.subsystems.val=Versión de red: {0}; Versión de mensajes P2P: {1}; Versión de Base de Datos local: {2}; Versión de protocolo de intercambio {3} + +setting.about.shortcuts=Atajos +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navegar menú principal +setting.about.shortcuts.menuNav.value=Para navegar por el menú principal pulse: 'Ctrl' or 'alt' or 'cmd' con una tecla numérica entre el '1-9' + +setting.about.shortcuts.close=Cerrar Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Cerrar la ventana emergente o ventana de diálogo +setting.about.shortcuts.closePopup.value=Tecla 'ESCAPE' + +setting.about.shortcuts.chatSendMsg=Enviar mensaje en chat de intercambio +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Abrir disputa +setting.about.shortcuts.openDispute.value=Seleccionar intercambios pendientes y pulsar: {0} + +setting.about.shortcuts.walletDetails=Abrir ventana de detalles de monedero + +setting.about.shortcuts.openEmergencyBtcWalletTool=Abrir herramienta de monedero de emergencia para el monedero BTC + +setting.about.shortcuts.openEmergencyBsqWalletTool=Abrir herramienta de monedero de emergencia para el monedero BSQ + +setting.about.shortcuts.showTorLogs=Cambiar nivel de registro para mensajes Tor entre DEBUG y WARN + +setting.about.shortcuts.manualPayoutTxWindow=Abrir ventana para pago manual desde la transacción de depósito multifirma 2de2 + +setting.about.shortcuts.reRepublishAllGovernanceData=Republicar datos de gobernanza DAO (propuestas, votos) + +setting.about.shortcuts.removeStuckTrade=Abrir ventana emergente para moverel intercambio a la pestaña de intercambios abiertos de nuevo +setting.about.shortcuts.removeStuckTrade.value=Seleccionar intercambio fallido y presione: {0} + +setting.about.shortcuts.registerArbitrator=Registrar árbitro (solo mediador/árbitro) +setting.about.shortcuts.registerArbitrator.value=Navegar a cuenta y pulsar: {0} + +setting.about.shortcuts.registerMediator=Registrar mediador (solo mediador/árbitro) +setting.about.shortcuts.registerMediator.value=Navegar a cuenta y pulsar: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir ventana para firmado de edad de cuenta (solo árbitros legacy) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navegar a vista de árbitro legacy y pulsar: {0} + +setting.about.shortcuts.sendAlertMsg=Enviar alerta o actualizar mensaje (actividad privilegiada) + +setting.about.shortcuts.sendFilter=Establecer filtro (actividad privilegiada) + +setting.about.shortcuts.sendPrivateNotification=Enviar notificación privada a los pares (actividad privilegiada) +setting.about.shortcuts.sendPrivateNotification.value=Abrir información de par en el avatar o disputa y pulsar: {0} + +setting.info.headline=Nueva función de autoconfirmación XMR +setting.info.msg=Al vender XMR por XMR puede usar la función de autoconfirmación para verificar que la cantidad correcta de XMR se envió al monedero con lo que Bisq pueda marcar el intercambio como completo, haciendo los intercambios más rápidos para todos.\n\nLa autoconfirmación comprueba que la transacción de XMR en al menos 2 nodos exploradores XMR usando la clave de transacción entregada por el emisor XMR. Por defecto, Bisq usa nodos exploradores ejecutados por contribuyentes Bisq, pero recomendamos usar sus propios nodos exploradores para un máximo de privacidad y seguridad.\n\nTambién puede configurar la cantidad máxima de BTC por intercambio para la autoconfirmación, así como el número de confirmaciones en Configuración.\n\nVea más detalles (incluído cómo configurar su propio nodo explorador) en la wiki Bisq: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Registro de mediador +account.tab.refundAgentRegistration=Registro de agente de devolución de fondos +account.tab.signing=Firmado +account.info.headline=Bienvenido a su cuenta Bisq +account.info.msg=Aquí puede añadir cuentas de intercambio para monedas nacionales y altcoins y crear una copia de su cartera y datos de cuenta.\n\nUna nueva cartera Bitcoin fue creada la primera vez que inició Bisq.\n\nRecomendamos encarecidamente que escriba sus palabras de la semilla de la cartera Bitcoin (mire pestaña arriba) y considere añadir una contraseña antes de enviar fondos. Los ingresos y retiros de Bitcoin se administran en la sección \"Fondos\".\n\nNota de privacidad y seguridad: Debido a que Bisq es un exchange descentralizado, todos sus datos se guardan en su ordenador. No hay servidores, así que no tenemos acceso a su información personal, sus saldos, o incluso su dirección IP. Datos como número de cuenta bancaria, direcciones altcoin y Bitcoin, etc son solo compartidos con su par de intercambio para completar intercambios iniciados (en caso de una disputa el mediador o árbitro verá los mismos datos que el par de intercambio). + +account.menu.paymentAccount=Cuentas de moneda nacional +account.menu.altCoinsAccountView=Cuentas de altcoin +account.menu.password=Contraseña de monedero +account.menu.seedWords=Semilla del monedero +account.menu.walletInfo=Información de monedero +account.menu.backup=Copia de seguridad +account.menu.notifications=Notificaciones + +account.menu.walletInfo.balance.headLine=Balances de monedero +account.menu.walletInfo.balance.info=Esto muestrta el balance interno del monedero, incluyendo transacciones no confirmadas.\nPara BTC, el balance interno de monedero mostrado abajo debe cuadrar con la suma de balances 'Disponible' y 'Reservado' mostrado arriba a la derecha de esta ventana. +account.menu.walletInfo.xpub.headLine=Claves centinela (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} monedero +account.menu.walletInfo.path.headLine=ruta HD keychain +account.menu.walletInfo.path.info=Si importa las palabras semilla en otro monedero (como Electru), tendrá que definir la ruta. Esto debería hacerse solo en casos de emergencia, cuando pierda acceso a el monedero Bisq y el directorio de datos.\nTenga en cuenta que gastar fondos desde un monedero no-Bisq puede estropear la estructura de datos interna de Bisq asociado a los datos de monedero, lo que puede llevar a intercambios fallidos.\n\nNUNCA envíe BSQ desde un monedero no-Bisq, ya que probablemente llevará a una transacción inválida de BSQ y le hará perder sus BSQ. + +account.menu.walletInfo.openDetails=Mostrar detalles en bruto y claves privadas + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Llave pública + +account.arbitratorRegistration.register=Registrar +account.arbitratorRegistration.registration={0} registro +account.arbitratorRegistration.revoke=Revocar +account.arbitratorRegistration.info.msg=Por favor, tenga en cuenta que necesita estar disponible 15 días después la revocación pues puede haber intercambios que le usen como {0}. El periodo máximo de intercambio permitido es de 8 días y el proceso de disputa puede llevar hasta 7 días.l +account.arbitratorRegistration.warn.min1Language=Necesita especificar al menos 1 idioma.\nHemos añadido el idioma por defecto para usted. +account.arbitratorRegistration.removedSuccess=Ha eliminado su registro de la red Bisq con éxito. +account.arbitratorRegistration.removedFailed=No se pudo eliminar el registro.{0} +account.arbitratorRegistration.registerSuccess=Se ha registrado con éxito en la red Bisq. +account.arbitratorRegistration.registerFailed=No se pudo completar el registro.{0} + +account.altcoin.yourAltcoinAccounts=Sus cuentas de altcoin +account.altcoin.popup.wallet.msg=Por favor, asegúrese que sigue los requisitos para el uso de carteras {0} como se describe en la página web {1}.\nUsando carteras desde exchanges centralizados donde (a) usted no controla sus claves o (b) que no usan monederos compatibles con el software es un riesgo: ¡puede llevar a la perdida de los fondos intercambiados!\nEl mediador o árbitro no es un especialista en {2} y no puede ayudar en tales casos. +account.altcoin.popup.wallet.confirm=Entiendo y confirmo que sé qué monedero tengo que utilizar. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Para intercambiar UPX en bisq es preciso que usted entienda y cumpla los siguientes requerimientos:\n\nPara enviar upx es preciso que use el monedero oficial de uPlexa GUI o el de uPlexa CLI con la opción "store-tx-info" habilitada (predeterminada en las nuevas versiones). Por favor, asegúrese de poder acceder a las llaves tx, ya que podrían requerirse en caso de disputa.\nuplexa-wallet-cli (utilice el comando get_tx_key)\nuplexa-wallet-gui (vaya a la pestaña historial y haga clic en el botón (P) para prueba de pago).\nEn los exploradores de bloques normales la transferencia no es verificable.\nUsted necesita proporcionar al árbitro los siguientes datos en caso de disputa:\n- La llave privada tx\n- El hash de la transacción\n- La dirección publica de recepción\nSi no proporciona los datos arriba señalados, o si utilizó una cartera incompatible, perderá la disputa.\nLa parte que envía UPX es responsable de proporcionar las evidencias de la transferencia de UPX al árbitro en caso de disputa.\nNo se requiere ID de pago, solo la dirección pública normal.\nSi no está seguro sobre el proceso, visite el canal de conflictos de uPlexa https://discord.gg/vhdNSrV) o el Chat de Telegram de uPlexa (https://t.me/uplexaOfficial) para encontrar más información. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=El ARQ de intercambio de Bisq requiere que entienda y cumpla los siguientes requerimientos:\n\nPara el ARQ de envío, necesita utilizar la cartera oficial ArQmA GUI o una cartera ArQmA CLI con la opción store-tx-info habilitada (predeterminada en nuevas versiones). Por favor asegúrese de poder acceder a la llave tx ya que podría ser requerido en caso de disputa.\narqma-wallet-cli (utilice el comando get_tx_key)\narqma-wallet-gui (vaya a la pestaña historial y pulse el botón (P) para prueba de pago)\n\nEn los exploradores de bloques normales la transferencia no es verificable.\n\nNecesita proveer al mediador o al árbitro los siguientes datos en caso de disputa:\n- El tx de la llave privada\n- El hash de transacción\n- La dirección publica de recepción\n\nNo proveer los datos arriba señalados, o si utilizó una cartera incompatible, resultará en perder la disputa. La parte ARQ que envía es responsable de proveer verificación de la transferencia ARQ al mediador o al árbitro en caso de disputa.\n\nNo se requiere ID de pago, solo la dirección pública normal.\nSi no está seguro sobre el proceso visite el canal ArQmA en discord (https://discord.gg/s9BQpJT) o el foro de ArQmA (https://labs.arqma.com) para encontrar más información. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Intercambiar XMR en Bisq requiere entender y cumplir los siguientes requisitos\n\nPruebas de pago:\n\n\nSi está vendiendo XMR, debe ser capaz de entregar la siguiente información en caso de disputa:\n\n- La clave de transacción (Tx key, Tx Secret Key o Tx Private Key)\n- La ID de transacción (Tx ID o Tx Hash)\n- La dirección de destino (recipient's address)\n\nVea la wiki para detalles sobre dónde encontrar esta información en los principales monederos XMR:\nhttps://bisq.wiki/Trading_Monero#Proving_payments\n\n\nNo entregar estos datos de transacción resultará en pérdida de la disputa.\n\nTenga en cuenta que Bisq ahora ofrece confirmación automática de transacciones XMR para realizar intercambios más rápido, pero necesita habilitarlo en Configuración. \n\nVea la wiki para más información sobre la función de autoconfirmación.\n[HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Para intercambiar MSR en Bisq es necesario que usted entienda y cumpla los siguientes requisitos:\n\nPara enviar MSR es necesario que use el monedero oficial de Masari monedero GUI o Masari monedero CLI con la opción "store-tx-info" habilitada (predeterminada en las nuevas versiones) o el monedero oficial Masari Web (https://wallet.getmasari.org). Por favor, asegúrese de poder acceder a las llaves tx, ya que podrían requerirse en caso de alguna disputa. \nmasari-wallet-cli (utilice el comando get_tx_key)\nmasari-wallet-gui (vaya a la pestaña historial y haga clic en el botón (P) para prueba de pago). \n\nMonedero Masari Web (vaya a Cuenta -> historial transacción y vea los detalles de su transacción enviada.)\n\nPuede lograr verificación de la transacción dentro del monedero:\nmasari-wallet-cli : usando el comando (check_tx_key).\nmasari-wallet-gui: vaya a la pestaña Avanzado > Comprobar/Revisar.\nVerificación de transacción también se puede lograr desde el explorador de bloques\nAbrir explorador de bloques (https://explorer.getmasari.org), use la barra de búsqueda para buscar el hash de transacción. \nCuando encuentre su transacción, navegue hacia el final donde dice "Comprobar Envío" y rellene los detalles necesarios. \nUsted necesita compartir esta información con el mediador o árbitro en caso de disputa:\n- La clave privada tx\n- El hash de transacción\n- La dirección pública del que recibe\n\nSi no proporciona los datos arriba señalados, o si utilizó una cartera incompatible, perderá la disputa. El que envía MSR es responsable de entregar la verificación de transferencia al mediador o árbitro en caso de disputa.\n\nNo se requiere el ID de pago, sólo la dirección pública normal.\nSi no está seguro sobre el proceso, pida ayuda visitando el canal oficial de Masari en Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Intercambiar BLUR en Bisq requiere que entienda y cumpla los siguientes requisitos:\n\nPara enviar BLUR debe usar Blur Network CLI o la cartera GUI. \n\nSi utiliza la cartera CLI, un hash de transacción (tx ID) se mostrará después de hacer una transferencia. Deberá guardar esta información. Inmediatamente después de enviar la transferencia, deberá utilizar el comando 'get_tx_key' para obtener la clave privada de transacción. Si no realiza este paso, es posible que no pueda obtener la llave mas tarde. \n\nSi utiliza la cartera Blur Network GUI, la clave privada de transacción y el ID de transacción puede ser encontrada convenientemente en la pestaña "Historia". Inmediatamente después del envío, localice la transacción de interés. Pulse en el símbolo "?" en la esquina inferior derecha de la caja que contiene la transacción. Debe guardar esta información. \n\nEn caso de que sea necesaria una disputa, deberá presentar lo siguiente al mediador o al árbitro: 1.) el ID de transacción, 2.) la clave privada de transacción, y 3.) la dirección de destino. El mediador o árbitro entonces verificará la transferencia BLUR utilizando el Visor de Transacción Blur (https://blur.cash/#tx-viewer).\n\nSi no se proporciona la información requerida al mediador o al árbitro se perderá el caso de disputa. En todos los casos de disputa, el remitente BLUR asume el 100% de la responsabilidad en la verificación de las transacciones a un mediador o a un árbitro.. \n\nSi no comprende estos requisitos, no realice transacciones en Bisq. Primero, busque ayuda en Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Para intercambiar Solo en Bisq es necesario que usted entienda y cumpla los siguientes requisitos:\n\nPara enviar Solo es necesario que use el monedero Solo Network Monedero CLI\n\nSi usted usa el monedero CLI el hash de transacción (tx ID) aparecerá después de la transferencia. Usted debe guardar esta información. Inmediatamente después de mandar de la transferencia, usted debe usar el comando 'get_tx_key' para guardar la clave privada de transacción. Si usted no guarda esta información, es posible que no podrá coleccionar esta clave después .\n\nUsted necesita compartir esta información con el mediador o árbitro en caso de disputa:\n1.) el ID de transacción, 2.) la clave privada de transacción y 3.) la dirección pública de recepción. El mediador o árbitro podrá verificar la transferencia de Solo usando el explorador de bloques Solo, buscando la transacción y usando la función "Prove sending" (https://explorer.minesolo.com/).\n\nSi no proporciona los datos arriba señalados al mediador o árbitro perderá la disputa. En todos los casos de disputa, el emisor de Solo es 100% responsable de entregar la verificación de envío al mediador o árbitro.\n\nSi no está seguro o no entiendo estos requisitos, no haga intercambio/transacción en Bisq. Primero, pida ayuda visitando el canal oficial de Solo Network en Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Intercambiar CASH2 en Bisq requiere que usted comprenda y cumpla con los siguientes requisitos:\n\nPara enviar CASH2, debe utilizar la cartera Cash2 versión 3 o superior. \n\nDespués de que se envía una transacción, se mostrará el ID de transacción. Debe guardar esta información. Inmediatamente después de enviar la transacción, debe usar el comando 'getTxKey' en simplewallet para recuperar la llave secreta de la transacción. \n\nEn el caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o a un árbitro: 1) ID de transacción, 2) la llave secreta de la transacción y 3) la dirección Cash2 del destinatario. El mediador o el árbitro entonces verificará la transferencia CASH2 usando el Explorador de Bloques Cash2 (https://blocks.cash2.org).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de CASH2 lleva el 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o a un árbitro.\n\nSi no comprende estos requisitos, no realice transacciones en Bisq. Primero, busque ayuda en Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Intercambiar Qwertycoin en Bisq requiere que usted comprenda y cumpla los siguientes requisitos:\n\nPara enviar QWC, debe utilizar la cartera oficial QWC versión 5.1.3 o superior. \n\nDespués de que se envía una transacción, se mostrará el ID de transacción. Debe guardar esta información. Inmediatamente después de enviar la transacción, debe usar el comando 'get_Tx_Key' en simplewallet para recuperar la llave secreta de la transacción.\n\nEn el caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o un árbitro: 1) ID de transacción, 2) la llave secreta de la transacción y 3) la dirección QWC del destinatario. El mediador o el árbitro entonces verificará la transferencia QWC usando el Explorador de Bloques QWC (https://explorer.qwertycoin.org).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de QWC lleva el 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o un árbitro.\n\nSi no comprende estos requisitos, no realice transacciones en Bisq. Primero, busque ayuda en QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Intercambiar Dragonglass on Bisq requiere que usted comprenda y cumpla los siguientes requisitos:\n\nDebido a la privacidad que Dragonglass provee, una transacción no es verificable en la blockchain pública. Si es requerido, usted puede probar su pago a través del uso de su TXN-Private-Key.\nLa TXN-clave privada es una one-time clave generada automáticamente para cada transacción que solo puede ser accedida desde dentro de su cartera DRGL.\nO por DRGL-wallet GUI (dentro del diálogo de detalles de transacción) o por la Dragonglass CLI simplewallet (usando el comando "get_tx_key").\n\nDRGL version 'Oathkeeper' y superior son REQUERIDAS para ambos.\n\nEn caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o un árbitro:\n- La TXN-clave privada\n- El hash de transacción\n- La dirección pública del destinatario\n\nLa verificación de pago puede ser hecha usando los datos de arriba en (http://drgl.info/#check_txn).\n\nNo proporcionar los datos anteriores, o si usted usó una cartera incompatible, resultará en la pérdida del caso de disputa. El Dragonglass remitente es responsable de proveer verificación de la transferencia DRGL al mediador o al árbitro en caso de una disputa. El uso de PaymentID no es requerido.\n\nSi usted no está seguro sobre cualquier parte de este proceso, visite Dragonglass en Discord (http://discord.drgl.info) para ayuda. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Al usar Zcash solo puede usar las direcciones transparentes (que comienzan con t), no las direcciones-z (privadas), porque el mediador o el árbitro no sería capaz de verificar la transacción con direcciones-z. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Al usar Zcoin puede usar únicamente las direcciones transparentes (trazables) y no las no-trazables, porque el mediador o el árbitro no sería capaz de verificar la transacción con direcciones no trazables en el explorador de bloques. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requiere un proceso interactivo entre el emisor y el receptor para crear la transacción. Asegúrese de seguir las instrucciones de la web del proyecto GRIN para enviar y recibir GRIN con seguridad (el receptor necesita estar en línea o al menos estar en línea durante un cierto periodo de tiempo).\nBisq solo soporta el Grinbox (Wallet713) monedero URL formato.\n\nEl emisor GRIN requiere proveer prueba que ha enviado GRIN correctamente. Si el monedero no puede proveer esa prueba, una posible controversia será resuelta a favor del GRIN receptor. Por favor asegúrese que usa el último Grinbox software que soporta la prueba de transacción y que usted entiende el proceso de transferir y recibir GRIN así como la forma de crear la prueba.\n\nVer https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para más información sobre la herramienta de prueba Grinbox. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM requiere un proceso interactivo entre el emisor y el receptor para crear la transacción.\n\nAsegúrese de seguir la instrucciones de la página web del proyecto BEAM para enviar y recibir BEAM con seguridad (el receptor necesita estar el línea o por lo menos estar en línea durante cierto periodo de tiempo).\n\nEl emisor BEAM requiere proveer prueba de que envió BEAM correctamente. Asegúrese de usar software de monedero que pueda producir tal prueba. Si el monedero no provee la prueba, una posible controversia será resuelta en favor del BEAM receptor. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Intercambiar ParsiCoin en Bisq requiere que usted comprenda y cumpla con los siguientes requisitos:\n\nPara enviar PARS, debe usar la cartera oficial ParsiCoin version 3.0.0 o superior.\n\nPuede comprobar su hash de transacción y la clave de la transacción en la sección de Transacciones en su cartera GUI (ParsiPay). Necesita pulsar el botón derecho en la transacción y pulsar mostrar detalles.\n\nEn el caso de que sea necesario arbitraje, debe presentar lo siguiente al mediador o al árbitro: 1) El hash de transacción, 2) La llave de la transacción, y 3) La dirección del receptor PARS. El árbitro entonces verificará la transferencia PARS usando el ParsiCoin Explorador de Bloques (http://explorer.parsicoin.net/#check_payment).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de ParsiCoin lleva 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o un árbitro\n\nSi no comprende estos requisitos, no realice transacciones en Bisq. Primero, busque ayuda en ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=Para intercambiar blackcoins quemados. usted necesita saber lo siguiente:\n\nBlackcoins quemados son indestructibles. Para intercambiarlos en Bisq, los guiones de output tienen que estar en la forma: OP_RETURN OP_PUSHDATA, seguidos por bytes de datos asociados que, después de codificarlos en hexadecimal, construyen direcciones. Por ejemplo, blackcoins quemados con una dirección 666f6f ("foo" en UTF-8) tendrán el siguiente guion:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPara crear blackcoins quemados, uno puede usar el comando RPC "quemar" disponible en algunas carteras.\n\nPara posibles casos de uso, uno puede mirar en https://ibo.laboratorium.ee .\n\nComo los blackcoins quemados son undestructibles, no pueden ser revendidos. "Vender" blackcoins quemados significa quemar blackcoins comunes (con datos asociados igual a la dirección de destino).\n\nEn caso de una disputa, el vendedor BLK necesita proveer el hash de transacción. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Comerciar L-BTC en Bisq requiere que entienda lo siguiente:\n\nAl recibir L-BTC de un intercambio en Bisq, no puede usar la app de monedero móvil Blockstream Green Wallet o un exchange/monedero que custodie sus fondos. Solo debe recibir L-BTC en el monedero Liquid Elements Core, u otro monedero L-BTC que le permita obtener la clave cegadora para su dirección L-BTC cegada.\n\nEn caso de ser necesaria mediación, o si se lleva a cabo una disputa, debe entregar la clave cegadora para su dirección receptora de L-BTC al mediador o agente de devolución de fondos Bisq para que verifique los detalles de su Transacción Confidencial en su nodo completo Elements Core.\n\nSi no entrega la información requerida al mediador o al agente de devolución de fondos se resolverá en una pérdida del caso en disputa. En todos los casos de disputa, el receptor de L-BTC es responsable del 100% de la carga de aportar la prueba criptográfica al mediador o agente de devolución de fondos.\n\nSi no entiende estos requerimientos, no intercambio L-BTC en Bisq. + +account.fiat.yourFiatAccounts=Sus cuentas de moneda nacional: + +account.backup.title=Copia de seguridad del monedero +account.backup.location=Ubicación de la copia de seguridad +account.backup.selectLocation=Seleccionar localización de la copia de seguridad +account.backup.backupNow=Hacer copia de seguridad ahora (la copia de seguridad no está encriptada!) +account.backup.appDir=Directorio de datos de aplicación +account.backup.openDirectory=Abrir directorio +account.backup.openLogFile=Abrir archivo de registro +account.backup.success=Copia de seguridad guardada con éxito en:\n{0} +account.backup.directoryNotAccessible=El directorio que ha elegido no es accesible. {0} + +account.password.removePw.button=Eliminar contraseña +account.password.removePw.headline=Eliminar protección por contraseña del monedero +account.password.setPw.button=Establecer contraseña +account.password.setPw.headline=Establecer protección por contraseña del monedero +account.password.info=Con protección por contraseña necesitará introducir su contraseña en el arranque de la aplicación, cuando retire bitcoin de su monedero, y cuando restaure su monedero desde las palabras semilla. + +account.seed.backup.title=Copia de seguridad de palabras semilla del monedero +account.seed.info=Por favor apunte en un papel tanto las palabras semilla del monedero como la fecha! Puede recuperar su monedero en cualquier momento con las palabras semilla y la fecha.\nLas mismas palabras semilla se usan para el monedero BTC como BSQ\n\nDebe apuntar las palabras semillas en una hoja de papel. No la guarde en su computadora.\n\nPor favor, tenga en cuenta que las palabras semilla no son un sustituto de la copia de seguridad.\nNecesita hacer la copia de seguridad de todo el directorio de aplicación en la pantalla \"Cuenta/Copia de Seguridad\" para recuperar un estado de aplicación válido y los datos.\nImportar las palabras semilla solo se recomienda para casos de emergencia. La aplicación no será funcional sin una buena copia de seguridad de los archivos de la base de datos y las claves! +account.seed.backup.warning=Por favor tenga en cuenta que las palabras semilla NO SON un sustituto de la copia de seguridad.\nTiene que crear una copia de todo el directorio de aplicación desde la pantalla \"Cuenta/Copia de seguridad\ para recuperar el estado y datos de la aplicación.\nImportar las palabras semilla solo se recomienda para casos de emergencia. La aplicación no funcionará sin una copia de seguridad adecuada de las llaves y archivos de sistema!\n\nVea la página wiki [HYPERLINK:https://bisq.wiki/Backing_up_application_data] para más información. +account.seed.warn.noPw.msg=No ha establecido una contraseña de cartera que proteja la visualización de las palabras semilla.\n\n¿Quiere que se muestren las palabras semilla? +account.seed.warn.noPw.yes=Sí, y no preguntar de nuevo +account.seed.enterPw=Introducir contraseña para ver las palabras semilla +account.seed.restore.info=Por favor haga una copia de seguridad antes de aplicar la restauración desde las palabras semilla. Tenga en cuenta que la restauración de cartera solo es para casos de emergencia y puede causar problemas con la base de datos interna del monedero.\nNo es el modo de aplicar una restauración de copia de seguridad! Por favor use una copia de seguridad desde el archivo de directorio de la aplicación para restaurar un estado de aplicación anterior.\n\nDespués de restaurar la aplicación se cerrará automáticamente. Después de reiniciar la aplicacion se resincronizará con la red Bitcoin. Esto puede llevar un tiempo y consumir mucha CPU, especialemente si la cartera es antigua y tiene muchas transacciones. Por favor evite interrumpir este proceso, o podría tener que borrar el archivo de la cadena SPV de nuevo o repetir el proceso de restauración. +account.seed.restore.ok=Ok, adelante con la restauración y el apagado de Bisq. + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Configuración +account.notifications.download.label=Descargar aplicación móvil +account.notifications.waitingForWebCam=Esperando a la cámara web... +account.notifications.webCamWindow.headline=Escanear código QR desde el teléfono +account.notifications.webcam.label=Usar webcam +account.notifications.webcam.button=Escanear código QR +account.notifications.noWebcam.button=No tengo cámara web +account.notifications.erase.label=Eliminar notificaciones en el teléfono +account.notifications.erase.title=Limpiar notificaciones +account.notifications.email.label=Emparejar token +account.notifications.email.prompt=Introducir token de emparejamiento recibido por emai +account.notifications.settings.title=Configuración +account.notifications.useSound.label=Reproducir sonido de notificación en el teléfono +account.notifications.trade.label=Recibir mensajes de intercambio +account.notifications.market.label=Recibir alertas de oferta +account.notifications.price.label=Recibir alertas de precio +account.notifications.priceAlert.title=Alertas de precio +account.notifications.priceAlert.high.label=Notificar si el precio de BTC está por encima +account.notifications.priceAlert.low.label=Notificar si el precio de BTC está por debajo +account.notifications.priceAlert.setButton=Establecer alerta de precio +account.notifications.priceAlert.removeButton=Eliminar alerta de precio +account.notifications.trade.message.title=Estado de intercambio cambiado +account.notifications.trade.message.msg.conf=La transacción de depósito para el intercambio con ID {0} está confirmado. Por favor abra su aplicación Bisq e inicie el pago. +account.notifications.trade.message.msg.started=El comprador de BTC ha iniciado el pago para el intercambio con ID {0} +account.notifications.trade.message.msg.completed=El intercambio con ID {0} se ha completado. +account.notifications.offer.message.title=Su oferta fue tomada. +account.notifications.offer.message.msg=Su oferta con ID {0} se ha tomado. +account.notifications.dispute.message.title=Nuevo mensaje de disputa +account.notifications.dispute.message.msg=Ha recibido un mensaje de disputa para el intercambio con ID {0} + +account.notifications.marketAlert.title=Altertas de oferta +account.notifications.marketAlert.selectPaymentAccount=Ofertas que concuerden con la cuenta de pago +account.notifications.marketAlert.offerType.label=Tipo de oferta en la que estoy interesado +account.notifications.marketAlert.offerType.buy=Ofertas de compra (quiero vender BTC) +account.notifications.marketAlert.offerType.sell=Ofertas de venta (quiero comprar BTC) +account.notifications.marketAlert.trigger=Distancia de precio en la oferta (%) +account.notifications.marketAlert.trigger.info=Con distancia de precio establecida, solamente recibirá una alerta cuando se publique una oferta que satisfaga (o exceda) sus requerimientos. Por ejemplo: quiere vender BTC, pero solo venderá con un 2% de premium sobre el precio de mercado actual. Configurando este campo a 2% le asegurará que solo recibirá alertas de ofertas con precios que estén al 2% (o más) sobre el precio de mercado actual. +account.notifications.marketAlert.trigger.prompt=Porcentaje de distancia desde el precio de mercado (e.g. 2.50%, -0.50%, etc) +account.notifications.marketAlert.addButton=Añadir alerta de oferta +account.notifications.marketAlert.manageAlertsButton=Gestionar alertas de oferta +account.notifications.marketAlert.manageAlerts.title=Gestionar alertas de oferta +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Cuenta de pago +account.notifications.marketAlert.manageAlerts.header.trigger=Precio de ejecución +account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta +account.notifications.marketAlert.message.title=Alerta de oferta +account.notifications.marketAlert.message.msg.below=por debajo +account.notifications.marketAlert.message.msg.above=por encima +account.notifications.marketAlert.message.msg=Una nueva oferta "{0} {1}" con el precio {2} ({3} {4} precio de mercado) y método de pago "{5}" se publicó en el libro de ofertas de Bisq.\nID de la oferta: {6}. +account.notifications.priceAlert.message.title=Alerta de precio para {0} +account.notifications.priceAlert.message.msg=Su alerta de precio se ejecutó. El precio actual de {0} es {1} {2} +account.notifications.noWebCamFound.warning=No se ha encontrado una webcam.\n\nPor favor use la opción de email para enviar el token y clave de encriptación desde su teléfono móvil a la aplicación Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=El precio superior debe ser mayor que el precio inferior. +account.notifications.priceAlert.warning.lowerPriceTooHigh=El precio inferior debe ser más bajo que el precio superior. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Hechos y gráficos +dao.tab.bsqWallet=Monedero BSQ +dao.tab.proposals=Gobernanza +dao.tab.bonding=Garantías +dao.tab.proofOfBurn=Comisión de listado de activos/Prueba de quemado +dao.tab.monitor=Monitor de red +dao.tab.news=Noticias + +dao.paidWithBsq=pagado con BSQ +dao.availableBsqBalance=Disponible para gastar (verificados + outputs de cambio no confirmados) +dao.verifiedBsqBalance=Balance de todas las UTXOs verificadas +dao.unconfirmedChangeBalance=Balance de todos los outputs de cambio no confirmados +dao.unverifiedBsqBalance=Balance de todas las transacciones no verificadas (esperando una confirmación en bloque) +dao.lockedForVoteBalance=Usado para votar +dao.lockedInBonds=Bloqueado en garantías +dao.availableNonBsqBalance=Saldo no-BSQ disponible (BTC) +dao.reputationBalance=Valor de mérito (no se puede gastar) + +dao.tx.published.success=Su transacción ha sido publicada satisfactoriamente. +dao.proposal.menuItem.make=Hacer propuesta +dao.proposal.menuItem.browse=Consultar propuestas abiertas +dao.proposal.menuItem.vote=Votar propuestas +dao.proposal.menuItem.result=Resultados de votaciones +dao.cycle.headline=Ciclo de votación +dao.cycle.overview.headline=Resumen del ciclo de votación +dao.cycle.currentPhase=Fase actual +dao.cycle.currentBlockHeight=Altura de bloque actual +dao.cycle.proposal=Fase de propuesta +dao.cycle.proposal.next=Siguiente fase de propuesta +dao.cycle.blindVote=Fase de votación secreta +dao.cycle.voteReveal=Fase de revelado de voto +dao.cycle.voteResult=Resultado de votación +dao.cycle.phaseDuration={0} bloques (≈{1}); Bloque {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Bloque {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Transacción de revelado de voto publicada +dao.voteReveal.txPublished=Su transacción de revelado de voto con ID de transacción {0} se publicó satisfactoriamente.\n\nEsto ocurre automáticamente por el software si ha participado en la votación DAO. + +dao.results.cycles.header=Ciclos +dao.results.cycles.table.header.cycle=Ciclo +dao.results.cycles.table.header.numProposals=Propuestas +dao.results.cycles.table.header.voteWeight=Peso de voto +dao.results.cycles.table.header.issuance=Emisión + +dao.results.results.table.item.cycle=Ciclo {0} comenzó: {1} + +dao.results.proposals.header=Propuestas del ciclo seleccionado +dao.results.proposals.table.header.nameLink=Nombre/enlace +dao.results.proposals.table.header.details=Detalles +dao.results.proposals.table.header.myVote=Mi voto +dao.results.proposals.table.header.result=Resultado de votación +dao.results.proposals.table.header.threshold=Umbral +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Resultados de votación para la propuesta seleccionada + +dao.results.exceptions=Excepción(es) del resultado de votación + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Indefinido + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=Comisión de creador BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=Comisión de tomador BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Comisión de creador BSQ mínima +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Comisión de tomador BSQ mínima +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Comisión de creador BTC +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Comisión de tomador BTC +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Comisión de creador BTC mínima +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Comisión de tomador mínima BTC +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Comisión de propuesta en BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Comisión de votación en BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Cantidad mínima BSQ para solicitud de compensación +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Cantidad máxima BSQ para solicitud de compensación +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Cantidad mínima BSQ para petición de reembolso +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Cantidad máxima BSQ para petición de reembolso + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Quorum requerido en BSQ para propuesta genérica +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Quorum requerido en BSQ para solicitud de compensación +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Quorum requerido en BSQ para solicitud de reembolso +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Quorum requerido en BSQ para cambiar un parámetro +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Quorum requerido en BSQ para eliminar un activo +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Quorum requerido en BSQ para una petición de confiscación +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Quorum requerido en BSQ para peticiones de rol en garantía + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Umbral requerido en % para una propuesta genérica +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Umbral requerido en % para una solicitud de compensación +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Umbral requerido en % para una solicitud de reembolso +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Umbral requerido en % para cambiar un parámetro +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Umbral requerido en % para eliminar un activo +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Umbral requerido en % para una solicitud de confiscación +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Umbral requerido en % para solicitudes de rol en garantía + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Dirección BTC del receptor + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Tasa de listado de activo por día +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Volumen mínimo de intercambio para activos + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Tiempo límite para un pago de transacción alternativo +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Comisión de arbitraje en BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Límite máximo de intercambio en BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Factor de unidad de rol en garantía en BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Límite de emisión por ciclo en BSQ + +dao.param.currentValue=Valor actual: {0} +dao.param.currentAndPastValue=Valor actual: {0} (Valor cuando la propuesta fue hecha: {1}) +dao.param.blocks={0} bloques + +dao.results.invalidVotes=Hubo votos inválidos en ese ciclo. Eso pueda ocurrir si un voto no fue bien distribuido en la red Bisq.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Fase de propuesta +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Pausa 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Fase de votación secreta +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Pausa 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Fase de revelado de voto +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Pausa 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Fase de resultado + +dao.results.votes.table.header.stakeAndMerit=Peso de voto +dao.results.votes.table.header.stake=Cantidad +dao.results.votes.table.header.merit=Conseguido +dao.results.votes.table.header.vote=Votar + +dao.bond.menuItem.bondedRoles=Roles en garantía +dao.bond.menuItem.reputation=Reputación con garantía +dao.bond.menuItem.bonds=Garantías + +dao.bond.dashboard.bondsHeadline=BSQ en garantías +dao.bond.dashboard.lockupAmount=Fondos bloqueados: +dao.bond.dashboard.unlockingAmount=Desbloqueando fondos (espere hasta que el tiempo de bloqueo finalice) + + +dao.bond.reputation.header=Bloquear una garantía para reputación +dao.bond.reputation.table.header=Mis garantías de reputación +dao.bond.reputation.amount=Cantidad de BSQ a bloquear +dao.bond.reputation.time=Tiempo de desbloqueo en bloques +dao.bond.reputation.salt=Salt +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Bloquear +dao.bond.reputation.lockup.headline=Confirmar transacción de bloqueo +dao.bond.reputation.lockup.details=Cantidad bloqueada: {0}\nTiempo de desbloqueo: {1} bloque(s) (≈{2})\n\nComisión de minado: {3} ({4} Satoshis/vbyte)\nTamaño de la transacción: {5} Kb\n\n¿Seguro que quiere proceder? +dao.bond.reputation.unlock.headline=Confirmar desbloqueo de transacción +dao.bond.reputation.unlock.details=Cantidad de desbloqueo: {0}\nTiempo de desbloqueo: {1} bloque(s) (≈{2})\n\nComisión de minado: {3} ({4} Satoshis/vbyte)\nTamaño de transacción: {5} Kb\n\n¿Seguro que quiere proceder? + +dao.bond.allBonds.header=Todas las garantías + +dao.bond.bondedReputation=Reputación en garantía +dao.bond.bondedRoles=Roles en garantía + +dao.bond.details.header=Detalles del rol +dao.bond.details.role=Rol +dao.bond.details.requiredBond=Garantía BSQ requerida +dao.bond.details.unlockTime=Tiempo de desbloqueo en bloques +dao.bond.details.link=Enlace a la descripción del rol +dao.bond.details.isSingleton=Puede tomarse por múltiples titulares +dao.bond.details.blocks={0} bloques + +dao.bond.table.column.name=Nombre +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Tipo de garantía +dao.bond.table.column.details=Detalles +dao.bond.table.column.lockupTxId=Tx ID de bloqueo +dao.bond.table.column.bondState=Estado de garantía +dao.bond.table.column.lockTime=Tiempo de desbloqueo +dao.bond.table.column.lockupDate=Fecha de bloqueo + +dao.bond.table.button.lockup=Bloquear +dao.bond.table.button.unlock=Desbloquear +dao.bond.table.button.revoke=Revocar + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Aún no en garantía +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Bloqueo pendiente +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Garantía bloqueada +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Desbloqueo pendiente +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Desbloqueo de tx confirmada +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Desbloqueando garantía +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Garantía desbloqueada +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Garantía confiscada + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Rol en garantía +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Reputación en garantía + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Admin Github +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Admin foro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Admin Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Administrador de Keybase +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Admin Youtube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Mantenedor Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Mantenedor BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Mantenedor Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Operador de la web +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Operador Foro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Operador de nodo semilla +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Operador nodo de precio +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Operador de nodo Bitcoin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Operador de mercados +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Operador de explorador +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Operador transmisión de notificaciones móvil +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Titular del nombre de dominio +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Admin DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediador +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Árbitro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Propietario de dirección de donaciones BTC + +dao.burnBsq.assetFee=Listado de activos +dao.burnBsq.menuItem.assetFee=Listado de comisiones de activo +dao.burnBsq.menuItem.proofOfBurn=Prueba de quemado +dao.burnBsq.header=Comisión por listar de activo +dao.burnBsq.selectAsset=Seleccionar activo +dao.burnBsq.fee=Comisión +dao.burnBsq.trialPeriod=Periodo de prueba +dao.burnBsq.payFee=Pagar comisión +dao.burnBsq.allAssets=Todos los activos +dao.burnBsq.assets.nameAndCode=Nombre del activo +dao.burnBsq.assets.state=Estado +dao.burnBsq.assets.tradeVolume=Volumen de intercambio +dao.burnBsq.assets.lookBackPeriod=Periodo de verificación +dao.burnBsq.assets.trialFee=Tasa de periodo de prueba +dao.burnBsq.assets.totalFee=Comisiones totales pagadas +dao.burnBsq.assets.days={0} días +dao.burnBsq.assets.toFewDays=La comisión de activo es demasiado baja. La cantidad mínima de días para el periodo de prueba es {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=En periodo de prueba +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Intercambiado activamente +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Delistados debido a inactividad +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Eliminado por votación + +dao.proofOfBurn.header=Prueba de quemado +dao.proofOfBurn.amount=Cantidad +dao.proofOfBurn.preImage=Pre-imagen +dao.proofOfBurn.burn=Quemar +dao.proofOfBurn.allTxs=Todas las transacciones de prueba de quemado +dao.proofOfBurn.myItems=Mis transacciones de prueba de quemado +dao.proofOfBurn.date=Fecha +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transacciones +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Firmar un mensaje con clave de transacción de quemado +dao.proofOfBurn.verify.window.title=Verificar un mensaje con clave de transacción de quemado. +dao.proofOfBurn.copySig=Copiar firma al portapapeles +dao.proofOfBurn.sign=Firma +dao.proofOfBurn.message=Mensaje +dao.proofOfBurn.sig=Firma +dao.proofOfBurn.verify=Verificar +dao.proofOfBurn.verificationResult.ok=Verificación exitosa +dao.proofOfBurn.verificationResult.failed=Verificación fallida + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Fase de propuesta +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Pausa antes de la fase de votación secreta +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Fase de votación secreta +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Pausa antes de la fase de revelado de vot +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Fase de revelado de voto +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Pausa antes de la fase de resultados +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Fase de resultado de votación + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Fase de propuesta +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Votación secreta +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Revelar voto +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Resultado de la votación + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Petición de compensación +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Solicitud de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Propuesta para un rol en garantía +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Propuesta para eliminar un activo +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Propuesta para cambiar un parámetro +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Propuesta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Propuesta para confiscar una garantía + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Petición de compensación +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Solicitud de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Rol en garantía +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Eliminar una altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Cambiar un parámetro +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Propuesta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscar una garantía + +dao.proposal.details=Detalles de propuesta +dao.proposal.selectedProposal=Propuesta seleccionada +dao.proposal.active.header=Propuestas del ciclo actual +dao.proposal.active.remove.confirm=¿Está seguro que quiere eliminar esta propuesta?\nLas comisiones de propuesta pagadas se perderán. +dao.proposal.active.remove.doRemove=Sí, eliminar mi propuesta +dao.proposal.active.remove.failed=No se pudo eliminar la propuesta +dao.proposal.myVote.title=Votando +dao.proposal.myVote.accept=Aceptar propuesta +dao.proposal.myVote.reject=Rechazar propuesta +dao.proposal.myVote.removeMyVote=Ignorar propuesta +dao.proposal.myVote.merit=Peso de voto por BSQ ganados +dao.proposal.myVote.stake=Peso de voto desde stake +dao.proposal.myVote.revealTxId=ID de transacción de revelado de voto +dao.proposal.myVote.stake.prompt=Cantidad máxima disponible para votaciones: {0} +dao.proposal.votes.header=Establecer cantidad para votaciones y publicar sus votos +dao.proposal.myVote.button=Publicar votos +dao.proposal.myVote.setStake.description=Después de votar en todas las propuestas tiene que establecer su cantidad para votaciones bloqueando BSQ. Cuantos más BSQ bloquee, más peso tendrá su voto.\n\nLos BSQ bloqueados para votaciones serán desbloqueados de nuevo durante la fase de revelado de votación. +dao.proposal.create.selectProposalType=Seleccionar tipo de propuesta +dao.proposal.create.phase.inactive=Por favor espere a la próxima fase de propuesta +dao.proposal.create.proposalType=Tipo de propuesta +dao.proposal.create.new=Hacer una nueva propuesta +dao.proposal.create.button=Hacer propuesta +dao.proposal.create.publish=Publicar propuesta +dao.proposal.create.publishing=Publicación de propuesta en progreso... +dao.proposal=propuesta +dao.proposal.display.type=Tipo de propuesta +dao.proposal.display.name=Nombre de usuario exacto en GitHub +dao.proposal.display.link=Enlace a información detallada +dao.proposal.display.link.prompt=Enlace a la propuesta +dao.proposal.display.requestedBsq=Cantidad solicitada en BSQ +dao.proposal.display.txId=ID de transacción de la propuesta +dao.proposal.display.proposalFee=Comisión de propuesta +dao.proposal.display.myVote=Mi voto +dao.proposal.display.voteResult=Resumen del resultado de la votación +dao.proposal.display.bondedRoleComboBox.label=Tipo de rol en garantía +dao.proposal.display.requiredBondForRole.label=Garantía requerida para el rol +dao.proposal.display.option=Opción + +dao.proposal.table.header.proposalType=Tipo de propuesta +dao.proposal.table.header.link=Enlace +dao.proposal.table.header.myVote=Mi voto +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Eliminar +dao.proposal.table.icon.tooltip.removeProposal=Eliminar mi propuesta +dao.proposal.table.icon.tooltip.changeVote=Voto actual: ''{0}''. Cambiar voto a: ''{1}'' + +dao.proposal.display.myVote.accepted=Aceptado +dao.proposal.display.myVote.rejected=Rechazado +dao.proposal.display.myVote.ignored=Ignorado +dao.proposal.display.myVote.unCounted=Voto no incluido en el resultado +dao.proposal.myVote.summary=Votos: {0}; Peso de voto: {1} (ganado: {2} + cantidad en juego: {3}) {4} +dao.proposal.myVote.invalid=Votación inválida + +dao.proposal.voteResult.success=Aceptado +dao.proposal.voteResult.failed=Rechazado +dao.proposal.voteResult.summary=Resultado: {0}; Umbral: {1} (requerido > {2}); Quorum: {3} (requerido > {4}) + +dao.proposal.display.paramComboBox.label=Seleccionar parámetro a cambiar +dao.proposal.display.paramValue=Valor de parámetro + +dao.proposal.display.confiscateBondComboBox.label=Elegir garantía +dao.proposal.display.assetComboBox.label=Activo a eliminar + +dao.blindVote=votación secreta + +dao.blindVote.startPublishing=Publicando transacción de voto secreto.... +dao.blindVote.success=Su voto secreto ha sido publicado satisfactoriamente.\n\nPor favor tenga en cuenta que tiene que estar en línea en la fase de revelado de votos para que la aplicación Bisq pueda publicar la transacción de revelado de voto. Sin la transacción de revelado de voto, su voto será inválido! + +dao.wallet.menuItem.send=Enviar +dao.wallet.menuItem.receive=Recibir +dao.wallet.menuItem.transactions=Transacciones + +dao.wallet.dashboard.myBalance=Balance de mi cartera + +dao.wallet.receive.fundYourWallet=Su dirección para recibir BSQ +dao.wallet.receive.bsqAddress=Dirección monedero BSQ (Dirección fresca, sin usar) + +dao.wallet.send.sendFunds=Enviar fondos +dao.wallet.send.sendBtcFunds=Enviar fondos no-BSQ (BTC) +dao.wallet.send.amount=Cantidad en BSQ +dao.wallet.send.btcAmount=Cantidad en BTC (fondos no-BSQ) +dao.wallet.send.setAmount=Indicar cantidad a retirar (la cantidad mínima es {0}) +dao.wallet.send.receiverAddress=Dirección BSQ del receptor +dao.wallet.send.receiverBtcAddress=Dirección BTC del receptor +dao.wallet.send.setDestinationAddress=Introduzca su dirección de destino +dao.wallet.send.send=Enviar fondos BSQ +dao.wallet.send.inputControl=Seleccionar entradas +dao.wallet.send.sendBtc=Enviar fondos BTC +dao.wallet.send.sendFunds.headline=Confirme la petición de retiro +dao.wallet.send.sendFunds.details=Enviando: {0}\nA la dirección receptora: {1}.\nLa tasa de minado requerida es: {2} ({3} satoshis/vbyte)\nTamaño de la transacción: {4} Kb\n\nEl receptor recibirá: {5}\n\nEstá seguro de que quiere retirar esa cantidad? +dao.wallet.chainHeightSynced=Último bloque verificado: {0} +dao.wallet.chainHeightSyncing=Esperando bloques... {0} bloques verificados de {1} +dao.wallet.tx.type=Tipo + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=No reconocido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Transacción BSQ no verificada +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Transacción BSQ inválida +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Transacción génesis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Transferir BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ recibidos +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ enviados +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Comisión de intercambio +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Comisión de solicitud de compensación +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Comisión para solicitud de reembolso +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Comisión para propuesta +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Comisión para voto secreto +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Revelar voto +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Bloquear garantía +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Desbloquear garantía +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Comisión de listado de activo +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Prueba de quemado +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregular + +dao.tx.withdrawnFromWallet=BTC retirados desde el monedero +dao.tx.issuanceFromCompReq=Solicitud/emisión de compensación +dao.tx.issuanceFromCompReq.tooltip=Solicitud de compensación que lleva a emitir nuevos BSQ.\nFecha de emisión: {0} +dao.tx.issuanceFromReimbursement=Solicitud de reembolso/emisión +dao.tx.issuanceFromReimbursement.tooltip=Solicitud de reembolso que lleva a una emisión de nuevos BSQ.\nFecha de emisión: {0} +dao.proposal.create.missingBsqFunds=No tiene suficientes fondos para crear la propuesta. Si tiene una transacción BSQ no confirmada necesita esperar a una confirmación porque los BSQ se validan solo si están incluidos en un bloque.\nSe necesitan: {0} + +dao.proposal.create.missingBsqFundsForBond=No tiene suficientes fondos BSQ para este rol. Aún puede publicar esta propuesta, pero necesitará la cantidad BSQ requerida para este rol si es aceptada.\nSe necesitan: {0} + +dao.proposal.create.missingMinerFeeFunds=No tiene suficientes fondos BTC para crear la transacción propuesta. Todas las transacciones BSQ requieren una comisión de minado en BTC.\nNecesarios: {0} + +dao.proposal.create.missingIssuanceFunds=No tiene suficientes fondos BTC para crear la transacción de propuesta. Todas las transacciones BSQ requieren una comisión de minado en BTC, y la emisión de transacciones también requieren BTC para la cantidad de BSQ solicitada ({0} Satoshis/BSQ).\nNecesarios: {1} + +dao.feeTx.confirm=Confirmar transacción {0} +dao.feeTx.confirm.details={0} comisión: {1}\nComisión de minado: {2} ({3} Satoshis/vbyte)\nTamaño de la transacción: {4} Kb\n\n¿Está seguro de que quiere publicar la transacción {5}? + +dao.feeTx.issuanceProposal.confirm.details={0} comisión: {1}\nBTC necesarios para emisión BSQ: {2} ({3} Satoshis/BSQ)\nTasa de minado: {4} ({5} Satoshis/vbyte)\nTamaño de transacción: {6} Kb\n\nSi la solicitud se aprueba, recibirá la cantidad neta que ha solicitado de las 2 BSQ de comisión de propuesta.\n¿Está seguro de que quiere publicar la transacción de {7}? + +dao.news.bisqDAO.title=LA DAO BISQ +dao.news.bisqDAO.description=Tal como el exchange Bisq es descentralizado y resistente a la censura, lo es su modelo de governanza - y la DAO BISQ y el token BSQ son herramientas que lo hacen posible. +dao.news.bisqDAO.readMoreLink=Aprender más acerca de la DAO Bisq. + +dao.news.pastContribution.title=¿HIZO CONTRIBUCIONES EN EL PASADO? SOLICITE BSQ +dao.news.pastContribution.description=Si ha contribuido a Bisq por favor use la dirección BSQ de abajo y haga una solicitud por haber participado en la distribución de BSQ génesis. +dao.news.pastContribution.yourAddress=Su dirección de monedero BSQ +dao.news.pastContribution.requestNow=Solicitar ahora + +dao.news.DAOOnTestnet.title=CORRER LA DAO BISQ EN TESTNET +dao.news.DAOOnTestnet.description=La red principal de la DAO Bisq aún no se ha lanzado pero puede aprender acerca de la DAO ejecutándola en la testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Cambiar a Modo Testnet +dao.news.DAOOnTestnet.firstSection.content=Cambiar a la testnet desde la pantalla de Configuración +dao.news.DAOOnTestnet.secondSection.title=2. Adquirir algunos BSQ +dao.news.DAOOnTestnet.secondSection.content=Solicitar BSQ en Slack o comprar BSQ en Bisq +dao.news.DAOOnTestnet.thirdSection.title=3. Participar en un ciclo de votación +dao.news.DAOOnTestnet.thirdSection.content=Realizar propuestas y votar propuestas para cambiar diversos aspectos de Bisq +dao.news.DAOOnTestnet.fourthSection.title=4. Explorar el explorador de bloques BSQ +dao.news.DAOOnTestnet.fourthSection.content=Como BSQ es simplemente bitcoin, puede ver las transacciones BSQ en cualquier explorador de bloques de bitcoin +dao.news.DAOOnTestnet.readMoreLink=Leer toda la documentación + +dao.monitor.daoState=Estado DAO +dao.monitor.proposals=Estado de propuestas +dao.monitor.blindVotes=Estado de votaciones secretas + +dao.monitor.table.peers=Pares +dao.monitor.table.conflicts=Conflictos +dao.monitor.state=Estado +dao.monitor.requestAlHashes=Solicitar todos los hashes +dao.monitor.resync=Resincronizar estado DAO +dao.monitor.table.header.cycleBlockHeight=Ciclo / altura de bloque +dao.monitor.table.cycleBlockHeight=Ciclo {0} / bloque {1} +dao.monitor.table.seedPeers=Nodo semilla: {0} + +dao.monitor.daoState.headline=Estado DAO +dao.monitor.daoState.table.headline=Cadena de hashes de estado de DAO +dao.monitor.daoState.table.blockHeight=Altura de bloque +dao.monitor.daoState.table.hash=Estado de hashes de DAO +dao.monitor.daoState.table.prev=Hash previo +dao.monitor.daoState.conflictTable.headline=Estado de hashes DAO desde pares en conflicto +dao.monitor.daoState.utxoConflicts=conflictos UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Altura de bloque: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Suma de todas las UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Suma de todas las BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=El estado de DAO no está sincronizado con la red. Después de reiniciar el estado DAO se resincronizará. + +dao.monitor.proposal.headline=Estado de propuestas +dao.monitor.proposal.table.headline=Estado de hashes de cadena de propuesta +dao.monitor.proposal.conflictTable.headline=Hashes de estado de propuesta desde pares en conflicto + +dao.monitor.proposal.table.hash=Estado de hash de propuesta +dao.monitor.proposal.table.prev=Hash previo +dao.monitor.proposal.table.numProposals=No. de propuestas + +dao.monitor.isInConflictWithSeedNode=Sus datos locales no están en consenso con al menos un nodo semilla. Por favor resincronice el estado DAO +dao.monitor.isInConflictWithNonSeedNode=Uno o más de sus pares no está en consenso con la red pero su nodo está en sincronía con la los nodos semilla. +dao.monitor.daoStateInSync=Su nodo local está en consenso con la red. + +dao.monitor.blindVote.headline=Estado de votaciones secretas +dao.monitor.blindVote.table.headline=Cadena de estado de hashes de votación secreta +dao.monitor.blindVote.conflictTable.headline=Estado de hashes de votación secreta desde pares en conflicto +dao.monitor.blindVote.table.hash=Hash de estado de votación secreta +dao.monitor.blindVote.table.prev=Hash previo +dao.monitor.blindVote.table.numBlindVotes=No. de votos secretos + +dao.factsAndFigures.menuItem.supply=Oferta BSQ +dao.factsAndFigures.menuItem.transactions=Transacciones BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Medía de 90 días del precio de intercambio BSQ/BTC +dao.factsAndFigures.dashboard.avgPrice30=Medía de 30 días del precio de intercambio BSQ/BTC +dao.factsAndFigures.dashboard.avgUSDPrice90=Precio medio de BSQ/USD a 90 días ponderado por volumen +dao.factsAndFigures.dashboard.avgUSDPrice30=Precio medio de BSQ/USD a 30 días ponderado por volumen +dao.factsAndFigures.dashboard.marketCap=Capitalización de mercado (basada en precio medio de BSQ/USD a 30 días) +dao.factsAndFigures.dashboard.availableAmount=BSQ totales disponibles +dao.factsAndFigures.dashboard.volumeUsd=Volumen de intercambio total en USD +dao.factsAndFigures.dashboard.volumeBtc=Volumen de intercambio total en BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Precio BSQ/USD medio de intercambio desde en el periodo de tiempo seleccionado en el gráfico +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Precio BSQ/BTC medio de intercambio desde en el periodo de tiempo seleccionado en el gráfico + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ emitidos v. BSQ quemados + +dao.factsAndFigures.supply.issued=BSQ emitidos +dao.factsAndFigures.supply.compReq=Solicitudes de compensación +dao.factsAndFigures.supply.reimbursement=Solicitudes de reembolso +dao.factsAndFigures.supply.genesisIssueAmount=BSQ emitidos en la transacción génesis +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ emitidos para solicitudes de compensación +dao.factsAndFigures.supply.reimbursementAmount=BSQ emitidos para solicitudes de reembolso +dao.factsAndFigures.supply.totalIssued=BSQ totales emitidos +dao.factsAndFigures.supply.totalBurned=BSQ totales quemados +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ quemados + +dao.factsAndFigures.supply.priceChat=Precio BSQ +dao.factsAndFigures.supply.volumeChat=Volumen de intercambio +dao.factsAndFigures.supply.tradeVolumeInUsd=Volumen de intercambio en USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Volumen de intercambio en BTC +dao.factsAndFigures.supply.bsqUsdPrice=Precio BSQ/USD +dao.factsAndFigures.supply.bsqBtcPrice=precio BSQ/BTC +dao.factsAndFigures.supply.btcUsdPrice=Precio BTC/USD + +dao.factsAndFigures.supply.locked=Estado global de BSQ bloqueados +dao.factsAndFigures.supply.totalLockedUpAmount=Bloqueados en garantías +dao.factsAndFigures.supply.totalUnlockingAmount=Desbloqueando BSQ de garantías +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ desbloqueados de garantías +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ confiscados de garantías +dao.factsAndFigures.supply.proofOfBurn=Prueba de quemado +dao.factsAndFigures.supply.bsqTradeFee=Tasas de intercambio en BSQ +dao.factsAndFigures.supply.btcTradeFee=Tasas de intercambio en BTC + +dao.factsAndFigures.transactions.genesis=Transacción génesis +dao.factsAndFigures.transactions.genesisBlockHeight=Altura de bloque génesis +dao.factsAndFigures.transactions.genesisTxId=ID transacción génesis +dao.factsAndFigures.transactions.txDetails=Estadísticas de transacción BSQ +dao.factsAndFigures.transactions.allTx=No. de todas las transacciones BSQ +dao.factsAndFigures.transactions.utxo=No. de todos los outputs de transacciones no gastadas +dao.factsAndFigures.transactions.compensationIssuanceTx=No. de todas las transacciones emitidas de solicitudes de compensación +dao.factsAndFigures.transactions.reimbursementIssuanceTx=No. de todas las transacciones emitidas de solicitud de reembolso +dao.factsAndFigures.transactions.burntTx=No. de todas las transacciones de tasa de pago +dao.factsAndFigures.transactions.invalidTx=No. de todas las transacciones inválidas +dao.factsAndFigures.transactions.irregularTx=No. de todas las transacciones irregulares + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Seleccionar inputs de transacción +inputControlWindow.balanceLabel=Saldo disponible + +contractWindow.title=Detalles de la disputa +contractWindow.dates=Fecha oferta / Fecha intercambio +contractWindow.btcAddresses=Dirección Bitcoin comprador BTC / vendedor BTC +contractWindow.onions=Dirección de red de comprador BTC / Vendedor BTC +contractWindow.accountAge=Edad de cuenta del comprador BTC / vendedor BTC +contractWindow.numDisputes=No. de disputas del comprador BTC / Vendedor BTC +contractWindow.contractHash=Hash del contrato + +displayAlertMessageWindow.headline=Información importante! +displayAlertMessageWindow.update.headline=Información de actualización importante! +displayAlertMessageWindow.update.download=Descargar: +displayUpdateDownloadWindow.downloadedFiles=Archivos: +displayUpdateDownloadWindow.downloadingFile=Descargando: {0} +displayUpdateDownloadWindow.verifiedSigs=Firma verificada con claves: +displayUpdateDownloadWindow.status.downloading=Descargando archivos... +displayUpdateDownloadWindow.status.verifying=Verificando firma... +displayUpdateDownloadWindow.button.label=Descargar instalador y verificar firma +displayUpdateDownloadWindow.button.downloadLater=Descargar más tarde +displayUpdateDownloadWindow.button.ignoreDownload=Ignorar esta versión +displayUpdateDownloadWindow.headline=¡Una nueva versión de Bisq está disponible! +displayUpdateDownloadWindow.download.failed.headline=Descarga fallida +displayUpdateDownloadWindow.download.failed=Descarga fallida.\nPor favor descargue y verifique manualmente en [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=No se puede determinar el instalador correcto. Por favor, descargue y verifique manualmente en [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verificación fallida.\nPor favor descargue y verifique manualmente en [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=La nueva versión ha sido descargada con éxito y la firma verificada.\n\nPor favor abra el directorio de descargas, cierre la aplicación e instale la nueva versión. +displayUpdateDownloadWindow.download.openDir=Abrir directorio de descargas + +disputeSummaryWindow.title=Resumen +disputeSummaryWindow.openDate=Fecha de apertura de ticket +disputeSummaryWindow.role=Rol del trader +disputeSummaryWindow.payout=Pago de la cantidad de intercambio +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} obtiene la cantidad de pago de intercambio +disputeSummaryWindow.payout.getsAll=Cantidad máxima de pago BTC {0} +disputeSummaryWindow.payout.custom=Pago personalizado +disputeSummaryWindow.payoutAmount.buyer=Cantidad de pago del comprador +disputeSummaryWindow.payoutAmount.seller=Cantidad de pago del vendedor +disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador +disputeSummaryWindow.reason=Razón de la disputa +disputeSummaryWindow.tradePeriodEnd=Fin de periodo de intercambio +disputeSummaryWindow.extraInfo=Información extra +disputeSummaryWindow.delayedPayoutStatus=Estado de pago retrasado + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidad +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violación del protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sin respuesta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Estafa +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Otro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Intercambio de opciones +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Comerciante no responde +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Cuenta de emisor errónea +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=El par actuó tarde +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=El intercambio ya había acabado + +disputeSummaryWindow.summaryNotes=Nota de resumen +disputeSummaryWindow.addSummaryNotes=Añadir notas de sumario +disputeSummaryWindow.close.button=Cerrar ticket + +# 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 cerrado el {0}\n{1} dirección de nodo: {2}\n\nResumen:\nID de intercambio: {3}\nMoneda: {4}\nCantidad del intercambio: {5}\nCantidad de pago para el comprador de BTC: {6}\nCantidad de pago para el vendedor de BTC: {7}\n\nMotivo de la disputa: {8}\n\nNotas resumidas:\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=\nSiguientes pasos:\nAbrir intercambio y aceptar o rechazar la sugerencia del mediador +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nSiguientes pasos:\nNo es necesario que realice ninguna otra acción. Si el árbitro decidió a su favor, verá una transacción de "Reembolso desde arbitraje" en Fondos / Transacciones +disputeSummaryWindow.close.closePeer=Necesitar cerrar también el ticket del par de intercambio! +disputeSummaryWindow.close.txDetails.headline=Publicar transacción de devolución de fondos +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=El comprador recibe {0} en la dirección: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=El vendedor recibe {0} en la dirección: {1}\n +disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3} ({4} satoshis/vbyte)\nTamaño virtual de transacción: {5} vKb\n\n¿Está seguro de que quiere publicar esta transacción?\n + +disputeSummaryWindow.close.noPayout.headline=Cerrar sin realizar algún pago +disputeSummaryWindow.close.noPayout.text=¿Quiere cerrar sin realizar algún pago? + +emptyWalletWindow.headline=Herramienta de monedero {0} de emergencia +emptyWalletWindow.info=Por favor usar sólo en caso de emergencia si no puede acceder a sus fondos desde la Interfaz de Usuario (UI).\n\nPor favor, tenga en cuenta que todas las ofertas abiertas se cerrarán automáticamente al usar esta herramienta.\n\nAntes de usar esta herramienta, por favor realice una copia de seguridad del directorio de datos. Puede hacerlo en \"Cuenta/Copia de Seguridad\".\n\nPor favor repórtenos su problema y envíe un reporte de fallos en Github en el foro de Bisq para que podamos investigar qué causa el problema. +emptyWalletWindow.balance=Su balance disponible en cartera +emptyWalletWindow.bsq.btcBalance=Balance de Satoshis no-BSQ + +emptyWalletWindow.address=Su dirección de destino +emptyWalletWindow.button=Enviar todos los fondos +emptyWalletWindow.openOffers.warn=Tiene ofertas abiertas que se eliminarán si vacía el monedero.\n¿Está seguro de que quiere vaciar su monedero? +emptyWalletWindow.openOffers.yes=Sí, estoy seguro +emptyWalletWindow.sent.success=El balance de su monedero fue transferido con éxito. + +enterPrivKeyWindow.headline=Introduzca la clave privada para registrarse + +filterWindow.headline=Editar lista de filtro +filterWindow.offers=Ofertas filtradas (separadas por coma) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Cuentas de intercambio filtradas:\nFormato: lista de [ID método de pago | campo de datos | valor] separada por coma. +filterWindow.bannedCurrencies=Códigos de moneda filtrados (separados por coma) +filterWindow.bannedPaymentMethods=ID's de métodos de pago filtrados (separados por coma) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtro de cuenta de las claves públicas de testigo de firmado (claves públicas en hexadecimal, separado por coma) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Árbitros filtrados (direcciones onion separadas por coma) +filterWindow.mediators=Mediadores filtrados (direcciones onion separadas por coma) +filterWindow.refundAgents=Agentes de devolución de fondos filtrados (direcciones onion separadas por coma) +filterWindow.seedNode=Nodos semilla filtrados (direcciones onion separadas por coma) +filterWindow.priceRelayNode=nodos de retransmisión de precio filtrados (direcciones onion separadas por coma) +filterWindow.btcNode=Nodos Bitcoin filtrados (direcciones + puerto separadas por coma) +filterWindow.preventPublicBtcNetwork=Prevenir uso de la red Bitcoin pública +filterWindow.disableDao=Deshabilitar DAO +filterWindow.disableAutoConf=Deshabilitar autoconfirmación +filterWindow.autoConfExplorers=Exploradores de autoconfirmación filtrados (direcciones separadas por coma) +filterWindow.disableDaoBelowVersion=Versión mínima requerida para DAO +filterWindow.disableTradeBelowVersion=Versión mínima requerida para intercambios. +filterWindow.add=Añadir filtro +filterWindow.remove=Eliminar filtro +filterWindow.btcFeeReceiverAddresses=Direcciones de recepción de la tasa BTC +filterWindow.disableApi=Deshabilitar API +filterWindow.disableMempoolValidation=Deshabilitar validación de Mempool + +offerDetailsWindow.minBtcAmount=Cantidad mínima BTC +offerDetailsWindow.min=(mínimo {0}) +offerDetailsWindow.distance=(distancia del precio de mercado: {0}) +offerDetailsWindow.myTradingAccount=MI cuenta de intercambio +offerDetailsWindow.offererBankId=(ID/BIC/SWIFT del banco del creador) +offerDetailsWindow.offerersBankName=(nombre del banco del creador) +offerDetailsWindow.bankId=ID de banco (v.g BIC o SWIFT) +offerDetailsWindow.countryBank=País del banco del creador +offerDetailsWindow.commitment=Compromiso +offerDetailsWindow.agree=Estoy de acuerdo +offerDetailsWindow.tac=Términos y condiciones: +offerDetailsWindow.confirm.maker=Confirmar: Poner oferta para {0} bitcoin +offerDetailsWindow.confirm.taker=Confirmar: Tomar oferta {0} bitcoin +offerDetailsWindow.creationDate=Fecha de creación +offerDetailsWindow.makersOnion=Dirección onion del creador + +qRCodeWindow.headline=Código QR +qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Bisq desde su billetera externa. +qRCodeWindow.request=Solicitud de pago:\n{0} + +selectDepositTxWindow.headline=Seleccione transacción de depósito para la disputa +selectDepositTxWindow.msg=La transacción de depósito no se almacenó en el intercambio.\nPor favor seleccione una de las transacciones multifirma existentes de su monedero en que se usó la transacción de depósito en el intercambio fallido.\nPuede encontrar la transacción correcta abriendo la ventana de detalles del intercambio (clic en la ID de intercambio en la lista) y a continuación el output de la transacción del pago de tasa de intercambio a la siguiente transacción donde puede ver la transacción de depósito multifirma (la dirección comienza con 3). La ID de transacción debería ser visible en la lista presentada aquí. Una vez haya encontrado la transacción correcta selecciónela aquí y continúe.\n\nDisculpe los inconvenientes, pero este error ocurre muy poco a menudo y en el futuro intentaremos encontrar mejores maneras de resolverlo. +selectDepositTxWindow.select=Selección depósito de transacción + +sendAlertMessageWindow.headline=Enviar notificación global +sendAlertMessageWindow.alertMsg=Mensaje de alerta: +sendAlertMessageWindow.enterMsg=Introducir mensaje +sendAlertMessageWindow.isSoftwareUpdate=Notificación de descarga de software +sendAlertMessageWindow.isUpdate=Lanzamiento completo +sendAlertMessageWindow.isPreRelease=Pre-lanzamiento +sendAlertMessageWindow.version=Nueva versión no. +sendAlertMessageWindow.send=Enviar notificación +sendAlertMessageWindow.remove=Eliminar notificación + +sendPrivateNotificationWindow.headline=Enviar mensaje privado +sendPrivateNotificationWindow.privateNotification=Notificación privada +sendPrivateNotificationWindow.enterNotification=Introducir notificación +sendPrivateNotificationWindow.send=Enviar notificación privada + +showWalletDataWindow.walletData=Datos del monedero +showWalletDataWindow.includePrivKeys=Incluir claves privadas: + +setXMRTxKeyWindow.headline=Prueba de envío de XMR +setXMRTxKeyWindow.note=Añadiendo la información de transacción a continuación, se habilita la autoconfirmación para intercambios más rápidos. Ver más: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=ID de transacción (opcional) +setXMRTxKeyWindow.txKey=Clave de transacción (opcional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Acuerdo de usuario +tacWindow.agree=Estoy de acuerdo +tacWindow.disagree=No estoy de acuerdo, salir +tacWindow.arbitrationSystem=Disputa resolución + +tradeDetailsWindow.headline=Intercambio +tradeDetailsWindow.disputedPayoutTxId=ID transacción de pago en disputa: +tradeDetailsWindow.tradeDate=Fecha de intercambio +tradeDetailsWindow.txFee=Comisión de minado +tradeDetailsWindow.tradingPeersOnion=Dirección onion de par de intercambio +tradeDetailsWindow.tradingPeersPubKeyHash=Hash de las llaves públicas de pares de intercambio +tradeDetailsWindow.tradeState=Estado del intercambio +tradeDetailsWindow.agentAddresses=Árbitro/Mediador +tradeDetailsWindow.detailData=Detalle de datos + +txDetailsWindow.headline=Detalles de transacción +txDetailsWindow.btc.note=Ha enviado BTC +txDetailsWindow.bsq.note=Ha enviado fondos BSQ. BSQ son bitcoin coloreados, con lo que la transacción no se mostrará en el explorador BSQ hasta que sea confirmada en un bloque BTC. +txDetailsWindow.sentTo=Enviado a +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Resume de historia de intercambio +closedTradesSummaryWindow.totalAmount.title=Cantidad intercambiada total +closedTradesSummaryWindow.totalAmount.value={0} ({1} con el precio de mercado actual) +closedTradesSummaryWindow.totalVolume.title=Cantidad total intercambiada en {0} +closedTradesSummaryWindow.totalMinerFee.title=Suma de todas las trasas de minado +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} de la cantidad total intercambiada) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Suma de todas las tasas de intercambio pagadas en BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} de la cantidad total intercambiada) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Suma de todas las tasas de intercambio pagadas en BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} de la cantidad total intercambiada) + +walletPasswordWindow.headline=Introducir contraseña para desbloquear + +torNetworkSettingWindow.header=Confirmación de red Tor +torNetworkSettingWindow.noBridges=No utilizar bridges +torNetworkSettingWindow.providedBridges=Conectar con los bridges proporcionados +torNetworkSettingWindow.customBridges=Introducir bridges personalizados +torNetworkSettingWindow.transportType=Tipo de transporte: +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (recomendado) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Introducir uno o más bridge relays (una por linea) +torNetworkSettingWindow.enterBridgePrompt=type address:port +torNetworkSettingWindow.restartInfo=Necesita reiniciar para aplicar los cambios +torNetworkSettingWindow.openTorWebPage=Abrir página web del proyecto Tor +torNetworkSettingWindow.deleteFiles.header=¿Problemas de conexión? +torNetworkSettingWindow.deleteFiles.info=Si tiene problemas de conexión repetidos al iniciar, borrar archivos Tor desactualizados podría ayudar. Para hacerlo haga clic en el botón inferior y luego reinicie. +torNetworkSettingWindow.deleteFiles.button=Borrar archivos Tor desactualizados y finalizar. +torNetworkSettingWindow.deleteFiles.progress=Cerrado de Tor en proceso +torNetworkSettingWindow.deleteFiles.success=Archivos Tor desactualizados borrados. Por favor, reinice. +torNetworkSettingWindow.bridges.header=¿Está Tor bloqueado? +torNetworkSettingWindow.bridges.info=Si Tor está bloqueado por su proveedor de internet o por su país puede intentar usar puentes Tor.\nVisite la página web Tor en https://bridges.torproject.org/bridges para saber más acerca de los puentes y transportes conectables. + +feeOptionWindow.headline=Elija la moneda para el pago de la comisiones de intercambio +feeOptionWindow.info=Puede elegir pagar la tasa de intercambio en BSQ o BTC. Si elige BSQ apreciará la comisión de intercambio descontada. +feeOptionWindow.optionsLabel=Elija moneda para el pago de comisiones de intercambio +feeOptionWindow.useBTC=Usar BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notificación +popup.headline.instruction=Por favor, tenga en cuenta: +popup.headline.attention=Atención +popup.headline.backgroundInfo=Información general +popup.headline.feedback=Completado +popup.headline.confirmation=Confirmación +popup.headline.information=Información +popup.headline.warning=Atención +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 bisq.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} + +error.spvFileCorrupted=Ocurrió un error al leer el archivo de cadena SPV.\nPuede ser que el archivo de cadena SPV se haya corrompido.\n\nMensaje de error: {0}\n\n¿Quiere borrarlo y comenzar una resincronización? +error.deleteAddressEntryListFailed=No se pudo borrar el archivo AddressEntryList.\nError: {0} +error.closedTradeWithUnconfirmedDepositTx=La transacción de depósito de el intercambio cerrado con la ID de intercambio {0} aún no se ha confirmado.\n\nPor favor haga una resincronización SPV en \"Configuración/Información de red\" para ver si la transacción es válida. +error.closedTradeWithNoDepositTx=El depósito de transacción de el intercambio cerrado con ID de intercambio {0} es inválido.\nPor favor reinicie la aplicación para limpiar la lista de intercambios cerrados. + +popup.warning.walletNotInitialized=La cartera aún no sea ha iniciado +popup.warning.osxKeyLoggerWarning=Debido a medidas de seguridad más estrictas en macOS 10.14 y siguientes, al iniciar una aplicación Java (Bisq usa Java) causa un popup de alarma en macOS ('Bisq would like to receive keystrokes from any application').\n\nPara evitar esto por favor abra su 'Configuración macOS' y vaya a 'Seguridad y privacidad' -> 'Privacidad¡ -> 'Monitorización de inputs' y elimine 'Bisq' de la lista a la derecha.\n\nBisq actualizara a una nueva versión de Java para evitar que este problema tan pronto como se resuelvan las limitaciones técnicas (el paquete de Java para la versión requerida de Java aún no se ha emitido). +popup.warning.wrongVersion=Probablemente tenga una versión de Bisq incorrecta para este ordenador.\nLa arquitectura de su ordenador es: {0}.\nLos binarios de Bisq instalados son: {1}.\nPor favor cierre y reinstale la versión correcta ({2}). +popup.warning.incompatibleDB=¡Hemos detectado archivos de base de datos incompatibles!\n\nEstos archivos de base de datos no son compatibles con nuestro actual código base:\n{0}\n\nHemos hecho una copia de seguridad de los archivos corruptos y aplicado los valores por defecto a la nueva versión de base de datos.\n\nLa copia de seguridad se localiza en:\n{1}/db/backup_of_corrupted_data.\n\nPor favor, compruebe si tiene la última versión de Bisq instalada.\nPuede descargarla en:\n[HYPERLINK:https://bisq.network/downloads]\n\nPor favor, reinicie la aplicación. +popup.warning.startupFailed.twoInstances=Ya está ejecutando Bisq. No puede ejecutar dos instancias de Bisq. +popup.warning.tradePeriod.halfReached=Su intercambio con ID {0} ha alcanzado la mitad de el periodo máximo permitido de intercambio y aún no está completada.\n\nEl periodo de intercambio termina el {1}\n\nPor favor, compruebe el estado de su intercambio en \"Portafolio/Intercambios abiertos\" para más información. +popup.warning.tradePeriod.ended=Su intercambio con ID {0} ha alcanzado el periodo máximo de intercambio y no se ha completado.\n\nEl periodo de intercambio finalizó en {1}\n\nPor favor, compruebe su intrecambio en \"Portafolio/Intercambios abiertos\" para contactar con el mediador. +popup.warning.noTradingAccountSetup.headline=No ha configurado una cuenta de intercambio +popup.warning.noTradingAccountSetup.msg=Necesita configurar una moneda nacional o cuenta de altcoin antes de crear una oferta.\n¿Quiere configurar una cuenta? +popup.warning.noArbitratorsAvailable=No hay árbitros disponibles. +popup.warning.noMediatorsAvailable=No hay mediadores disponibles. +popup.warning.notFullyConnected=Necesita esperar hasta que esté completamente conectado a la red.\nPuede llevar hasta 2 minutos al inicio. +popup.warning.notSufficientConnectionsToBtcNetwork=Necesita esperar hasta que tenga al menos {0} conexiones a la red Bitcoin. +popup.warning.downloadNotComplete=Tiene que esperar hasta que finalice la descarga de los bloques Bitcoin que faltan. +popup.warning.chainNotSynced=La cadena de bloques del monedero Bisq no está sincronizada correctamente. Si ha iniciado la aplicación recientemente, espere a que se haya publicado al menos un bloque Bitcoin.\n\nPuede comprobar la altura de la cadena de bloques en Configuración/Información de red. Si se encuentra más de un bloque y el problema persiste podría estar estancado, en cuyo caso deberá hacer una resincronización SPV.\n[HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=¿Está seguro que quiere eliminar la oferta?\nLa comisión de creador de {0} se perderá si elimina la oferta. +popup.warning.tooLargePercentageValue=No puede establecer un porcentaje del 100% o superior. +popup.warning.examplePercentageValue=Por favor, introduzca un número de porcentaje como \"5.4\" para 5.4% +popup.warning.noPriceFeedAvailable=No hay una fuente de precios disponible para esta moneda. No puede utilizar un precio basado en porcentaje.\nPor favor, seleccione un precio fijo. +popup.warning.sendMsgFailed=El envío de mensaje a su compañero de intercambio falló.\nPor favor, pruebe de nuevo y si continúa fallando, reporte el fallo. +popup.warning.insufficientBtcFundsForBsqTx=No tiene suficientes fondos BTC para pagar la comisión de minado para esta transacción.\nPor favor ingrese fondos en su monedero BTC.\nFondos faltantes: {0} +popup.warning.bsqChangeBelowDustException=Esta transacción crea un output BSQ de cambio que está por debajo del límite dust (5.46 BSQ) y sería rechazado por la red Bitcoin.\n\nTiene que enviar una cantidad mayor para evitar el output de cambio (Ej. agregando la cantidad de dust a su cantidad de envío) o añadir más fondos BSQ a su cartera para evitar generar un output de dust.\n\nEl output dust es {0}. +popup.warning.btcChangeBelowDustException=Esta transacción crea un output de cambio que está por debajo del límite de dust (546 Satoshi) y sería rechazada por la red Bitcoin.\n\nDebe agregar la cantidad de dust a su cantidad de envío para evitar generar un output de dust.\n\nEl output de dust es {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=Necesitará más BSQ para hacer esta transacción -los últimos 5.46BSQ en su monedero no pueden usarse para pagar tasas de intercambio debido a límites dust en el protocolo Bitcoin.\n\nPuede comprar más BSQ o pagar las tasas de intercambio con BTC.\n\nFondos necesarios: {0} +popup.warning.noBsqFundsForBtcFeePayment=Su monedero BSQ no tiene suficientes fondos para pagar la comisión de intercambio en BSQ. +popup.warning.messageTooLong=Su mensaje excede el tamaño máximo permitido. Por favor, envíelo por partes o súbalo a un servicio como https://pastebin.com +popup.warning.lockedUpFunds=Ha bloqueado fondos de un intercambio fallido.\nBalance bloqueado: {0}\nDirección de depósito TX: {1}\nID de intercambio: {2}.\n\nPor favor, abra un ticket de soporte seleccionando el intercambio en la pantalla de intercambios pendientes y haciendo clic en \"alt + o\" o \"option + o\"." + +popup.warning.makerTxInvalid=Esta oferta no es válida. Por favor seleccione otra oferta diferente.\n\n +takeOffer.cancelButton=Cancelar toma de oferta +takeOffer.warningButton=Ignorar y continuar de todos modos + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=retransmisión de precio +popup.warning.seed=semilla +popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Bisq. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Bisq para más información\n +popup.warning.mandatoryUpdate.dao=Por favor, actualice a la última versión de Bisq. Se lanzó una actualización obligatoria que inhabilita la DAO Bisq y BSQ para las versiones anteriores. Por favor, visite el Foro de Bisq para más información +popup.warning.disable.dao=El DAO Bisq y BSQ estén temporalmente deshabilitados. Por favor revise el foro de Bisq para más informació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 Bisq. +popup.warning.burnBTC=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 BTC para transferir. + +popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Bitcoin.\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 Bisq en el canal de Bisq en Keybase. + +popup.warning.trade.txRejected.tradeFee=tasa de intercambio +popup.warning.trade.txRejected.deposit=depósito +popup.warning.trade.txRejected=La transacción {0} para el intercambio con ID {1} se rechazó por la red Bitcoin.\nID de transacción={2}\nEl intercambio se movió a intercambios fallidos.\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 Bisq en el canal de Bisq en Keybase. + +popup.warning.openOfferWithInvalidMakerFeeTx=La transacción de tasa de creador para la oferta con ID {0} es inválida.\nID de transacción={1}.\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 Bisq en el canal de Bisq de Keybase. + +popup.info.securityDepositInfo=Para asegurarse de que ambos comerciantes siguen el protocolo de intercambio, ambos necesitan pagar un depósito de seguridad.\n\nEl depósito se guarda en su monedero de intercambio hasta que el intercambio se complete, y entonces se devuelve.\n\nPor favor, tenga en cuenta que al crear una nueva oferta, Bisq necesita estar en ejecución para que otro comerciante la tome. Para mantener sus ofertas en línea, mantenga Bisq funcionando y asegúrese de que su computadora está en línea también (Ej. asegúrese de que no pasa a modo standby...el monitor en standby no es problema!) + +popup.info.cashDepositInfo=Por favor asegúrese de que tiene una oficina bancaria donde pueda hacer el depósito de efectivo.\nEl ID del banco (BIC/SWIFT) de del vendedor es: {0} +popup.info.cashDepositInfo.confirm=Confirmo que puedo hacer el depósito +popup.info.shutDownWithOpenOffers=Bisq se está cerrando, pero hay ofertas abiertas.\n\nEstas ofertas no estarán disponibles en la red P2P mientras Bisq esté cerrado, pero serán re-publicadas a la red P2P la próxima vez que inicie Bisq.\n\nPara mantener sus ofertas en línea, mantenga Bisq ejecutándose y asegúrese de que la computadora permanece en línea también (Ej. asegúrese de que no se pone en modo standby... el monitor en espera no es un problema). +popup.info.qubesOSSetupInfo=Parece que está ejecutando Bisq en Qubes OS\n\nAsegúrese de que su Bisq qube esté configurado de acuerdo con nuestra Guía de configuración en [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes] +popup.warn.downGradePrevention=Degradar desde la versión {0} a la versión {1} no está soportado. Por favor use la última versión de Bisq. +popup.warn.daoRequiresRestart=Hubo un problema sincronizando el estado de la DAO. Tiene que reiniciar la aplicación para solucionar el problema. + +popup.privateNotification.headline=Notificación privada importante! + +popup.securityRecommendation.headline=Recomendación de seguridad importante +popup.securityRecommendation.msg=Nos gustaría recordarle que considere usar protección por contraseña para su cartera, si no la ha activado ya.\n\nTambién es muy recomendable que escriba en un papel las palabras semilla del monedero. Esas palabras semilla son como una contraseña maestra para recuperar su cartera Bitcoin.\nEn la sección \"Semilla de cartera\" encontrará más información.\n\nAdicionalmente, debería hacer una copia de seguridad completa del directorio de aplicación en la sección \"Copia de seguridad\" + +popup.bitcoinLocalhostNode.msg=Bisq ha detectado un nodo de Bitcoin Core ejecutándose en esta máquina (en local).\n\nPor favor, asegúrese de:\n- que el nodo está completamente sincronizado al iniciar Bisq\n- que el podado está desabilitado ('prune=0' en bitcoin.conf)\n- que los filtros bloom están deshabilitados ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Cerrando aplicación... +popup.shutDownInProgress.msg=Cerrar la aplicación puede llevar unos segundos.\nPor favor no interrumpa el proceso. + +popup.attention.forTradeWithId=Se requiere atención para el intercambio con ID {0} +popup.attention.reasonForPaymentRuleChange=La versión 1.5.5 introduce un cambio crítico en las reglas de intercambio acerca del campo \"motivo de pago\" de las transferencias bancarias. Por favor deje este campo vacío -- NO USE NUNCA Más la ID de intercambio como \"motivo de pago\". + +popup.info.multiplePaymentAccounts.headline=Múltiples cuentas de pago disponibles +popup.info.multiplePaymentAccounts.msg=Tiene múltiples cuentes de pago disponibles para esta oferta. Por favor, asegúrese de que ha elegido la correcta. + +popup.accountSigning.selectAccounts.headline=Seleccionar cuentas de pago +popup.accountSigning.selectAccounts.description=Basado en el método de pago y el momento de tiempo todas las cuentas de pago que estén conectadas a una disputa donde ocurra el pago a el comprador será seleccionada por usted para firmarlas. +popup.accountSigning.selectAccounts.signAll=Firma todos los métodos de pago +popup.accountSigning.selectAccounts.datePicker=Seleccione momento de tiempo hasta que las cuentas sean firmadas + +popup.accountSigning.confirmSelectedAccounts.headline=Confirmar cuentas de pago seleccionadas +popup.accountSigning.confirmSelectedAccounts.description=Basado en su valor introducido, {0} cuentas de pago serán seleccionadas. +popup.accountSigning.confirmSelectedAccounts.button=Confirmar cuentas de pago +popup.accountSigning.signAccounts.headline=Confirmar firma de cuentas de pago +popup.accountSigning.signAccounts.description=Según su selección, se firmarán {0} cuentas de pago. +popup.accountSigning.signAccounts.button=Firmar cuentas de pago +popup.accountSigning.signAccounts.ECKey=Introduzca clave privada del árbitro +popup.accountSigning.signAccounts.ECKey.error=ECKey de mal árbitro + +popup.accountSigning.success.headline=Felicidades +popup.accountSigning.success.description=Todas las cuentas de pago {0} se firmaron con éxito! +popup.accountSigning.generalInformation=Encontrará el estado de firma de todas sus cuentas en la sección de cuentas.\n\nPara más información, por favor visite [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Una de sus cuentas de pago ha sido verificada y firmada por un árbitro. Intercambiar con esta cuenta firmará automáticamente la cuenta de su par de intercambio después de un intercambio exitoso.\n\n{0} +popup.accountSigning.signedByPeer=Una de sus cuentas de pago ha sido verificada y firmada por un par de intercambio. Su límite inicial de intercambio ha sido elevado y podrá firmar otras cuentas en {0} días desde ahora.\n\n{1} +popup.accountSigning.peerLimitLifted=El límite inicial para una de sus cuentas se ha elevado.\n\n{0} +popup.accountSigning.peerSigner=Una de sus cuentas es suficiente antigua para firmar otras cuentas de pago y el límite inicial para una de sus cuentas se ha elevado.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Importar edad de cuenta de testigos no firmados +popup.accountSigning.confirmSingleAccount.headline=Confirmar el testigo de cuenta seleccionado +popup.accountSigning.confirmSingleAccount.selectedHash=Hash del testigo seleccionado +popup.accountSigning.confirmSingleAccount.button=Firmar testigo de edad de cuenta +popup.accountSigning.successSingleAccount.description=Se seleccionón el testigo {0} +popup.accountSigning.successSingleAccount.success.headline=Éxito + +popup.accountSigning.unsignedPubKeys.headline=Claves públicas no firmadas +popup.accountSigning.unsignedPubKeys.sign=Firmar claves públicas +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 + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notificación de intercambio con ID {0} +notification.ticket.headline=Ticket de soporte de intercambio con ID {0} +notification.trade.completed=El intercambio se ha completado y puede retirar sus fondos. +notification.trade.accepted=Su oferta ha sido aceptada por un {0} BTC +notification.trade.confirmed=Su intercambio tiene al menos una confirmación en la cadena de bloques.\nPuede comenzar el pago ahora. +notification.trade.paymentStarted=El comprador de BTC ha comenzado el pago. +notification.trade.selectTrade=Seleccionar intercambio +notification.trade.peerOpenedDispute=Su pareja de intercambio ha abierto un {0}. +notification.trade.disputeClosed={0} se ha cerrado. +notification.walletUpdate.headline=Actualizar monedero de intercambio. +notification.walletUpdate.msg=Su monedero de intercambio tiene fondos suficientes.\nCantidad: {0} +notification.takeOffer.walletUpdate.msg=Su monedero de intercambio ya tiene fondos suficientes de un intento de toma de oferta anterior.\nCantidad: {0} +notification.tradeCompleted.headline=Intercambio completado +notification.tradeCompleted.msg=Ahora puede retirar sus fondos a una billetera externa de Bitcoin o transferirlos a la billetera Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Mostrar ventana de aplicación +systemTray.hide=Esconder ventana de aplicación +systemTray.info=Información sobre Bisq +systemTray.exit=Salir +systemTray.tooltip=Bisq: Una red de intercambio de bitcoin descentralizada + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Por favor asegúrese de que la comisión de minado usada en su monedero externo es de al menos {0} sat/vbyte. De lo contrario, las transacciones de intercambio podrían no confirmarse y el intercambio acabaría en disputa. + +guiUtil.accountExport.savedToPath=Las cuentas de intercambio se han guardado en el directorio:\n{0} +guiUtil.accountExport.noAccountSetup=No tiene cuentas de intercambio configuradas para exportar. +guiUtil.accountExport.selectPath=Seleccionar directorio a {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Cuenta de intercambio con id {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=No hemos importado la cuenta de intercambio con id {0} porque ya existe.\n +guiUtil.accountExport.exportFailed=La exportación a CSV ha fallado por un error.\nError = {0} +guiUtil.accountExport.selectExportPath=Seleccionar directorio de exportación. +guiUtil.accountImport.imported=Cuenta de intercambio importada desde la ruta:\n{0}\n\nCuentas importadas:\n{1} +guiUtil.accountImport.noAccountsFound=No se han encontrado cuentas de intercambio exportadas en la ruta: {0}.\nEl nombre del archivo es {1}." +guiUtil.openWebBrowser.warning=Va a abrir una página web en el navegador de su sistema.\n¿Quiere abrir la página web ahora?\n\nSi no está usando \"Navegador Tor\" como su navegador de sistema predeterminado, se conectará a la página web en la red abierta.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Abrir la página web y no preguntar de nuevo +guiUtil.openWebBrowser.copyUrl=Copiar URL y cancelar +guiUtil.ofTradeAmount=de cantidad de intercambio +guiUtil.requiredMinimum=(mínimo requerido) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Seleccionar moneda +list.currency.showAll=Mostrar todo +list.currency.editList=Editar lista de monedas + +table.placeholder.noItems=Actualmente no hay {0} disponible/s +table.placeholder.noData=Actualmente no hay datos disponibles +table.placeholder.processingData=Procesando datos... + + +peerInfoIcon.tooltip.tradePeer=Pareja de intercambio +peerInfoIcon.tooltip.maker=Creador +peerInfoIcon.tooltip.trade.traded={0} dirección onion: {1}\nYa ha intercambiado en {2} ocasión/es con esa persona.\n{3} +peerInfoIcon.tooltip.trade.notTraded=Dirección onion: {0}\nNo ha intercambiado con esta persona.\n{2} +peerInfoIcon.tooltip.age=Cuenta de pago creada hace {0} +peerInfoIcon.tooltip.unknownAge=Edad de cuenta de pago no conocida. + +tooltip.openPopupForDetails=Abrir popup para detalles +tooltip.invalidTradeState.warning=Esta transacción está en un estado inválido. Abra la ventana de detalles para obtener más información +tooltip.openBlockchainForAddress=Abrir explorador de cadena de bloques externo para la dirección: {0} +tooltip.openBlockchainForTx=Abrir explorador de cadena de bloques externo para la la transacción: {0} + +confidence.unknown=Estado de transacción desconocido +confidence.seen=Visto por {0} par/es / 0 confirmaciones +confidence.confirmed=Confirmado en {0} bloque/s +confidence.invalid=La transacción es inválida + +peerInfo.title=Información del par +peerInfo.nrOfTrades=Número de intercambios completados +peerInfo.notTradedYet=No ha comerciado con este usuario. +peerInfo.setTag=Configurar etiqueta para ese par +peerInfo.age.noRisk=Antigüedad de la cuenta de pago +peerInfo.age.chargeBackRisk=Tiempo desde el firmado +peerInfo.unknownAge=Edad desconocida + +addressTextField.openWallet=Abrir su cartera Bitcoin predeterminada +addressTextField.copyToClipboard=Copiar dirección al portapapeles +addressTextField.addressCopiedToClipboard=La dirección se ha copiado al portapapeles +addressTextField.openWallet.failed=Fallo al abrir la cartera Bitcoin predeterminada. ¿Tal vez no tenga una instalada? + +peerInfoIcon.tooltip={0}\nEtiqueta: {1} + +txIdTextField.copyIcon.tooltip=Copiar ID de transacción al monedero +txIdTextField.blockExplorerIcon.tooltip=Abrir un explorador de bloques con esta ID de transacción +txIdTextField.missingTx.warning.tooltip=Falta la transacción requerida + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Cuenta\" +navigation.account.walletSeed=\"Cuenta/Semilla de cartera\" +navigation.funds.availableForWithdrawal=\"Fondos/Enviar fondos"\" +navigation.portfolio.myOpenOffers=\"Portafolio/Mis ofertas abiertas\" +navigation.portfolio.pending=\"Portafolio/Intercambios abiertos\" +navigation.portfolio.closedTrades=\"Portafolio/Historial\" +navigation.funds.depositFunds=\"Fondos/Recibir fondos\" +navigation.settings.preferences=\"Ajustes/Preferencias\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Fondos/Transacciones\" +navigation.support=\"Soporte\" +navigation.dao.wallet.receive=\"DAO/Monedero BSQ/Recibir\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} cantidad{1} +formatter.makerTaker=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.fiat=Está creando una oferta a {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Está creando una oferta a {0} {1} ({2} {3}) +formatter.asMaker={0} {1} como creador +formatter.asTaker={0} {1} como tomador + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Red principal de Bitcoin +# suppress inspection "UnusedProperty" +BTC_TESTNET=Red de prueba de Bitcoin +# suppress inspection "UnusedProperty" +BTC_REGTEST=Regtest Bitcoin +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Testnet de Bitcoin DAO (depreciada) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=DAO Bisq Betanet (red principal Bitcoin) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Regtest de Bitcoin DAO + +time.year=Año +time.month=Mes +time.week=Semana +time.day=Día +time.hour=Hora +time.minute10=10 minutos +time.hours=horas +time.days=días +time.1hour=1 hora +time.1day=1 día +time.minute=minuto +time.second=segundo +time.minutes=minutos +time.seconds=segundos + + +password.enterPassword=Introducir contraseña +password.confirmPassword=Confirmar contraseña +password.tooLong=La contraseña debe de tener menos de 500 caracteres. +password.deriveKey=Derivar clave desde contraseña +password.walletDecrypted=El monedero se desencriptó con éxito y se eliminó la protección por contraseña. +password.wrongPw=Ha introducido la contraseña incorrecta.\n\nPor favor, introduzca nuevamente la contraseña, evitando errores. +password.walletEncrypted=El monedero se encriptó con éxito y se activó la protección por contraseña. +password.walletEncryptionFailed=No se pudo establecer la contraseña de de la cartera. Puede haber importado palabras semilla que no corresponden a la base de datos del monedero. Por favor contacte con los desarrolladores en Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=Las 2 contraseñas introducidas no coinciden. +password.forgotPassword=¿Ha olvidado la contraseña? +password.backupReminder=Por favor, al establecer una contraseña para la cartera, tenga en cuenta que todas las copias de seguridad creadas de la cartera no encriptada serán borradas automáticamente +password.backupWasDone=Ya he hecho una copia de seguridad +password.setPassword=Establecer password (ya tengo una copia de seguridad) +password.makeBackup=Hacer copia de seguridad + +seed.seedWords=Palabras semilla de la cartera +seed.enterSeedWords=Introduzca palabras semilla de la cartera +seed.date=Fecha de la cartera +seed.restore.title=Restaurar monederos desde las palabras semilla +seed.restore=Restaurar monederos +seed.creationDate=Fecha de creación +seed.warn.walletNotEmpty.msg=Su cartera de Bitcoin no está vacía.\n\nDebe vaciar esta cartera antes de intentar restaurar a otra más antigua, ya que mezclar monederos puede llevar a copias de seguridad inválidas.\n\nPor favor, finalice sus intercambios, cierre todas las ofertas abiertas y vaya a la sección Fondos para retirar sus bitcoins.\nEn caso de que no pueda acceder a sus bitcoins puede utilizar la herramienta de emergencia para vaciar el monedero.\nPara abrir la herramienta de emergencia pulse \"alt + e\" o \"Cmd/Ctrl + e\". +seed.warn.walletNotEmpty.restore=Quiero restaurar de todos modos +seed.warn.walletNotEmpty.emptyWallet=Vaciaré mi monedero antes +seed.warn.notEncryptedAnymore=Sus carteras están cifradas.\n\nDespués de restaurarlas, las carteras no estarán cifradas y tendrá que introducir una nueva contraseña.\n\n¿Quiere continuar? +seed.warn.walletDateEmpty=Como no ha especificado una fecha específica para el monedero, bisq tendrá que escanear la cadena de bloques desde el 2013.10.09 (la fecha de BIP39).\n\nLos monederos BIP39 se introdujeron en bisq en 2017.06.28 (publicación v.0.5). Puede ahorrar tiempo utilizando esa fecha.\n\nIdealmente, debería especificar la fecha en que su semilla fue creada.\n\n\nEstá seguro de que quiere continuar sin especificar una fecha para el monedero? +seed.restore.success=Las carteras se restauraron con éxito con las nuevas palabras semilla.\n\nDebe cerrar y reiniciar la aplicación +seed.restore.error=Un error ocurrió el restaurar los monederos con las palabras semilla. {0} +seed.restore.openOffers.warn=Tiene ofertas abiertas que serán eliminadas si restaura desde las palabras semilla.\n¿Está seguro de que quiere continuar? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Cuenta +payment.account.no=Número de cuenta +payment.account.name=Nombre de cuenta +payment.account.userName=Nombre de usuario +payment.account.phoneNr=Número de teléfono +payment.account.owner=Nombre completo del propietario de la cuenta +payment.account.fullName=Nombre completo +payment.account.state=Estado/Provincia/Región +payment.account.city=Ciudad +payment.bank.country=País del banco +payment.account.name.email=Nombre completo / correo electrónico del titular de la cuenta: +payment.account.name.emailAndHolderId=Nombre completo / correo electrónico del titular de la cuenta: {0} +payment.bank.name=Nombre del banco +payment.select.account=Seleccione tipo de cuenta +payment.select.region=Seleccione región +payment.select.country=Seleccione país +payment.select.bank.country=Seleccione país del banco +payment.foreign.currency=¿Está seguro de que quiere elegir una moneda diferente que la del país por defecto? +payment.restore.default=No, restaurar moneda por defecto. +payment.email=Email +payment.country=País +payment.extras=Requerimientos extra +payment.email.mobile=Email o número de móvil +payment.altcoin.address=Dirección altcoin +payment.altcoin.tradeInstantCheckbox=Intercambio instantáneo (en una hora) con esta altcoin +payment.altcoin.tradeInstant.popup=Para intercambios instantáneos se requiere que ambos pares estén en linea para poder completar el intercambio en menos de 1 hora.\n\nSi tiene ofertas abiertas y no está disponible, por favor deshabilite esas ofertas en la pantalla 'Portafolio'. +payment.altcoin=Altcoin +payment.select.altcoin=Seleccionar o buscar altcoin +payment.secret=Pregunta secreta +payment.answer=Respuesta +payment.wallet=ID de cartera: +payment.amazon.site=Compre una tarjeta regalo en +payment.ask=Pregunte en el Chat de Intercambio +payment.uphold.accountId=Nombre de usuario, correo electrónico o núm de teléfono +payment.moneyBeam.accountId=Correo electrónico o núm. de telefóno +payment.venmo.venmoUserName=Nombre de usuario Venmo +payment.popmoney.accountId=Correo electrónico o núm. de telefóno +payment.promptPay.promptPayId=Citizen ID/Tax ID o número de teléfono +payment.supportedCurrencies=Monedas soportadas +payment.supportedCurrenciesForReceiver=Monedas para recibir fondos +payment.limitations=Límitaciones: +payment.salt="Salt" de la verificación de edad de la cuenta. +payment.error.noHexSalt=El "salt" necesitar estar en formato HEX.\nSolo se recomienda editar el "salt" si quiere transferir el "salt" desde una cuenta antigua para mantener su edad de cuenta. La edad de cuenta se verifica usando el "salt" de la cuenta y datos de identificación de cuenta (Ej. IBAN). +payment.accept.euro=Aceptar tratos desde estos países Euro. +payment.accept.nonEuro=Aceptar tratos desde estos países no-Euro +payment.accepted.countries=Países aceptados +payment.accepted.banks=Bancos aceptados (ID) +payment.mobile=Número de móvil +payment.postal.address=Dirección postal +payment.national.account.id.AR=Número CBU +shared.accountSigningState=Status de firmado de cuentas + +#new +payment.altcoin.address.dyn=Dirección {0}: +payment.altcoin.receiver.address=Dirección altcoin del receptor +payment.accountNr=Número de cuenta +payment.emailOrMobile=Email o número de móvil +payment.useCustomAccountName=Utilizar nombre de cuenta personalizado +payment.maxPeriod=Periodo máximo de intercambio +payment.maxPeriodAndLimit=Duración máxima de intercambio: {0} / Compra máx: {1} / Venta máx: {2} / Edad de cuenta: {3} +payment.maxPeriodAndLimitCrypto=Duración máxima de intercambio: {0} / Límite máximo de intercambio: {1} +payment.currencyWithSymbol=Moneda: {0} +payment.nameOfAcceptedBank=Nombre de banco aceptado +payment.addAcceptedBank=Añadir banco aceptado +payment.clearAcceptedBanks=Despejar bancos aceptados +payment.bank.nameOptional=Nombre del banco (opcional) +payment.bankCode=Código bancario +payment.bankId=ID bancario (BIC/SWIFT) +payment.bankIdOptional=ID bancaria (BIC/SWIFT) (opcional) +payment.branchNr=Número de sucursal +payment.branchNrOptional=Número de sucursal (opcional) +payment.accountNrLabel=Número de cuenta (IBAN) +payment.accountType=Tipo de cuenta +payment.checking=Comprobando +payment.savings=Ahorros +payment.personalId=ID personal: +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC tiene que enviar el número de autorización y una foto del recibo al vendedor de BTC 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 BTC tiene que enviar el número de seguimiento (MTCN) y una foto del recibo al vendedor de BTC 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 BTC necesita enviar al vendedor de BTC 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 BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut requiere el 'nombre de usuario' como ID de cuenta y no el número de teléfono o el e-mail que se requería anteriormente. +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.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 + +payment.usPostalMoneyOrder.info=Los intercambios usando US Postal Money Orders (USPMO) en Bisq requiere que entienda lo siguiente:\n\n- Los compradores de BTC deben escribir la dirección del vendedor en los campos de "Payer" y "Payee" y tomar una foto en alta resolución de la USPMO y del sobre con la prueba de seguimiento antes de enviar.\n- Los compradores de BTC deben enviar la USPMO con confirmación de entrega.\n\nEn caso de que sea necesaria la mediación, se requerirá al comprador que entregue las fotos al mediador o agente de devolución de fondos, junto con el número de serie de la USPMO, número de oficina postal, y la cantidad de USD, para que puedan verificar los detalles en la web de US Post Office.\n\nNo entregar la información requerida al Mediador o Árbitro resultará en pérdida del caso de disputa. \n\nEn todos los casos de disputa, el emisor de la USPMO tiene el 100% de responsabilidad en aportar la evidencia al Mediador o Árbitro.\n\nSi no entiende estos requerimientos, no comercie usando USPMO en Bisq. + +payment.cashByMail.info=Comerciar usando efectivo por correo (CBM) en Bisq requiere que entienda lo siguiente:\n\n● El comprador de BTC debe empaquetar el efectivo en una bolsa de efectivo a prueba de manipulación.\n● El comprador de BTC debe filmar o tomar fotos de alta resolución del empaquetado junto con la dirección y el número de seguimiento ya añadido al paquete.\n● El comprador de BTC debe enviar el paquete de efectivo al vendedor con la confirmación de entrega y un seguro apropiado.\n● El vendedor de BTC debe filmar la apertura del paquete, asegurándose de que el número de seguimiento entregado por el emisor es visible en todo el video.\n● El creador de la oferta debe especificar cualquier términos o condiciones especiales en el campo 'Información adicional' de la cuenta de pago.\n● Al tomar la oferta, el tomador indica estar de acuredo con los términos y condiciones del tomador.\n\nLos intercambios CBM responsabilizan a ambos pares de actuar honestamente.\n\n● Los intercambios CBM tienen menos acciones verificables que otrosintrecambios de fiat. Esto hace más complicado manejar disputas.\n● Intente a resolver las disputas directamente con su par utilizando el chat de intercambio. Esta es la ruta más prometedora.\n● Los mediadores pueden considerar su caso y hacer una sugerencia, pero no está garantizado que vayan a ayudar.\n● Si se solicita mediación, y si algún par rechaza la sugerencia de mediación, los fondos de ambos pares se enviarán a la dirección de 'donación' de Bisq[HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], y el intercambio concluirá.\n● Si un comerciante rechaza la sugerencia de mediación y abre arbitraje, podría llevar a la pérdida de todos los fondos, de intercambio y depósitos de seguridad.\n● El árbitro tomará una decisión basada en la evidencia entregada. Por tanto, por favor siga y documente el proceso indicado arriba para tener evidencia en caso de disputa.\n● Las solicitudes de reembolso de fondos perdidos resultantes de CBM en la DAO no serán considerados.\n\nAsegúrese de que entiende los requerimientos de los intercambios CBM leyendo: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nSi no entiende estos requisitos, no intercambie usando CBM. + +payment.cashByMail.contact=Información de contacto +payment.cashByMail.contact.prompt=El sobre con nombre o pseudónimo debería ser dirigido a +payment.f2f.contact=Información de contacto +payment.f2f.contact.prompt=Cómo le gustaría ser contactado por el par de intercambio? (dirección email, número de teléfono...) +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.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 BTC 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.bisq.network/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.japan.bank=Banco +payment.japan.branch=Branch +payment.japan.account=Cuenta +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 BTC a través de su cuenta Amazon.\n\nBisq mostrará la dirección e-mail del vendedor de BTC o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Transferencia bancaria nacional +SAME_BANK=Transferir con el mismo banco +SPECIFIC_BANKS=Transferencias con bancos específicos +US_POSTAL_MONEY_ORDER=Giro postal US Postal +CASH_DEPOSIT=Depósito en efectivo +CASH_BY_MAIL=Efectivo por Correo +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Cara a cara (en persona) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=PayID australiano + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Bancos nacionales +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Mismo banco +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Bancos específicos +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=Giro postal US +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Depósito en efectivo +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=EfectivoPorCorreo +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=Pagos instantáneos SEPA +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Tarjeta Amazon eGift +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Tarjeta Amazon eGift +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=No se permiten entradas vacías +validation.NaN=El valor introducido no es válido +validation.notAnInteger=El valor introducido no es entero +validation.zero=El 0 no es un valor permitido. +validation.negative=No se permiten entradas negativas. +validation.fiat.toSmall=No se permite introducir un valor menor que el mínimo posible +validation.fiat.toLarge=No se permiten entradas más grandes que la mayor posible. +validation.btc.fraction=El valor introducido resulta en un valor de bitcoin menor a 1 satoshi +validation.btc.toLarge=No se permiten valores mayores que {0}. +validation.btc.toSmall=Valores menores que {0} no se permiten. +validation.passwordTooShort=El password introducido es muy corto. Necesita tener al menos 8 caracteres. +validation.passwordTooLong=La clave introducida es demasiado larga. Máximo 50 caracteres. +validation.sortCodeNumber={0} debe consistir en {1} números. +validation.sortCodeChars={0} debe consistir en {1} caracteres +validation.bankIdNumber={0} debe consistir en {1} números. +validation.accountNr=El número de cuenta debe consistir en {0} números. +validation.accountNrChars=El número de cuenta debe consistir en {0} caracteres. +validation.btc.invalidAddress=La dirección no es correcta. Por favor compruebe el formato de la dirección. +validation.integerOnly=Por favor, introduzca sólo números enteros. +validation.inputError=Su entrada causó un error:\n{0} +validation.bsq.insufficientBalance=Su saldo disponible es {0} +validation.btc.exceedsMaxTradeLimit=Su límite de intercambio es {0}. +validation.bsq.amountBelowMinAmount=La cantidad mínima es {0} +validation.nationalAccountId={0} debe consistir de {1} número(s). + +#new +validation.invalidInput=Entrada inválida: {0} +validation.accountNrFormat=El número de cuenta debe ser del formato: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=La validación de dirección falló porque no se corresponde con la estructura de una dirección {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=Las drecciones LTZ deben empezar con L. Las direcciones que empiecen por z no están soportadas. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=Las direcciones ZEC deben empezar con t. Las direcciones empezando con z no están soportadas. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=La dirección no es una dirección {0} válida! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Direcciones de segwit nativas (las que empiezan con 'lq') no son compatibles. +validation.bic.invalidLength=La longitud del valor introducido debe ser 8 u 11. +validation.bic.letters=El código de banco y país deben ser letras +validation.bic.invalidLocationCode=BIC contiene un código de localización inválido +validation.bic.invalidBranchCode=BIC contiene una sucursal inválida +validation.bic.sepaRevolutBic=Cuentas Revolut Sepa no soportadas. +validation.btc.invalidFormat=Formato inválido para una dirección Bitcoin. +validation.bsq.invalidFormat=Formato inválido para una dirección BSQ. +validation.email.invalidAddress=Dirección inválida +validation.iban.invalidCountryCode=Código de país inválido +validation.iban.checkSumNotNumeric=El checksum debe ser numérico +validation.iban.nonNumericChars=Detectado carácter no alfanumérico +validation.iban.checkSumInvalid=El checksum de IBAN es inválido +validation.iban.invalidLength=El número debe tener una longitud de 15 a 34 caracteres. +validation.interacETransfer.invalidAreaCode=Código de area no canadiense +validation.interacETransfer.invalidPhone=Por favor introduzca un número de teléfono de 11 dígitos (p.ej. 1-123-456-7890) o una dirección email. +validation.interacETransfer.invalidQuestion=Debe contener solamente letras, números, espacios y/o símbolos ' _ , . ? - +validation.interacETransfer.invalidAnswer=Debe ser una palabra y contener solamente letras, números, y/o el símbolo - +validation.inputTooLarge=El valor introducido no debe ser mayor que {0} +validation.inputTooSmall=Lo introducido tiene que ser mayor que {0} +validation.inputToBeAtLeast=El valor introducido tiene que ser al menos {0} +validation.amountBelowDust=No se permite una cantidad por debajo del límite de polvo {0} +validation.length=La longitud debe estar entre {0} y {1} +validation.fixedLength=La longitud debe ser {0} +validation.pattern=El valor introducido debe ser de formato: {0} +validation.noHexString=El valor introducido no es un formato HEX. +validation.advancedCash.invalidFormat=Tiene que ser una dirección de email o ID de cartera de formato: X000000000000 +validation.invalidUrl=No es una URL válida +validation.mustBeDifferent=Su entrada debe ser diferente del valor actual +validation.cannotBeChanged=Este parámetro no se puede cambiar +validation.numberFormatException=Excepción en formato de número {0} +validation.mustNotBeNegative=El valor introducido no debe ser negativo +validation.phone.missingCountryCode=Se necesitan dos letras de código de país para validar el número de teléfono +validation.phone.invalidCharacters=Número de teléfono {0} tiene caracteres inválidos +validation.phone.insufficientDigits=No hay suficientes dígitos en {0} para ser un número válido de teléfono +validation.phone.tooManyDigits=Hay demasiados dígitos en {0} para ser un número de teléfono válido. +validation.phone.invalidDialingCode=El código de país para el número {0} es inválido para el país {1}. El código de país correcto es {2}. +validation.invalidAddressList=La lista de direcciones válidas debe ser separada por coma diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties new file mode 100644 index 0000000000..6f217862e6 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=بیشتر بخوانید +shared.openHelp=بخش «راهنما» را باز کنید +shared.warning=هشدار +shared.close=بستن +shared.cancel=لغو +shared.ok=باشه +shared.yes=بله +shared.no=خیر +shared.iUnderstand=فهمیدم +shared.na=بدون پاسخ +shared.shutDown=خاموش +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=خرید بیتکوین +shared.sellBitcoin=بیتکوین بفروشید +shared.buyCurrency=خرید {0} +shared.sellCurrency=فروش {0} +shared.buyingBTCWith=خرید بیتکوین با {0} +shared.sellingBTCFor=فروش بیتکوین با {0} +shared.buyingCurrency=خرید {0} ( فروش بیتکوین) +shared.sellingCurrency=فروش {0} (خرید بیتکوین) +shared.buy=خرید +shared.sell=فروش +shared.buying=خریدن +shared.selling=فروختن +shared.P2P=P2P +shared.oneOffer=پیشنهاد +shared.multipleOffers=پیشنهادها +shared.Offer=پیشنهاد +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=پیشنهادهای باز +shared.trade=معامله +shared.trades=معاملات +shared.openTrades=معاملات باز +shared.dateTime=تاریخ/زمان +shared.price=قیمت +shared.priceWithCur=قیمت در {0} +shared.priceInCurForCur=قیمت در {0} برای 1 {1} +shared.fixedPriceInCurForCur=قیمت مقطوع در {0} برای 1 {1} +shared.amount=مقدار +shared.txFee=Transaction Fee +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Buyer Deposit +shared.sellerSecurityDeposit=Seller Deposit +shared.amountWithCur=مقدار در {0} +shared.volumeWithCur=حجم در {0} +shared.currency=ارز +shared.market=بازار +shared.deviation=Deviation +shared.paymentMethod=نحوه پرداخت +shared.tradeCurrency=ارز معامله +shared.offerType=نوع پیشنهاد +shared.details=جزئیات +shared.address=آدرس +shared.balanceWithCur=تراز در {0} +shared.utxo=Unspent transaction output +shared.txId=شناسه تراکنش +shared.confirmations=تاییدیه‌ها +shared.revert=بازگرداندن تراکنش +shared.select=انتخاب +shared.usage=کاربرد +shared.state=وضعیت +shared.tradeId=شناسه معامله +shared.offerId=شناسه پیشنهاد +shared.bankName=نام بانک +shared.acceptedBanks=بانک‌های مورد پذیرش +shared.amountMinMax=مقدار (حداقل - حداکثر) +shared.amountHelp=اگر پیشنهادی دسته‌ی حداقل و حداکثر مقدار دارد، شما می توانید هر مقداری در محدوده پیشنهاد را معامله کنید. +shared.remove=حذف +shared.goTo=به {0} بروید +shared.BTCMinMax=بیتکوین (حداقل - حداکثر) +shared.removeOffer=حذف پیشنهاد +shared.dontRemoveOffer=پیشنهاد را حذف نکنید +shared.editOffer=ویرایش پیشنهاد +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=حساب معاملات +shared.faq=Visit FAQ page +shared.yesCancel=بله، لغو شود +shared.nextStep=گام بعدی +shared.selectTradingAccount=حساب معاملات را انتخاب کنید +shared.fundFromSavingsWalletButton=انتقال وجه از کیف Bisq +shared.fundFromExternalWalletButton=برای تهیه پول، کیف پول بیرونی خود را باز کنید +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent= ٪ زیر قیمت بازار +shared.aboveInPercent= ٪ بالای قیمت بازار +shared.enterPercentageValue=ارزش ٪ را وارد کنید +shared.OR=یا +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=در انتظار دریافت وجه... +shared.depositTransactionId=شناسه تراکنش وجه دریافتی +shared.TheBTCBuyer=خریدار بیتکوین +shared.You=شما +shared.sendingConfirmation=در حال ارسال تاییدیه... +shared.sendingConfirmationAgain=لطفاً تاییدیه را دوباره ارسال نمایید +shared.exportCSV=Export to CSV +shared.exportJSON=به JSON خروجی بگیر +shared.summary=Show summary +shared.noDateAvailable=تاریخ موجود نیست +shared.noDetailsAvailable=جزئیاتی در دسترس نیست +shared.notUsedYet=هنوز مورد استفاده قرار نگرفته +shared.date=تاریخ +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.copyToClipboard=کپی در کلیپ‌بورد +shared.language=زبان +shared.country=کشور +shared.applyAndShutDown=اعمال و خاموش کردن +shared.selectPaymentMethod=انتخاب روش پرداخت +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=از حذف حساب انتخاب شده مطمئن هستید؟ +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=هنوز هیچ حساب کاربری تنظیم نشده است +shared.manageAccounts=مدیریت حساب‌ها +shared.addNewAccount=افزودن حساب جدید +shared.ExportAccounts=صادر کردن حساب‌ها +shared.importAccounts=وارد کردن حساب‌ها +shared.createNewAccount=ایجاد حساب جدید +shared.saveNewAccount=ذخیره‌ی حساب جدید +shared.selectedAccount=حساب انتخاب شده +shared.deleteAccount=حذف حساب +shared.errorMessageInline=\nپیغام خطا: {0} +shared.errorMessage=پیام خطا +shared.information=اطلاعات +shared.name=نام +shared.id=شناسه +shared.dashboard=داشبورد +shared.accept=پذیرش +shared.balance=موجودی +shared.save=ذخیره +shared.onionAddress=آدرس شبکه Onion +shared.supportTicket=بلیط پشتیبانی +shared.dispute=مناقشه +shared.mediationCase=mediation case +shared.seller=فروشنده +shared.buyer=خریدار +shared.allEuroCountries=تمام کشورهای یورو +shared.acceptedTakerCountries=کشورهای هدف برای پذیرش طرف معامله +shared.tradePrice=قیمت معامله +shared.tradeAmount=مقدار معامله +shared.tradeVolume=حجم معامله +shared.invalidKey=کلید وارد شده صحیح نیست. +shared.enterPrivKey=کلید خصوصی را برای بازگشایی وارد کنید +shared.makerFeeTxId=شناسه تراکنش کارمزد سفارش‌گذار +shared.takerFeeTxId=شناسه تراکنش کارمزد پذیرنده +shared.payoutTxId=شناسه تراکنش پرداخت +shared.contractAsJson=قرارداد در قالب JSON +shared.viewContractAsJson=مشاهده‌ی قرارداد در قالب JSON: +shared.contract.title=قرارداد برای معامله با شناسه ی {0} +shared.paymentDetails=جزئیات پرداخت BTC {0} +shared.securityDeposit=سپرده‌ی اطمینان +shared.yourSecurityDeposit=سپرده ی اطمینان شما +shared.contract=قرارداد +shared.messageArrived=پیام رسید. +shared.messageStoredInMailbox=پیام در پیام‌های دریافتی ذخیره شد. +shared.messageSendingFailed=ارسال پیام ناموفق بود. خطا: {0} +shared.unlock=باز کردن +shared.toReceive=قابل دریافت +shared.toSpend=قابل خرج کردن +shared.btcAmount=مقدار بیتکوین +shared.yourLanguage=زبان‌های شما +shared.addLanguage=افزودن زبان +shared.total=مجموع +shared.totalsNeeded=وجه مورد نیاز +shared.tradeWalletAddress=آدرس کیف‌پول معاملات +shared.tradeWalletBalance=موجودی کیف‌پول معاملات +shared.makerTxFee=سفارش گذار: {0} +shared.takerTxFee=پذیرنده سفارش: {0} +shared.iConfirm=تایید می‌کنم +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=باز {0} +shared.fiat=فیات +shared.crypto=کریپتو +shared.all=همه +shared.edit=ویرایش +shared.advancedOptions=گزینه‌های پیشرفته +shared.interval=دوره +shared.actions=عملیات +shared.buyerUpperCase=خریدار +shared.sellerUpperCase=فروشنده +shared.new=جدید +shared.blindVoteTxId=شناسه تراکنش رای ناشناس +shared.proposal=پیشنهاد +shared.votes=آرا +shared.learnMore=بیشتر بدانید +shared.dismiss=رد کردن +shared.selectedArbitrator=داور انتخاب شده +shared.selectedMediator=Selected mediator +shared.selectedRefundAgent=داور انتخاب شده +shared.mediator=واسط +shared.arbitrator=داور +shared.refundAgent=داور +shared.refundAgentForSupportStaff=Refund agent +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=بازار +mainView.menu.buyBtc=خرید بیتکوین +mainView.menu.sellBtc=فروش بیتکوین +mainView.menu.portfolio=سبد سرمایه +mainView.menu.funds=وجوه +mainView.menu.support=پشتیبانی +mainView.menu.settings=تنظیمات +mainView.menu.account=حساب +mainView.menu.dao=DAO (موسسه خودمختار غیرمتمرکز) + +mainView.marketPriceWithProvider.label=قیمت بازار بر اساس {0} +mainView.marketPrice.bisqInternalPrice=قیمت آخرین معامله‌ی Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=قیمت بازارهای خارجی موجود نیست.\nقیمت نمایش داده شده، از آخرین معامله‌ی Bisq برای ارز موردنظر اتخاذ شده است. +mainView.marketPrice.tooltip=قیمت بازار توسط {0}{1} ارائه شده است\nآخرین به روز رسانی: {2}\nURL لینک Node ارائه دهنده: {3} +mainView.balance.available=موجودی در دسترس +mainView.balance.reserved=رزرو شده در پیشنهادها +mainView.balance.locked=قفل شده در معاملات +mainView.balance.reserved.short=اندوخته +mainView.balance.locked.short=قفل شده + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(لوکال هاست) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=در حال ارتباط با شبکه بیت‌کوین +mainView.footer.bsqInfo.synchronizing=/ همگام‌سازی DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=در حال ایجاد ارتباط با +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=گره کامل DAO + +mainView.bootstrapState.connectionToTorNetwork=(1/4) در حال ارتباط با شبکه Tor ... +mainView.bootstrapState.torNodeCreated=(2/4) گره Tor ایجاد شد +mainView.bootstrapState.hiddenServicePublished=(3/4) سرویس پنهان منتشر شد +mainView.bootstrapState.initialDataReceived=(4/4) داده های اولیه دریافت شد + +mainView.bootstrapWarning.noSeedNodesAvailable=عدم وجود Node های اولیه +mainView.bootstrapWarning.noNodesAvailable=Node ها و همتایان اولیه موجود نیستند +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Bisq network failed + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Nodeی برای درخواست داده موجود نیست.\nلطفاً ارتباط اینترنت خود را بررسی کنید یا برنامه را مجدداً راه اندازی کنید. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Bisq network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. + +mainView.walletServiceErrorMsg.timeout=ارتباط با شبکه‌ی بیتکوین به دلیل وقفه، ناموفق بود. +mainView.walletServiceErrorMsg.connectionError=ارتباط با شبکه‌ی بیتکوین به دلیل یک خطا: {0}، ناموفق بود. + +mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} + +mainView.networkWarning.allConnectionsLost=اتصال شما به تمام {0} همتایان شبکه قطع شد.\nشاید ارتباط کامپیوتر شما قطع شده است یا کامپیوتر در حالت Standby است. +mainView.networkWarning.localhostBitcoinLost=اتصال شما به Node لوکال هاست بیتکوین قطع شد.\nلطفاً به منظور اتصال به سایر Nodeهای بیتکوین، برنامه‌ی Bisq یا Node لوکال هاست بیتکوین را مجددا راه اندازی کنید. +mainView.version.update=(به روز رسانی موجود است) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=دفتر پیشنهادها +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=معاملات + +# OfferBookChartView +market.offerBook.buyAltcoin=خرید {0} (فروش {1}) +market.offerBook.sellAltcoin=فروش {1} (خرید {0}) +market.offerBook.buyWithFiat=خرید {0} +market.offerBook.sellWithFiat=فروش {0} +market.offerBook.sellOffersHeaderLabel=فروش {0} به +market.offerBook.buyOffersHeaderLabel=خرید {0} از +market.offerBook.buy=می‌خواهم بیتکوین بخرم. +market.offerBook.sell=می‌خواهم بیتکوین بفروشم. + +# SpreadView +market.spread.numberOfOffersColumn=تمام پیشنهادها ({0}) +market.spread.numberOfBuyOffersColumn=خرید بیتکوین ({0}) +market.spread.numberOfSellOffersColumn=فروش بیتکوین ({0}) +market.spread.totalAmountColumn=مجموع بیتکوین ({0}) +market.spread.spreadColumn=تفاوت نرخ +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=معاملات: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=باز: +market.trades.tooltip.candle.close=بسته: +market.trades.tooltip.candle.high=بالا: +market.trades.tooltip.candle.low=پایین: +market.trades.tooltip.candle.average=میانگین: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=تاریخ: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=ایجاد پیشنهاد +offerbook.takeOffer=برداشتن پیشنهاد +offerbook.takeOfferToBuy=پیشنهاد خرید {0} را بردار +offerbook.takeOfferToSell=پیشنهاد فروش {0} را بردار +offerbook.trader=معامله‌گر +offerbook.offerersBankId=شناسه بانک سفارش‌گذار (BIC/SWIFT): {0} +offerbook.offerersBankName= نام بانک سفارش‌گذار : {0} +offerbook.offerersBankSeat=کشور بانک سفارش‌گذار: {0} +offerbook.offerersAcceptedBankSeatsEuro=بانک‌های کشورهای پذیرفته شده (پذیرنده): تمام کشورهای یورو +offerbook.offerersAcceptedBankSeats=بانک‌های کشورهای پذیرفته شده (پذیرنده): \n{0} +offerbook.availableOffers=پیشنهادهای موجود +offerbook.filterByCurrency=فیلتر بر اساس ارز +offerbook.filterByPaymentMethod=فیلتر بر اساس روش پرداخت +offerbook.matchingOffers=Offers matching my accounts +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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) +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.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.ageDays={0} روز +offerbook.timeSinceSigning.notSigned.noNeed=بدون پاسخ +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=تعداد پیشنهادها: {0} +offerbook.volume={0} (حداقل - حداکثر) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=پیشنهاد جدید برای خرید {0} ایجاد کن +offerbook.createOfferToSell=پیشنهاد جدید برای فروش {0} ایجاد کن +offerbook.createOfferToBuy.withFiat=پیشنهاد جدید برای خرید {0} با {1} ایجاد کن +offerbook.createOfferToSell.forFiat=پیشنهاد جدید برای فروش {0} به ازای {1} ایجاد کن +offerbook.createOfferToBuy.withCrypto=پیشنهاد جدید برای فروش {0} (خرید {1}) +offerbook.createOfferToSell.forCrypto=پیشنهاد جدید برای خرید {0} (فروش {1}) + +offerbook.takeOfferButton.tooltip=پیشنهاد را برای {0} بردار +offerbook.yesCreateOffer=بلی، ایجاد پیشنهاد +offerbook.setupNewAccount=تنظیم یک حساب معاملات جدید +offerbook.removeOffer.success=حذف پیشنهاد موفقیت آمیز بود. +offerbook.removeOffer.failed=حذف پیشنهاد ناموفق بود:\n{0} +offerbook.deactivateOffer.failed=غیرفعالسازی پیشنهاد ناموفق بود:\n{0} +offerbook.activateOffer.failed=انتشار پیشنهاد ناموفق بود:\n{0} +offerbook.withdrawFundsHint=می‌توانید مبلغی را که از صفحه {0} پرداخت کرده اید، بردارید. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} + +offerbook.warning.wrongTradeProtocol=این پیشنهاد نیاز به نسخه پروتکل متفاوتی مانند پروتکل نسخه نرم‌افزار خودتان دارد.\n\nلطفا پس از نصب آخرین آپدیت نرم‌افزار دوباره تلاش کنید. در غیر این صورت، کاربری که این پیشنهاد را ایجاد کرده است، از نسخه‌ای قدیمی‌تر استفاده می‌کند.\n\nکاربران نمی توانند با نسخه‌های پروتکل معاملاتی ناسازگار، معامله کنند. +offerbook.warning.userIgnored=شما آدرس onion کاربر را به لیست بی‌اعتنایی خودتان افزوده‌اید. +offerbook.warning.offerBlocked=پیشنهاد توسط توسعه دهندگان Bisq مسدود شد.\nاحتمالاً هنگام گرفتن پیشنهاد، یک اشکال خارج از کنترل موجب پدید آمدن مشکلاتی شده است. +offerbook.warning.currencyBanned=ارز مورد استفاده در آن پیشنهاد، توسط توسعه‌دهندگان Bisq مسدود شد.\nبرای اطلاعات بیشتر، لطفاً از انجمن Bisq بازدید نمایید. +offerbook.warning.paymentMethodBanned=روش پرداخت مورد استفاده در آن پیشنهاد، توسط توسعه دهندگان Bisq مسدود شد.\nلطفاً برای اطلاعات بیشتر، از انجمن Bisq بازدید نمایید. +offerbook.warning.nodeBlocked=آدرس onion آن معامله گر، توسط توسعه دهندگان Bisq مسدود شد.\nاحتمالاً هنگام گرفتن پیشنهاد از جانب آن معامله گر، یک اشکال ناامن موجب پدید آمدن مسائلی شده است. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=با قیمت روز بازار خواهید فروخت (به روز رسانی در هر دقیقه). +offerbook.info.buyAtMarketPrice=با قیمت روز بازار خرید خواهید کرد (به روز رسانی در هر دقیقه). +offerbook.info.sellBelowMarketPrice={0} کمتر از قیمت روز فعلی بازار، دریافت خواهید کرد (به روز رسانی در هر دقیقه). +offerbook.info.buyAboveMarketPrice={0} بیشتر از قیمت روز فعلی بازار، پرداخت خواهید کرد (به روز رسانی در هر دقیقه). +offerbook.info.sellAboveMarketPrice={0} بیشتر از قیمت روز فعلی بازار، دریافت خواهید کرد (به روز رسانی در هر دقیقه). +offerbook.info.buyBelowMarketPrice={0} کمتر از قیمت روز فعلی بازار، پرداخت خواهید کرد (به روز رسانی در هر دقیقه). +offerbook.info.buyAtFixedPrice=با این قیمت مقطوع خرید خواهید کرد. +offerbook.info.sellAtFixedPrice=با این قیمت مقطوع، خواهید فروخت. +offerbook.info.noArbitrationInUserLanguage=در صورت اختلاف، لطفا توجه داشته باشید که داوری برای این پیشنهاد در {0} مدیریت خواهد شد. زبان در حال حاضر {1} تنظیم شده است. +offerbook.info.roundedFiatVolume=مقدار برای حفظ حریم خصوصی شما گرد شده است. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=مقدار را به بیتکوین وارد کنید. +createOffer.price.prompt=قیمت را وارد کنید +createOffer.volume.prompt=مقدار را در {0} وارد کنید +createOffer.amountPriceBox.amountDescription=مقدار BTC برای {0} +createOffer.amountPriceBox.buy.volumeDescription=مقدار در {0} به منظور خرج کردن +createOffer.amountPriceBox.sell.volumeDescription=مقدار در {0} به منظور دریافت نمودن +createOffer.amountPriceBox.minAmountDescription=حداقل مقدار بیتکوین +createOffer.securityDeposit.prompt=سپرده‌ی اطمینان +createOffer.fundsBox.title=پیشنهاد خود را تامین وجه نمایید +createOffer.fundsBox.offerFee=کارمزد معامله +createOffer.fundsBox.networkFee=کارمزد استخراج +createOffer.fundsBox.placeOfferSpinnerInfo=انتشار پیشنهاد در حال انجام است ... +createOffer.fundsBox.paymentLabel=معامله Bisq با شناسه‌ی {0} +createOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} کارمزد معامله، {2} کارمزد تراکنش) +createOffer.fundsBox.fundsStructure.BSQ=({0} سپرده‌ی اطمینان، {1} کارمزد تراکنش) + {2} کارمزد معامله +createOffer.success.headline=پیشنهاد شما، منتشر شد. +createOffer.success.info=شما می توانید پیشنهادهای باز خود را در \"سبد سهام/پیشنهادهای باز من\" مدیریت نمایید. +createOffer.info.sellAtMarketPrice=شما همیشه به نرخ روز بازار خواهید فروخت، زیرا قیمت پیشنهادتان به طور مداوم به روزرسانی خواهد شد. +createOffer.info.buyAtMarketPrice=شما همیشه به نرخ روز بازار خرید خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روزرسانی خواهد شد. +createOffer.info.sellAboveMarketPrice=شما همیشه {0}% بیشتر از نرخ روز فعلی بازار دریافت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. +createOffer.info.buyBelowMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار پرداخت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. +createOffer.warning.sellBelowMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار دریافت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. +createOffer.warning.buyAboveMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار پرداخت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. +createOffer.tradeFee.descriptionBTCOnly=کارمزد معامله +createOffer.tradeFee.descriptionBSQEnabled=انتخاب ارز برای کارمزد معامله + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +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- از کیف پول Bisq خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. + +# 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لطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید. +createOffer.setAmountPrice=تنظیم مقدار و قیمت +createOffer.warnCancelOffer=در حال حاضر،شما آن پیشنهاد را تامین وجه کرده‌اید.\nاگر اکنون لغو کنید، وجوه شما به کیف پول محلی Bisq منتقل شده و برای برداشت در صفحه ی \"وجوه/ارسال وجوه\" در درسترس است.\nآیا شما مطمئن هستید که می‌خواهید لغو کنید؟ +createOffer.timeoutAtPublishing=یک وقفه در انتشار پیشنهاد، رخ داده است. +createOffer.errorInfo=\n\nهزینه سفارش گذار، از قبل پرداخت شده است. در بدترین حالت، شما آن هزینه را از دست داده‌اید.\nلطفاً سعی کنید برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر. +createOffer.tooLowSecDeposit.warning=شما سپرده‌های اطمینان را با مقداری کمتر از مقدار پیش‌فرض {0} تنظیم کرده‌‍اید.\nآیا شما مطمئن هستید که می‌خواهید از یک سپرده اطمینان کمتر استفاده کنید؟ +createOffer.tooLowSecDeposit.makerIsSeller=در صورتی که همتای معاملاتی، از پروتکل معامله پیروی نکند، این کار محافظت کمتری برای شما دارد. +createOffer.tooLowSecDeposit.makerIsBuyer=از آنجا که شما سپرده کمتری برای ضمانت دارید، طرف معامله شما اطمینان کمی به این معامله دارد.\nمعامله‌گران ممکن است ترجیح دهند به جای پیشنهادهای شما، پیشنهادهای دیگر را بپذیرند. +createOffer.resetToDefault=خیر، راه اندازی مجدد برای ارزش پیشفرض +createOffer.useLowerValue=بلی، استفاده از ارزش پایین تر من +createOffer.priceOutSideOfDeviation=قیمتی که شما وارد کرده اید، بیشتر از حداکثر انحراف مجاز از قیمت روز بازار است.\nحداکثر انحراف مجاز، {0} است و می تواند در اولویت ها، تنظیم شود. +createOffer.changePrice=تغییر قیمت +createOffer.tac=با انتشار این پیشنهاد، می‌پذیرم که با هر معامله گری که شرایط تعیین شده در این صفحه را دارا می‌باشد، معامله کنم. +createOffer.currencyForFee=هزینه‌ی معامله +createOffer.setDeposit=تنظیم سپرده‌ی اطمینان خریدار (%) +createOffer.setDepositAsBuyer=تنظیم سپرده‌ی اطمینان من به عنوان خریدار (%) +createOffer.setDepositForBothTraders=Set both traders' security deposit (%) +createOffer.securityDepositInfo=سپرده‌ی اطمینان خریدار شما {0} خواهد بود +createOffer.securityDepositInfoAsBuyer=سپرده‌ی اطمینان شما به عنوان خریدار {0} خواهد بود +createOffer.minSecurityDepositUsed=Min. buyer security deposit is used + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=مقدار را به بیتکوین وارد کنید. +takeOffer.amountPriceBox.buy.amountDescription=مقدار بیتکوین به منظور فروش +takeOffer.amountPriceBox.sell.amountDescription=مقدار بیتکوین به منظور خرید +takeOffer.amountPriceBox.priceDescription=قیمت به ازای هر بیتکوین در {0} +takeOffer.amountPriceBox.amountRangeDescription=محدوده‌ی مقدار ممکن +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=مقداری که شما وارد کرده‌اید، از تعداد عددهای اعشاری مجاز فراتر رفته است.\nمقدار به 4 عدد اعشاری تنظیم شده است. +takeOffer.validation.amountSmallerThanMinAmount=مقدار نمی‌تواند کوچکتر از حداقل مقدار تعیین شده در پیشنهاد باشد. +takeOffer.validation.amountLargerThanOfferAmount=مقدار ورودی نمی‌تواند بالاتر از مقدار تعیین شده در پیشنهاد باشد. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=مقدار ورودی، باعث ایجاد تغییر جزئی برای فروشنده بیتکوین می شود. +takeOffer.fundsBox.title=معامله خود را تأمین وجه نمایید +takeOffer.fundsBox.isOfferAvailable=بررسی کنید آیا پیشنهاد در دسترس است... +takeOffer.fundsBox.tradeAmount=مبلغ فروش +takeOffer.fundsBox.offerFee=کارمزد معامله +takeOffer.fundsBox.networkFee=کل کارمزد استخراج +takeOffer.fundsBox.takeOfferSpinnerInfo=برداشتن پیشنهاد در حال انجام است... +takeOffer.fundsBox.paymentLabel=معامله Bisq با شناسه‌ی {0} +takeOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} هزینه‌ی معامله، {2} هزینه تراکنش شبکه) +takeOffer.success.headline=با موفقیت یک پیشنهاد را قبول کرده‌اید. +takeOffer.success.info=شما می‌توانید وضعیت معامله‌ی خود را در \"سبد سهام /معاملات باز\" ببینید. +takeOffer.error.message=هنگام قبول کردن پیشنهاد، اتفاقی رخ داده است.\n\n{0} + +# new entries +takeOffer.takeOfferButton=بررسی: برای {0} بیتکوین پیشنهاد بگذارید. +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- از کیف پول Bisq خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. +takeOffer.alreadyPaidInFunds=اگر شما در حال حاضر در وجوه، پرداختی داشته اید، می توانید آن را در صفحه ی \"وجوه/ارسال وجوه\" برداشت کنید. +takeOffer.paymentInfo=اطلاعات پرداخت +takeOffer.setAmountPrice=تنظیم مقدار +takeOffer.alreadyFunded.askCancel=شما در حال حاضر، آن پیشنهاد را تامین وجه کرده‌اید.\nاگر اکنون لغو کنید، وجوه شما به کیف پول محلی Bisq منتقل خواهد شد و برای برداشت در صفحه ی \"وجوه/ارسال وجوه\" در درسترس است.\nآیا شما مطمئن هستید که می‌خواهید لغو کنید؟ +takeOffer.failed.offerNotAvailable=درخواست پذیرفتن پیشنهاد ناموفق بود، چون پیشنهاد دیگر در دسترس نیست. شاید معامله‌گر دیگری همزمان پیشنهاد را برداشته است. +takeOffer.failed.offerTaken=شما نمی توانید آن پیشنهاد را بپذیرید، چون قبلاً توسط معامله‌گر دیگری پذیرفته شده است. +takeOffer.failed.offerRemoved=شما نمی‌توانید آن پیشنهاد را بپذیرید، چون پیشنهاد در این فاصله حذف شده است. +takeOffer.failed.offererNotOnline=درخواست پذیرش پیشنهاد ناموفق بود، زیرا سفارش گذار دیگر آنلاین نیست. +takeOffer.failed.offererOffline=شما نمی‌توانید آن پیشنهاد را بپذیرید، زیرا سفارش گذار آفلاین است. +takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. + +takeOffer.error.noFundsLost=\n\nهیچ پولی تاکنون از کیف پول شما کم نشده است.\nلطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید تا ببینید آیا می‌توانید مشکل را حل کنید یا خیر. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nتراکنش سپرده از قبل منتشر شده است.\nلطفاً سعی کنید تا برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر.\nاگر مشکل همچنان پابرجا است، لطفاً برای پشتیبانی با توسعه دهندگان تماس بگیرید. +takeOffer.error.payoutPublished=\n\nتراکنش پرداخت از قبل منتشر شده است.\nلطفاً سعی کنید تا برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر.\nاگر مشکل همچنان پابرجا است، لطفاً برای پشتیبانی با توسعه دهندگان تماس بگیرید. +takeOffer.tac=با پذیرفتن این پیشنهاد، من قبول می‌کنم تا با شرایط تعیین شده در این صفحه معامله کنم. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=قیمت نشان‌شده +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=تنظیم قیمت +editOffer.confirmEdit=تأیید: ویرایش پیشنهاد +editOffer.publishOffer=انتشار پیشنهاد شما. +editOffer.failed=ویرایش پیشنهاد، ناموفق بود:\n{0} +editOffer.success=پیشنهاد شما با موفقیت ویرایش شد. +editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by the Bisq DAO and can no longer be edited. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=معاملات باز من +portfolio.tab.pendingTrades=معاملات باز +portfolio.tab.history=تاریخچه +portfolio.tab.failed=ناموفق +portfolio.tab.editOpenOffer=ویرایش پیشنهاد + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=برای تأییدیه بلاک چین منتظر باشید +portfolio.pending.step2_buyer.startPayment=آغاز پرداخت +portfolio.pending.step2_seller.waitPaymentStarted=صبر کنید تا پرداخت شروع شود +portfolio.pending.step3_buyer.waitPaymentArrived=صبر کنید تا پرداخت حاصل شود +portfolio.pending.step3_seller.confirmPaymentReceived=تأیید رسید پرداخت +portfolio.pending.step5.completed=تکمیل شده + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=لطفاً از کیف پول {0} خارجی شما انتقال دهید.\n{1} به فروشنده بیتکوین\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=لطفاً به یک بانک بروید و {0} را به فروشنده ی بیتکوین پرداخت نمایید.\n\n +portfolio.pending.step2_buyer.cash.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، روی کاغذ رسید بنویسید: بدون استرداد.\nسپس آن را به 2 قسمت پاره کنید، از آن ها عکس بگیرید و به آدرس ایمیل فروشنده‌ی بیتکوین ارسال نمایید. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=لطفاً {0} را توسط مانی‌گرام، به فروشنده ی بیتکوین پرداخت نمایید.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، شماره مجوز و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال کنید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=لطفاً {0} را با استفاده از Western Union به فروشنده‌ی بیتکوین پرداخت کنید.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، MTCN (عدد پیگیری) و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال کنید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Money Order\" به فروشنده‌ی بیتکوین پرداخت کنید.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=لطفا با استفاده از راه‌های ارتباطی ارائه شده توسط فروشنده با وی تماس بگیرید و قرار ملاقاتی را برای پرداخت {0} تنظیم کنید.\n +portfolio.pending.step2_buyer.startPaymentUsing=آغاز پرداخت با استفاده از {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=مبلغ انتقال +portfolio.pending.step2_buyer.sellersAddress=آدرس {0} فروشنده +portfolio.pending.step2_buyer.buyerAccount=حساب پرداخت مورد استفاده +portfolio.pending.step2_buyer.paymentStarted=پرداخت آغاز شد +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. +portfolio.pending.step2_buyer.paperReceipt.headline=آیا کاغذ رسید را برای فروشنده‌ی بیتکوین فرستادید؟ +portfolio.pending.step2_buyer.paperReceipt.msg=به یاد داشته باشید:\nباید روی کاغذ رسید بنویسید: غیر قابل استرداد.\nبعد آن را به 2 قسمت پاره کنید، عکس بگیرید و آن را به آدرس ایمیل فروشنده ارسال کنید. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=شماره و رسید مجوز را ارسال کنید +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=شما باید شماره مجوز و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال نمایید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}.\nآیا شماره مجوز و قرارداد را برای فروشنده فرستادید؟ +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCN و رسید را ارسال کنید +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=شما باید MTCN (شماره پیگیری) و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال نمایید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}.\n\nآیا MTCN و قرارداد را برای فروشنده فرستادید؟ +portfolio.pending.step2_buyer.halCashInfo.headline=ارسال کد HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=باید کد HalCash و شناسه‌ی معامله ({0}) را به فروشنده بیتکوین پیامک بفرستید. شماره موبایل فروشنده بیتکوین {1} است. آیا کد را برای فروشنده فرستادید؟ +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Bisq clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=تأیید کنید که پرداخت را آغاز کرده‌اید +portfolio.pending.step2_buyer.confirmStart.msg=آیا شما پرداخت {0} را به شریک معاملاتی خود آغاز کردید؟ +portfolio.pending.step2_buyer.confirmStart.yes=بلی، پرداخت را آغاز کرده‌ام +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=برای پرداخت منتظر باشید +portfolio.pending.step2_seller.f2fInfo.headline=اطلاعات تماس خریدار +portfolio.pending.step2_seller.waitPayment.msg=تراکنش سپرده، حداقل یک تأییدیه بلاکچین دارد.شما\nباید تا آغاز پرداخت {0} از جانب خریدار بیتکوین، صبر نمایید. +portfolio.pending.step2_seller.warn=خریدار بیت‌کوین هنوز پرداخت {0} را انجام نداده است.\nشما باید تا آغاز پرداخت از جانب او، صبر نمایید.\nاگر معامله تا {1} تکمیل نشد، داور بررسی خواهد کرد. +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. +tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Bisq (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +message.state.SENT=پیام ارسال شد +# suppress inspection "UnusedProperty" +message.state.ARRIVED=پیام به همتا رسید +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=همتا رسید پیام را تأیید کرد +# suppress inspection "UnusedProperty" +message.state.FAILED=ارسال پیام ناموفق بود + +portfolio.pending.step3_buyer.wait.headline=برای تأییدیه‌ی پرداخت فروشنده‌ی بیتکوین منتظر باشید +portfolio.pending.step3_buyer.wait.info=برای تأییدیه رسید پرداخت {0} از جانب فروشنده‌ی بیتکوین، منتظر باشید +portfolio.pending.step3_buyer.wait.msgStateInfo.label=وضعیت پیام آغاز شدن پرداخت +portfolio.pending.step3_buyer.warn.part1a=بر بلاکچین {0} +portfolio.pending.step3_buyer.warn.part1b=در ارائه دهنده‌ی پرداخت شما (برای مثال بانک) +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=شریک معاملاتی شما تأیید کرده که پرداخت {0} را آغاز نموده است.\n\n +portfolio.pending.step3_seller.altcoin.explorer=در کاوشگر بلاکچین محبوبتان {0} +portfolio.pending.step3_seller.altcoin.wallet=در کیف‌پول {0} شما +portfolio.pending.step3_seller.altcoin={0} لطفا بررسی کنید {1} که آیا تراکنش مربوط به آدرس شما\n{2}\n تعداد تاییدیه‌های کافی بر روی بلاکچین دریافت کرده است یا خیر.\nمبلغ پرداخت باید {3} باشد\nشما می‌توانید آدرس {4} خود را پس از بستن پنجره از صفحه اصلی کپی کنید. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=چون پرداخت از طریق سپرده‌ی نقدی انجام شده است، خریدار BTC باید عبارت \"غیر قابل استرداد\" را روی رسید کاغذی بنویسد، آن را به 2 قسمت پاره کند و از طریق ایمیل به شما یک عکس ارسال کند.\n\nبه منظور اجتناب از استرداد وجه، تنها در صورتی تایید کنید که ایمیل را دریافت کرده باشید و از صحت رسید کاغذی مطمئن باشید.\nاگر مطمئن نیستید، {0} +portfolio.pending.step3_seller.moneyGram=خریدار باید شماره مجوز و عکسی از رسید را به ایمیل شما ارسال کند.\nرسید باید به طور واضح نام کامل شما ، کشور، ایالت فروشنده و مقدار را نشان دهد. لطفاً ایمیل خود را بررسی کنید که آیا شماره مجوز را دریافت کرده‌اید یا خیر.\n\nپس از بستن پنجره، نام و آدرس خریدار بیتکوین را برای برداشت پول از مانی‌گرام خواهید دید.\n\nتنها پس از برداشت موفقیت آمیز پول، رسید را تأیید کنید! +portfolio.pending.step3_seller.westernUnion=خریدار باید MTCN (شماره پیگیری) و عکسی از رسید را به ایمیل شما ارسال کند.\nرسید باید به طور واضح نام کامل شما، کشور، ایالت فروشنده و مقدار را نشان دهد. لطفاً ایمیل خود را بررسی کنید که آیا MTCN را دریافت کرده اید یا خیر.\nپس از بستن پنجره، نام و آدرس خریدار بیتکوین را برای برداشت پول از Western Union خواهید دید.\nتنها پس از برداشت موفقیت آمیز پول، رسید را تأیید کنید! +portfolio.pending.step3_seller.halCash=خریدار باید کد HalCash را برای شما با پیامک بفرستد. علاوه‌ برآن شما از HalCash پیامی را محتوی اطلاعات موردنیاز برای برداشت EUR از خودپردازهای پشتیبان HalCash دریافت خواهید کرد.\n\nپس از اینکه پول را از دستگاه خودپرداز دریافت کردید، لطفا در اینجا رسید پرداخت را تایید کنید. +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=تأیید رسید پرداخت +portfolio.pending.step3_seller.amountToReceive=مبلغ قابل دریافت +portfolio.pending.step3_seller.yourAddress=آدرس {0} شما +portfolio.pending.step3_seller.buyersAddress=آدرس {0} خریدار +portfolio.pending.step3_seller.yourAccount=حساب معاملاتی شما +portfolio.pending.step3_seller.xmrTxHash=شناسه تراکنش +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=تأیید رسید پرداخت +portfolio.pending.step3_seller.buyerStartedPayment=خریدار بیتکوین پرداخت {0} را آغاز کرده است.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=تأییدیه‌های بلاکچین را در کیف پول آلتکوین خود یا بلاکچین اکسپلورر بررسی کنید و هنگامی که تأییدیه های بلاکچین کافی دارید، پرداخت را تأیید کنید. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=حساب معاملاتی خود را بررسی کنید (برای مثال بانک) و وقتی وجه را دریافت کردید، تأیید نمایید. +portfolio.pending.step3_seller.warn.part1a=در بلاکچین {0} +portfolio.pending.step3_seller.warn.part1b=در ارائه دهنده‌ی پرداخت شما (برای مثال بانک) +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=آیا وجه {0} را از شریک معاملاتی خود دریافت کرده‌اید؟\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=تأیید کنید که وجه را دریافت کرده‌اید +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=بله وجه را دریافت کرده‌ام +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. + +portfolio.pending.step5_buyer.groupTitle=خلاصه‌ای از معامله‌ی کامل شده +portfolio.pending.step5_buyer.tradeFee=کارمزد معامله +portfolio.pending.step5_buyer.makersMiningFee=کارمزد استخراج +portfolio.pending.step5_buyer.takersMiningFee=کل کارمزد استخراج +portfolio.pending.step5_buyer.refunded=سپرده اطمینان مسترد شده +portfolio.pending.step5_buyer.withdrawBTC=برداشت بیتکوین شما +portfolio.pending.step5_buyer.amount=مبلغ قابل برداشت +portfolio.pending.step5_buyer.withdrawToAddress=برداشت به آدرس +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=برداشت به کیف پول خارجی +portfolio.pending.step5_buyer.alreadyWithdrawn=وجوه شما در حال حاضر برداشت شده است.\nلطفاً تاریخچه‌ی تراکنش را بررسی کنید. +portfolio.pending.step5_buyer.confirmWithdrawal=تأیید درخواست برداشت +portfolio.pending.step5_buyer.amountTooLow=مقدار مورد انتقال کمتر از هزینه تراکنش و حداقل ارزش tx (dust) است. +portfolio.pending.step5_buyer.withdrawalCompleted.headline=برداشت تکمیل شد +portfolio.pending.step5_buyer.withdrawalCompleted.msg=معاملات تکمیل شده‌ی شما در \"سبد سهام/تاریخچه\" ذخیره شده است.\nشما میتوانید تمام تراکنش‌های بیتکوین خود را در \"وجوه/تراکنش‌ها\" مرور کنید. +portfolio.pending.step5_buyer.bought=شما خریده‌اید +portfolio.pending.step5_buyer.paid=پرداخت کرده‌اید + +portfolio.pending.step5_seller.sold=فروخته‌اید +portfolio.pending.step5_seller.received=دریافت کرده‌اید + +tradeFeedbackWindow.title=تبریک، معامله شما کامل شد. +tradeFeedbackWindow.msg.part1=دوست داریم تجربه شما را بشنویم. این امر به ما کمک می کند تا نرم افزار را بهبود بخشیم و مشکلات را حل کنیم. اگر می خواهید بازخوردی ارائه کنید، لطفا این نظرسنجی کوتاه (بدون نیاز به ثبت نام) را در زیر پر کنید: +tradeFeedbackWindow.msg.part2=اگر سوالی دارید یا مشکلی را تجربه کرده‌اید، لطفا با سایر کاربران و شرکت کننده ها از طریق انجمن Bisq که در ذیل ارائه شده، به اشتراک بگذارید: +tradeFeedbackWindow.msg.part3=بابت استفاده از Bisq، از شما متشکریم! + +portfolio.pending.role=نقش من +portfolio.pending.tradeInformation=اطلاعات معامله +portfolio.pending.remainingTime=زمان باقیمانده +portfolio.pending.remainingTimeDetail={0} (تا {1}) +portfolio.pending.tradePeriodInfo=پس از تأییدیه بلاکچین اولیه، دوره ی زمانی معامله آغاز می شود. بسته به نوع روش پرداخت، حداکثر مهلت مجاز مختلفی اعمال می گردد. +portfolio.pending.tradePeriodWarning=اگر مهلت به پایان برسد، هر دو معامله گر می توانند یک مناقشه را باز کنند. +portfolio.pending.tradeNotCompleted=معامله به موقع (تا {0}) تکمیل نشد +portfolio.pending.tradeProcess=فرآیند معامله +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=باز کردن مجدد مناقشه +portfolio.pending.openSupportTicket.headline=باز کردن تیکت پشتیبانی +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. + +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. +portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Bisq community at https://bisq.community. If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Trade period is over + +portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested +portfolio.pending.openSupport=باز کردن تیکت پشتیبانی +portfolio.pending.supportTicketOpened=تیکت پشتیبانی باز شد +portfolio.pending.communicateWithArbitrator=لطفا در صفحه‌ی \"پشتیبانی\" با داور در ارتباط باشید. +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. +portfolio.pending.disputeOpenedMyUser=شما در حال حاضر یک مناقشه باز کرده‌اید.\n{0} +portfolio.pending.disputeOpenedByPeer=طرف معامله شما یک مناقشه باز کرده است\n{0} +portfolio.pending.noReceiverAddressDefined=آدرسی برای گیرنده تعیین نشده است + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=View proposed resolution +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=تکمیل شده +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated +portfolio.closed.canceled=لغو شده است +portfolio.failed.Failed=ناموفق +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=دریافت وجوه +funds.tab.withdrawal=ارسال وجوه +funds.tab.reserved=وجوه اندوخته +funds.tab.locked=وجوه قفل شده +funds.tab.transactions=تراکنش‌ها + +funds.deposit.unused=استفاده نشده +funds.deposit.usedInTx=مورد استفاده در تراکنش (های) {0} +funds.deposit.fundBisqWallet=تأمین مالی کیف پول Bisq  +funds.deposit.noAddresses=آدرس‌هایی برای سپرده ایجاد نشده است +funds.deposit.fundWallet=تأمین مالی کیف پول شما +funds.deposit.withdrawFromWallet=ارسال وجه از کیف‌پول +funds.deposit.amount=مبلغ به BTC (اختیاری) +funds.deposit.generateAddress=ایجاد آدرس جدید +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=لطفاً به جای ایجاد یک آدرس جدید، یک آدرس استفاده نشده را از جدول بالا انتخاب کنید. + +funds.withdrawal.arbitrationFee=هزینه‌ی داوری +funds.withdrawal.inputs=انتخاب ورودی‌ها +funds.withdrawal.useAllInputs=استفاده از تمام ورودی‌های موجود +funds.withdrawal.useCustomInputs=استفاده از ورودی‌های سفارشی +funds.withdrawal.receiverAmount=مقدار گیرنده +funds.withdrawal.senderAmount=مقدار فرستنده +funds.withdrawal.feeExcluded=این مبلغ کارمزد تراکنش در شبکه را شامل نمی‌شود +funds.withdrawal.feeIncluded=این مبلغ کارمزد تراکنش در شبکه را شامل می‌شود +funds.withdrawal.fromLabel=برداشت از آدرس +funds.withdrawal.toLabel=برداشت به آدرس +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=برداشت انتخاب شد +funds.withdrawal.noFundsAvailable=وجهی برای برداشت وجود ندارد +funds.withdrawal.confirmWithdrawalRequest=تأیید درخواست برداشت +funds.withdrawal.withdrawMultipleAddresses=برداشت از چندین آدرس ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=برداشت از چندین آدرس\n{0} +funds.withdrawal.notEnoughFunds=وجه کافی در کیف پول خود ندارید. +funds.withdrawal.selectAddress=یک آدرس مرجع از جدول انتخاب کنید +funds.withdrawal.setAmount=مبلغ مورد نظر برای برداشت را تعیین نمایید +funds.withdrawal.fillDestAddress=آدرس مقصد خود را پر کنید +funds.withdrawal.warn.noSourceAddressSelected=یک آدرس مرجع از جدول بالا انتخاب نمایید +funds.withdrawal.warn.amountExceeds=وجه کافی موجود از آدرس انتخاب شده ندارید.\nدرنظر بگیرید که چندین آدرس را در جدول بالا انتخاب کنید یا هزینه را تغییر دهید تا کارمزد تراکنش در شبکه را نیز شامل گردد. + +funds.reserved.noFunds=هیچ وجهی در پیشنهادهای باز اندوخته نشده است. +funds.reserved.reserved=اندوخته‌ی کیف پول محلی برای پیشنهاد با شناسه: {0} + +funds.locked.noFunds=هیچ وجهی در معاملات قفل نشده است +funds.locked.locked=قفل شده به صورت چند امضایی برای معامله با شناسه‌ی {0} + +funds.tx.direction.sentTo=ارسال به: +funds.tx.direction.receivedWith=دریافت با: +funds.tx.direction.genesisTx=از تراکنش پیدایش: +funds.tx.txFeePaymentForBsqTx=کارمزد استخراج برای تراکنش BSQ +funds.tx.createOfferFee=سفارش‌گذار و هزینه تراکنش: {0} +funds.tx.takeOfferFee=پذیرنده و هزینه تراکنش: {0} +funds.tx.multiSigDeposit=سپرده چند امضایی: {0} +funds.tx.multiSigPayout=پرداخت چند امضایی: {0} +funds.tx.disputePayout=پرداخت مناقشه: {0} +funds.tx.disputeLost=مورد مناقشه‌ی شکست خورده: {0} +funds.tx.collateralForRefund=Refund collateral: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} +funds.tx.unknown=دلیل ناشناخته: {0} +funds.tx.noFundsFromDispute=عدم بازپرداخت از مناقشه +funds.tx.receivedFunds=وجوه دریافت شده +funds.tx.withdrawnFromWallet=برداشت شده از کیف پول +funds.tx.withdrawnFromBSQWallet=مقدار BTC برداشت شده از کیف پول BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=هیچ تراکنشی موجود نیست +funds.tx.revert=عودت +funds.tx.txSent=تراکنش به طور موفقیت آمیز به یک آدرس جدید در کیف پول محلی Bisq ارسال شد. +funds.tx.direction.self=ارسال شده به خودتان +funds.tx.daoTxFee=کارمزد استخراج برای تراکنش BSQ +funds.tx.reimbursementRequestTxFee=درخواست بازپرداخت +funds.tx.compensationRequestTxFee=درخواست خسارت +funds.tx.dustAttackTx=Received dust +funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount to your wallet and might be an attempt from chain analysis companies to spy on your wallet.\n\nIf you use that transaction output in a spending transaction they will learn that you are likely the owner of the other address as well (coin merge).\n\nTo protect your privacy the Bisq wallet ignores such dust outputs for spending purposes and in the balance display. You can set the threshold amount when an output is considered dust in the settings. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.legacyArbitration.support=Legacy Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets +support.filter=Search disputes +support.filter.prompt=Enter trade ID, date, onion address or account data + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=اعلان خصوصی +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=هیچ تیکتی به صورت باز وجود ندارد +support.sendingMessage=در حال ارسال پیام ... +support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. +support.sendMessageError=ارسال پیام ناموفق بود. خطا: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=پیشنهاد در آن مناقشه با یک نسخه‌ی قدیمی از Bisq ایجاد شده است.\nشما نمی توانید آن مناقشه را با نسخه‌ی برنامه‌ی خودتان ببندید.\n\nلطفاً از یک نسخه‌ی قدیمی‌تر با پروتکل نسخه‌ی {0} استفاده کنید +support.openFile=انتخاب فایل به منظور پیوست (حداکثر اندازه فایل: {0} کیلوبایت) +support.attachmentTooLarge=مجموع اندازه ضمائم شما {0} کیلوبایت است و از حداکثر اندازه ی مجاز پیام {1} کیلوبایت، بیشتر شده است. +support.maxSize=حداکثر اندازه‌ی مجاز فایل {0} کیلوبایت است. +support.attachment=ضمیمه +support.tooManyAttachments=شما نمی‌توانید بیشتر از 3 ضمیمه در یک پیام ارسال کنید. +support.save=ذخیره فایل در دیسک +support.messages=پیام‌ها +support.input.prompt=Enter message... +support.send=ارسال +support.addAttachments=افزودن ضمیمه +support.closeTicket=بستن تیکت +support.attachments=ضمیمه‌ها: +support.savedInMailbox=پیام در صندوق پستی گیرنده ذخیره شد +support.arrived=پیام به گیرنده تحویل داده شد +support.acknowledged=تحویل پیام از طرف گیرنده تأیید شد +support.error=گیرنده نتوانست پیام را پردازش کند. خطا: {0} +support.buyerAddress=آدرس خریدار بیتکوین +support.sellerAddress=آدرس فروشنده بیتکوین +support.role=نقش +support.agent=Support agent +support.state=حالت +support.chat=Chat +support.closed=بسته +support.open=باز +support.process=Process +support.buyerOfferer=خریدار/سفارش گذار بیتکوین +support.sellerOfferer=فروشنده/سفارش گذار بیتکوین +support.buyerTaker=خریدار/پذیرنده‌ی بیتکوین +support.sellerTaker=فروشنده/پذیرنده‌ی بیتکوین + +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\nTraders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and suggest a payout of trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. If one or both traders do not agree to the mediator's suggested payout, they can request arbitration.The arbitrator will re-evaluate the situation and, if warranted, personally pay the trader back and request reimbursement for this payment from the Bisq DAO. +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Bisq are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.systemMsg=پیغام سیستم: {0} +support.youOpenedTicket=شما یک درخواست برای پشتیبانی باز کردید.\n\n{0}\n\nنسخه Bisq شما: {1} +support.youOpenedDispute=شما یک درخواست برای یک اختلاف باز کردید.\n\n{0}\n\nنسخه Bisq شما: {1} +support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq version: {1} +support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=اولویت‌ها +settings.tab.network=اطلاعات شبکه +settings.tab.about=درباره + +setting.preferences.general=اولویت‌های عمومی +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=حداکثر تفاوت از قیمت روز بازار +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=حالت «آماده باش» را نادیده بگیر +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=مقادیر بزرگتر از {0}% مجاز نیست. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=استفاده از ارزش سفارشی +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Ignored peers [onion address:port] +setting.preferences.ignoreDustThreshold=Min. non-dust output value +setting.preferences.currenciesInList=ارزها در لیست قیمت روز بازار +setting.preferences.prefCurrency=ارز مطلوب +setting.preferences.displayFiat=نمایش ارزهای ملی +setting.preferences.noFiat=هیچ ارز ملی انتخاب نشده است +setting.preferences.cannotRemovePrefCurrency=شما نمی‌توانید ارز مطلوب انتخاب شده‌ی خود را حذف کنید +setting.preferences.displayAltcoins=نمایش آلت‌کوین‌ها +setting.preferences.noAltcoins=هیچ آلتکوینی انتخاب نشده است +setting.preferences.addFiat=افزودن ارز ملی +setting.preferences.addAltcoin=افزودن آلتکوین +setting.preferences.displayOptions=نمایش گزینه‌ها +setting.preferences.showOwnOffers=نمایش پیشنهادهای من در دفتر پیشنهاد +setting.preferences.useAnimations=استفاده از انیمیشن‌ها +setting.preferences.useDarkMode=Use dark mode +setting.preferences.sortWithNumOffers=مرتب سازی لیست‌ها با تعداد معاملات/پیشنهادها +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=تنظیم مجدد تمام پرچم‌های \"دوباره نشان نده\" +settings.preferences.languageChange=اعمال تغییر زبان به تمام صفحات مستلزم یک راه‌اندازی مجدد است. +settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. +setting.preferences.daoOptions=گزینه‌های DAO +setting.preferences.dao.resyncFromGenesis.label=بازسازی وضعیت DAO از تراکنش پیدایش +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Bisq را به عنوان یک گره کامل DAO اجرا کن +setting.preferences.dao.rpcUser=نام کاربری RPC +setting.preferences.dao.rpcPw=رمزعبور RPC +setting.preferences.dao.blockNotifyPort=Block notify port +setting.preferences.dao.fullNodeInfo=For running Bisq as DAO full node you need to have Bitcoin Core locally running and RPC enabled. All requirements are documented in ''{0}''.\n\nAfter changing the mode you need to restart. +setting.preferences.dao.fullNodeInfo.ok=باز کردن صفحه مستندات +setting.preferences.dao.fullNodeInfo.cancel=خیر، من با حالت «گره سبک» ادامه می‌دهم +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=نام +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=شبکه بیتکوین +settings.net.p2pHeader=Bisq network +settings.net.onionAddressLabel=آدرس onion من +settings.net.btcNodesLabel=استفاده از گره‌های Bitcoin Core اختصاصی +settings.net.bitcoinPeersLabel=همتایان متصل +settings.net.useTorForBtcJLabel=استفاده از Tor برای شبکه بیت‌کوین +settings.net.bitcoinNodesLabel=گره‌های Bitcoin Core در دسترس +settings.net.useProvidedNodesRadio=استفاده از نودهای بیتکوین ارائه شده +settings.net.usePublicNodesRadio=استفاده از شبکه بیتکوین عمومی +settings.net.useCustomNodesRadio=استفاده از نودهای بیتکوین اختصاصی +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=خیر، از نودهای فراهم شده استفاده کنید. +settings.net.warn.usePublicNodes.usePublic=بلی، از شبکه عمومی استفاده کنید. +settings.net.warn.useCustomNodes.B2XWarning=لطفا مطمئن شوید که گره بیت‌کوین شما یک گره مورد اعتماد Bitcoin Core است!\n\nمتصل شدن به گره‌هایی که از قوانین مورد اجماع موجود در Bitcoin Core پیروی نمی‌کنند می‌تواند باعث خراب شدن کیف پول شما شود و در فرآیند معامله مشکلاتی را به وجود بیاورد.\n\nکاربرانی که از گره‌های ناقض قوانین مورد اجماع استفاده می‌کند مسئول هر گونه آسیب ایجاد شده هستند. اگر هر گونه اختلافی به وجود بیاید به نفع دیگر گره‌هایی که از قوانین مورد اجماع پیروی می‌کنند درمورد آن تصمیم گیری خواهد شد. به کاربرانی که این هشدار و سازوکار محافظتی را نادیده می‌گیرند هیچ‌گونه پشتیبانی فنی ارائه نخواهد شد! +settings.net.warn.invalidBtcConfig=Connection to the Bitcoin network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Bitcoin nodes instead. You will need to restart the application. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=همتایان متصل +settings.net.onionAddressColumn=آدرس Onion +settings.net.creationDateColumn=تثبیت شده +settings.net.connectionTypeColumn=درون/بیرون +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=تاخیر چرخشی +settings.net.sentBytesColumn=ارسال شده +settings.net.receivedBytesColumn=دریافت شده +settings.net.peerTypeColumn=نوع همتا +settings.net.openTorSettingsButton=تنظیمات Tor را باز کنید.   + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Height + +settings.net.needRestart=به منظور اعمال آن تغییر باید برنامه را مجدداً راه اندازی کنید.\nآیا می‌خواهید این کار را هم اکنون انجام دهید؟ +settings.net.notKnownYet=هنوز شناخته شده نیست ... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[آدرس آی پی: پورت | نام میزبان: پورت | آدرس Onion : پورت] (جدا شده با ویرگول). اگر از پیش فرض (8333) استفاده می شود، پورت می تواند حذف شود. +settings.net.seedNode=گره ی اصلی +settings.net.directPeer=همتا (مستقیم) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=همتا +settings.net.inbound=وارد شونده +settings.net.outbound=خارج شونده +settings.net.reSyncSPVChainLabel=همگام سازی مجدد زنجیره SPV  +settings.net.reSyncSPVChainButton=حذف فایل SPV  و همگام سازی مجدد +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=فایل زنجیره SPV حذف شده است. لطفاً صبور باشید، همگام سازی مجدد با شبکه کمی طول خواهد کشید. +settings.net.reSyncSPVAfterRestartCompleted=همگام سازی مجدد هم اکنون تکمیل شده است. لطفاً برنامه را مجدداً راه اندازی نمایید. +settings.net.reSyncSPVFailed=حذف فایل زنجیره SPV امکان پذیر نیست. \nخطا: {0} +setting.about.aboutBisq=درباره Bisq +setting.about.about=Bisq یک پروژه منبع باز و یک شبکه غیر متمرکز از کاربرانی است که می‌خواهند بیت‌کوین را با ارزهای ملی (یا رمزارزهای جایگزین) به روشی امن تبادل کنند. در وب سایت ما با Bisq بیشتر آشنا شوید. +setting.about.web=صفحه وب Bisq +setting.about.code=کد منبع +setting.about.agpl=مجوز AGPL +setting.about.support=پشتیبانی از Bisq +setting.about.def=Bisq یک شرکت نیست — یک پروژه اجتماعی است و برای مشارکت آزاد است. اگر می‌خواهید در آن مشارکت کنید یا از آن حمایت نمایید، لینک‌‌های زیر را دنبال کنید. +setting.about.contribute=مشارکت +setting.about.providers=ارائه دهندگان داده +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=قیمت‌های بازار ارائه شده توسط +setting.about.feeEstimation.label=برآورد کارمزد استخراج ارائه شده توسط +setting.about.versionDetails=جزئیات نسخه +setting.about.version=نسخه برنامه +setting.about.subsystems.label=نسخه‌های زیرسیستم‌ها +setting.about.subsystems.val=نسخه ی شبکه: {0}; نسخه ی پیام همتا به همتا: {1}; نسخه ی Local DB: {2}; نسخه پروتکل معامله: {3} + +setting.about.shortcuts=Short cuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigate main menu +setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' + +setting.about.shortcuts.close=Close Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Close popup or dialog window +setting.about.shortcuts.closePopup.value='ESCAPE' key + +setting.about.shortcuts.chatSendMsg=Send trader chat message +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Open dispute +setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} + +setting.about.shortcuts.walletDetails=Open wallet details window + +setting.about.shortcuts.openEmergencyBtcWalletTool=Open emergency wallet tool for BTC wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool for BSQ wallet + +setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN + +setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) +setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} + +setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) +setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} + +setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) + +setting.about.shortcuts.sendFilter=Set Filter (privileged activity) + +setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing +account.info.headline=به حساب Bisq خود خوش آمدید +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). + +account.menu.paymentAccount=حساب های ارز ملی +account.menu.altCoinsAccountView=حساب های آلت کوین +account.menu.password=رمز کیف پول +account.menu.seedWords=رمز پشتیبان کیف پول +account.menu.walletInfo=Wallet info +account.menu.backup=پشتیبان +account.menu.notifications=اعلان‌ها + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=کلید عمومی + +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=ابطال +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.warn.min1Language=شما باید حداقل 1 زبان را انتخاب کنید.\nما زبان پیشفرض را برای شما اضافه کردیم. +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} + +account.altcoin.yourAltcoinAccounts=حساب‌های آلت‌کوین شما +account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.altcoin.popup.wallet.confirm=من می فهمم و تأیید می کنم که می دانم از کدام کیف پول باید استفاده کنم. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN (the receiver needs to be online or at least be online during a certain time frame). \n\nBisq supports only the Grinbox (Wallet713) wallet URL format. \n\nThe GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the latest Grinbox software which supports the transaction proof and that you understand the process of transferring and receiving GRIN as well as how to create the proof. \n\nSee https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more information about the Grinbox proof tool. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the transaction. \n\nBe sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM (the receiver needs to be online or at least be online during a certain time frame). \n\nThe BEAM sender is required to provide proof that they sent BEAM successfully. Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential dispute will be resolved in favor of the BEAM receiver. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Trading L-BTC on Bisq requires that you understand the following:\n\nWhen receiving L-BTC for a trade on Bisq, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-BTC into the Liquid Elements Core wallet, or another L-BTC wallet which allows you to obtain the blinding key for your blinded L-BTC address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-BTC address to the Bisq mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-BTC receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-BTC on Bisq. + +account.fiat.yourFiatAccounts=حساب‌های ارزهای ملی شما + +account.backup.title=کیف پول پشتیبان +account.backup.location=محل پشتیبان‌گیری +account.backup.selectLocation=انتخاب محل پشتیبان گیری +account.backup.backupNow=پشتیبان گیری در حال حاضر (پشتیبان رمزگذاری نشده است!) +account.backup.appDir=راهنمای داده های برنامه +account.backup.openDirectory=باز کردن راهنما +account.backup.openLogFile=باز کردن فایل گزارش +account.backup.success=پشتیبان به طور موفقیت آمیز در {0} ذخیره شد. +account.backup.directoryNotAccessible=راهنمایی که انتخاب کرده اید، قابل دسترسی نیست. \n{0} + +account.password.removePw.button=حذف رمز +account.password.removePw.headline=حذف رمز محافظ برای کیف پول +account.password.setPw.button=تنظیم رمز +account.password.setPw.headline=تنظیم رمز محافظ برای کیف پول +account.password.info=با محافظت رمزعبوری شما باید با هر بار اجرای برنامه، برداشت بیت‌کوین از کیف‌پول و یا بازگردانی کیف‌پول از طریق کلمات seed رمزعبور خود را وارد کنید. + +account.seed.backup.title=پشتیبان گیری از کلمات رمز خصوصی کیف های پول شما +account.seed.info=لطفا هم کلمات seed و هم تاریخ را یادداشت کنید! شما هر زمانی که بخواهید می‌توانید کیف‌پولتان را با استفاده از کلمات seed و تاریخ بازیابی کنید.\nهمین کلمات seed برای کیف‌پول‌های BTC و BSQ هم استفاده می‌شود.\n\nشما باید کلمات seed را روی یک برگ کاغذ یادداشت کنید. آنها را روی کامپیوتر خودتان ذخیره نکنید.\n\nلطفا توجه کنید که کلمات seed جایگزینی برای یک پشتیبان نیستند.\nبرای بازیابی وضعیت و داده‌های برنامه باید از طریق صفحه \"Account/Backup\" از کل پوشه برنامه پشتیبان بسازید.\nوارد کردن کلمات seed فقط در موارد اورژانسی توصیه می‌شود. برنامه بدون پشتیبان از پایگاه داده و کلیدهای مناسب درست عمل نخواهد کرد! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=شما یک رمز عبور کیف پول تنظیم نکرده اید که از نمایش کلمات رمز خصوصی محافظت کند.\n\nآیا می خواهید کلمات رمز خصوصی نشان داده شود؟ +account.seed.warn.noPw.yes=بلی، و دوباره از من نپرس +account.seed.enterPw=وارد کردن رمز عبور به منظور مشاهده ی کلمات رمز خصوصی +account.seed.restore.info=لطفا قبل از درخواست بازیابی، از کلماتSeed ، یک نسخه پشتیبان تهیه کنید. توجه داشته باشید که بازیابی کیف پول تنها در موارد اضطراری صورت می گیرد و ممکن است منجر به بروز مسائلی در رابطه با پایگاه داده کیف پول داخلی شود.\n این روش، راه مناسبی برای درخواست یک پشتیبان نیست! لطفا از یک پشتیبان دایرکتوری داده برنامه برای بازیابی وضعیت قبلی استفاده کنید.\n\nپس از بازیابی، برنامه به صورت خودکار متوقف می شود. بعد از اینکه مجددا برنامه را اجرا کنید، این برنامه دوباره با شبکه بیت کوین همگام سازی خواهد شد. این کار زمانبر خواهد بود و همچنین تا حد زیادی پردازنده را به کار خواهد گرفت، مخصوصا اگر کیف پول، قدیمی بوده و شامل تراکنش های زیادی باشد. لطفا از ایجاد وقفه برای فرآیند اجتناب کنید. در غیر اینصورت ممکن است که مجددا نیاز به حذف فایل زنجیره SPV و یا تکرار فرآیند بازیابی شوید. +account.seed.restore.ok=بسیار خب، Bisq را بازیابی و خاموش نمایید. + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=آماده سازی +account.notifications.download.label=بارگزاری برنامه موبایل +account.notifications.waitingForWebCam=منتظر دوربین... +account.notifications.webCamWindow.headline=اسکن کد QR از طریق تلفن +account.notifications.webcam.label=استفاده از دوربین +account.notifications.webcam.button=اسکن کد QR +account.notifications.noWebcam.button=دوربین ندارم +account.notifications.erase.label=پاک کردن اعلان‌ها در تلفن +account.notifications.erase.title=پاک کردن اعلان‌ها +account.notifications.email.label=توکن جفت سازی +account.notifications.email.prompt=توکن جفت سازی که از طریق ایمیل دریافت کرده‌اید را وارد کنید +account.notifications.settings.title=تنظیمات +account.notifications.useSound.label=پخش کردن صدای اعلان روی تلفن +account.notifications.trade.label=دریافت پیام‌های معامله +account.notifications.market.label=دریافت هشدارهای مربوط به پیشنهادها +account.notifications.price.label=دریافت هشدارهای مربوط به قیمت +account.notifications.priceAlert.title=هشدارهای قیمت +account.notifications.priceAlert.high.label=با خبر کردن در صورتی که قیمت BTC بالاتر باشد +account.notifications.priceAlert.low.label=با خبر کردن در صورتی که قیمت BTC پایین‌تر باشد +account.notifications.priceAlert.setButton=تنظیم هشدار قیمت +account.notifications.priceAlert.removeButton=حذف هشدار قیمت +account.notifications.trade.message.title=تغییر وضعیت معامله +account.notifications.trade.message.msg.conf=تراکنش سپرده برای معامله با شناسه {0} تایید شده است. لطفا برنامه Bisq خود را بازکنید و پرداخت را شروع کنید. +account.notifications.trade.message.msg.started=خریدار BTC پرداخت با شناسه {0} را آغاز کرده است. +account.notifications.trade.message.msg.completed=معامله با شناسه {0} انجام شد. +account.notifications.offer.message.title=پیشنهاد شما پذیرفته شد +account.notifications.offer.message.msg=پیشنهاد شما با شناسه {0} پذیرفته شد +account.notifications.dispute.message.title=پیغام جدید مربوط به اختلاف +account.notifications.dispute.message.msg=شما یک پیغام مرتبط با اختلاف برای معامله با شناسه {0} دریافت کردید + +account.notifications.marketAlert.title=هشدارهای مربوط به پیشنهادها +account.notifications.marketAlert.selectPaymentAccount=پیشنهادهای مرتبط با حساب پرداخت +account.notifications.marketAlert.offerType.label=نوع پیشنهادهایی که من به آنها علاقمندم +account.notifications.marketAlert.offerType.buy=پیشنهادهای خرید (می‌خواهم BTC بفروشم) +account.notifications.marketAlert.offerType.sell=پیشنهادهای فروش (می‌خواهم BTC بخرم) +account.notifications.marketAlert.trigger=فاصله قیمتی پیشنهاد (%) +account.notifications.marketAlert.trigger.info=با تنظیم یک فاصله قیمتی، تنها در صورتی هشدار دریافت می‌کنید که پیشنهادی با پیشنیازهای شما (یا بهتر از آن) منتشر بشود. برای مثال: شما می‌خواهید BTC بفروشید، ولی می‌خواهید با 2% حق صراف نسبت به قیمت بازار آن را بفروشید. تنظیم این فیلد روی 2% به شما این اطمینان را می‌دهد که تنها بابت پیشنهادهایی هشدار دریافت کنید که حداقل 2% (یا بیشتر) بالای قیمت فعلی بازار هستند. +account.notifications.marketAlert.trigger.prompt=درصد فاصله از قیمت بازار (برای مثال 2.50%, -0.50%) +account.notifications.marketAlert.addButton=اضافه کردن هشدار برای پیشنهادها +account.notifications.marketAlert.manageAlertsButton=مدیریت هشدارهای مربوط به پیشنهادها +account.notifications.marketAlert.manageAlerts.title=مدیریت هشدارهای مربوط به پیشنهادها +account.notifications.marketAlert.manageAlerts.header.paymentAccount=حساب پرداخت +account.notifications.marketAlert.manageAlerts.header.trigger=قیمت نشان‌شده +account.notifications.marketAlert.manageAlerts.header.offerType=نوع پیشنهاد +account.notifications.marketAlert.message.title=هشدار پیشنهاد +account.notifications.marketAlert.message.msg.below=پایین +account.notifications.marketAlert.message.msg.above=بالای +account.notifications.marketAlert.message.msg=پیشنهاد جدید ''{0} {1}'' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت ''{5}'' در دفتر پیشنهادات Bisq منتشر شده است.\nشناسه پیشنهاد: {6}. +account.notifications.priceAlert.message.title=هشدار قیمت برای {0} +account.notifications.priceAlert.message.msg=هشدار قیمت شما فعال شده است. قیمت {0} فعلی {1} {2} است +account.notifications.noWebCamFound.warning=دوبین پیدا نشد.\n\nلطفا از گزینه ایمیل برای ارسال توکن و کلید رمزنگاری از تلفن همراهتان به برنامه Bisq استفاده کنید. +account.notifications.priceAlert.warning.highPriceTooLow=قیمت بالاتر باید از قیمت پایین‌تر بزرگتر باشد. +account.notifications.priceAlert.warning.lowerPriceTooHigh=قیمت پایین‌تر باید از قیمت بالاتر کوچکتر باشد. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=واقعیت ها و شکل ها +dao.tab.bsqWallet=کیف پول BSQ  +dao.tab.proposals=حکمرانی +dao.tab.bonding=ضمانت +dao.tab.proofOfBurn=کارمزد ثبت دارایی/اثبات امحا +dao.tab.monitor=مانیتور شبکه +dao.tab.news=اخبار + +dao.paidWithBsq=پرداخت شده با BSQ +dao.availableBsqBalance=آمادگی برای ارسال (تغییر خروجی های تائید شده + تائید نشده) +dao.verifiedBsqBalance=متعادل کردن همه UTXO های تائید شده +dao.unconfirmedChangeBalance=متعادل کردن همه تغییر خروجی های تائید نشده +dao.unverifiedBsqBalance=متعادل کردن همه تراکنش های تائید نشده (تائید بلاک های در انتظار) +dao.lockedForVoteBalance=در حال استفاده برای رای دادن +dao.lockedInBonds=قفل شده در ضمانت +dao.availableNonBsqBalance=موجودی غیر BSQ در دسترس (BTC) +dao.reputationBalance=Merit Value (not spendable) + +dao.tx.published.success=تراکنش شما به طور موفقیت آمیز منتشر شد. +dao.proposal.menuItem.make=ارائه ی پیشنهاد +dao.proposal.menuItem.browse=مرور طرح‌های پیشنهادی باز +dao.proposal.menuItem.vote=رای دادن به طرح‌های پیشنهادی +dao.proposal.menuItem.result=تنایج رای‌گیری +dao.cycle.headline=دوره رای‌گیری +dao.cycle.overview.headline=مرور دوره رای‌گیری +dao.cycle.currentPhase=مرحله فعلی +dao.cycle.currentBlockHeight=طول بلاک فعلی +dao.cycle.proposal=مرحله طرح پیشنهادی +dao.cycle.proposal.next=مرحله بعدی طرح پیشنهادی +dao.cycle.blindVote=مرحله رای‌گیری ناشناس +dao.cycle.voteReveal=مرحله آشکار کردن رای +dao.cycle.voteResult=تنیجه را‌ی‌گیری +dao.cycle.phaseDuration={0} بلاک (≈{1})؛ بلاک {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=بلاک {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=تراکنش انتشار رای، منتشر شد +dao.voteReveal.txPublished=رای شما نشان داد که تراکنش با شناسه تراکنش {0} با موفقیت منتشر شده بود.\n\nاگر شما در رای گیری DAO شرکت داشته باشید، این امر به صورت خودکار توسط نرم افزار اتفاق می افتد. + +dao.results.cycles.header=دوره‌ها +dao.results.cycles.table.header.cycle=دوره +dao.results.cycles.table.header.numProposals=طرح‌های پیشنهادی +dao.results.cycles.table.header.voteWeight=وزن رای +dao.results.cycles.table.header.issuance=صدور + +dao.results.results.table.item.cycle=دوره {0} شروع شد: {1} + +dao.results.proposals.header=طرح‌های پیشنهادی دوره انتخاب شده +dao.results.proposals.table.header.nameLink=Name/link +dao.results.proposals.table.header.details=جزئیات +dao.results.proposals.table.header.myVote=رای من +dao.results.proposals.table.header.result=تنیجه را‌ی‌گیری +dao.results.proposals.table.header.threshold=Threshold +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=نتایج رای‌گیری برای طرح پیشنهادی انتخاب شده + +dao.results.exceptions=استثنائات نتایج رای گیری + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=تعریف نشده + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=کارمزد BSQ سفارش‌گذار +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=کارمزد BSQ پذیرنده +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=حداقل کارمزد BSQ سفارش‌گذار +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=حداقل کارمزد BSQ پذیرنده +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=کارمزد BTC سفارش‌گذار +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=کارمزد BTC پذیرنده +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=حداقل کارمزد BTC سفارش‌گذار +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=حداقل کارمزد BTC پذیرنده +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=کارمزد طرح پیشنهادی به BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=کارمزد رای دادن به BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=حداقل مقدار BSQ برای درخواست خسارت +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=حداکثر مقدار BSQ برای درخواست خسارت +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=حداقل مقدار BSQ برای درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=حداکثر مقدار BSQ برای درخواست بازپرداخت + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=حدنصاب مورد نیاز به BSQ برای طرح پیشنهادی کلی +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=حدنصاب مورد نیاز به BSQ برای درخواست خسارت +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=حدنصاب مورد نیاز به BSQ برای درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=حدنصاب مورد نیاز به BSQ برای تغییر یک پارامتر +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=حدنصاب موردنیاز به BSQ برای حذف کردن یک دارایی +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=حدنصاب مورد نیاز به BSQ برای درخواست یک مصادره +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=حدنصاب مورد نیاز به BSQ برای درخواست‌های نقش ضامن + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=آستانه مورد نیاز به % برای طرح پیشنهادی کلی +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=آستانه مورد نیاز به % برای درخوسات خسارت +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=آستانه مورد نیاز به % برای درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=آستانه مورد نیاز به % برای تغییر یک پارامتر +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=آستانه مورد نیاز به % برای حذف یک دارایی +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=آستانه مورد نیاز به % برای درخواست یک مصادره +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=آستانه مورد نیاز به % برای درخواست‌های نقش ضامن + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=آدرس BTC گیرنده + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=کارمزد ثبت دارایی در روز +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=حداقل حجم معامله برای دارایی‌ها + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=زمان قفل کردن برای پرداخت معامله جایگزین tx  +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=دستمزد داور در BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=حداکثر محدودیت معامله در BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=ویژگی واحد نقش تضمین شده در BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=محدودیت صدور در چرخه در BSQ + +dao.param.currentValue=مقدار فعلی: {0} +dao.param.currentAndPastValue=Current value: {0} (Value when proposal was made: {1}) +dao.param.blocks={0} بلاک + +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was not distributed well in the Bisq network.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=مرحله طرح پیشنهادی +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=وقفه 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=مرحله رای‌گیری ناشناس +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=وقفه 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=مرحله آشکار کردن رای +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=وقفه 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=نتیجه مرحله + +dao.results.votes.table.header.stakeAndMerit=وزن رای +dao.results.votes.table.header.stake=سهام +dao.results.votes.table.header.merit=کسب شده +dao.results.votes.table.header.vote=رأی + +dao.bond.menuItem.bondedRoles=نقش‌های ضمانتی +dao.bond.menuItem.reputation=اعتبار ضمانتی +dao.bond.menuItem.bonds=ضمانت‌ها + +dao.bond.dashboard.bondsHeadline=BSQ ضمانت شده +dao.bond.dashboard.lockupAmount=وجوه قفل شده +dao.bond.dashboard.unlockingAmount=رها کردن وجوه (صبر کن تا زمان قفل بودن به پایان برسد) + + +dao.bond.reputation.header=ضمانتی را برای اعتبار قفل کن +dao.bond.reputation.table.header=ضمانت‌های اعتبار من +dao.bond.reputation.amount=مقدار BSQ برای قفل کردن +dao.bond.reputation.time=زمان رها شدن به بلاک +dao.bond.reputation.salt=داده تصادفی +dao.bond.reputation.hash=تابع درهم ساز (هش) +dao.bond.reputation.lockupButton=قفل کردن +dao.bond.reputation.lockup.headline=تایید تراکنش قفل کردن وجه +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=تایید تراکنش رها کردن وجه +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=همه ضمانت‌ها + +dao.bond.bondedReputation=اعتبار ضمانت شده +dao.bond.bondedRoles=نقش‌های ضمانت شده + +dao.bond.details.header=جزئیات نقش +dao.bond.details.role=نقش +dao.bond.details.requiredBond=ضمانت BSQ مورد نیاز +dao.bond.details.unlockTime=زمان رها شدن به بلاک +dao.bond.details.link=پیوند اینترنتی به توضیحات نقش +dao.bond.details.isSingleton=می‌تواند توسط چند نقش آفرین گرفته شود +dao.bond.details.blocks={0} بلاک + +dao.bond.table.column.name=نام +dao.bond.table.column.link=پیوند +dao.bond.table.column.bondType=نوع ضمانت +dao.bond.table.column.details=جزئیات +dao.bond.table.column.lockupTxId=شناسه تراکنش قفل وجوه +dao.bond.table.column.bondState=وضعیت ضمانت +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=تاریخ قفل کردن وجه + +dao.bond.table.button.lockup=قفل کردن +dao.bond.table.button.unlock=باز کردن +dao.bond.table.button.revoke=ابطال + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=هنوز ضمانت نشده +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=در انتظار قفل کردن +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=ضمانت قفل شده +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=در انتظار رها شدن +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=تراکنش رها شدن وجه تایید شده +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=رها کردن وجه ضمانتی +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=وجه ضمانت شده رها شده +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=ضمانت مصادره شده + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=نقش ضمانت شده +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=اعتبار ضمانت شده + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=مدیر Github +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=مدیر تالار +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=مدیر توئیتر +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=مدیر یوتیوب +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=نگهدارنده Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=نگهدارنده BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=نگهدارنده لایه شبکه +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=گرداننده سایت +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=گرداننده تالار +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=گرداننده گره seed +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=گرداننده گره قیمت +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=گرداننده بازارها +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=اپراتور بازپخش اعلان های موبایل +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=صاحب نام دامنه +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=مدیر DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=واسط +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=داور +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=مالک آدرس کمک مالی BTC + +dao.burnBsq.assetFee=ثبت دارایی +dao.burnBsq.menuItem.assetFee=کارمزد ثبت دارایی +dao.burnBsq.menuItem.proofOfBurn=اثبات امحا +dao.burnBsq.header=کارمزد ثبت دارایی +dao.burnBsq.selectAsset=انتخاب دارایی +dao.burnBsq.fee=کارمزد +dao.burnBsq.trialPeriod=دوره زمانی امتحانی +dao.burnBsq.payFee=کارمزد پرداخت +dao.burnBsq.allAssets=همه دارایی‌ها +dao.burnBsq.assets.nameAndCode=نام دارایی +dao.burnBsq.assets.state=حالت +dao.burnBsq.assets.tradeVolume=حجم معامله +dao.burnBsq.assets.lookBackPeriod=دوره زمانی تایید +dao.burnBsq.assets.trialFee=کارمزد دوره زمانی امتحانی +dao.burnBsq.assets.totalFee=مجموع کارمزدهای پرداختی +dao.burnBsq.assets.days={0} روز +dao.burnBsq.assets.toFewDays=کارمزد دارایی کافی نیست. حداقل مقدار برای دوره زمانی امتحانی {0} است. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=در دوره زمانی امتحانی +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=فعالانه در حال معامله +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=حذف شده به دلیل عدم فعالیت +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=حذف شده به واسطه رای گیری + +dao.proofOfBurn.header=اثبات امحا +dao.proofOfBurn.amount=مقدار +dao.proofOfBurn.preImage=پیش نسخه +dao.proofOfBurn.burn=امحا +dao.proofOfBurn.allTxs=تمام تراکنش‌های اثبات امحا +dao.proofOfBurn.myItems=تراکنش‌های اثبات امحا من +dao.proofOfBurn.date=تاریخ +dao.proofOfBurn.hash=هش +dao.proofOfBurn.txs=تراکنش‌ها +dao.proofOfBurn.pubKey=کلید عمومی +dao.proofOfBurn.signature.window.title=امضاء یک پیغام با کلیدی از اثبات سوختن تراکنش +dao.proofOfBurn.verify.window.title=تائید یک پیغام با کلیدی از اثبات سوختن تراکنش +dao.proofOfBurn.copySig=کپی کردن امضا به حافظه موقت +dao.proofOfBurn.sign=امضا کردن +dao.proofOfBurn.message=پیام +dao.proofOfBurn.sig=امضا +dao.proofOfBurn.verify=تایید کردن +dao.proofOfBurn.verificationResult.ok=تایید با موفقیت انجام شد +dao.proofOfBurn.verificationResult.failed=تایید نا موفق بود + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=مرحله طرح پیشنهادی +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=وقفه قبل از مرحله رای‌گیری ناشناس +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=مرحله رای‌گیری ناشناس +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=وقفه قبل از مرحله آشکارسازی رای +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=مرحله آشکار کردن رای +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=وقفه قبل از مرحله اعلام نتایج +# suppress inspection "UnusedProperty" +dao.phase.RESULT=مرحله نتایج رای‌گیری + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=مرحله طرح پیشنهادی +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=رای ناشناس +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=آشکارسازی رأی +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=تنیجه را‌ی‌گیری + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=درخواست خسارت +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=طرح پیشنهادی برای یک نقش ضمانتی +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=طرح پیشنهادی برای حذف کردن یک دارایی +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=طرح پیشنهادی برای تغییر یک پارامتر +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=پیشنهاد عمومی +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=طرح پیشنهادی برای مصادره کردن یک ضمانت + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=درخواست خسارت +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=نقش ضمانت شده +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=حذف یک آلت‌کوین +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=تغییر یک پارامتر +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=پیشنهاد کلی +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=مصادره کردن یک ضمانت + +dao.proposal.details=جزئیات پیشنهاد +dao.proposal.selectedProposal=پیشنهادهای انتخاب شده +dao.proposal.active.header=طرح‌های پیشنهادی برای دوره فعلی +dao.proposal.active.remove.confirm=آیا از حذف کردن آن طرح پیشنهادی مطمئنید؟\nکارمزد طرح پیشنهادی که قبلا پرداخت شده است از بین خواهد رفت. +dao.proposal.active.remove.doRemove=بله، طرح پیشنهادی من را حذف کن +dao.proposal.active.remove.failed=نمی توان پیشنهاد را حذف کرد. +dao.proposal.myVote.title=رأی گیری +dao.proposal.myVote.accept=قبول پیشنهاد +dao.proposal.myVote.reject=رد پیشنهاد +dao.proposal.myVote.removeMyVote=نادیده گرفتن طرح پیشنهادی +dao.proposal.myVote.merit=وزن رای به سبب مقدار BSQ بدست آورده +dao.proposal.myVote.stake=وزن رای به سبب سهام +dao.proposal.myVote.revealTxId=شناسه تراکنش آشکاری سازی رای +dao.proposal.myVote.stake.prompt=حداکثر استک آماده برای رای گیری: {0} +dao.proposal.votes.header=تنظیم استک برای رای گیری و انتشار رای های شما +dao.proposal.myVote.button=انتشار رای‌ها +dao.proposal.myVote.setStake.description=پس از رأی دادن به همه پیشنهادها، شما باید از طریق قفل کردن BSQ، استک خود را برای رای دادن تنظیم کنید. هر چه BSQ بیشتری را قفل کنید، رای شما ارزش بیشتری خواهد داشت.\n\nBSQ قفل شده برای رای گیری، مجددا در طی رای گیری فاز انتشار باز خواهد شد. +dao.proposal.create.selectProposalType=انتخاب نوع پیشنهاد +dao.proposal.create.phase.inactive=لطفا منتظر مرحله بعدی طرح پیشنهادی باشید +dao.proposal.create.proposalType=نوع پیشنهاد +dao.proposal.create.new=ارائه ی پیشنهاد جدید +dao.proposal.create.button=ارائه ی پیشنهاد +dao.proposal.create.publish=انتشار طرح پیشنهادی +dao.proposal.create.publishing=انتشار طرح پیشنهادی در حال انجام است ... +dao.proposal=طرح پیشنهادی +dao.proposal.display.type=نوع طرح پیشنهادی +dao.proposal.display.name=Exact GitHub username +dao.proposal.display.link=پیوند اینترنتی به اطلاعات جزئی +dao.proposal.display.link.prompt=پیوند اینترنتی به طرح پیشنهادی +dao.proposal.display.requestedBsq=مبلغ درخواستی به BSQ +dao.proposal.display.txId=شناسه تراکنش طرح پیشنهادی +dao.proposal.display.proposalFee=کارمزد طرح پیشنهادی +dao.proposal.display.myVote=رای من +dao.proposal.display.voteResult=خلاصه نتایج رای‌گیری +dao.proposal.display.bondedRoleComboBox.label=نوع نقش ضمانت شده +dao.proposal.display.requiredBondForRole.label=ضمانت مورد نیاز برای نقش +dao.proposal.display.option=گزینه + +dao.proposal.table.header.proposalType=نوع طرح پیشنهادی +dao.proposal.table.header.link=پیوند +dao.proposal.table.header.myVote=رای من +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=حذف +dao.proposal.table.icon.tooltip.removeProposal=طرح پیشنهادی من را حذف کن +dao.proposal.table.icon.tooltip.changeVote=رای فعلی: ''{0}''. تغییر رای به: ''{1}'' + +dao.proposal.display.myVote.accepted=قبول شده +dao.proposal.display.myVote.rejected=رد شده +dao.proposal.display.myVote.ignored=نادیده گرفته شده +dao.proposal.display.myVote.unCounted=Vote was not included in result +dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=رای نامعتبر بود + +dao.proposal.voteResult.success=قبول شده +dao.proposal.voteResult.failed=رد شده +dao.proposal.voteResult.summary=نتیجه: {0}؛ آستانه: {1} (مورد نیاز > {2})؛ حدنصاب: {3} (مورد نیاز > {4}) + +dao.proposal.display.paramComboBox.label=پارامتری برای تغییر انتخاب کنید +dao.proposal.display.paramValue=مقدار پارامتر + +dao.proposal.display.confiscateBondComboBox.label=انتخاب ضمانت +dao.proposal.display.assetComboBox.label=دارایی مورد حذف + +dao.blindVote=رای ناشناس + +dao.blindVote.startPublishing=در حال انتشار تراکنش رای ناشناس... +dao.blindVote.success=تراکنش رأی کور شما با موفقیت منتشر شده است.\n\nلطفا توجه داشته باشید که باید در فاز انتشار رای آنلاین باشید تا برنامه کیف پول Bisq  شما بتواند تراکنش انتشار رای را منتشر کند. بدون تراکنش انتشار رای، رای شما غیر معتبر خواهد بود! + +dao.wallet.menuItem.send=ارسال +dao.wallet.menuItem.receive=دریافت +dao.wallet.menuItem.transactions=تراکنش ها + +dao.wallet.dashboard.myBalance=موجودی کیف‌پول من + +dao.wallet.receive.fundYourWallet=آدرس دریافت BSQ شما: +dao.wallet.receive.bsqAddress=آدرس کیف پول BSQ (آدرس استفاده نشده جدید) + +dao.wallet.send.sendFunds=ارسال وجوه +dao.wallet.send.sendBtcFunds=ارسال وجوه غیر BSQ (به BTC) +dao.wallet.send.amount=مقدار به BSQ +dao.wallet.send.btcAmount=مقدار به BTC (وجوه غیر BSQ) +dao.wallet.send.setAmount=تعیین مبلغ به منظور برداشت (حداقل مبلغ {0} است) +dao.wallet.send.receiverAddress=آدرس BSQ گیرنده +dao.wallet.send.receiverBtcAddress=آدرس BTC گیرنده +dao.wallet.send.setDestinationAddress=آدرس مقصد خود را پر کنید +dao.wallet.send.send=ارسال وجوه BSQ  +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=ارسال وجوه BTC +dao.wallet.send.sendFunds.headline=تأیید درخواست برداشت +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=آخرین بلاک تایید شده: {0} +dao.wallet.chainHeightSyncing=منتظر بلاک‌ها... {0} تا از {1} بلاک تایید شده است +dao.wallet.tx.type=نوع + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=تعریف نشده +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=شناسایی نشده +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=تراکنش BSQ تأیید نشده +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=تراکنش BSQ نامعتبر +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=تراکنش جنسیس +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=انتقال BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ دریافت شده +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ ارسال شده +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=هزینه ی معامله +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=هزینه برای درخواست خسارت +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=کارمزد درخواست بازپرداخت +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=هزینه برای پیشنهاد +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=کارمزد برای رای ناشناس +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=آشکارسازی رأی +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=قفل کردن ضمانت +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=رها سازی ضمانت +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=کارمزد ثبت دارایی +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=اثبات امحا +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=نامنظم + +dao.tx.withdrawnFromWallet=خروج BTC از کیف پول +dao.tx.issuanceFromCompReq=درخواست/صدور خسارت +dao.tx.issuanceFromCompReq.tooltip=درخواست خسارت که منجر به صدور BSQ جدید می‌شود.\nتاریخ صدور: {0} +dao.tx.issuanceFromReimbursement=درخواست/صدور بازپرداخت +dao.tx.issuanceFromReimbursement.tooltip=درخواست بازپرداختی که منجر به صدور BSQ جدید می‌شود.\nتاریخ صدور: {0} +dao.proposal.create.missingBsqFunds=شما سرمایه BSQ مناسبی برای ایجاد پروپوزال ندارید. اگر یک تراکنش BSQ تائید نشده دارید، لازم است که برای یک تائیدیه بلاک چین منتظر بمانید، زیرا BSQ در صورتی تائید می شود در یک بلاک قرار داشته باشد. از دست رفته: {0} + +dao.proposal.create.missingBsqFundsForBond=شما سرمایه BSQ مناسبی برای این نقش ندارید. شما هنوز قادرید که این پروپوزال را انتشار دهید. اما در صورت پذیرش، نیاز به مقداری کافی از BSQ برای این نقش خواهید داشت.\nاز دست رفته: {0} + +dao.proposal.create.missingMinerFeeFunds=شما سرمایه BSQ مناسبی برای ایجاد تراکنش پروپوزال ندارید. همه تراکنش های BSQ نیازمند دستمزد استخراج کننده در BTC هستند.\nاز دست رفته: {0} + +dao.proposal.create.missingIssuanceFunds=شما سرمایه BTC مناسبی برای ایجاد تراکنش پروپوزال ندارید. همه تراکنش های BSQ نیازمند دستمزد استخراج کننده در BTC هستند و تراکنش های صدور نیز به BTC برای میزان BSQ درخواستی نیاز دارند (BSQ/ساتوشی ها {0}).\nاز دست رفته: {1} + +dao.feeTx.confirm=تایید {0} تراکنش +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=سازمان مستقل غیر متمرکز (Bisq (DAO +dao.news.bisqDAO.description=درست همانطور که مبادله کیف پول Bisq، غیر متمرکز و مقاوم در برابر سانسور است، مدل حاکمیت آن نیز وجود دارد و توکن های Bisq DAO و BSQ ابزارهایی هستند که آنرا محقق می سازند. +dao.news.bisqDAO.readMoreLink=درباره Bisq DAO بیشتر بدانید + +dao.news.pastContribution.title=آیا مشارکت قبلی داشته اید؟ برای BSQ درخواست دهید. +dao.news.pastContribution.description=اگر در رابطه با کیف پول Bisq مشارکت داشته اید، لطفا از آدرس BSQ زیر استفاده کنید و برای دریافت بخشی از توزیع جنسیس BSQ درخواست دهید. +dao.news.pastContribution.yourAddress=آدرس کیف‌پول BSQ شما +dao.news.pastContribution.requestNow=حالا درخواست دهید. + +dao.news.DAOOnTestnet.title=BISQ DAO را روی شبکه تستی، اجرا کنید. +dao.news.DAOOnTestnet.description=کیف پول Bisq DAO شبکه اصلی هنوز راه اندازی نشده است، اما شما می توانید با اجرای کیف پول Bisq DAO روی شبکه تستی، در مورد آن چیزهایی را یاد بگیرید. +dao.news.DAOOnTestnet.firstSection.title=1. به حالت شبکه تستی DAO تغییر وضعیت دهید. +dao.news.DAOOnTestnet.firstSection.content=از صفحه تنظیمات، به شبکه تستی DAO بروید. +dao.news.DAOOnTestnet.secondSection.title=2. چند BSQ را خریداری نمایید. +dao.news.DAOOnTestnet.secondSection.content=روی اسلک برای BSQ درخواست دهید و یا روی کیف پول Bisq ، BSQ را خریداری کنید. +dao.news.DAOOnTestnet.thirdSection.title=3. در یک چرخه رای گیری شرکت کنید. +dao.news.DAOOnTestnet.thirdSection.content=پروپوزال هایی تهیه کنید و برای تغییر جنبه های مختلف کیف پول Bisq، روی آنها رای گیری نمایید. +dao.news.DAOOnTestnet.fourthSection.title=4. Explore a BSQ Block Explorer +dao.news.DAOOnTestnet.fourthSection.content=از آنجا که BSQ تنها بیت کوین است، می توانید تراکنش های BSQ را روی مرورگر بلاک بیت کوین مشاهده کنید. +dao.news.DAOOnTestnet.readMoreLink=اسناد را به طور کامل مطالعه کنید. + +dao.monitor.daoState=وضعیت DAO +dao.monitor.proposals=وضعیت طرح‌های پیشنهادی +dao.monitor.blindVotes=وضعیت رای‌های ناشناس + +dao.monitor.table.peers=جفت ها +dao.monitor.table.conflicts=تعارضات +dao.monitor.state=وضعیت +dao.monitor.requestAlHashes=درخواست همه هش ها +dao.monitor.resync=همگام سازی مجدد وضعیت DAO +dao.monitor.table.header.cycleBlockHeight=ارتفاع بلاک / چرخه +dao.monitor.table.cycleBlockHeight={1} بلاک / {0} چرخه +dao.monitor.table.seedPeers={0} گره Seed: + +dao.monitor.daoState.headline=وضعیت DAO +dao.monitor.daoState.table.headline=زنجیره هش های وضعیت DAO +dao.monitor.daoState.table.blockHeight=ارتفاع بلاک +dao.monitor.daoState.table.hash=هش وضعیت DAO +dao.monitor.daoState.table.prev=هش قبلی +dao.monitor.daoState.conflictTable.headline=هش های وضعیت DAO از جفت هایی که با هم در تعارض هستند. +dao.monitor.daoState.utxoConflicts=تناقضات UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=ارتفاع بلاک: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=مجموع همه UTXO ها:{0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=مجموع همه BSQ ها:{BSQ {0 +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. After restart the DAO state will resync. + +dao.monitor.proposal.headline=وضعیت طرح‌های پیشنهادی +dao.monitor.proposal.table.headline=زنجیره هش های وضعیت پروپوزال +dao.monitor.proposal.conflictTable.headline=هش های وضعیت پروپوزال از جفت هایی که با هم در تعارض هستند. + +dao.monitor.proposal.table.hash=هش وضعیت پروپوزال +dao.monitor.proposal.table.prev=هش قبلی +dao.monitor.proposal.table.numProposals=تعداد پروپوزال ها + +dao.monitor.isInConflictWithSeedNode=داده های محلی شما حداقل با یک گره Seed در اشتراک نیستند. لطفا  مجددا وضعیت DAO را همگام سازی کنید. +dao.monitor.isInConflictWithNonSeedNode=یکی از همتایان شما با شبکه در اشتراک نیست، اما گره شما با گره های Seed همگام است. +dao.monitor.daoStateInSync=گره محلی شما با شبکه در اشتراک است. + +dao.monitor.blindVote.headline=وضعیت رای‌های ناشناس +dao.monitor.blindVote.table.headline=زنجیره هش های وضعیت رای کور +dao.monitor.blindVote.conflictTable.headline=هش های وضعیت رای کور، از جفت هایی که با هم در تعارض هستند. +dao.monitor.blindVote.table.hash=هش وضعیت رای کور +dao.monitor.blindVote.table.prev=هش قبلی +dao.monitor.blindVote.table.numBlindVotes=No. blind votes + +dao.factsAndFigures.menuItem.supply=BSQ Supply +dao.factsAndFigures.menuItem.transactions=تراکنش‌های BSQ + +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=مجموع BSQ در دسترس +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt + +dao.factsAndFigures.supply.issued=BSQ issued +dao.factsAndFigures.supply.compReq=درخواست های خسارت +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ صادر شده در تراکنش پیدایش +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ صادر شده برای درخواست‌های مصادره +dao.factsAndFigures.supply.reimbursementAmount=BSQ صادر شده برای درخواست‌های بازپرداخت +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ burnt + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=حجم معامله +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=وضعیت جهانی BSQ های قفل شده +dao.factsAndFigures.supply.totalLockedUpAmount=قفل شده در ضمانت‌ها +dao.factsAndFigures.supply.totalUnlockingAmount=رها کردن BSQ از ضمانت‌ها +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ رها شده از ضمانت‌ها +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ مصادره شده از ضمانت‌ها +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=تراکنش پیدایش +dao.factsAndFigures.transactions.genesisBlockHeight=طول بلاک پیدایش +dao.factsAndFigures.transactions.genesisTxId=شناسه تراکنش پیدایش +dao.factsAndFigures.transactions.txDetails=آمار تراکنش‌های BSQ +dao.factsAndFigures.transactions.allTx=تعداد تمام تراکنش‌های BSQ +dao.factsAndFigures.transactions.utxo=تعداد تمام خروجی تراکنش‌های خرج نشده +dao.factsAndFigures.transactions.compensationIssuanceTx=تعداد تمام تراکنش‌های صدور درخواست مصادره +dao.factsAndFigures.transactions.reimbursementIssuanceTx=تعداد تمام تراکنش‌های صدور درخواست بازپرداخت +dao.factsAndFigures.transactions.burntTx=تعداد تمام تراکنش‌های کارمزد پرداخت +dao.factsAndFigures.transactions.invalidTx=No. of all invalid transactions +dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=موجودی در دسترس + +contractWindow.title=جزئیات مناقشه +contractWindow.dates=تاریخ پیشنهاد / تاریخ معامله +contractWindow.btcAddresses=آدرس بیت‌کوین خریدار BTC / فروشنده BTC +contractWindow.onions=آدرس شبکه خریدار BTC / فروشنده BTC +contractWindow.accountAge=Account age BTC buyer / BTC seller +contractWindow.numDisputes=تعداد اختلافات خریدار BTC / فروشنده BTC +contractWindow.contractHash=هش قرارداد + +displayAlertMessageWindow.headline=اطلاعات مهم! +displayAlertMessageWindow.update.headline=اطلاعات به روز مهم! +displayAlertMessageWindow.update.download=دانلود: +displayUpdateDownloadWindow.downloadedFiles=فایل ها: +displayUpdateDownloadWindow.downloadingFile=در حال دانلود: {0} +displayUpdateDownloadWindow.verifiedSigs=امضا با کلیدها تایید شده است: +displayUpdateDownloadWindow.status.downloading=در حال دانلود فایل ها... +displayUpdateDownloadWindow.status.verifying=در حال اعتبارسنجی امضا... +displayUpdateDownloadWindow.button.label=دانلود نصب کننده و تأیید امضا +displayUpdateDownloadWindow.button.downloadLater=بعداً دانلود کن +displayUpdateDownloadWindow.button.ignoreDownload=نادیده گرفتن این نسخه +displayUpdateDownloadWindow.headline=یک به روز رسانی جدید برای Bisq موجود است! +displayUpdateDownloadWindow.download.failed.headline=دانلود ناموفق بود +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=نسخه ی جدید به طور موفقیت آمیز دانلود و امضا تأیید شد.\n\nلطفاً راهنمای دانلود را باز کرده، برنامه را ببندید و نسخه ی جدید را نصب نمایید. +displayUpdateDownloadWindow.download.openDir=باز کردن راهنمای دانلود + +disputeSummaryWindow.title=خلاصه +disputeSummaryWindow.openDate=تاریخ ایجاد تیکت +disputeSummaryWindow.role=نقش معامله گر +disputeSummaryWindow.payout=پرداختی مقدار معامله +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} پرداختی مبلغ معامله را دریافت می کند +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=پرداخت سفارشی +disputeSummaryWindow.payoutAmount.buyer=مقدار پرداختی خریدار +disputeSummaryWindow.payoutAmount.seller=مقدار پرداختی فروشنده +disputeSummaryWindow.payoutAmount.invert=استفاده از بازنده به عنوان منتشر کننده +disputeSummaryWindow.reason=دلیل اختلاف +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=اشکال +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=قابلیت استفاده +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=نقض پروتکل +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=بدون پاسخ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=کلاهبرداری +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=سایر +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=بانک +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=نکات خلاصه +disputeSummaryWindow.addSummaryNotes=افزودن نکات خلاصه +disputeSummaryWindow.close.button=بستن تیکت + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=شما باید همچنین تیکت همتایان معامله را هم ببندید! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline=ابزار اضطراری کیف پول {0} +emptyWalletWindow.info=لطفاً تنها در مورد اضطراری از آن استفاده کنید اگر نمی توانید به وجه خود از UI دسترسی داشته باشید.\n\nلطفاً توجه داشته باشید که تمام معاملات باز به طور خودکار در هنگام استفاده از این ابزار، بسته خواهد شد.\n\nقبل از به کار گیری این ابزار، از راهنمای داده ی خود پشتیبان بگیرید. می توانید این کار را در \"حساب/پشتیبان\" انجام دهید.\n\nلطفاً مشکل خود را به ما گزارش کنید و گزارش مشکل را در GitHub یا تالار گفتگوی Bisq بایگانی کنید تا ما بتوانیم منشأ مشکل را بررسی نماییم. +emptyWalletWindow.balance=موجودی در دسترس کیف‌پول شما +emptyWalletWindow.bsq.btcBalance=موجودی غیر BSQ بر اساس ساتوشی + +emptyWalletWindow.address=آدرس مقصد شما +emptyWalletWindow.button=ارسال تمام وجوه +emptyWalletWindow.openOffers.warn=شما معاملات بازی دارید که اگر کیف پول را خالی کنید، حذف خواهند شد.\nآیا شما مطمئن هستید که می خواهید کیف پول را خالی کنید؟ +emptyWalletWindow.openOffers.yes=بله، مطمئن هستم +emptyWalletWindow.sent.success=تراز کیف پول شما به طور موفقیت آمیز منتقل شد. + +enterPrivKeyWindow.headline=Enter private key for registration + +filterWindow.headline=ویرایش لیست فیلتر +filterWindow.offers=پیشنهادهای فیلتر شده (جدا شده با ویرگول) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=داده های حساب معاملاتی فیلترشده:\nفرمت: لیست جدا شده با ویرگول [شناسه روش پرداخت، زمینه داده، ارزش] +filterWindow.bannedCurrencies=کدهای ارز فیلترشده (جدا شده با ویرگول) +filterWindow.bannedPaymentMethods=شناسه‌های روش پرداخت فیلتر شده (جدا شده با ویرگول) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=داوران فیلتر شده (آدرس های Onion جدا شده با ویرگول) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) +filterWindow.seedNode=گره های seed فیلتر شده (آدرس های Onion جدا شده با ویرگول) +filterWindow.priceRelayNode=گره های رله قیمت فیلترشده (آدرس های Onion جدا شده با ویرگول) +filterWindow.btcNode=گره‌های بیت‌کوین فیلترشده (آدرس + پورت جدا شده با ویرگول) +filterWindow.preventPublicBtcNetwork=جلوگیری از استفاده ازشبکه عمومی بیت‌کوین +filterWindow.disableDao=Disable DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Min. version required for DAO +filterWindow.disableTradeBelowVersion=Min. version required for trading +filterWindow.add=افزودن فیلتر +filterWindow.remove=حذف فیلتر +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=حداقل مقدار BTC +offerDetailsWindow.min=(حداقل {0}) +offerDetailsWindow.distance=(فاصله از قیمت روز بازار: {0}) +offerDetailsWindow.myTradingAccount=حساب معاملاتی من +offerDetailsWindow.offererBankId=(/BIC/SWIFT/شناسه بانک سفارش گذار) +offerDetailsWindow.offerersBankName=(نام بانک سفارش گذار) +offerDetailsWindow.bankId=شناسه بانک (برای مثال BIC یا SWIFT) +offerDetailsWindow.countryBank=کشور بانک سفارش‌گذار +offerDetailsWindow.commitment=تعهد +offerDetailsWindow.agree=من موافقم +offerDetailsWindow.tac=شرایط و الزامات +offerDetailsWindow.confirm.maker=تأیید: پیشنهاد را به {0} بگذارید +offerDetailsWindow.confirm.taker=تأیید: پیشنهاد را به {0} بپذیرید +offerDetailsWindow.creationDate=تاریخ ایجاد +offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=درخواست پرداخت:\n{0} + +selectDepositTxWindow.headline=تراکنش سپرده را برای مناقشه انتخاب کنید +selectDepositTxWindow.msg=تراکنش سپرده در معامله ذخیره نشده بود.\nلطفاً یکی از تراکنش های چندامضایی موجود از کیف پول خود را انتخاب کنید که تراکنش سپرده در معامله ی ناموفق، استفاده شده بود.\n\nشما می توانید تراکنش صحیح را با باز کردن پنجره جزئیات معامله (با کلیک بر روی شناسه معامله در لیست) و دنبال کردن خروجی تراکنش پرداخت هزینه معامله تا تراکنش بعدی که در آن شما می توانید تراکنش سپرده ی چند امضایی را ببینید (آدرس با 3 شروع می شود)، پیدا کنید. آن شناسه تراکنش باید در لیست ارائه شده در اینجا قابل مشاهده باشد. هنگامی که تراکنش صحیح را یافتید، آن تراکنش را در اینجا انتخاب نموده و ادامه دهید.\n\nبا عرض پوزش برای این مشکل، اما این خطا ندرتاً رخ دهد و در آینده ما سعی خواهیم کرد راه های بهتری برای حل آن پیدا کنیم. +selectDepositTxWindow.select=انتخاب تراکنش سپرده + +sendAlertMessageWindow.headline=ارسال اطلاع رسانی جهانی +sendAlertMessageWindow.alertMsg=پیام هشدار +sendAlertMessageWindow.enterMsg=وارد کردن پیام +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=شماره نسخه جدید +sendAlertMessageWindow.send=ارسال اطلاع رسانی +sendAlertMessageWindow.remove=حذف اطلاع رسانی + +sendPrivateNotificationWindow.headline=ارسال پیام اختصاصی +sendPrivateNotificationWindow.privateNotification=اعلان خصوصی +sendPrivateNotificationWindow.enterNotification=وارد کردن اعلان +sendPrivateNotificationWindow.send=ارسال اطلاع رسانی خصوصی + +showWalletDataWindow.walletData=داده های کیف پول +showWalletDataWindow.includePrivKeys=شامل کلیدهای خصوصی + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=موافقتنامه کاربر +tacWindow.agree=من موافقم +tacWindow.disagree=من مخالفم و خارج می شوم +tacWindow.arbitrationSystem=Dispute resolution + +tradeDetailsWindow.headline=معامله +tradeDetailsWindow.disputedPayoutTxId=شناسه تراکنش پرداختی مورد مناقشه: +tradeDetailsWindow.tradeDate=تاریخ معامله +tradeDetailsWindow.txFee=کارمزد استخراج +tradeDetailsWindow.tradingPeersOnion=آدرس Onion همتایان معامله: +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=وضعیت معامله +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=وارد کردن رمز عبور به منظور باز کردن + +torNetworkSettingWindow.header=تنظیمات شبکه Tor  +torNetworkSettingWindow.noBridges=از پل ها استفاده نکنید +torNetworkSettingWindow.providedBridges=اتصال با پل های ارائه شده +torNetworkSettingWindow.customBridges=وارد کردن پل های سفارشی +torNetworkSettingWindow.transportType=نوع انتقال +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (توصیه شده) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=یک یا چند رله وارد کنید (یکی در هر خط) +torNetworkSettingWindow.enterBridgePrompt=نوع آدرس:پورت +torNetworkSettingWindow.restartInfo=برای اعمال تغییرات باید برنامه را مجدداً راه اندازی کنید. +torNetworkSettingWindow.openTorWebPage=صفحه وب پروژه ی Tor را باز کنید +torNetworkSettingWindow.deleteFiles.header=مشکلاتی برای اتصال وجود دارد؟ +torNetworkSettingWindow.deleteFiles.info=اگر برای اتصال در راه اندازی مشکلات مکرر دارید، حذف فایل های قدیمی Tor می تواند گره گشا باشد. برای انجام آن، روی دکمه زیر کلیک کرده و پس از آن برنامه را مجدداً راه اندازی کنید. +torNetworkSettingWindow.deleteFiles.button=حذف فایل های قدیمی Tor و خاموش کردن +torNetworkSettingWindow.deleteFiles.progress=خاموش کردن Tor در حال انجام است +torNetworkSettingWindow.deleteFiles.success=حذف فایل های قدیمی Tor با موفقیت انجام شد. لطفاً مجدداً راه اندازی کنید. +torNetworkSettingWindow.bridges.header=آیا Tor مسدود شده است؟ +torNetworkSettingWindow.bridges.info=اگر Tor توسط ارائه دهنده اینترنت شما یا توسط کشور شما مسدود شده است، شما می توانید از پل های Tor استفاده کنید.\nاز صفحه وب Tor در https://bridges.torproject.org/bridges دیدن کنید تا مطالب بیشتری در مورد پل ها و نقل و انتقالات قابل اتصال یاد بگیرید. + +feeOptionWindow.headline=انتخاب ارز برای پرداخت هزینه معامله +feeOptionWindow.info=شما می توانید انتخاب کنید که هزینه معامله را در BSQ یا در BTC بپردازید. اگر BSQ را انتخاب می کنید، از تخفیف هزینه معامله برخوردار می شوید. +feeOptionWindow.optionsLabel=انتخاب ارز برای پرداخت کارمزد معامله +feeOptionWindow.useBTC=استفاده از BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=اطلاع رسانی +popup.headline.instruction=لطفاً درنظر داشته باشید: +popup.headline.attention=توجه +popup.headline.backgroundInfo=اطلاعات پس زمینه +popup.headline.feedback=تکمیل شده +popup.headline.confirmation=تائیدیه +popup.headline.information=اطلاعات +popup.headline.warning=هشدار +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 bisq.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} + +error.spvFileCorrupted=هنگام خواندن فایل زنجیره SPV خطایی رخ داد.\nممکن است فایل زنجیره SPV خراب شده باشد.\n\nپیغام خطا: {0}\n\nآیا می خواهید آن را حذف کنید و یک همگام سازی را شروع نمایید؟ +error.deleteAddressEntryListFailed=قادر به حذف فایل AddressEntryList نیست.\nخطا: {0} +error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. +error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. + +popup.warning.walletNotInitialized=کیف پول هنوز راه اندازی اولیه نشده است +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=شما احتمالاً نسخه اشتباه Bisq را برای این رایانه دارید.\nمعماری کامپیوتر شما این است: {0}.\nباینری Bisq که شما نصب کرده اید،عبارت است از: {1}.\nلطفاً نسخه فعلی را خاموش کرده و مجدداً نصب نمایید ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq در حال اجرا است. شما نمیتوانید دو نمونه از Bisq را اجرا کنید. +popup.warning.tradePeriod.halfReached=معامله شما با شناسه {0} نیمی از حداکثر مجاز دوره زمانی معامله را به پایان رسانده و هنوز کامل نشده است. \n\nدوره معامله در {1} به پایان می رسد\n\n لطفا وضعیت معامله خود را در \"سبد سهام/معاملات باز\" برای اطلاعات بیشتر، بررسی کنید. +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the mediator. +popup.warning.noTradingAccountSetup.headline=شما یک حساب معاملاتی را راه اندازی نکرده اید +popup.warning.noTradingAccountSetup.msg=قبل از اینکه بتوانید یک پیشنهاد ایجاد کنید، باید یک ارز ملی یا حساب کاربری آلت کوین را تنظیم کنید. \nآیا می خواهید یک حساب کاربری را راه اندازی کنید؟ +popup.warning.noArbitratorsAvailable=هیچ داوری در دسترس نیست. +popup.warning.noMediatorsAvailable=There are no mediators available. +popup.warning.notFullyConnected=شما باید منتظر بمانید تا به طور کامل به شبکه متصل شوید. \nاین ممکن است در هنگام راه اندازی حدود 2 دقیقه طول بکشد. +popup.warning.notSufficientConnectionsToBtcNetwork=شما باید منتظر بمانید تا حداقل {0} اتصال به شبکه بیتکوین داشته باشید. +popup.warning.downloadNotComplete=شما باید منتظر بمانید تا بارگیری بلاک های بیتکوین باقیمانده کامل شود. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=آیا شما مطمئن هستید که می خواهید این پیشنهاد را حذف کنید؟\nاگر آن پیشنهاد را حذف کنید، هزینه سفارش گذار {0} از دست خواهد رفت . +popup.warning.tooLargePercentageValue=شما نمیتوانید درصد 100٪ یا بیشتر را تنظیم کنید. +popup.warning.examplePercentageValue=لطفا یک عدد درصد مانند \"5.4\" برای 5.4% وارد کنید +popup.warning.noPriceFeedAvailable=برای این ارز هیچ خوراک قیمتی وجود ندارد. شما نمیتوانید از یک درصد بر اساس قیمت استفاده کنید. \nلطفا قیمت مقطوع را انتخاب کنید. +popup.warning.sendMsgFailed=ارسال پیام به شریک معاملاتی شما ناموفق بود. \nلطفا دوباره امتحان کنید و اگر همچنان ناموفق بود، گزارش یک اشکال را ارسال کنید. +popup.warning.insufficientBtcFundsForBsqTx=شما BTC کافی برای پرداخت کارمزد استخراج آن تراکنش BSQ را ندارید.\nلطفاً کیف پول BTC خود را شارژ نموده تا قادر به انتقال BSQ باشید.\nBTC موردنیاز: {0} +popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. +popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=کیف‌پول BSQ شما BSQ کافی برای پرداخت کارمزد معامله به BSQ را ندارد. +popup.warning.messageTooLong=پیام شما بیش از حداکثر اندازه مجاز است. لطفا آن را در چند بخش ارسال کنید یا آن را در یک سرویس مانند https://pastebin.com آپلود کنید. +popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=رله قیمت +popup.warning.seed=دانه +popup.warning.mandatoryUpdate.trading=Please update to the latest Bisq version. A mandatory update was released which disables trading for old versions. Please check out the Bisq Forum for more information. +popup.warning.mandatoryUpdate.dao=Please update to the latest Bisq version. A mandatory update was released which disables the Bisq DAO and BSQ for old versions. Please check out the Bisq Forum for more information. +popup.warning.disable.dao=The Bisq DAO and BSQ are temporary disabled. Please check out the Bisq Forum for more information. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 BTC to transfer. + +popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Bitcoin 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 Bisq support channel at the Bisq Keybase team. + +popup.warning.trade.txRejected.tradeFee=trade fee +popup.warning.trade.txRejected.deposit=deposit +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.info.securityDepositInfo=برای اطمینان از اینکه هر دو معامله گر پروتکل معامله را رعایت می‌کنند، هر دو معامله گر باید مبلغی را تحت عنوان سپرده اطمینان پرداخت کنند.\n\nاین سپرده در کیف‌پول معامله شما نگهداری می‌شود و زمانی که معامله شما با موفقیت انجام شد به خود شما بازگردانده خواهد شد.\n\nلطفا توجه کنید: اگر می‌خواهید یک پیشنهاد جدید ایجاد کنید، Bisq باید برای در سمت معامله دیگر اجرا باشد تا بتوانند آن را بپذیرد. برای اینکه پیشنهادات شما برخط بمانند، بگذارید Bisq در حال اجرابماند و همچنین مطمئن شوید که این کامپیوتر به اینترنت متصل است. (به عنوان مثال مطمئن شوید که به حالت آماده باش نمی‌رود.. البته حالت آماده باش برای نمایشگر ایرادی ندارد). + +popup.info.cashDepositInfo=لطفا مطمئن شوید که شما یک شعبه بانک در منطقه خود دارید تا بتوانید سپرده نقدی را بپردازید. شناسه بانکی (BIC/SWIFT) بانک فروشنده: {0}. +popup.info.cashDepositInfo.confirm=تأیید می کنم که می توانم سپرده را ایجاد کنم +popup.info.shutDownWithOpenOffers=Bisq در حال خاموش شدن است ولی پیشنهاداتی وجود دارند که باز هستند.\n\nزمانی که Bisq بسته باشد این پیشنهادات در شبکه P2P در دسترس نخواهند بود، ولی هر وقت دوباره Bisq را باز کنید این پیشنهادات دوباره در شبکه P2P منتشر خواهند شد.\n\n برای اینکه پیشنهادات شما برخط بمانند، بگذارید Bisq در حال اجرابماند و همچنین مطمئن شوید که این کامپیوتر به اینترنت متصل است. (به عنوان مثال مطمئن شوید که به حالت آماده باش نمی‌رود.. البته حالت آماده باش برای نمایشگر ایرادی ندارد). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=اعلان خصوصی مهم! + +popup.securityRecommendation.headline=توصیه امنیتی مهم +popup.securityRecommendation.msg=ما می خواهیم به شما یادآوری کنیم که استفاده از رمز محافظت برای کیف پول خود را در نظر بگیرید اگر از قبل آن را فعال نکرده اید.\n\nهمچنین شدیداً توصیه می شود که کلمات رمز خصوصی کیف پول را بنویسید. این کلمات رمز خصوصی مانند یک رمزعبور اصلی برای بازیابی کیف پول بیتکوین شما هستند. \nدر قسمت \"کلمات رمز خصوصی کیف پول\" اطلاعات بیشتری کسب می کنید.\n\n علاوه بر این شما باید از پوشه داده های کامل نرم افزار در بخش \"پشتیبان گیری\" پشتیبان تهیه کنید. + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=خاموش شدن در حال انجام است +popup.shutDownInProgress.msg=خاتمه دادن به برنامه می تواند چند ثانیه طول بکشد.\n لطفا این روند را قطع نکنید. + +popup.attention.forTradeWithId=توجه الزامی برای معامله با شناسه {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available +popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. + +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.signAll=Sign all payment methods +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=اعلان برای معامله با شناسه {0} +notification.ticket.headline=تیکت پشتیبانی برای معامله با شناسه {0} +notification.trade.completed=معامله اکنون کامل شده است و می توانید وجوه خود را برداشت کنید. +notification.trade.accepted=پیشنهاد شما توسط BTC {0} پذیرفته شده است. +notification.trade.confirmed=معامله شما دارای حداقل یک تایید بلاک چین است.\n شما اکنون می توانید پرداخت را شروع کنید. +notification.trade.paymentStarted=خریدار BTC پرداخت را آغاز کرده است. +notification.trade.selectTrade=انتخاب معامله +notification.trade.peerOpenedDispute=همتای معامله شما یک {0} را باز کرده است. +notification.trade.disputeClosed={0} بسته شده است. +notification.walletUpdate.headline=به روز رسانی کیف پول معاملاتی +notification.walletUpdate.msg=کیف پول معاملاتی شما به میزان کافی تأمین وجه شده است.\nمبلغ: {0} +notification.takeOffer.walletUpdate.msg=کیف پول معاملاتی شما قبلاً از یک تلاش اخذ پیشنهاد، تأمین وجه شده است.\nمبلغ: {0} +notification.tradeCompleted.headline=معامله تکمیل شد +notification.tradeCompleted.msg=شما اکنون می توانید وجوه خود را به کیف پول بیتکوین خارجی خود برداشت کنید یا آن را به کیف پول Bisq منتقل نمایید. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=نمایش پنجره ی برنامه +systemTray.hide=مخفی کردن پنجره ی برنامه +systemTray.info=اطلاعات درباره ی Bisq  +systemTray.exit=خروج +systemTray.tooltip=Bisq: A decentralized bitcoin exchange network + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=حساب های معاملاتی در مسیر ذیل ذخیره شد:\n{0} +guiUtil.accountExport.noAccountSetup=شما حساب های معاملاتی برای صادرات ندارید. +guiUtil.accountExport.selectPath=انتخاب مسیر به {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=حساب معاملاتی با شناسه {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=ما حساب معاملاتی با شناسه {0} را وارد نکردیم چون در حال حاضر وجود دارد.\n +guiUtil.accountExport.exportFailed=صادرات به CSV به دلیل یک خطا ناموفق بود.\n خطا = {0} +guiUtil.accountExport.selectExportPath=انتخاب مسیر صدور +guiUtil.accountImport.imported=حساب معاملاتی وارد شده از مسیر:\n {0}\n\n حساب های وارد شده:\n {1} +guiUtil.accountImport.noAccountsFound=هیچ حساب معاملاتی صادر شده ای در مسیر یافت نشد: {0}.\n نام فایل {1} است." +guiUtil.openWebBrowser.warning=شما می خواهید یک صفحه وب را در مرورگر وب سیستم خود باز کنید. آیا شما می خواهید صفحه وب را اکنون باز کنید؟\n\n اگر شما از مرورگر \"Tor Browser\" به عنوان مرورگر پیش فرض وب خود استفاده نمی کنید، شما در شبکه پاک به اینترنت متصل خواهید شد.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=صفحه وب را باز کن و دوباره سوال نپرس +guiUtil.openWebBrowser.copyUrl=کپی URL و لغو +guiUtil.ofTradeAmount=از مبلغ معامله +guiUtil.requiredMinimum=(required minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=انتخاب ارز +list.currency.showAll=نمایش همه +list.currency.editList=ویرایش لیست ارز + +table.placeholder.noItems=در حال حاضر هیچ {0} موجود نیست +table.placeholder.noData=در حال حاضر داده ای موجود نیست +table.placeholder.processingData=Processing data... + + +peerInfoIcon.tooltip.tradePeer=همتای معامله +peerInfoIcon.tooltip.maker=سفارش گذار +peerInfoIcon.tooltip.trade.traded={0} آدرس پیازی: {1}\nتاکنون {2} بار(ها) با آن همتا معامله داشته اید\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} آدرس پیازی: {1}\nتاکنون با آن همتا معامله نداشته اید.\n{2} +peerInfoIcon.tooltip.age=حساب معاملاتی {0} قبل ایجاد شده است. +peerInfoIcon.tooltip.unknownAge=عمر حساب پرداخت ناشناخته است. + +tooltip.openPopupForDetails=باز کردن پنجره برای جزئیات +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=مرورگرهای بلاک چین خارجی را برای آدرس باز کنید: {0} +tooltip.openBlockchainForTx=باز کردن مرورگر بلاک چین خارجی برای تراکنش: {0} + +confidence.unknown=وضعیت معامله ناشناخته +confidence.seen=دیده شده توسط {0} همتا (s) / تأیید 0 +confidence.confirmed=تأیید شده در {0} بلاک(s) +confidence.invalid=تراکنش نامعتبر است + +peerInfo.title=اطلاعات همتا +peerInfo.nrOfTrades=تعداد معاملات انجام شده +peerInfo.notTradedYet=شما تاکنون با آن کاربر معامله نداشته اید. +peerInfo.setTag=تنظیم برچسب برای آن همتا +peerInfo.age.noRisk=عمر حساب پرداخت +peerInfo.age.chargeBackRisk=Time since signing +peerInfo.unknownAge=عمر شناخته شده نیست + +addressTextField.openWallet=باز کردن کیف پول بیتکوین پیشفرضتان +addressTextField.copyToClipboard=رونوشت آدرس در حافظه ی موقتی +addressTextField.addressCopiedToClipboard=آدرس در حافظه موقتی رونوشت شد +addressTextField.openWallet.failed=باز کردن یک برنامه کیف پول بیتکوین پیش فرض، ناموفق بوده است. شاید شما برنامه را نصب نکرده باشید؟ + +peerInfoIcon.tooltip={0}\nتگ: {1} + +txIdTextField.copyIcon.tooltip=رونوشت شناسه تراکنش در حافظه ی موقتی +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"حساب\" +navigation.account.walletSeed=\"حساب/رمز پشتیبان کیف پول\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"سبد سهام /پیشنهادهای باز من\" +navigation.portfolio.pending=\"سبد سهام /معاملات باز\" +navigation.portfolio.closedTrades=\"سبد سهام /تاریخچه\" +navigation.funds.depositFunds=\"وجوه/دریافت وجوه\" +navigation.settings.preferences=\"تنظیمات/اولویت ها\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"وجوه/تراکنش ها\" +navigation.support=\"پشتیبانی\" +navigation.dao.wallet.receive=\"DAO/کیف پول BSQ/دریافت\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} مبلغ {1} +formatter.makerTaker=سفارش گذار به عنوان {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.fiat=شما در حال ایجاد یک پیشنهاد به {0} {1} هستید +formatter.youAreCreatingAnOffer.altcoin=شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید +formatter.asMaker={0} {1} به عنوان سفارش گذار +formatter.asTaker={0} {1} به عنوان پذیرنده + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=سال +time.month=ماه +time.week=هفته +time.day=روز +time.hour=ساعت +time.minute10=10 دقیقه +time.hours=ساعات +time.days=روزها +time.1hour=1 ساعت +time.1day=1 روز +time.minute=دقیقه +time.second=ثانیه +time.minutes=دقائق +time.seconds=ثانیه ها + + +password.enterPassword=رمز عبور +password.confirmPassword=تایید رمز عبور +password.tooLong=رمز عبور باید کمتر از 500 کاراکتر باشد. +password.deriveKey=کلید را از رمز عبور استنتاج کنید +password.walletDecrypted=کیف پول با موفقیت رمزگشایی شد و حفاظت با رمز عبور حذف شد. +password.wrongPw=شما رمز عبور را اشتباه وارد کرده اید.\n\n لطفا سعی کنید رمز عبور خود را وارد کنید و با دقت خطاها و اشتباهات املایی را بررسی کنید. +password.walletEncrypted=کیف پول به طور موفقیت آمیز کدگذاری و حفاظت کیف پول فعال شد. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=2 رمز عبوری که وارد نموده اید باهم مطابقت ندارند. +password.forgotPassword=رمز عبور را فراموش کرده اید؟ +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=کلمات seed کیف‌پول +seed.enterSeedWords=کلمات seed کیف‌پول را وارد کنید +seed.date=تاریخ کیف‌پول +seed.restore.title=بازگرداندن کیف های پول از کلمات رمز خصوصی +seed.restore=بازگرداندن کیف های پول +seed.creationDate=تاریخ ایجاد +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=میخواهم به هر حال بازگردانی کنم +seed.warn.walletNotEmpty.emptyWallet=من ابتدا کیف پول هایم را خالی می کنم +seed.warn.notEncryptedAnymore=کیف های پول شما رمزگذاری شده اند. \n\nپس از بازگرداندن، کیف های پول دیگر رمزگذاری نخواهند شد و شما باید رمز عبور جدید را تنظیم کنید.\n\n آیا می خواهید ادامه دهید؟ +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=کیف های پول با کلمات کلمات رمز خصوصی جدید بازیابی شده است. \n\nشما باید برنامه را خاموش و مجددا راه اندازی کنید. +seed.restore.error=هنگام بازگرداندن کیف پول با کلمات رمز خصوصی، خطایی روی داد. {0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=حساب +payment.account.no=شماره حساب +payment.account.name=نام حساب +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=نام کامل مالک حساب +payment.account.fullName=نام کامل (اول، وسط، آخر) +payment.account.state=ایالت/استان/ناحیه +payment.account.city=شهر +payment.bank.country=کشور بانک +payment.account.name.email=نام کامل/ایمیل مالک حساب +payment.account.name.emailAndHolderId=نام کامل/ایمیل/{0} مالک حساب +payment.bank.name=نام بانک +payment.select.account=انتخاب نوع حساب +payment.select.region=انتخاب ناحیه +payment.select.country=انتخاب کشور +payment.select.bank.country=انتخاب کشور بانک +payment.foreign.currency=آیا مطمئن هستید که می خواهید ارزی به جز ارز پیش فرض کشور انتخاب کنید؟ +payment.restore.default=خیر، ارز پیشفرض را تعیین کن +payment.email=ایمیل +payment.country=کشور +payment.extras=الزامات اضافی +payment.email.mobile=ایمیل یا شماره موبایل +payment.altcoin.address=آدرس آلت‌کوین +payment.altcoin.tradeInstantCheckbox=Trade instant (within 1 hour) with this Altcoin +payment.altcoin.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able to complete the trade in less than 1 hour.\n\nIf you have offers open and you are not available please disable those offers under the 'Portfolio' screen. +payment.altcoin=آلت‌کوین +payment.select.altcoin=Select or search Altcoin +payment.secret=سوال محرمانه +payment.answer=پاسخ +payment.wallet=شناسه کیف پول +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=نام کاربری یا ایمیل یا شماره تلفن +payment.moneyBeam.accountId=ایمیل یا شماره تلفن +payment.venmo.venmoUserName=نام کاربری Venmo +payment.popmoney.accountId=ایمیل یا شماره تلفن +payment.promptPay.promptPayId=شناسه شهروندی/شناسه مالیاتی یا شماره تلفن +payment.supportedCurrencies=ارزهای مورد حمایت +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=محدودیت‌ها +payment.salt=داده‌های تصافی برای اعتبارسنجی سن حساب +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=پذیرش معاملات از این کشورهای Euro ای +payment.accept.nonEuro=پذیرش معاملات از کشورهای غیر Euro ای +payment.accepted.countries=کشورهای پذیرفته شده +payment.accepted.banks=بانک های پذیرفته شده (شناسه) +payment.mobile=شماره موبایل +payment.postal.address=آدرس پستی +payment.national.account.id.AR=شماره CBU +shared.accountSigningState=Account signing status + +#new +payment.altcoin.address.dyn=آدرس {0} +payment.altcoin.receiver.address=آدرس آلت‌کوین گیرنده +payment.accountNr=شماره حساب +payment.emailOrMobile=ایمیل یا شماره موبایل +payment.useCustomAccountName=استفاده از نام حساب سفارشی +payment.maxPeriod=حداکثر دوره ی زمانی مجاز معامله +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} +payment.maxPeriodAndLimitCrypto=حداکثر طول مدت معامله: {0} / حداکثر معامله: {1} +payment.currencyWithSymbol=ارز: {0} +payment.nameOfAcceptedBank=نام بانک پذیرفته شده +payment.addAcceptedBank=افزودن بانک پذیرفته شده +payment.clearAcceptedBanks=پاک کردن بانک های پذیرفته شده +payment.bank.nameOptional=نام بانک (اختیاری) +payment.bankCode=کد بانک +payment.bankId=شناسه بانک (BIC/SWIFT) +payment.bankIdOptional=شناسه بانک (BIC/SWIFT) (اختیاری) +payment.branchNr=شماره شعبه +payment.branchNrOptional=شماره شعبه (اختیاری) +payment.accountNrLabel=شماره حساب (IBAN) +payment.accountType=نوع حساب +payment.checking=بررسی +payment.savings=اندوخته ها +payment.personalId=شناسه شخصی +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 را از طریق پیام کوتاه موبایل به فروشنده BTC ارسال کند.\n\nلطفا مطمئن شوید که از حداکثر میزانی که بانک شما برای انتقال از طریق HalCash مجاز می‌داند تجاوز نکرده‌اید. حداقل مقداردر هر برداشت معادل 10 یورو و حداکثر مقدار 600 یورو می‌باشد. این محدودیت برای برداشت‌های تکراری برای هر گیرنده در روز 3000 یورو و در ماه 6000 یورو می‌باشد. لطفا این محدودیت‌ها را با بانک خود مطابقت دهید و مطمئن شوید که آنها هم همین محدودی‌ها را دارند.\n\nمقدار برداشت باید شریبی از 10 یورو باشد چرا که مقادیر غیر از این را نمی‌توانید از طریق ATM برداشت کنید. رابط کاربری در صفحه ساخت پینشهاد و پذیرش پیشنهاد مقدار BTC را به گونه‌ای تنظیم می‌کنند که مقدار EUR درست باشد. شما نمی‌توانید از قیمت بر مبنای بازار استفاده کنید چون مقدار یورو با تغییر قیمت‌ها عوض خواهد شد.\n\nدر صورت بروز اختلاف خریدار BTC باید شواهد مربوط به ارسال یورو را ارائه دهد. +# suppress inspection "UnusedMessageFormatParameter" +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/Account_limits]. + +payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=اطلاعات تماس +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=اطلاعات تماس +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/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.japan.bank=بانک +payment.japan.branch=Branch +payment.japan.account=حساب +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=انتقال بانک ملی +SAME_BANK=انتقال با همان بانک +SPECIFIC_BANKS=نقل و انتقالات با بانک های مشخص +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=سپرده ی نقدی +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=مانی گرام +WESTERN_UNION=Western Union +F2F=رو در رو (به طور فیزیکی) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=بانک های ملی +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=همان بانک +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=بانک های مشخص +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=سپرده ی نقدی +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=مانی گرام +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=رو در رو +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=پرداخت های فوری SEPA +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=پرداخت سریع تر +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=آلت کوین ها +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY=OKPay +# suppress inspection "UnusedProperty" +CASH_APP=درخواست نقدی +# suppress inspection "UnusedProperty" +VENMO=Venmo + + +# suppress inspection "UnusedProperty" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney\n +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=پرداخت WeChat +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=پرداخت سریع تر +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=آلت کوین ها +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=درخواست نقدی +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=ورودی خالی مجاز نیست. +validation.NaN=ورودی، یک عدد معتبر نیست. +validation.notAnInteger=ورودی یک مقدار صحیح نیست. +validation.zero=ورودی 0 مجاز نیست. +validation.negative=یک مقدار منفی مجاز نیست. +validation.fiat.toSmall=ورودی کوچکتر از حداقل مقدار ممکن مجاز نیست. +validation.fiat.toLarge=ورودی بزرگتر از حداکثر مقدار ممکن مجاز نیست. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=ورودی بزرگتر از {0} مجاز نیست. +validation.btc.toSmall=ورودی کوچکتر از {0} مجاز نیست. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=رمز عبور که شما وارد کرده اید خیلی طولانی است.رمز عبور بیش از 50 کاراکتر نمی تواند باشد. +validation.sortCodeNumber={0} باید شامل {1} عدد باشد. +validation.sortCodeChars={0} باید شامل {1} کاراکتر باشد. +validation.bankIdNumber={0} باید شامل {1} عدد باشد. +validation.accountNr=عدد حساب باید متشکل از {0} عدد باشد. +validation.accountNrChars=عدد حساب باید متشکل از {0} کاراکتر باشد. +validation.btc.invalidAddress=آدرس درست نیست. لطفا فرمت آدرس را بررسی کنید +validation.integerOnly=لطفا فقط اعداد صحیح را وارد کنید. +validation.inputError=ورودی شما یک خطا ایجاد کرد: {0} +validation.bsq.insufficientBalance=موجودی در دسترس شما {0} است. +validation.btc.exceedsMaxTradeLimit=حدمعامله شما {0} است. +validation.bsq.amountBelowMinAmount=مقدار حداقل {0} است +validation.nationalAccountId={0} باید شامل {1} عدد باشد. + +#new +validation.invalidInput=ورودی نامعتبر: {0} +validation.accountNrFormat=شماره حساب باید از فرمت {0} باشد +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=تأیید آدرس ناموفق بود زیرا آن با ساختار یک آدرس {0} مطابقت ندارد. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=آدرس یک آدرس {0} معتبر نیست! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=کد بانک و کد کشور باید حروف باشند +validation.bic.invalidLocationCode=BIC حاوی کد مکان نامعتبر است +validation.bic.invalidBranchCode=BIC حاوی کد شعبه نامعتبر است +validation.bic.sepaRevolutBic=حساب های Revolut Sepa پشتیبانی نمی شود. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=آدرس نامعتبر است +validation.iban.invalidCountryCode=کد کشور نامعتبر است +validation.iban.checkSumNotNumeric=سرجمع باید عددی باشد +validation.iban.nonNumericChars=کاراکتر غیر الفبایی و غیر عددی شناسایی شد +validation.iban.checkSumInvalid=سرجمع IBAN نامعتبر است +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=کد ناحیه غیر کانادایی +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=باید فقط شامل حروف، اعداد، فاصله و یا نمادهای ' _ , . ? - باشد +validation.interacETransfer.invalidAnswer=باید یک کلمه باشد و فقط شامل حروف، اعداد و یا نماد - باشد +validation.inputTooLarge=ورودی نباید بزرگتر از {0} باشد +validation.inputTooSmall=ورودی باید بزرگتر از {0} باشد +validation.inputToBeAtLeast=Input has to be at least {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=طول باید بین {0} و {1} باشد +validation.fixedLength=Length must be {0} +validation.pattern=ورودی باید در این قالب باشد: {0} +validation.noHexString=ورودی در قالب HEX نیست +validation.advancedCash.invalidFormat=باید یک ایمیل درست باشد و یا یک شناسه کیف‌پول در قالب: X000000000000 +validation.invalidUrl=این یک URL معتبر نیست +validation.mustBeDifferent=Your input must be different from the current value +validation.cannotBeChanged=Parameter cannot be changed +validation.numberFormatException=Number format exception {0} +validation.mustNotBeNegative=ورودی نباید منفی باشد +validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.invalidCharacters=Phone number {0} contains invalid characters +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Must be comma separated list of valid addresses diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties new file mode 100644 index 0000000000..49c762a3e9 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=En savoir plus +shared.openHelp=Ouvrir l'aide +shared.warning=Attention +shared.close=Fermer +shared.cancel=Annuler +shared.ok=OK +shared.yes=Oui +shared.no=Non +shared.iUnderstand=Je comprends +shared.na=N/A +shared.shutDown=Éteindre +shared.reportBug=Signaler le bug sur Github +shared.buyBitcoin=Achat Bitcoin +shared.sellBitcoin=Vendre des Bitcoins +shared.buyCurrency=Achat {0} +shared.sellCurrency=Vendre {0} +shared.buyingBTCWith=achat BTC avec {0} +shared.sellingBTCFor=vendre BTC pour {0} +shared.buyingCurrency=achat {0} (vente BTC) +shared.sellingCurrency=vente {0} (achat BTC) +shared.buy=acheter +shared.sell=vendre +shared.buying=achat +shared.selling=vente +shared.P2P=P2P +shared.oneOffer=ordre +shared.multipleOffers=ordres +shared.Offer=Ordre +shared.offerVolumeCode={0} Volume d'offre +shared.openOffers=ordres ouverts +shared.trade=transaction +shared.trades=transactions +shared.openTrades=Échanges en cours +shared.dateTime=Date/Heure +shared.price=Prix +shared.priceWithCur=Prix en {0} +shared.priceInCurForCur=Prix en {0} pour 1 {1} +shared.fixedPriceInCurForCur=Prix fixé en {0} pour 1 {1} +shared.amount=Montant +shared.txFee=Frais de transaction +shared.tradeFee=Frais de transaction +shared.buyerSecurityDeposit=Dépôt de l'acheteur +shared.sellerSecurityDeposit=Dépôt du vendeur +shared.amountWithCur=Montant en {0} +shared.volumeWithCur=Volume en {0} +shared.currency=Devise +shared.market=Marché +shared.deviation=Déviation +shared.paymentMethod=Mode de paiement +shared.tradeCurrency=Devise d'échange +shared.offerType=Type d'ordre +shared.details=Détails +shared.address=Adresse +shared.balanceWithCur=Balance en {0} +shared.utxo=Transaction de sortie non-dépensée +shared.txId=ID de la transaction +shared.confirmations=Confirmations +shared.revert=Revertir le Tx +shared.select=Sélectionner +shared.usage=Utilisation +shared.state=Statut +shared.tradeId=ID de la transaction +shared.offerId=ID de l'ordre +shared.bankName=Nom de la banque +shared.acceptedBanks=Banques acceptées +shared.amountMinMax=Montant (min-max) +shared.amountHelp=Si un ordre comporte un montant minimum et un montant maximum, alors vous pouvez échanger n'importe quel montant dans cette fourchette. +shared.remove=Enlever +shared.goTo=Aller à {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Retirer l'ordre +shared.dontRemoveOffer=Ne pas retirer l'ordre +shared.editOffer=Éditer l'ordre +shared.openLargeQRWindow=Ouvrez et agrandissez la fenêtre du code QR +shared.tradingAccount=Compte de trading +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 Bisq +shared.fundFromExternalWalletButton=Ouvrez votre portefeuille externe pour provisionner +shared.openDefaultWalletFailed=L'ouverture de l'application de portefeuille Bitcoin par défaut a échoué. Êtes-vous sûr de l'avoir installée? +shared.belowInPercent=% sous le prix du marché +shared.aboveInPercent=% au-dessus du prix du marché +shared.enterPercentageValue=Entrez la valeur en % +shared.OR=OU +shared.notEnoughFunds=Il n'y a pas suffisamment de fonds dans votre portefeuille Bisq pour payer cette transaction. La transaction a besoin de {0} Votre solde disponible est de {1}. \n\nVeuillez ajouter des fonds à partir d'un portefeuille Bitcoin externe ou recharger votre portefeuille Bisq dans «Fonds / Dépôts > Recevoir des Fonds». +shared.waitingForFunds=En attente des fonds... +shared.depositTransactionId=ID de la transaction de dépôt +shared.TheBTCBuyer=L'acheteur de BTC +shared.You=Vous +shared.sendingConfirmation=Envoi de la confirmation... +shared.sendingConfirmationAgain=Veuillez envoyer de nouveau la confirmation +shared.exportCSV=Exporter en CSV +shared.exportJSON=Exporter vers JSON +shared.summary=Afficher le résumé +shared.noDateAvailable=Pas de date disponible +shared.noDetailsAvailable=Pas de détails disponibles +shared.notUsedYet=Pas encore utilisé +shared.date=Date +shared.sendFundsDetailsWithFee=Envoi: {0}\nDepuis l'adresse: {1}\nVers l'adresse de réception: {2}\nLes frais de minage requis sont : {3} ({4} satoshis/byte)\nVsize de la transaction: {5} vKb\n\nLe destinataire recevra: {6}\n\nÊtes-vous certain de vouloir retirer ce montant? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq détecte que la transaction produira une sortie inférieure au seuil de fraction minimum (non autorisé par les règles de consensus Bitcoin). 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 +shared.language=Langue +shared.country=Pays +shared.applyAndShutDown=Appliquer et éteindre +shared.selectPaymentMethod=Sélectionner un mode de paiement +shared.accountNameAlreadyUsed=Ce nom de compte a été utilisé par un compte enregistré. Veuillez utiliser un autre nom. +shared.askConfirmDeleteAccount=Voulez-vous vraiment supprimer le compte sélectionné? +shared.cannotDeleteAccount=Vous ne pouvez pas supprimer ce compte car il est utilisé dans des devis (ou dans des transactions). +shared.noAccountsSetupYet=Il n'y a pas encore de comptes établis. +shared.manageAccounts=Gérer les comptes +shared.addNewAccount=Ajouter un nouveau compte +shared.ExportAccounts=Exporter les comptes +shared.importAccounts=Importer les comptes +shared.createNewAccount=Créer un nouveau compte +shared.saveNewAccount=Sauvegarder un nouveau compte +shared.selectedAccount=Sélectionner un compte +shared.deleteAccount=Supprimer le compte +shared.errorMessageInline=\nMessage d''erreur: {0} +shared.errorMessage=Message d'erreur +shared.information=Information +shared.name=Nom +shared.id=ID +shared.dashboard=Tableau de bord +shared.accept=Accepter +shared.balance=Solde +shared.save=Sauvegarder +shared.onionAddress=Adresse Onion +shared.supportTicket=Ticket de support +shared.dispute=conflit +shared.mediationCase=Litige en médiation +shared.seller=vendeur +shared.buyer=acheteur +shared.allEuroCountries=Tous les pays de la zone Euro +shared.acceptedTakerCountries=Pays acceptés par le taker +shared.tradePrice=Prix de l'échange +shared.tradeAmount=Montant de l'échange +shared.tradeVolume=Volume d'échange +shared.invalidKey=La clé que vous avez entrée n'était pas correcte. +shared.enterPrivKey=Entrer la clé privée pour déverrouiller +shared.makerFeeTxId=Maker fee transaction ID +shared.takerFeeTxId=Taker fee transaction ID +shared.payoutTxId=ID du versement de la transaction +shared.contractAsJson=Contrat au format JSON +shared.viewContractAsJson=Voir le contrat en format JSON +shared.contract.title=Contrat pour la transaction avec l''ID : {0} +shared.paymentDetails=BTC {0} détails du paiement +shared.securityDeposit=Dépôt de garantie +shared.yourSecurityDeposit=Votre dépôt de garantie +shared.contract=Contrat +shared.messageArrived=Message reçu. +shared.messageStoredInMailbox=Message stocké dans la boîte de réception. +shared.messageSendingFailed=Échec de l''envoi du message. Erreur: {0} +shared.unlock=Déverrouiller +shared.toReceive=à recevoir +shared.toSpend=à dépenser +shared.btcAmount=Montant en BTC +shared.yourLanguage=Vos langues +shared.addLanguage=Ajouter une langue +shared.total=Total +shared.totalsNeeded=Fonds nécessaires +shared.tradeWalletAddress=Adresse du portefeuille de trading +shared.tradeWalletBalance=Solde du portefeuille de trading +shared.makerTxFee=Maker: {0} +shared.takerTxFee=Taker: {0} +shared.iConfirm=Je confirme +shared.tradingFeeInBsqInfo=environ {0} +shared.openURL=Ouvert {0} +shared.fiat=Fiat +shared.crypto=Crypto +shared.all=Tout +shared.edit=Modifier +shared.advancedOptions=Options avancées +shared.interval=Intervalle +shared.actions=Actions +shared.buyerUpperCase=Acheteur +shared.sellerUpperCase=Vendeur +shared.new=NOUVEAU +shared.blindVoteTxId=ID de la transaction du vote caché +shared.proposal=Proposition +shared.votes=Votes +shared.learnMore=En savoir plus +shared.dismiss=Rejeter +shared.selectedArbitrator=Arbitre sélectionné +shared.selectedMediator=Médiateur sélectionné +shared.selectedRefundAgent=Arbitre sélectionné +shared.mediator=Médiateur +shared.arbitrator=Arbitre +shared.refundAgent=Arbitre +shared.refundAgentForSupportStaff=Agent de remboursement +shared.delayedPayoutTxId=ID de versement de la transaction délayé +shared.delayedPayoutTxReceiverAddress=Transaction à versement delayé envoyée à +shared.unconfirmedTransactionsLimitReached=Vous avez trop de transactions non confirmées pour le moment. Veuillez réessayer plus tard. +shared.numItemsLabel=Nombres d'entrées: {0} +shared.filter=Filtre +shared.enabled=Activé + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Marché +mainView.menu.buyBtc=Achat BTC +mainView.menu.sellBtc=Vendre des BTC +mainView.menu.portfolio=Portfolio +mainView.menu.funds=Fonds +mainView.menu.support=Assistance +mainView.menu.settings=Paramètres +mainView.menu.account=Compte +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Prix du marché par {0} +mainView.marketPrice.bisqInternalPrice=Cours de la dernière transaction Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=Il n'y a pas de cours de marché disponible depuis une source externe.\nLe cours affiché est celui de la dernière transaction Bisq pour cette devise. +mainView.marketPrice.tooltip=Le prix de marché est fourni par {0}{1}\nDernière mise à jour: {2}\nURL du noeud: {3} +mainView.balance.available=Solde disponible +mainView.balance.reserved=Réservé en ordres +mainView.balance.locked=Bloqué en transactions +mainView.balance.reserved.short=Réservé +mainView.balance.locked.short=Vérouillé + +mainView.footer.usingTor=(à travers Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Taux des frais: {0} sat/vB +mainView.footer.btcInfo.initializing=Connexion au réseau Bitcoin en cours +mainView.footer.bsqInfo.synchronizing=/ Synchronisation DAO en cours +mainView.footer.btcInfo.synchronizingWith=Synchronisation avec {0} au block: {1}/ {2} +mainView.footer.btcInfo.synchronizedWith=Synchronisé avec {0} au block {1} +mainView.footer.btcInfo.connectingTo=Se connecte à +mainView.footer.btcInfo.connectionFailed=Échec de la connexion à +mainView.footer.p2pInfo=Pairs du réseau bitcoin: {0} / pairs du réseau Bisq: {1} +mainView.footer.daoFullNode=DAO full node + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Connection au réseau Tor... +mainView.bootstrapState.torNodeCreated=(2/4) Noeud Tor créé +mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service published +mainView.bootstrapState.initialDataReceived=(4/4) Données initiales reçues + +mainView.bootstrapWarning.noSeedNodesAvailable=Pas de seed nodes disponible +mainView.bootstrapWarning.noNodesAvailable=Pas de noeuds de seed ou de pairs disponibles +mainView.bootstrapWarning.bootstrappingToP2PFailed=L'initialisation du réseau Bisq a échoué + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Il n'y a pas de noeud de seed ou de persisted pairs disponibles pour demander des données.\nVeuillez vérifier votre connexion Internet ou essayer de redémarrer l'application. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=La connexion au réseau Bisq a échoué (erreur signalé: {0}).\nVeuillez vérifier votre connexion internet ou essayez de redémarrer l'application. + +mainView.walletServiceErrorMsg.timeout=La connexion au réseau Bitcoin a échoué car le délai d'attente a expiré. +mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Bitcoin a échoué à cause d''une erreur: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Le réseau a rejeté une transaction.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Vous avez perdu la connexion avec tous les {0} pairs du réseau.\nVous avez peut-être perdu votre connexion Internet ou votre ordinateur était passé en mode veille. +mainView.networkWarning.localhostBitcoinLost=Vous avez perdu la connexion avec le localhost Bitcoin node.\nVeuillez redémarrer l'application Bisq pour vous connecter à d'autres Bitcoin nodes ou redémarrer le localhost Bitcoin node. +mainView.version.update=(Mise à jour disponible) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Livre des ordres +market.tabs.spreadCurrency=Offres par devise +market.tabs.spreadPayment=Offres par mode de paiement +market.tabs.trades=Échanges + +# OfferBookChartView +market.offerBook.buyAltcoin=Achat {0} (vente {1}) +market.offerBook.sellAltcoin=Vente {0} (achat {1}) +market.offerBook.buyWithFiat=Achat {0} +market.offerBook.sellWithFiat=Vente {0} +market.offerBook.sellOffersHeaderLabel=Vendre des {0} à +market.offerBook.buyOffersHeaderLabel=Acheter des {0} à +market.offerBook.buy=Je veux acheter des Bitcoins +market.offerBook.sell=Je veux vendre des Bitcoins + +# SpreadView +market.spread.numberOfOffersColumn=Tout les ordres ({0}) +market.spread.numberOfBuyOffersColumn=Achat BTC ({0}) +market.spread.numberOfSellOffersColumn=Vente BTC ({0}) +market.spread.totalAmountColumn=Total BTC ({0}) +market.spread.spreadColumn=Écart +market.spread.expanded=Vue étendue + +# TradesChartsView +market.trades.nrOfTrades=Échanges: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNombre de trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Ouvrir: +market.trades.tooltip.candle.close=Fermer: +market.trades.tooltip.candle.high=Haut: +market.trades.tooltip.candle.low=Bas: +market.trades.tooltip.candle.average=Moyenne: +market.trades.tooltip.candle.median=Médiane: +market.trades.tooltip.candle.date=Date: +market.trades.showVolumeInUSD=Afficher le volume en USD + +#################################################################### +# OfferView +#################################################################### + +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.trader=Échanger +offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} +offerbook.offerersBankName=Nom de la banque du maker: {0} +offerbook.offerersBankSeat=Pays du siège de la banque du maker: {0} +offerbook.offerersAcceptedBankSeatsEuro=Pays acceptés où se situe le siège de la banque (taker): tout les pays de la zone euro +offerbook.offerersAcceptedBankSeats=Pays acceptés où se situe le siège de la banque (taker)\n{0} +offerbook.availableOffers=Ordres disponibles +offerbook.filterByCurrency=Filtrer par devise +offerbook.filterByPaymentMethod=Filtrer par mode de paiement +offerbook.matchingOffers=Offres correspondants à mes comptes +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 +offerbook.timeSinceSigning.info.peer=Signé par un pair, attendre %d jours pour que les limites soient levées +offerbook.timeSinceSigning.info.peerLimitLifted=signé par un pair et les limites ont été levées +offerbook.timeSinceSigning.info.signer=signé par un pair et pouvant signer des comptes de pairs (limites levées) +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.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d''un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d''un autre pair. +offerbook.timeSinceSigning.notSigned=Pas encore signé +offerbook.timeSinceSigning.notSigned.ageDays={0} jours +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=Ce compte n'a pas encore été signé et a été créée il y'a {0} jours +shared.notSigned.noNeed=Ce type de compte ne nécessite pas de signature +shared.notSigned.noNeedDays=Ce type de compte ne nécessite pas de signature et a été créée il y'a {0} jours +shared.notSigned.noNeedAlts=Les comptes pour altcoin ne supportent pas la signature ou le vieillissement + +offerbook.nrOffers=Nombre d''ordres: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Déposer BTC (%) +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.createOfferToBuy=Créer un nouvel ordre d''achat pour {0} +offerbook.createOfferToSell=Créer un nouvel ordre de vente pour {0} +offerbook.createOfferToBuy.withFiat=Créer un nouvel ordre d''achat pour {0} avec {1} +offerbook.createOfferToSell.forFiat=Créer un nouvel ordre de vente pour {0} for {1} +offerbook.createOfferToBuy.withCrypto=Créer un nouvel ordre de vente pour {0} (achat{1}) +offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d''achat pour {0} (vente{1}) + +offerbook.takeOfferButton.tooltip=Accepter un ordre pour {0} +offerbook.yesCreateOffer=Oui, créer un ordre +offerbook.setupNewAccount=Configurer un nouveau compte de change +offerbook.removeOffer.success=L'ordre a bien été retiré. +offerbook.removeOffer.failed=Le retrait de l''ordre a échoué:\n{0} +offerbook.deactivateOffer.failed=La désactivation de l''ordre a échoué:\n{0} +offerbook.activateOffer.failed=La publication de l''ordre a échoué:\n{0} +offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l''écran {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=Aucun compte de paiement pour la devise sélectionnée +offerbook.warning.noTradingAccountForCurrency.msg=Vous n'avez pas de compte de paiement mis en place pour la devise sélectionnée.\n\nVoudriez-vous créer une offre pour une autre devise à la place? +offerbook.warning.noMatchingAccount.headline=Pas de compte de paiement correspondant +offerbook.warning.noMatchingAccount.msg=Cette offre utilise un mode de paiement que vous n'avez pas créé. \n\nVoulez-vous créer un nouveau compte de paiement maintenant? + +offerbook.warning.counterpartyTradeRestrictions=Cette offre ne peut être acceptée en raison de restrictions d'échange imposées par les contreparties + +offerbook.warning.newVersionAnnouncement=Grâce à cette version du logiciel, les partenaires commerciaux peuvent confirmer et vérifier les comptes de paiement de chacun pour créer un réseau de comptes de paiement de confiance.\n\nUne fois la transaction réussie, votre compte de paiement sera vérifié et les restrictions de transaction seront levées après une certaine période de temps (cette durée est basée sur la méthode de vérification).\n\nPour plus d'informations sur la vérification de votre compte, veuillez consulter le document sur https://docs.bisq.network/payment-methods#account-signing + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l''acheteur n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l''acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} + +offerbook.warning.wrongTradeProtocol=Cet ordre exige une version de protocole différente de celle utilisée actuellement par votre logiciel.\n\nVeuillez vérifier que vous avez bien la dernière version d'installée, il est possible que l'utilisateur qui a créé cet ordre utilise une ancienne version.\n\nIl n'est pas possible de trader avec des versions différentes de protocole. +offerbook.warning.userIgnored=Vous avez ajouté l'adresse onion de cet utilisateur à votre liste noire. +offerbook.warning.offerBlocked=L'ordre a été bloqué par des développeurs de Bisq.\nIl s'agit peut être d'un bug qui cause des problèmes lors de l'acceptation de cet ordre. +offerbook.warning.currencyBanned=La devise utilisée pour cet ordre a été bloquée par les développeurs de Bisq.\nVeuillez visiter le Forum Bisq pour obtenir plus d'informations. +offerbook.warning.paymentMethodBanned=Le mode de paiement utilisé pour cet ordre a été bloqué par les développeurs de Bisq.\nVeuillez visiter le Forum Bisq pour obtenir plus d'informations. +offerbook.warning.nodeBlocked=L'adresse onion de ce trader a été bloquée par les développeurs de Bisq.\nIl s'agit peut être d'un bug qui cause des problèmes lors de l'acceptation de cet ordre. +offerbook.warning.requireUpdateToNewVersion=Votre version Bisq n'est plus compatible avec les transactions. Veuillez mettre à jour la dernière version de Bisq via https://bisq.network/downloads +offerbook.warning.offerWasAlreadyUsedInTrade=Vous ne pouvez pas prendre la commande car vous avez déjà terminé l'opération. Il se peut que votre précédente tentative de prise de commandes ait entraîné l'échec de la transaction. + +offerbook.info.sellAtMarketPrice=Vous vendrez au prix du marché (mis à jour chaque minute). +offerbook.info.buyAtMarketPrice=Vous achèterez au prix du marché (mis à jour chaque minute). +offerbook.info.sellBelowMarketPrice=Vous obtiendrez {0} de moins que le prix actuel du marché (mis à jour chaque minute). +offerbook.info.buyAboveMarketPrice=Vous paierez {0} de plus que le prix actuel du marché (mis à jour chaque minute). +offerbook.info.sellAboveMarketPrice=Vous obtiendrez {0} de plus que le prix actuel du marché (mis à jour chaque minute). +offerbook.info.buyBelowMarketPrice=Vous paierez {0} de moins que le prix actuel du marché (mis à jour chaque minute). +offerbook.info.buyAtFixedPrice=Vous achèterez à ce prix déterminé. +offerbook.info.sellAtFixedPrice=Vous vendrez à ce prix déterminé. +offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l''arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. +offerbook.info.roundedFiatVolume=Le montant a été arrondi pour accroître la confidentialité de votre transaction. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Entrer le montant en BTC +createOffer.price.prompt=Entrer le prix +createOffer.volume.prompt=Entrer le montant en {0} +createOffer.amountPriceBox.amountDescription=Somme en Bitcoin à {0} +createOffer.amountPriceBox.buy.volumeDescription=Somme en {0} à envoyer +createOffer.amountPriceBox.sell.volumeDescription=Montant en {0} à recevoir +createOffer.amountPriceBox.minAmountDescription=Montant minimum de BTC +createOffer.securityDeposit.prompt=Dépôt de garantie +createOffer.fundsBox.title=Financer votre ordre +createOffer.fundsBox.offerFee=Frais de transaction +createOffer.fundsBox.networkFee=Frais de minage +createOffer.fundsBox.placeOfferSpinnerInfo=Publication de l'ordre en cours ... +createOffer.fundsBox.paymentLabel=Transaction Bisq avec l''ID {0} +createOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) +createOffer.fundsBox.fundsStructure.BSQ=({0} dépôt de garantie, {1} frais de minage) + {2} frais de transaction +createOffer.success.headline=Votre ordre a été publiée +createOffer.success.info=Vous pouvez gérer vos ordres en cours dans \"Portfolio/Mes ordres\". +createOffer.info.sellAtMarketPrice=Vous vendrez toujours au prix du marché car le prix de votre ordre sera continuellement mis à jour. +createOffer.info.buyAtMarketPrice=Vous achèterez toujours au prix du marché car le prix de votre ordre sera continuellement mis à jour. +createOffer.info.sellAboveMarketPrice=Vous recevrez toujours {0}% de plus que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. +createOffer.info.buyBelowMarketPrice=Vous paierez toujours {0}% de moins que le prix actuel du marché car prix de votre ordre sera continuellement mis à jour. +createOffer.warning.sellBelowMarketPrice=Vous obtiendrez toujours {0}% de moins que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. +createOffer.warning.buyAboveMarketPrice=Vous paierez toujours {0}% de plus que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. +createOffer.tradeFee.descriptionBTCOnly=Frais de transaction +createOffer.tradeFee.descriptionBSQEnabled=Choisir la devise des frais de transaction + +createOffer.triggerPrice.prompt=Réglez le prix de déclenchement, optionnel +createOffer.triggerPrice.label=Désactiver l'offre si le prix du marché est {0} +createOffer.triggerPrice.tooltip=Afin de protéger contre les brusques variations des prix, vous pouvez mettre en place un prix de déclenchement qui désactive l'offre si le prix du marché atteint cette valeur. +createOffer.triggerPrice.invalid.tooLow=La valeur doit être supérieure à {0} +createOffer.triggerPrice.invalid.tooHigh=La valuer doit être inférieure à {0] + +# new entries +createOffer.placeOfferButton=Review: Placer un ordre de {0} Bitcoin +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 Bisq (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. + +# only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) +createOffer.amountPriceBox.error.message=Une erreur s''est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n''a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l''application et vérifier votre connexion réseau. +createOffer.setAmountPrice=Définir le montant et le prix +createOffer.warnCancelOffer=Vous avez déjà financé cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille bisq local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nÊtes-vous certain de vouloir annuler ? +createOffer.timeoutAtPublishing=Un timeout est survenu au moment de la publication de l'ordre. +createOffer.errorInfo=\n\nLes frais de maker ont déjà été payés. Dans le pire des cas, vous avez perdu ces frais.\nVeuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre le problème. +createOffer.tooLowSecDeposit.warning=Vous avez défini le dépôt de garantie à une valeur inférieure à la valeur par défaut recommandée de {0}.\nÊtes-vous sûr de vouloir utiliser un dépôt de garantie moins élevé ? +createOffer.tooLowSecDeposit.makerIsSeller=Ceci vous donne moins de protection dans le cas où le pair de trading ne suit pas le protocole de transaction. +createOffer.tooLowSecDeposit.makerIsBuyer=cela offre moins de protection pour le pair que de suivre le protocole de trading car vous avez moins de dépôt à risque. D'autres utilisateurs préféreront peut-être accepter d'autres ordres que le vôtre. +createOffer.resetToDefault=Non, revenir à la valeur par défaut +createOffer.useLowerValue=Oui, utiliser ma valeur la plus basse +createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l''écart max. du prix du marché autorisé\nL''écart maximum autorisé est {0} et peut être ajusté dans les préférences. +createOffer.changePrice=Modifier le prix +createOffer.tac=En plaçant cet ordre vous acceptez d'effectuer des transactions avec n'importe quel trader remplissant les conditions affichées à l'écran. +createOffer.currencyForFee=Frais de transaction +createOffer.setDeposit=Etablir le dépôt de garantie de l'acheteur (%) +createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'acheteur (%) +createOffer.setDepositForBothTraders=Établissez le dépôt de sécurité des deux traders (%) +createOffer.securityDepositInfo=Le dépôt de garantie de votre acheteur sera de {0} +createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu''acheteur sera de {0} +createOffer.minSecurityDepositUsed=Le minimum de dépôt de garantie de l'acheteur est utilisé + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Entrez le montant en BTC +takeOffer.amountPriceBox.buy.amountDescription=Montant en BTC à vendre +takeOffer.amountPriceBox.sell.amountDescription=Montant de BTC à acheter +takeOffer.amountPriceBox.priceDescription=Prix par Bitcoin en {0} +takeOffer.amountPriceBox.amountRangeDescription=Fourchette du montant possible +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=Le montant que vous avez saisi dépasse le nombre maximum de décimales autorisées.\nLe montant a été défini à 4 décimales près. +takeOffer.validation.amountSmallerThanMinAmount=Le montant ne peut pas être plus petit que le montant minimum défini dans l'ordre. +takeOffer.validation.amountLargerThanOfferAmount=La saisie ne peut pas être plus grande que le montant défini dans l'ordre. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=La somme saisie va créer des dusts résultantes de la transaction pour le vendeur de BTC. +takeOffer.fundsBox.title=Provisionner votre trade +takeOffer.fundsBox.isOfferAvailable=Vérifiez si l'ordre est disponible... +takeOffer.fundsBox.tradeAmount=Montant à vendre +takeOffer.fundsBox.offerFee=Frais de transaction du trade +takeOffer.fundsBox.networkFee=Total des frais de minage +takeOffer.fundsBox.takeOfferSpinnerInfo=Take offer in progress ... +takeOffer.fundsBox.paymentLabel=Transaction Bisq avec l''ID {0} +takeOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) +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} + +# new entries +takeOffer.takeOfferButton=Vérifier: Accepter l''ordre de {0} Bitcoin +takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui-ci utilise un prix en pourcentage basé sur le prix du marché, mais il n'y a pas de prix de référence de disponible. +takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade +# suppress inspection "TrailingSpacesInProperty" +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 Bisq (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.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 +takeOffer.alreadyFunded.askCancel=Vous avez déjà provisionner cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille bisq local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nVoulez vous vraiment annuler? +takeOffer.failed.offerNotAvailable=La demande de prise d'ordre a échoué car l'ordre n'est plus disponible. Peut-être qu'un autre trader a accepté l'ordre entre-temps. +takeOffer.failed.offerTaken=Vous ne pouvez pas saisir cet ordre car elle a déjà été pris par un autre trader. +takeOffer.failed.offerRemoved=Vous ne pouvez pas saisir cet ordre car elle a été supprimée entre-temps. +takeOffer.failed.offererNotOnline=La demande de prise de l'ordre a échoué parce que le maker n'est plus en ligne. +takeOffer.failed.offererOffline=Vous ne pouvez pas saisir cet ordre car le maker n'est pas connecté. +takeOffer.warning.connectionToPeerLost=Vous avez perdu la connexion avec le maker.\nIl se peut qu'ils se soient déconnectés ou qu'ils aient interrompu la connexion avec vous en raison d'un trop grand nombre de connexions en cours.\n\nSi vous pouvez encore voir leur offre dans le livre des ordres, vous pouvez essayer d'accepter une nouvelle fois l'offre. + +takeOffer.error.noFundsLost=\n\nAucun fonds n'a quitté votre portefeuille pour le moment.\nVeuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre le problème. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\nnot valid\n +takeOffer.error.depositPublished=\n\nLa transaction du dépôt de garantie à déjà été publiée.\nVeuillez redémarrer l'application et vérifier votre connexion réseau pour voir si le problème peut être résolu.\nSi le problème persiste, merci de contacter les développeurs afin d'obtenir de l'aide. +takeOffer.error.payoutPublished=\n\nLe versement de la transaction à déjà été publiée.\nVeuillez redémarrer l'application et vérifier votre connexion réseau pour voir si le problème peut être résolu.\nSi le problème persiste, veuillez contacter les développeurs afin d'obtenir de l'aide. +takeOffer.tac=En acceptant cet ordre vous acceptez les conditions de transactions définies à l'écran. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Prix de déclenchement +openOffer.triggerPrice=Prix de déclenchement {0} +openOffer.triggered=Cette offre a été désactivée car le prix du marché a atteint votre prix de déclenchement\nVeuillez éditer votre offre pour définir un nouveau prix de déclenchement + +editOffer.setPrice=Définir le prix +editOffer.confirmEdit=Confirmation: Modification de l'ordre +editOffer.publishOffer=Publication de votre ordre. +editOffer.failed=Échec de la modification de l''ordre:\n{0} +editOffer.success=Votre ordre a été modifié avec succès. +editOffer.invalidDeposit=Le dépôt de garantie de l'acheteur ne respecte pas le cadre des contraintes définies par Bisq DAO et ne peut plus être modifié. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Mes ordres en cours +portfolio.tab.pendingTrades=Échanges en cours +portfolio.tab.history=Historique +portfolio.tab.failed=Échec +portfolio.tab.editOpenOffer=Éditer l'ordre + +portfolio.closedTrades.deviation.help=Pourcentage de déviation du prix par rapport au marché + +portfolio.pending.invalidTx=Il y'a un problème avec une transaction manquante ou invalide.\n\nVeuillez NE PAS envoyer le payement Fiat ou altcoin.\n\nOuvrez un ticket de support pour avoir l'aide d'un médiateur.\n\nMessage d'erreur: {0} + +portfolio.pending.step1.waitForConf=Attendre la confirmation de la blockchain +portfolio.pending.step2_buyer.startPayment=Initier le paiement +portfolio.pending.step2_seller.waitPaymentStarted=Patientez jusqu'à ce que le paiement soit commencé. +portfolio.pending.step3_buyer.waitPaymentArrived=Patientez jusqu'à la réception du paiement +portfolio.pending.step3_seller.confirmPaymentReceived=Confirmation de paiement reçu +portfolio.pending.step5.completed=Terminé + +portfolio.pending.step3_seller.autoConf.status.label=Statut de l'auto-confirmation +portfolio.pending.autoConf=Auto-confirmé +portfolio.pending.autoConf.blocks=Confirmations XMR : {0}/ Requises: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Clé de transaction réutilisée. Veuillez ouvrir une contestation +portfolio.pending.autoConf.state.confirmations=Confirmations XMR: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction pas encore vue dans le mem-pool +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=Pas d'ID de transaction valide / de clé de transaction +portfolio.pending.autoConf.state.filterDisabledFeature=Désactivé par les développeurs + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=La fonctionnalité d'auto-confirmation est désactivée. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Le montant du trade dépasse la limite de l'auto-confirmation +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Le pair a fourni des données invalides. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Le versement de la transaction a déjà été publié. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=La contestation a été ouverte. L'auto-confirmation est désactivée pour ce trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Requête de preuve de transaction lancée +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Résultats ayant succédé: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Preuve à tous les services réussies. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=Une erreur lors de la demande du service a eu lieu. L'auto-confirmation n'est pas possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Votre trade a atteint au moins une confirmation de la part de la blockchain.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: Quand vous effectuez le paiement, laissez le champ \"raison du paiement\" vide. NE METTEZ PAS l'ID du trade ou n'importe quel autre texte, par exemple 'bitcoin', 'BTC' ou 'Bisq'. Vous êtez autorisés à discuter via le chat des trader si un autre \"raison du paiement\" est préférable pour vous deux. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees=Si votre banque vous facture des frais pour effectuer le transfert, vous êtes responsable de payer ces frais. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.altcoin=Veuillez transférer à partir de votre portefeuille externe {0}.\n{1} au vendeur de BTC.\n\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Veuillez vous rendre dans une banque et payer {0} au vendeur de BTC.\n +portfolio.pending.step2_buyer.cash.extra=CONDITIONS REQUISES: \nAprès avoir effectué le paiement veuillez écrire sur le reçu papier : PAS DE REMBOURSEMENT.\nPuis déchirer le en 2, prenez en une photo et envoyer le à l'adresse email du vendeur de BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Veuillez s''il vous plaît payer {0} au vendeur de BTC en utilisant MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d''autorisation et une photo du reçu par e-mail au vendeur de BTC.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Veuillez s''il vous plaît payer {0} au vendeur de BTC en utilisant Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de BTC.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Merci d''envoyer {0} par \"US Postal Money Order\" au vendeur de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Veuillez envoyer {0} en utlisant \"Cash by Mail\" au vendeur de BTC. 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 Cash by Mail, allez sur le wiki Bisq \n[LIEN:https://bisq.wiki/Cash_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 BTC. Vous trouverez les informations du compte du vendeur à l'écran suivant.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Veuillez s''il vous plaît contacter le vendeur de BTC via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Initier le paiement en utilisant {0} +portfolio.pending.step2_buyer.recipientsAccountData=Destinataires {0} +portfolio.pending.step2_buyer.amountToTransfer=Montant à transférer +portfolio.pending.step2_buyer.sellersAddress=Adresse {0} du vendeur +portfolio.pending.step2_buyer.buyerAccount=Votre compte de paiement à utiliser +portfolio.pending.step2_buyer.paymentStarted=Paiement initié +portfolio.pending.step2_buyer.fillInBsqWallet=Payer depuis le portefeuille BSQ +portfolio.pending.step2_buyer.warn=Vous n''avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l''échange doit être achevé avant {1}. +portfolio.pending.step2_buyer.openForDispute=Vous n'avez pas effectué votre paiement !\nLe délai maximal alloué pour l'échange est écoulé, veuillez contacter le médiateur pour obtenir de l'aide. +portfolio.pending.step2_buyer.paperReceipt.headline=Avez-vous envoyé le reçu papier au vendeur de BTC? +portfolio.pending.step2_buyer.paperReceipt.msg=Rappelez-vous: \nVous devez écrire sur le reçu papier: PAS DE REMBOURSEMENT.\nEnsuite, veuillez le déchirer en 2, faire une photo et l'envoyer à l'adresse email du vendeur. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Envoyer le numéro d'autorisation ainsi que le reçu +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d''autorisation et une photo du reçu par email au vendeur de BTC.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d''autorisation et le contrat au vendeur ? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Envoyer le MTCN et le reçu +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de BTC.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? +portfolio.pending.step2_buyer.halCashInfo.headline=Envoyer le code HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l''ID de la transaction ({0}) au vendeur de BTC.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Certaines banques pourraient vérifier le nom du receveur. Des comptes de paiement plus rapides créés dans des clients Bisq plus anciens ne fournissent pas le nom du receveur, veuillez donc utiliser le chat de trade pour l'obtenir (si nécessaire). +portfolio.pending.step2_buyer.confirmStart.headline=Confirmez que vous avez initié le paiement +portfolio.pending.step2_buyer.confirmStart.msg=Avez-vous initié le {0} paiement auprès de votre partenaire de trading? +portfolio.pending.step2_buyer.confirmStart.yes=Oui, j'ai initié le paiement +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Vous n'avez pas fourni de preuve de paiement +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Lorsque vous terminez une transaction BTC / XMR, vous pouvez utiliser la fonction de confirmation automatique pour vérifier si le montant correct de XMR a été envoyé à votre portefeuille, afin que Bisq puisse automatiquement marquer la transaction comme terminée et pour que tout le monde puisse aller plus vite. \n\nConfirmez automatiquement que les transactions XMR sont vérifiées sur au moins 2 nœuds d'explorateur de blocs XMR à l'aide de la clé de transaction fournie par l'expéditeur XMR. Par défaut, Bisq utilise un nœud d'explorateur de blocs exécuté par des contributeurs Bisq, mais nous vous recommandons d'exécuter votre propre nœud d'explorateur de blocs XMR pour maximiser la confidentialité et la sécurité. \n\nVous pouvez également définir le nombre maximum de BTC par transaction dans «Paramètres» pour confirmer automatiquement et le nombre de confirmations requises. \n\nPlus de détails sur Bisq Wiki (y compris comment configurer votre propre nœud d'explorateur de blocs): [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=La sasie n'est pas une valeur hexadécimale de 32 bits +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorer et continuer tout de même +portfolio.pending.step2_seller.waitPayment.headline=En attende du paiement +portfolio.pending.step2_seller.f2fInfo.headline=Coordonnées de l'acheteur +portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l''acheteur de BTC lance le {0} payment. +portfolio.pending.step2_seller.warn=L''acheteur de BTC n''a toujours pas effectué le paiement {0}.\nVeuillez attendre qu''il effectue celui-ci.\nSi la transaction n''est pas effectuée le {1}, un arbitre enquêtera. +portfolio.pending.step2_seller.openForDispute=L'acheteur de BTC n'a pas initié son paiement !\nLa période maximale autorisée pour ce trade est écoulée.\nVous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. +tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l''ID ''{0}'' +tradeChat.openChat=Ouvrir une fenêtre de discussion +tradeChat.rules=Vous pouvez communiquer avec votre pair de trading pour résoudre les problèmes potentiels liés à cet échange.\nIl n'est pas obligatoire de répondre sur le chat.\nSi un trader enfreint l'une des règles ci-dessous, ouvrez un litige et signalez-le au médiateur ou à l'arbitre.\n\nRègles sur le chat:\n\t● N'envoyez pas de liens (risque de malware). Vous pouvez envoyer l'ID de transaction et le nom d'un explorateur de blocs.\n\t● N'envoyez pas les mots de votre seed, clés privées, mots de passe ou autre information sensible !\n\t● N'encouragez pas le trading en dehors de Bisq (non sécurisé).\n\t● Ne vous engagez dans aucune forme d'escroquerie d'ingénierie sociale.\n\t● Si un pair ne répond pas et préfère ne pas communiquer par chat, respectez sa décision.\n\t● Limitez la portée de la conversation à l'échange en cours. Ce chat n'est pas une alternative à messenger ou une troll-box.\n\t● Entretenez une conversation amicale et respectueuse. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +message.state.SENT=Message envoyé +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Message reçu par le pair +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message de paiement bien envoyé mais pas encore reçu par le pair +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Le pair a confirmé la réception du message +# suppress inspection "UnusedProperty" +message.state.FAILED=Echec de l'envoi du message + +portfolio.pending.step3_buyer.wait.headline=Attendre la confirmation de paiement du vendeur BTC +portfolio.pending.step3_buyer.wait.info=En attente de la confirmation du vendeur BTC pour la réception du paiement {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=État du message de lancement du paiement +portfolio.pending.step3_buyer.warn.part1a=sur la {0} blockchain +portfolio.pending.step3_buyer.warn.part1b=chez votre prestataire de paiement (par ex. banque) +portfolio.pending.step3_buyer.warn.part2=Le vendeur de BTC n''a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l''envoi du paiement a bien fonctionné. +portfolio.pending.step3_buyer.openForDispute=Le vendeur de BTC n'a pas confirmé votre paiement ! Le délai maximal alloué pour ce trade est écoulé. Vous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu''il a initié le paiement {0}.\n +portfolio.pending.step3_seller.altcoin.explorer=Sur votre explorateur blockchain {0} favori +portfolio.pending.step3_seller.altcoin.wallet=Dans votre portefeuille {0} +portfolio.pending.step3_seller.altcoin={0}Veuillez s''il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l''écran principal après avoir fermé ce popup. +portfolio.pending.step3_seller.postal={0}Veuillez vérifier si vous avez reçu {1} avec \"US Postal Money Order\" de la part de l'acheteur de BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Veuillez vérifier si vous avez reçu {1} avec \"Cash by Mail\" de la part de l'acheteur de BTC +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Votre partenaire de trading a confirmé qu'il a initié le {0} paiement.\n\nVeuillez vous rendre sur votre banque en ligne et vérifier si vous avez reçu {1} de la part de l'acheteur de BTC. +portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l''acheteur de BTC doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n''êtes pas sûr, {0} +portfolio.pending.step3_seller.moneyGram=L'acheteur doit vous envoyer le numéro d'autorisation et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier que vous avez bien reçu par e-mail le numéro d'autorisation.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de BTC et l'adresse où retirer l'argent depuis MoneyGram.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! +portfolio.pending.step3_seller.westernUnion=L'acheteur doit vous envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier si vous avez reçu par e-mail le MTCN.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de BTC et l'adresse où retirer l'argent depuis Western Union.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! +portfolio.pending.step3_seller.halCash=L'acheteur doit vous envoyer le code HalCash par message texte SMS. Par ailleurs, vous recevrez un message de la part d'HalCash avec les informations nécessaires pour retirer les EUR depuis un DAB Bancaire supportant HalCash.\n\nAprès avoir retiré l'argent au DAB, veuillez confirmer ici la réception du paiement ! +portfolio.pending.step3_seller.amazonGiftCard=L'acheteur vous a envoyé une e-carte cadeau Amazon via email ou SMS vers votre téléphone. Veuillez récupérer maintenant la carte cadeau sur votre compte Amazon, et une fois activée, confirmez le reçu de paiement. + +portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, associé au contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmer la réception du paiement +portfolio.pending.step3_seller.amountToReceive=Montant à recevoir +portfolio.pending.step3_seller.yourAddress=Votre adresse {0} +portfolio.pending.step3_seller.buyersAddress=Adresse {0} des acheteurs +portfolio.pending.step3_seller.yourAccount=Votre compte de trading +portfolio.pending.step3_seller.xmrTxHash=ID de la transaction +portfolio.pending.step3_seller.xmrTxKey=Clé de Transaction +portfolio.pending.step3_seller.buyersAccount=Données du compte de l'acheteur +portfolio.pending.step3_seller.confirmReceipt=Confirmer la réception du paiement +portfolio.pending.step3_seller.buyerStartedPayment=L''acheteur BTC a commencé le {0} paiement.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Vérifiez la présence de confirmations par la blockchain dans votre portefeuille altcoin ou sur un explorateur de blocs et confirmez le paiement lorsque vous aurez suffisamment de confirmations sur la blockchain. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Vérifiez sur votre compte de trading (par ex. compte bancaire) et confirmez quand vous avez reçu le paiement. +portfolio.pending.step3_seller.warn.part1a=sur la {0} blockchain +portfolio.pending.step3_seller.warn.part1b=Auprès de votre prestataire de paiement (par ex. banque) +portfolio.pending.step3_seller.warn.part2=Vous n''avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. +portfolio.pending.step3_seller.openForDispute=Vous n'avez pas confirmé la réception du paiement !\nLe délai maximal alloué pour ce trade est écoulé.\nVeuillez confirmer ou demander l'aide du médiateur. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Avez-vous reçu le paiement {0} de votre partenaire de trading?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, avec le contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Veuillez noter que dès que vous aurez confirmé la réception, le montant verrouillé pour l'échange sera remis à l'acheteur de BTC et le dépôt de garantie vous sera remboursé.\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmez que vous avez bien reçu le paiement +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Oui, j'ai reçu le paiement +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT : En confirmant la réception du paiement, vous vérifiez également le compte de la contrepartie et le signez en conséquence. Comme le compte de la contrepartie n'a pas encore été signé, vous devriez retarder la confirmation du paiement le plus longtemps possible afin de réduire le risque de rétrofacturation. + +portfolio.pending.step5_buyer.groupTitle=Résumé de l'opération réalisée +portfolio.pending.step5_buyer.tradeFee=Frais de transaction +portfolio.pending.step5_buyer.makersMiningFee=Frais de minage +portfolio.pending.step5_buyer.takersMiningFee=Total des frais de minage +portfolio.pending.step5_buyer.refunded=Dépôt de garantie remboursé +portfolio.pending.step5_buyer.withdrawBTC=Retirer vos Bitcoins +portfolio.pending.step5_buyer.amount=Montant à retirer +portfolio.pending.step5_buyer.withdrawToAddress=Retirer vers l'adresse +portfolio.pending.step5_buyer.moveToBisqWallet=Garder les fonds dans le portefeuille Bisq +portfolio.pending.step5_buyer.withdrawExternal=Retrait vers un portefeuille externe +portfolio.pending.step5_buyer.alreadyWithdrawn=Vos fonds ont déjà été retirés. Merci de vérifier votre historique de transactions. +portfolio.pending.step5_buyer.confirmWithdrawal=Confirmer la demande de retrait +portfolio.pending.step5_buyer.amountTooLow=Le montant à transférer est inférieur aux frais de transaction et à la valeur min. possible du tx (dust). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retrait effectué +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vos transactions terminées sont stockées sous /"Historique du portefeuille\".\nVous pouvez voir toutes vos transactions en bitcoin dans \"Fonds/Transactions\" +portfolio.pending.step5_buyer.bought=Vous avez acheté +portfolio.pending.step5_buyer.paid=Vous avez payé + +portfolio.pending.step5_seller.sold=Vous avez vendu +portfolio.pending.step5_seller.received=Vous avez reçu + +tradeFeedbackWindow.title=Félicitations pour avoir achevé votre trade +tradeFeedbackWindow.msg.part1=Nous aimerions avoir de vos commentaires sur votre expérience. Cela nous aidera à améliorer le logiciel et à aplanir les aspérités. Si vous souhaitez nous faire part de vos commentaires, veuillez remplir ce court sondage (aucune inscription requise) à l'adresse suivante: +tradeFeedbackWindow.msg.part2=Si vous avez la moindre question, ou rencontrez un problème, veuillez s'il vous plaît vous mettre en relation avec les autres utilisateurs et contributeurs via le forum Bisq sur: +tradeFeedbackWindow.msg.part3=Merci d'utiliser Bisq! + +portfolio.pending.role=Mon rôle +portfolio.pending.tradeInformation=Information sur le trade +portfolio.pending.remainingTime=Temps restant +portfolio.pending.remainingTimeDetail={0} (jusqu'’à {1}) +portfolio.pending.tradePeriodInfo=Après la première confirmation de la blockchain, la période de trade commence. En fonction de la méthode de paiement utilisée, une période maximale allouée pour la transaction sera appliquée. +portfolio.pending.tradePeriodWarning=Si le délai est dépassé, l'es deux participants du trade peuvent ouvrir un litige. +portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu''à {0}) +portfolio.pending.tradeProcess=Processus de transaction +portfolio.pending.openAgainDispute.msg=Si vous n'êtes pas certain que le message addressé au médiateur ou à l'arbitre soit arrivé (par exemple si vous n'avez pas reçu de réponse dans un délai de 1 jour), n'hésitez pas à réouvrir un litige avec Cmd/ctrl+O. Vous pouvez aussi demander de l'aide en complément sur le forum bisq à [LIEN:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Ouvrir à nouveau le litige +portfolio.pending.openSupportTicket.headline=Ouvrir un ticket d'assistance +portfolio.pending.openSupportTicket.msg=S'il vous plaît n'utilisez seulement cette fonction qu'en cas d'urgence si vous ne pouvez pas voir le bouton \"Open support\" ou \"Ouvrir un litige\.\n\nLorsque vous ouvrez un ticket de support, l'échange sera interrompu et pris en charge par le médiateur ou par l'arbitre. + +portfolio.pending.timeLockNotOver=Vous devez patienter jusqu''au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. +portfolio.pending.error.depositTxNull=La transaction de dépôt est nulle. Vous ne pouvez pas ouvrir un litige sans une transaction de dépôt valide. Allez dans \"Paramètres/Info sur le réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Bisq est disponible sur Keybase. +portfolio.pending.mediationResult.error.depositTxNull=La transaction de dépôt est nulle. Vous pouvez déplacer le trade vers les trades n'ayant pas réussi. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=Le paiement de la transaction différée est nul. Vous pouvez déplacer le trade vers les trades échoués. +portfolio.pending.error.depositTxNotConfirmed=La transaction de dépôt n'est pas confirmée. Vous ne pouvez pas ouvrir un arbitrage pour le litige avec une transaction de dépôt non confirmée. Veuillez patienter jusqu'à ce qu'elle soit confirmée ou allez à \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Bisq est disponible sur Keybase. + +portfolio.pending.support.headline.getHelp=Besoin d'aide ? +portfolio.pending.support.text.getHelp=Si vous rencontrez des problèmes, vous pouvez essayer de contacter votre pair de trading dans le chat de l'échange ou demander à la communauté Bisq sur https://bisq.community. Si votre problème n'est toujours pas résolu, vous pouvez demander l'aide d'un médiateur. +portfolio.pending.support.button.getHelp=Ouvrir le chat de trade +portfolio.pending.support.headline.halfPeriodOver=Vérifier le paiement +portfolio.pending.support.headline.periodOver=Le délai alloué pour ce trade est écoulé. + +portfolio.pending.mediationRequested=Médiation demandée +portfolio.pending.refundRequested=Remboursement demandé +portfolio.pending.openSupport=Ouvrir un ticket d'assistance +portfolio.pending.supportTicketOpened=Ticket d'assistance ouvert +portfolio.pending.communicateWithArbitrator=Veuillez communiquer avec l'arbitre depuis l'écran "Support". +portfolio.pending.communicateWithMediator=Veuillez communiquer avec le médiateur dans l'onglet \"Support \". +portfolio.pending.disputeOpenedMyUser=Vous avez déjà ouvert un litige.\n{0} +portfolio.pending.disputeOpenedByPeer=Votre pair de trading à ouvert un litige\n{0} +portfolio.pending.noReceiverAddressDefined=Aucune adresse de destinataire définie + +portfolio.pending.mediationResult.headline=Montant suggéré par la médiation +portfolio.pending.mediationResult.info.noneAccepted=Terminez la transaction en acceptant la suggestion du médiateur concernant le paiement de la transaction. +portfolio.pending.mediationResult.info.selfAccepted=Vous avez accepté la suggestion du médiateur. En attente que le pair l'accepte également. +portfolio.pending.mediationResult.info.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur. L'acceptez-vous également ? +portfolio.pending.mediationResult.button=Voir la résolution proposée +portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l''ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur pour la transaction {0} +portfolio.pending.mediationResult.popup.info=Les frais recommandés par le médiateur sont les suivants: \nVous paierez: {0} \nVotre partenaire commercial paiera: {1} \n\nVous pouvez accepter ou refuser ces frais de médiation. \n\nEn acceptant, vous avez vérifié l'opération de paiement du contrat. Si votre partenaire commercial accepte et vérifie également, le paiement sera effectué et la transaction sera clôturée. \n\nSi l'un de vous ou les deux refusent la proposition, vous devrez attendre le {2} (bloc {3}) pour commencer le deuxième tour de discussion sur le différend avec l'arbitre, et ce dernier étudiera à nouveau le cas. Le paiement sera fait en fonction de ses résultats. \n\nL'arbitre peut facturer une somme modique (la limite supérieure des honoraires: la marge de la transaction) en compensation de son travail. Les deux commerçants conviennent que la suggestion du médiateur est une voie agréable. La demande d'arbitrage concerne des circonstances particulières, par exemple si un professionnel est convaincu que le médiateur n'a pas fait une recommandation de d'indemnisation équitable (ou si l'autre partenaire n'a pas répondu). \n\nPlus de détails sur le nouveau modèle d'arbitrage: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Vous avez accepté la proposition de paiement du médiateur, mais il semble que votre contrepartie ne l'ait pas acceptée. \n\nUne fois que le temps de verrouillage atteint {0} (bloc {1}), vous pouvez ouvrir le second tour de litige pour que l'arbitre réétudie le cas et prend une nouvelle décision de dépenses. \n\nVous pouvez trouver plus d'informations sur le modèle d'arbitrage sur:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Refuser et demander un arbitrage +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'altcoin au vendeur de BTC, 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 Bisq.\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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Déplacer le trade vers les trades échoués +portfolio.pending.failedTrade.warningIcon.tooltip=Cliquer pour avoir plus de détails à propos des problèmes ayant eu lieu lors de ce trade +portfolio.failed.revertToPending.popup=Voulez-vous déplacer ce trade vers les trades ouverts? +portfolio.failed.revertToPending=Déplacer le trade vers les trades ouverts + +portfolio.closed.completed=Terminé +portfolio.closed.ticketClosed=Arbitré +portfolio.closed.mediationTicketClosed=Ayant fait l'objet d'une médiation +portfolio.closed.canceled=Annulé +portfolio.failed.Failed=Échec +portfolio.failed.unfail=Avant de procéder, veuillez vous assurer que vous avez une sauvegarde de votre répertoire de données!\nVoulez-vous redéplacer de trade vers les trades ouverts?\nC'est une manière de déverrouiller les fonds coincés dans un trade échoué. +portfolio.failed.cantUnfail=Ce trade ne peut être redéplacé vers les trades ouverts pour l'instant.\nVeuillez réessayer après la complétion du/des trade(s) {0} +portfolio.failed.depositTxNull=Le trade ne peut être reconverti en trade ouvert. La transaction de dépôt est nulle. +portfolio.failed.delayedPayoutTxNull=Le trade ne peut être reconverti en trade ouvert. La transaction de paiement différée est nulle. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Recevoir des fonds +funds.tab.withdrawal=Envoyer des fonds +funds.tab.reserved=Fonds reservés +funds.tab.locked=Fonds vérouillés +funds.tab.transactions=Transactions + +funds.deposit.unused=Inutilisé +funds.deposit.usedInTx=Utilisé dans {0} transaction(s) +funds.deposit.fundBisqWallet=Alimenter le portefeuille Bisq +funds.deposit.noAddresses=Aucune adresse de dépôt n'a encore été générée +funds.deposit.fundWallet=Alimenter votre portefeuille +funds.deposit.withdrawFromWallet=Transférer des fonds depuis le portefeuille +funds.deposit.amount=Montant en BTC (optionnel) +funds.deposit.generateAddress=Générer une nouvelle adresse +funds.deposit.generateAddressSegwit=Format segwit natif (Bech32) +funds.deposit.selectUnused=Merci de sélectionner une adresse inutilisée dans le champ ci-dessus plutôt que d'en générer une nouvelle. + +funds.withdrawal.arbitrationFee=Frais d'arbitrage +funds.withdrawal.inputs=Sélection de la valeur à saisir +funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles +funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée +funds.withdrawal.receiverAmount=Montant du destinataire +funds.withdrawal.senderAmount=Montant de l'expéditeur +funds.withdrawal.feeExcluded=Montant excluant les frais de minage +funds.withdrawal.feeIncluded=Montant incluant frais de minage +funds.withdrawal.fromLabel=Retirer depuis l'adresse +funds.withdrawal.toLabel=Retirer vers l'adresse +funds.withdrawal.memoLabel=Résumé du retrait +funds.withdrawal.memo=Optionnellement, complétez le mémo +funds.withdrawal.withdrawButton=Retrait selectionné +funds.withdrawal.noFundsAvailable=Aucun fonds n'est disponible pour le retrait +funds.withdrawal.confirmWithdrawalRequest=Confirmer la requête de retrait +funds.withdrawal.withdrawMultipleAddresses=Retrait depuis plusieurs adresses ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Retrait depuis plusieurs adresses:\n{0} +funds.withdrawal.notEnoughFunds=Vous ne disposez pas de suffisamment de fonds dans votre portefeuille. +funds.withdrawal.selectAddress=Sélectionnez une adresse source depuis le champ +funds.withdrawal.setAmount=Définir le montant à retirer +funds.withdrawal.fillDestAddress=Complétez votre adresse de destination +funds.withdrawal.warn.noSourceAddressSelected=Vous devez sélectionner une adresse source dans le champ ci-dessus. +funds.withdrawal.warn.amountExceeds=Vous ne disposez pas de fonds suffisants provenant de l'adresse sélectionnée.\nEnvisagez de sélectionner plusieurs adresses dans le champ ci-dessus ou changez les frais pour inclure les frais du mineur. + +funds.reserved.noFunds=Aucun fonds n'est réservé pour les ordres en cours +funds.reserved.reserved=Réversé dans votre portefeuille local pour l''ordre avec l''ID: {0} + +funds.locked.noFunds=Aucun fonds n'est verrouillé dans les trades +funds.locked.locked=Vérouillé en multisig pour le trade avec l''ID: {0} + +funds.tx.direction.sentTo=Envoyer à: +funds.tx.direction.receivedWith=Reçu depuis: +funds.tx.direction.genesisTx=Depuis le tx Genesis: +funds.tx.txFeePaymentForBsqTx=Frais de minage du tx BSQ +funds.tx.createOfferFee=Frais du maker et du tx: {0} +funds.tx.takeOfferFee=Frais du taker et du tx: {0} +funds.tx.multiSigDeposit=Dépôt multisig: {0} +funds.tx.multiSigPayout=Versement Multisig: {0} +funds.tx.disputePayout=Versement du litige: {0} +funds.tx.disputeLost=Cas de litige perdu: {0} +funds.tx.collateralForRefund=Remboursement du dépôt de garantie: {0} +funds.tx.timeLockedPayoutTx=Tx de paiement verrouillée dans le temps: {0} +funds.tx.refund=Remboursement venant de l''arbitrage: {0} +funds.tx.unknown=Raison inconnue: {0} +funds.tx.noFundsFromDispute=Aucun remboursement en cas de litige +funds.tx.receivedFunds=Fonds reçus +funds.tx.withdrawnFromWallet=Retiré depuis le portefeuille +funds.tx.withdrawnFromBSQWallet=BTC retiré depuis le portefeuille BSQ +funds.tx.memo=Résumé +funds.tx.noTxAvailable=Pas de transactions disponibles +funds.tx.revert=Revertir +funds.tx.txSent=Transaction envoyée avec succès vers une nouvelle adresse dans le portefeuille local bisq. +funds.tx.direction.self=Envoyé à vous même +funds.tx.daoTxFee=Frais de mineur de la tx BSQ +funds.tx.reimbursementRequestTxFee=Demande de remboursement +funds.tx.compensationRequestTxFee=Requête de compensation +funds.tx.dustAttackTx=dust reçues +funds.tx.dustAttackTx.popup=Cette transaction va envoyer un faible montant en BTC sur votre portefeuille ce qui pourrait constituer une tentative d'espionnage de la part de sociétés qui analyse la chaine.\n\nSi vous utilisez cette transaction de sortie des données dans le cadre d'une transaction représentant une dépense il sera alors possible de comprendre que vous êtes probablement aussi le propriétaire de l'autre adresse (coin merge).\n\nAfin de protéger votre vie privée, le portefeuille Bisq ne tient pas compte de ces "dust outputs" dans le cadre des transactions de vente et dans l'affichage de la balance. Vous pouvez définir une quantité seuil lorsqu'une "output" est considérée comme poussière dans les réglages. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Médiation +support.tab.arbitration.support=Arbitrage +support.tab.legacyArbitration.support=Conclusion d'arbitrage +support.tab.ArbitratorsSupportTickets=Tickets de {0} +support.filter=Chercher les litiges +support.filter.prompt=Saisissez l'ID du trade, la date, l'adresse "onion" ou les données du compte. + +support.sigCheck.button=Vérifier la signature +support.sigCheck.popup.info=Dans le cas d'une demande de remboursement au DAO vous devez copier-coller le message résumant la médiation et le processus de jugement dans votre demande de remboursement sur Github. Pour que cette information soit vérifiable, n'importe quel utilisateur peut vérifier avec cet outil si la signature du médiateur ou de l'arbitre correspond à celle du résumé. +support.sigCheck.popup.header=Vérifier la signature du résultat du litige +support.sigCheck.popup.msg.label=Message de résumé +support.sigCheck.popup.msg.prompt=Copiez et collez le message résumant le litige +support.sigCheck.popup.result=Résultat de la validation +support.sigCheck.popup.success=La signature est valide +support.sigCheck.popup.failed=Vérification de la signature échouée +support.sigCheck.popup.invalidFormat=Le message n'est pas au format attendu. Copiez et collez le message résumant la dispute. + +support.reOpenByTrader.prompt=Êtes-vous sûr de vouloir réouvrir le litige? +support.reOpenButton.label=Réouvrir +support.sendNotificationButton.label=Notification privée +support.reportButton.label=Effectuer un rapport +support.fullReportButton.label=Tous les litiges +support.noTickets=Il n'y a pas de tickets ouverts +support.sendingMessage=Envoi du message... +support.receiverNotOnline=Le destinataire n'est pas en ligne. Le message est enregistré dans leur boîte mail. +support.sendMessageError=Échec de l''envoi du message. Erreur: {0} +support.receiverNotKnown=Destinataire inconnu +support.wrongVersion=L''ordre relatif au litige en question a été créé avec une ancienne version de Bisq.\nVous ne pouvez pas clore ce litige avec votre version de l''application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} +support.openFile=Ouvrir le fichier à joindre (taille max. du fichier : {0} kb) +support.attachmentTooLarge=La taille totale de vos pièces jointes est de {0} ko ce qui dépasse la taille maximale autorisée de {1} ko pour les messages. +support.maxSize=La taille maximale autorisée pour le fichier est {0} kB. +support.attachment=Pièces jointes +support.tooManyAttachments=Vous ne pouvez envoyer plus de 3 pièces jointes dans un message. +support.save=Sauvegarder le fichier sur le disque +support.messages=Messages +support.input.prompt=Entrer le message... +support.send=Envoyer +support.addAttachments=Ajouter des pièces jointes +support.closeTicket=Fermer le ticket +support.attachments=Pièces jointes: +support.savedInMailbox=Message sauvegardé dans la boîte mail du destinataire +support.arrived=Message reçu par le destinataire +support.acknowledged=Réception du message confirmée par le destinataire +support.error=Le destinataire n''a pas pu traiter le message. Erreur : {0} +support.buyerAddress=Adresse de l'acheteur BTC +support.sellerAddress=Adresse du vendeur BTC +support.role=Rôle +support.agent=Agent d'assistance +support.state=État +support.chat=Chat +support.closed=Fermé +support.open=Ouvert +support.process=Processus +support.buyerOfferer=Acheteur BTC/Maker +support.sellerOfferer=Vendeur BTC/Maker +support.buyerTaker=Acheteur BTC/Taker +support.sellerTaker=Vendeur BTC/Taker + +support.backgroundInfo=Bisq n'est pas une entreprise, donc elle traite les litiges différemment.\n\nLes traders peuvent communiquer au sein de l'application via un chat sécurisé sur l'écran des transactions ouvertes pour essayer de résoudre les litiges par eux-mêmes. Si cela ne suffit pas, un médiateur peut intervenir pour les aider. Le médiateur évaluera la situation et suggérera un paiement des fonds de transaction. Si les deux traders acceptent cette suggestion, la transaction de paiement est réalisée et l'échange est clos. Si un ou les deux traders n'acceptent pas le paiement suggéré par le médiateur, ils peuvent demander un arbitrage. L'arbitre réévaluera la situation et, si cela est justifié, remboursera personnellement le négociateur et demandera le remboursement de ce paiement à la DAO Bisq. +support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d''informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l''acheteur BTC : Avez-vous effectué le paiement Fiat ou Altcoin ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l''application ?\n● Si vous êtes le vendeur BTC : Avez-vous reçu le paiement Fiat ou Altcoin ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l''application ?\n● Quelle version de Bisq utilisez-vous ?\n● Quel système d''exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu''ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l''accord d''utilisation lorsque vous avez lancé l''application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l''adresse suivante {2} +support.systemMsg=Message du système: {0} +support.youOpenedTicket=Vous avez ouvert une demande de support.\n\n{0}\n\nBisq version: {1} +support.youOpenedDispute=Vous avez ouvert une demande de litige.\n\n{0}\n\nBisq version: {1} +support.youOpenedDisputeForMediation=Vous avez demandé une médiation.\n\n{0}\n\nVersion de Bisq: {1} +support.peerOpenedTicket=Votre pair de trading a demandé une assistance en raison de problèmes techniques.\n\n{0}\n\nVersion de Bisq: {1} +support.peerOpenedDispute=Votre pair de trading a fait une demande de litige.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Votre pair de trading a demandé une médiation.\n\n{0}\n\nVersion de Bisq: {1} +support.mediatorsDisputeSummary=Message système: Résumé de la dispute du médiateur:\n{0} +support.mediatorsAddress=Adresse du nœud du médiateur: {0} +support.warning.disputesWithInvalidDonationAddress=La transaction de paiement différé a été utilisée pour une adresse de destinataire indisponible. Il ne correspond aux paramètres dans aucun DAO de l'adresse de donation valide. \n\nCela peut être une escroquerie. Veuillez informer le développeur et ne fermez pas le dossier jusqu'à ce que le problème est résolu! \n\nAdresse pour les litiges: {0} \n\nAdresse de donation dans tous les paramètres DAO: {1} \n\nTransaction: {2} {3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nVoulez-vous toujours fermer le litige? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nVous ne devez pas effectuer le paiement. +support.warning.traderCloseOwnDisputeWarning=Les traders peuvent uniquement fermer eux-même les tickets d'assistance quand le trade a été payé. +support.info.disputeReOpened=Le ticket de litige a été réouvert. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Préférences +settings.tab.network=Info sur le réseau +settings.tab.about=À propos + +setting.preferences.general=Préférences générales +setting.preferences.explorer=Exploreur Bitcoin +setting.preferences.explorer.bsq=Exploreur Bisq +setting.preferences.deviation=Ecart maximal par rapport au prix du marché +setting.preferences.bsqAverageTrimThreshold=Seuil de valeur trop élévé pour le BSQ +setting.preferences.avoidStandbyMode=Éviter le mode veille +setting.preferences.autoConfirmXMR=Auto-confirmation XMR +setting.preferences.autoConfirmEnabled=Activé +setting.preferences.autoConfirmRequiredConfirmations=Confirmations requises +setting.preferences.autoConfirmMaxTradeSize=Montant maximum du trade (BTC) +setting.preferences.autoConfirmServiceAddresses=URLs de l'explorateur de Monero (utilise Tor, à part pour l'hôte local, les addresses IP locales, et les noms de domaine en *.local) +setting.preferences.deviationToLarge=Les valeurs supérieures à {0}% ne sont pas autorisées. +setting.preferences.txFee=Frais de transaction du retrait (satoshis/vbyte) +setting.preferences.useCustomValue=Utiliser une valeur personnalisée +setting.preferences.txFeeMin=Les frais de transaction doivent être d'au moins {0} satoshis/vBit +setting.preferences.txFeeTooLarge=Votre sasie est au-delà de toute valeur raisonnable (plus de 5000 satoshis/vBit). Les frais de transaction sont habituellement de l'ordre de 50-400 satoshis/vBit. +setting.preferences.ignorePeers=Pairs ignorés [adresse onion:port] +setting.preferences.ignoreDustThreshold=Valeur de l'output considérée comme "non-dust" minimale +setting.preferences.currenciesInList=Devises disponibles dans le flux de cotation du marché +setting.preferences.prefCurrency=Devise privilégiée +setting.preferences.displayFiat=Afficher les monnaies nationales +setting.preferences.noFiat=Il n'y a pas de devise nationale sélectionnée +setting.preferences.cannotRemovePrefCurrency=Vous ne pouvez pas enlever la devise choisie pour l'affichage. +setting.preferences.displayAltcoins=Afficher les altcoins +setting.preferences.noAltcoins=Il n'y a pas d'altcoins sélectionnés +setting.preferences.addFiat=Ajouter une devise nationale +setting.preferences.addAltcoin=Ajouter un altcoin +setting.preferences.displayOptions=Afficher les options +setting.preferences.showOwnOffers=Montrer mes ordres dans le livre des ordres +setting.preferences.useAnimations=Utiliser des animations +setting.preferences.useDarkMode=Utiliser le mode sombre +setting.preferences.sortWithNumOffers=Trier les listes de marché avec le nombre d'ordres/de transactions +setting.preferences.onlyShowPaymentMethodsFromAccount=Masquer les méthodes de paiement non supportées +setting.preferences.denyApiTaker=Refuser les preneurs utilisant l'API +setting.preferences.notifyOnPreRelease=Recevoir les notifications de pré-sortie +setting.preferences.resetAllFlags=Réinitialiser toutes les balises de notification \"Don't show again\" +settings.preferences.languageChange=Un redémarrage est nécessaire pour appliquer le changement de langue à tous les écrans. +settings.preferences.supportLanguageWarning=En cas de litige, veuillez noter que la médiation est traitée en {0} et l'arbitrage en {1}. +setting.preferences.daoOptions=Options DAO +setting.preferences.dao.resyncFromGenesis.label=Reconstituer l'état de la DAO à partir du tx genesis +setting.preferences.dao.resyncFromResources.label=Reconstruire l'état du DAO à partir des ressources +setting.preferences.dao.resyncFromResources.popup=Après un redémarrage de l'application les données de gouvernance du réseau Bisq seront rechargées à partir des noeuds sources et l'état du consensus BSQ sera reconstruit à partir des derniers fichiers de ressources. +setting.preferences.dao.resyncFromGenesis.popup=La synchronisation à partir de la transaction d'origine consomme beaucoup de temps et de ressources CPU. Êtes-vous sûr de vouloir resynchroniser ? En général, la resynchronisation à partir du dernier fichier de ressources est suffisante et plus rapide. \n\nAprès le redémarrage de l'application, les données de gestion du réseau Bisq seront rechargées à partir du nœud d'amorçage et l'état de synchronisation BSQ sera reconstruit à partir de la transaction initiale. +setting.preferences.dao.resyncFromGenesis.resync=Resynchroniser depuis Genesis et fermer +setting.preferences.dao.isDaoFullNode=Exécuter la DAO de Bisq en tant que full node +setting.preferences.dao.rpcUser=Nom d'utilisateur RPC +setting.preferences.dao.rpcPw=Mot de passe RPC +setting.preferences.dao.blockNotifyPort=Bloquer le port de notification +setting.preferences.dao.fullNodeInfo=Pour exécuter la DAO de Bisq en tant que full node, vous devez avoir Bitcoin Core en exécution locale et avec le RPC activé. Toutes les recommandations sont indiquées dans ''{0}''.\n\nAprès avoir changé de mode, vous serez contraint de redémarrer.. +setting.preferences.dao.fullNodeInfo.ok=Ouvrir la page des docs +setting.preferences.dao.fullNodeInfo.cancel=Non, je m'en tiens au mode lite node +settings.preferences.editCustomExplorer.headline=Paramètres de l'explorateur +settings.preferences.editCustomExplorer.description=Choisissez un explorateur défini par le système depuis la liste à gauche, et/où customisez-le pour satisfaire vos préférences. +settings.preferences.editCustomExplorer.available=Explorateurs disponibles +settings.preferences.editCustomExplorer.chosen=Paramètres choisis pour l'explorateur +settings.preferences.editCustomExplorer.name=Nom +settings.preferences.editCustomExplorer.txUrl=URL de la transaction +settings.preferences.editCustomExplorer.addressUrl=Addresse URL + +settings.net.btcHeader=Réseau Bitcoin +settings.net.p2pHeader=Le réseau Bisq +settings.net.onionAddressLabel=Mon adresse onion +settings.net.btcNodesLabel=Utiliser des nœuds Bitcoin Core personnalisés +settings.net.bitcoinPeersLabel=Pairs connectés +settings.net.useTorForBtcJLabel=Utiliser Tor pour le réseau Bitcoin +settings.net.bitcoinNodesLabel=Nœuds Bitcoin Core pour se connecter à +settings.net.useProvidedNodesRadio=Utiliser les nœuds Bitcoin Core fournis +settings.net.usePublicNodesRadio=Utiliser le réseau Bitcoin public +settings.net.useCustomNodesRadio=Utiliser des nœuds Bitcoin Core personnalisés +settings.net.warn.usePublicNodes=Si vous utilisez le réseau public Bitcoin, vous serez confronté à de sérieux problèmes de confidentialité. Ceci est dû à la conception et à la mise en œuvre du bloom filter cassé. Il convient aux portefeuilles SPV comme BitcoinJ (utilisé dans Bisq). Tout nœud complet que vous connectez peut découvrir que toutes les adresses de votre portefeuille appartiennent à une seule entité. \n\nPour plus d'informations, veuillez visiter: [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare] \n\nÊtes-vous sûr de vouloir utiliser un nœud public? +settings.net.warn.usePublicNodes.useProvided=Non, utiliser les nœuds fournis. +settings.net.warn.usePublicNodes.usePublic=Oui, utiliser un réseau public +settings.net.warn.useCustomNodes.B2XWarning=Veuillez vous assurer que votre nœud Bitcoin est un nœud Bitcoin Core de confiance !\n\nLa connexion à des nœuds qui ne respectent pas les règles du consensus de Bitcoin Core peut corrompre votre portefeuille et causer des problèmes dans le processus de trading.\n\nLes utilisateurs qui se connectent à des nœuds qui ne respectent pas les règles du consensus sont responsables des dommages qui en résultent. Tout litige qui en résulte sera tranché en faveur de l'autre pair. Aucune assistance technique ne sera apportée aux utilisateurs qui ignorent ces mécanismes d'alertes et de protections ! +settings.net.warn.invalidBtcConfig=La connection au réseau Bitcoin a échoué car votre configuration est invalide.\n\nVotre configuration a été réinitialisée afin d'utiliser les noeuds Bitcoin fournis à la place. Vous allez avoir besoin de relancer l'application. +settings.net.localhostBtcNodeInfo=Information additionnelle : Bisq cherche un noeud Bitcoin local au démarrage. Si il est trouvé, Bisq communiquera avec le réseau Bitcoin uniquement à travers ce noeud. +settings.net.p2PPeersLabel=Pairs connectés +settings.net.onionAddressColumn=Adresse onion +settings.net.creationDateColumn=Établi +settings.net.connectionTypeColumn=In/Out +settings.net.sentDataLabel=Statistiques des données envoyées +settings.net.receivedDataLabel=Statistiques des données reçues +settings.net.chainHeightLabel=Hauteur du dernier block BTC +settings.net.roundTripTimeColumn=Roundtrip +settings.net.sentBytesColumn=Envoyé +settings.net.receivedBytesColumn=Reçu +settings.net.peerTypeColumn=Type de pair +settings.net.openTorSettingsButton=Ouvrez les paramètres de Tor + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Sous-version +settings.net.heightColumn=Hauteur + +settings.net.needRestart=Vous devez redémarrer l'application pour appliquer cette modification.\nVous voulez faire cela maintenant? +settings.net.notKnownYet=Pas encore connu... +settings.net.sentData=Données envoyées: {0}, {1} messages, {2} messages/seconde +settings.net.receivedData=Données reçues: {0}, {1} messages, {2} messages/seconde +settings.net.chainHeight=Hauteur de la chaîne DAO de Bisq: {0} | Hauteur de la chaîne des pairs Bitcoin: {1} +settings.net.ips=[IP address:port | host name:port | onion address:port] (séparés par des virgules). Le port peut être ignoré si utilisé par défaut (8333). +settings.net.seedNode=Seed node +settings.net.directPeer=Pair (direct) +settings.net.initialDataExchange={0}[Amorçage] +settings.net.peer=Pair +settings.net.inbound=inbound +settings.net.outbound=outbound +settings.net.reSyncSPVChainLabel=Resynchronisation de la chaîne SPV +settings.net.reSyncSPVChainButton=Supprimer le fichier SPV et resynchroniser +settings.net.reSyncSPVSuccess=Êtes-vous sûr de vouloir effectuer une resynchronisation SPV? Si vous procédez, les fichiers de la chaîne SPV seront supprimés au prochain démarrage.\n\nAprès le redémarrage il est possible que l'opération de resynchronisation avec le réseau prenne un peu de temps, vous verrez toutes les transactions uniquement à la fin de la resynchronisation.\n\nLa durée de la resynchronisation dépend du nombre de transaction et de l'âge de votre portefeuille, elle peut durer plusieurs heures et utiliser 100% des ressources du processeur. N'interrompez pas le processus sinon vous devrez le recommencer. +settings.net.reSyncSPVAfterRestart=Le fichier de la chaîne SPV a été supprimé. Veuillez s'il vous plaît patienter. La resynchronisation avec le réseau peut nécessiter un certain temps. +settings.net.reSyncSPVAfterRestartCompleted=La resynchronisation est maintenant terminée. Veuillez redémarrer l'application. +settings.net.reSyncSPVFailed=Impossible de supprimer le fichier de la chaîne SPV.\nErreur: {0} +setting.about.aboutBisq=À propos de Bisq +setting.about.about=Bisq est un logiciel libre qui facilite l'échange de Bitcoins avec les devises nationales (et d'autres cryptomonnaies) au moyen d'un réseau pair-to-pair décentralisé, de manière à protéger au mieux la vie privée des utilisateurs. Pour en savoir plus sur Bisq, consultez la page Web du projet. +setting.about.web=Page web de Bisq +setting.about.code=Code source +setting.about.agpl=Licence AGPL +setting.about.support=Soutenir Bisq +setting.about.def=Bisq n'est pas une entreprise, c'est un projet ouvert vers la communauté. Si vous souhaitez participer ou soutenir Bisq, veuillez suivre les liens ci-dessous. +setting.about.contribute=Contribuer +setting.about.providers=Fournisseurs de données +setting.about.apisWithFee=Bisq utilise les indices de prix Bisq pour les prix des marchés Fiat et Altcoin, et Bisq utilise les noeuds du Mempool pour estimer les frais de minage. +setting.about.apis=Bisq utilise les indices de prix Bisq pour les prix des marchés Fiat et Altcoin. +setting.about.pricesProvided=Prix de marché fourni par +setting.about.feeEstimation.label=Estimation des frais de minage fournie par +setting.about.versionDetails=Détails sur la version +setting.about.version=Version de l'application +setting.about.subsystems.label=Versions des sous-systèmes +setting.about.subsystems.val=Version du réseau: {0}; version des messages P2P: {1}; Version DB Locale: {2}; Version du protocole de trading: {3} + +setting.about.shortcuts=Raccourcis +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Naviguer dans le menu principal +setting.about.shortcuts.menuNav.value=Pour naviguer dans le menu principal, appuyez sur: 'Ctrl' ou 'alt' ou 'cmd' avec une touche numérique entre '1-9'. + +setting.about.shortcuts.close=Fermer Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Fermer le popup ou la fenêtre de dialogue +setting.about.shortcuts.closePopup.value=Touche 'ECHAP' + +setting.about.shortcuts.chatSendMsg=Envoyer un message chat au trader +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTRÉE'' ou ''alt + ENTREE'' ou ''cmd + ENTRÉE'' + +setting.about.shortcuts.openDispute=Ouvrir un litige +setting.about.shortcuts.openDispute.value=Sélectionnez l''échange en cours et cliquez sur: {0} + +setting.about.shortcuts.walletDetails=Ouvrir la fenêtre avec les détails sur le portefeuille + +setting.about.shortcuts.openEmergencyBtcWalletTool=Ouvrir l'outil de portefeuille d'urgence pour BTC + +setting.about.shortcuts.openEmergencyBsqWalletTool=Ouvrir l'outil de portefeuille d'urgence pour BSQ + +setting.about.shortcuts.showTorLogs=Basculer le niveau de log pour les messages Tor entre DEBUG et WARN + +setting.about.shortcuts.manualPayoutTxWindow=Ouvrir la fenêtre pour le paiement manuel à partir du tx de dépôt Multisig 2of2 + +setting.about.shortcuts.reRepublishAllGovernanceData=Publier à nouveau les données sur la gouvernance de la DAO (propositions, votes) + +setting.about.shortcuts.removeStuckTrade=Ouvrez la popup pour déplacer ce trade échoué vers l'onglet des trades ouverts. +setting.about.shortcuts.removeStuckTrade.value=Sélectionnez l'échange échoué et appuyez sur: {0} + +setting.about.shortcuts.registerArbitrator=Inscrire l'arbitre (médiateur/arbitre seulement) +setting.about.shortcuts.registerArbitrator.value=Naviguez jusqu'au compte et appuyez sur: {0} + +setting.about.shortcuts.registerMediator=Inscrire le médiateur (médiateur/arbitre seulement) +setting.about.shortcuts.registerMediator.value=Naviguez jusqu'au compte et appuyez sur: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Ouvrir la fenêtre pour la signature de l'âge du compte (anciens arbitres seulement) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l''ancienne vue de l''arbitre et appuyer sur: {0} + +setting.about.shortcuts.sendAlertMsg=Envoyer un message d'alerte ou de mise à jour (activité privilégiée) + +setting.about.shortcuts.sendFilter=Définir le filtre (activité privilégiée) + +setting.about.shortcuts.sendPrivateNotification=Envoyer une notification privée à un pair (activité privilégiée) +setting.about.shortcuts.sendPrivateNotification.value=Ouvrez l'information du pair via l'avatar et appuyez sur: {0} + +setting.info.headline=Nouvelle fonctionnalité, l'auto-confirmation XMR +setting.info.msg=Vous n'avez pas saisi l'ID et la clé de transaction. \n\nSi vous ne fournissez pas ces données, votre partenaire commercial ne peut pas utiliser la fonction de confirmation automatique pour libérer rapidement le BTC après avoir reçu le XMR.\nEn outre, Bisq demande aux expéditeurs XMR de fournir ces informations aux médiateurs et aux arbitres en cas de litige.\nPlus de détails sont dans Bisq Wiki: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Enregistrement du médiateur +account.tab.refundAgentRegistration=Enregistrement de l'agent de remboursement +account.tab.signing=Signature en cours +account.info.headline=Bienvenue sur votre compte Bisq +account.info.msg=Ici, vous pouvez ajouter des comptes de trading en devises nationales et en altcoins et créer une sauvegarde de votre portefeuille ainsi que des données de votre compte.\n\nUn nouveau portefeuille Bitcoin a été créé un premier lancement de Bisq.\n\nNous vous recommandons vivement d'écrire les mots-clés de votre seed de portefeuille Bitcoin (voir l'onglet en haut) et d'envisager d'ajouter un mot de passe avant le transfert de fonds. Les dépôts et retraits de Bitcoin sont gérés dans la section \"Fonds\".\n\nNotice de confidentialité et de sécurité : Bisq étant une plateforme d'échange décentralisée, toutes vos données sont conservées sur votre ordinateur. Il n'y a pas de serveurs, nous n'avons donc pas accès à vos informations personnelles, à vos fonds ou même à votre adresse IP. Les données telles que les numéros de compte bancaire, les adresses altcoin & Bitcoin, etc ne sont partagées avec votre pair de trading que pour effectuer les transactions que vous initiez (en cas de litige, le médiateur et l’arbitre verront les mêmes données que votre pair de trading). + +account.menu.paymentAccount=Comptes en devise nationale +account.menu.altCoinsAccountView=Compte Altcoins +account.menu.password=Mot de passe du portefeuille +account.menu.seedWords=Seed du portefeuille +account.menu.walletInfo=Information du portefeuille +account.menu.backup=Sauvegarde +account.menu.notifications=Notifications + +account.menu.walletInfo.balance.headLine=Solde du portefeuille +account.menu.walletInfo.balance.info=Ceci montre le solde du portefeuille interne en incluant les transactions non-confirmées.\nPour le BTC, le solde du portefeuille interne affiché ci-dessous devrait correspondre à la somme des soldes 'Disponibles' et 'Réservés' affichés en haut à droite de cette fenêtre. +account.menu.walletInfo.xpub.headLine=Afficher les clés (clés xpub) +account.menu.walletInfo.walletSelector={0} {1} portefeuille +account.menu.walletInfo.path.headLine=Chemin du trousseau HD +account.menu.walletInfo.path.info=Si vous importez vos mots de graine dans un autre portefeuille (comme Electrum), vous aurez besoin de définir le chemin. Ceci devrait être effectué uniquement en cas d'urgence quand vous perdez accès au portefeuille Bisq et au répertoire de données.\nGardez à l'esprit que dépenser des fonds depuis un portefeuille autre que Bisq peut dérégler les structures de données internes de Bisq associées au données du portefeuille, ce qui peut mener à des trades échoués.\n\nN'envoyez JAMAIS de BSQ depuis un portefeuille autre que Bisq, cela va probablement conduire à une transaction BSQ invalide, vous faisant ainsi perdre votre BSQ. + +account.menu.walletInfo.openDetails=Afficher les détails bruts du portefeuille et les clés privées + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Clé publique + +account.arbitratorRegistration.register=S'inscrire +account.arbitratorRegistration.registration={0} Enregistrement +account.arbitratorRegistration.revoke=Révoquer +account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d''échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu''à 7 jours. +account.arbitratorRegistration.warn.min1Language=Vous devez définir au moins 1 langue.\nNous avons ajouté la langue par défaut pour vous. +account.arbitratorRegistration.removedSuccess=Vous avez supprimé votre inscription au réseau Bisq avec succès. +account.arbitratorRegistration.removedFailed=Impossible de supprimer l''enregistrement.{0} +account.arbitratorRegistration.registerSuccess=Vous vous êtes inscrit au réseau Bisq avec succès. +account.arbitratorRegistration.registerFailed=Impossible de terminer l''enregistrement.{0} + +account.altcoin.yourAltcoinAccounts=Vos comptes altcoin +account.altcoin.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l''utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL''utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d''un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l''arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. +account.altcoin.popup.wallet.confirm=Je comprends et confirme que je sais quel portefeuille je dois utiliser. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Pour échanger UPX sur Bisq, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer UPX, vous devez utiliser le portefeuille officiel UPXmA GUI ou le portefeuille UPXmA CLI avec le logo store-tx-info activé (valeur par défaut dans la nouvelle version) . Assurez-vous d'avoir accès à la clé tx, car elle est nécessaire dans l'état du litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nCes transactions ne sont pas vérifiables dans le navigateur blockchain ordinaire. \n\nEn cas de litige, vous devez fournir à l'arbitre les informations suivantes: \n\n- Clé privée Tx- hachage de transaction- adresse publique du destinataire \n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur UPX est responsable de fournir la vérification du transfert UPX à l'arbitre. \n\nAucun paiement d'identité n'est requis, juste une adresse publique commune. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le canal UPXmA Discord (https://discord.gg/vhdNSrV) ou le groupe d'échanges Telegram (https://t.me/uplexaOfficial) pour plus d'informations. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Le trading d'ARQ sur Bisq exige que vous compreniez et remplissiez les exigences suivantes:\n\nPour envoyer des ARQ, vous devez utiliser soit le portefeuille officiel ArQmA GUI soit le portefeuille ArQmA CLI avec le flag store-tx-info activé (par défaut dans les nouvelles versions). Veuillez vous assurer que vous pouvez accéder à la tx key car cela pourrait être nécessaire en cas de litige.\narqma-wallet-cli (utiliser la commande get_tx_key)\narqma-wallet-gui (allez dans l'onglet historique et cliquez sur le bouton (P) pour accéder à la preuve de paiement).\n\nAvec un l'explorateur de bloc normal, le transfert n'est pas vérifiable.\n\nVous devez fournir au médiateur ou à l'arbitre les données suivantes en cas de litige:\n- Le tx de la clé privée\n- Le hash de la transaction\n- L'adresse publique du destinataire\n\nSi vous manquez de communiquer les données ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. L'expéditeur des ARQ est responsable de la transmission au médiateur ou à l'arbitre de la vérification du transfert ces informations relatives au litige.\n\nIl n'est pas nécessaire de fournir l'ID du paiement, seulement l'adresse publique normale.\nSi vous n'êtes pas sûr de ce processus, visitez le canal discord ArQmA (https://discord.gg/s9BQpJT) ou le forum ArQmA (https://labs.arqma.com) pour obtenir plus d'informations. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Pour échanger XMR sur Bisq, vous devez comprendre et respecter les exigences suivantes: \n\nSi vous vendez XMR, en cas de litige, vous devez fournir au médiateur ou à l'arbitre les informations suivantes: - clé de transaction (clé publique Tx, clé Tx, clé privée Tx) - ID de transaction (ID Tx Ou hachage Tx) - Adresse de destination de la transaction (adresse du destinataire) \n\nConsultez plus d'informations sur le portefeuille Monero dans le wiki: https: //bisq.wiki/Trading_Monero#Proving_payments \n\nSi vous ne fournissez pas les données de transaction requises, vous serez directement jugé échoue dans le litige. \n\nNotez également que Bisq fournit désormais la fonction de confirmation automatique des transactions XMR pour effectuer plus rapidement des transactions, mais vous devez l'activer dans les paramètres. \n\nPour plus d'informations sur la fonction de confirmation automatique, veuillez consulter le Wiki: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Le navigateur blockchain pour échanger MSR sur Bisq vous oblige à comprendre et à respecter les exigences suivantes: \n\nLors de l'envoi de MSR, vous devez utiliser le portefeuille officiel Masari GUI, le portefeuille Masari CLI avec le logo store-tx-info activé (activé par défaut) ou le portefeuille web Masari (https://wallet.getmasari.org). Assurez-vous d'avoir accès à la clé tx, car cela est nécessaire en cas de litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nLe portefeuille web Masari (accédez à Compte-> Historique des transactions et vérifiez les détails de la transaction que vous avez envoyés) \n\nLa vérification peut être effectuée dans le portefeuille. monero-wallet-cli: utilisez la commande (check_tx_key). monero-wallet-gui: sur la page Avancé> Preuve / Vérification La vérification peut être effectuée dans le navigateur blockchain. Ouvrez le navigateur blockchain (https://explorer.getmasari.org) et utilisez la barre de recherche pour trouver votre hachage de transaction. Une fois que vous avez trouvé la transaction, faites défiler jusqu'à la zone «certificat à envoyer» en bas et remplissez les détails requis. En cas de litige, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: - Clé privée Tx- Hachage de transaction- Adresse publique du destinataire \n\nAucun ID de transaction n'est requis, seule une adresse publique normale est requise. Si vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur XMR est responsable de fournir la vérification du transfert XMR au médiateur ou un arbitre. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le Masari Discord officiel (https://discord.gg/sMCwMqs) pour obtenir de l'aide. +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=ntes: \n\nPour envoyer des informations anonymes, vous devez utiliser un portefeuille CLI ou GUI de réseau anonyme. Si vous utilisez un portefeuille CLI, le hachage de la transaction (tx ID) sera affiché après la transmission. Vous devez enregistrer ces informations. Après l'envoi de la transmission, vous devez immédiatement utiliser la commande «get_tx_key» pour récupérer la clé privée de la transaction. Si vous ne parvenez pas à effectuer cette étape, vous ne pourrez peut-être pas récupérer la clé ultérieurement. \n\nSi vous utilisez le portefeuille Blur Network GUI, vous pouvez facilement trouver la clé privée de transaction et l'ID de transaction dans l'onglet «Historique». Localisez la transaction d'intérêt immédiatement après l'envoi. Cliquez sur le symbole «?» dans le coin inférieur droit de la boîte contenant la transaction. Vous devez enregistrer ces informations. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1.) ID de transaction, 2.) clé privée de transaction, 3.) adresse du destinataire. Le processus de médiation ou d'arbitrage utilisera le visualiseur de transactions BLUR (https://blur.cash/#tx-viewer) pour vérifier les transferts BLUR. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte du litige. Dans tous les litiges, l'expéditeur anonyme porte à 100% la responsabilité de vérifier la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Bisq. Tout d'abord, demandez de l'aide dans Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Echanger Solo sur Bisq nécessite que vous compreniez et remplissiez les conditions suivantes: \n\nPour envoyer Solo, vous devez utiliser la version 5.1.3 ou supérieure du portefeuille Web Solo CLI. \n\nSi vous utilisez un portefeuille CLI, après l'envoi de la transaction, ID de transaction sera affiché. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez immédiatement utiliser la commande «get_tx_key» pour récupérer la clé de transaction. Si vous ne parvenez pas à effectuer cette étape, vous ne pourrez peut-être pas récupérer la clé ultérieurement. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs Solo (https://explorer.Solo.org) pour rechercher des transactions puis utilisera la fonction «envoyer une preuve» (https://explorer.minesolo.com/). \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de QWC assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Bisq. Tout d'abord, demandez de l'aide dans Solo Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Pour échanger CASH2 sur Bisq, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer CASH2, vous devez utiliser la version 3 ou supérieure du portefeuille CASH2. \n\nAprès l'envoi de la transaction, ID de la transaction s'affiche. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez utiliser la commande «getTxKey» dans simplewallet pour récupérer immédiatement la clé de transaction.\n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse CASH2 du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs CASH2 (https://blocks.cash2.org) pour vérifier le transfert CASH2. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de CASH2 assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Bisq. Tout d'abord, demandez de l'aide dans le Discord Cash2 (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Pour échanger Qwertycoin sur Bisq, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer Qwertycoin, vous devez utiliser la version 5.1.3 ou supérieure du portefeuille Qwertycoin. \n\nAprès l'envoi de la transaction, ID de la transaction s'affiche. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez utiliser la commande «get_Tx_Key» dans simplewallet pour récupérer immédiatement la clé de transaction. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse QWC du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs QWC (https://explorer.qwertycoin.org) pour vérifier les transferts QWC. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de QWC assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Bisq. Tout d'abord, demandez de l'aide dans QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Echanger Dragonglass sur Bisq vous oblige à comprendre et à respecter les exigences suivantes: ~\n\nComme Dragonglass offre une protection de la confidentialité, les transactions ne peuvent pas être vérifiées sur la blockchain publique. Si nécessaire, vous pouvez prouver votre paiement en utilisant votre TXN-Private-Key. TXN-Private est une clé d'un temps générée automatiquement, utilisée pour chaque transaction qui est accessible uniquement à partir du portefeuille DESP. Soit via DRGL-wallet GUI (boîte de dialogue des détails de transaction interne), soit via Dragonglass CLI simplewallet (en utilisant la commande "get_tx_key"). \n\nLes deux nécessitent la version DRGL de «Oathkeeper» ou supérieure. \n\nEn cas de litige, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: \n\n- txn-Privite-ket- hachage de transaction- adresse publique du destinataire ~\n\nLa vérification du paiement peut utiliser les données ci-dessus comme entrée (http://drgl.info/#check_txn).\n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. L'expéditeur Dragonglass est responsable de fournir la vérification de transfert DRGL au médiateur ou à l'arbitre en cas de litige. Aucun ID de paiement n'est requis. \n\nSi vous n'êtes pas sûr d'une partie de ce processus, veuillez visiter Dragonglass sur (http://discord.drgl.info) pour obtenir de l'aide. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Lors de l'utilisation de Zcash, vous ne pouvez utiliser que les adresses transparentes (commençant par t), et non les z-adresses (privées), car le médiateur ou l'arbitre ne seraient pas en mesure de vérifier la transaction avec les z-adresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Lors de l'utilisation de Zcoin, vous ne pouvez utiliser que les adresses transparentes (traçables), et non les adresses intraçables, car le médiateur ou l'arbitre ne seraient pas en mesure de vérifier la transaction avec des adresses intraçables dans un explorateur de blocs. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN nécessite un échange interactif entre l'émetteur et le récepteur pour créer la transaction. Assurez-vous de suivre les instructions de la page Web du projet GRIN pour envoyer et recevoir des GRIN de façon fiable (le récepteur doit être en ligne au moins pendant un certain temps).\n\nBisq ne supporte que le portefeuille Grinbox (Wallet713) format URL.\n\nL'expéditeur des GRIN doit fournir la preuve qu'il a envoyé les GRIN avec succès. Si le portefeuille ne peut pas fournir cette preuve, un litige potentiel sera résolu en faveur du destinataire des GRIN. Veuillez vous assurer que vous utilisez le dernier logiciel Grinbox qui supporte la preuve de transaction et que vous comprenez le processus de transfert et de réception des GRIN ainsi que la façon de créer la preuve.\n\nVisitez https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only pour plus d'informations sur l'outil de preuve de Grinbox. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM nécessite un processus interactif entre l'émetteur et le récepteur pour créer la transaction.\n\nAssurez-vous de suivre les instructions de la page Web du projet BEAM pour envoyer et recevoir les BEAM de façon fiable (le récepteur doit être en ligne pendant au moins un certain temps).\n\nL'expéditeur de BEAM est tenu de fournir la preuve qu'il a envoyé BEAM avec succès. Assurez-vous d'utiliser un portefeuille qui peut produire une telle preuve. Si le portefeuille ne peut fournir la preuve, un litige potentiel sera résolu en faveur du récepteur des BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Echanger ParsiCoin sur Bisq nécessite que vous compreniez et remplissiez les conditions suivantes: \n\nPour envoyer PARS, vous devez utiliser la version 3.0.0 ou supérieure du portefeuille ParsiCoin officiel. \n\nVous pouvez vérifier votre hachage de transaction et votre clé de transaction dans la section transaction du portefeuille GUI (ParsiPay). Vous devez cliquer avec le bouton droit de la souris sur «Transaction» puis cliquer sur «Afficher les détails». \n\nSi l'arbitrage est à 100% nécessaire, vous devez fournir au médiateur ou à l'arbitre les éléments suivants: 1) hachage de transaction, 2) clé de transaction et 3) adresse PARS du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs ParsiCoin (http://explorer.parsicoin.net/#check_payment) pour vérifier les transmissions PARS. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Bisq. Tout d'abord, demandez de l'aide sur le ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=Pour échanger les monnaies brûlées, vous devez savoir ce qui suit: \n\nLes monnaies brûlées ne peuvent pas être dépensée. Pour les échanger sur Bisq, le script de sortie doit prendre la forme suivante: OP_RETURN OP_PUSHDATA, suivi des octets de données pertinents, ces octets forment l'adresse après le codage hexadécimal. Par exemple, une devise brûlée avec l'adresse 666f6f ("foo" en UTF-8) aura le script suivant: \n\nOP_RETURN OP_PUSHDATA 666f6f \n\nPour créer de la monnaie brûlée, vous pouvez utiliser la commande RPC «brûler», disponible dans certains portefeuilles. \n\nPour d'éventuelles situations, vous pouvez vérifier https://ibo.laboratorium.ee \n\nPuisque la monnaie brûlée ne peut pas être utilisée, elle ne peut pas être revendue. «Vendre» une devise brûlée signifie brûler la devise d'origine (données associées à l'adresse de destination). \n\nEn cas de litige, le vendeur BLK doit fournir le hachage de la transaction. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Pour échanger L-BTC sur Bisq, vous devez comprendre les termes suivants: \n\nLorsque vous acceptez des transactions L-BTC sur Bisq, vous ne pouvez pas utiliser Blockstream Green Wallet sur le téléphone mobile ou un portefeuille de dépôt / commercial. Vous ne devez recevoir du L-BTC que dans le portefeuille Liquid Elements Core ou un autre portefeuille L-BTC avec une adresse L-BTC et une clé de sécurité qui vous permettre d'être anonyme. \n\nEn cas de médiation ou en cas de litige de transaction, vous devez divulguer la clé de sécurité de l'adresse L-BTC au médiateur Bisq ou à l'agent de remboursement afin qu'ils puissent vérifier les détails de votre transaction anonyme sur leur propre nœud complet Elements Core. \n\nSi vous ne comprenez pas ou ne comprenez pas ces exigences, n'échangez pas de L-BTC sur Bisq. + +account.fiat.yourFiatAccounts=Vos comptes en devise nationale + +account.backup.title=Sauvegarder le portefeuille +account.backup.location=Emplacement de la sauvegarde +account.backup.selectLocation=Sélectionner l'emplacement de sauvegarde +account.backup.backupNow=Sauvegarder maintenant (la sauvegarde n'est pas cryptée !) +account.backup.appDir=Répertoire des données de l'application +account.backup.openDirectory=Ouvrir le répertoire +account.backup.openLogFile=Ouvrir le fichier de log +account.backup.success=Sauvegarder réussite vers l''emplacement:\n{0} +account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n''est pas accessible. {0} + +account.password.removePw.button=Supprimer le mot de passe +account.password.removePw.headline=Supprimer la protection par mot de passe du portefeuille +account.password.setPw.button=Définir un mot de passe +account.password.setPw.headline=Définir le mot de passe de protection pour le portefeuille +account.password.info=Avec la protection par mot de passe, vous devrez entrer votre mot de passe au démarrage de l'application, lors d'un retrait en Bitcoin depuis votre portefeuille et lors de la restauration de votre portefeuille à partir des mots qui composent la seed. + +account.seed.backup.title=Sauvegarder les mots composant la seed de votre portefeuille +account.seed.info=Veuillez noter les mots de la seed du portefeuille ainsi que la date! Vous pouvez récupérer votre portefeuille à tout moment avec les mots de la seed et la date.\nLes mêmes mots-clés de la seed sont utilisés pour les portefeuilles BTC et BSQ.\n\nVous devriez écrire les mots de la seed sur une feuille de papier. Ne les enregistrez pas sur votre ordinateur.\n\nVeuillez noter que les mots de la seed ne remplacent PAS une sauvegarde.\nVous devez créer une sauvegarde de l'intégralité du répertoire de l'application à partir de l'écran \"Compte/Sauvergarde\" pour restaurer correctement les données de l'application.\nL'importation de mots de la seed n'est recommandée qu'en cas d'urgence. L'application ne sera pas fonctionnelle sans une sauvegarde adéquate des fichiers et des clés de la base de données ! +account.seed.backup.warning=Veuillez noter que les mots de départ ne peuvent pas remplacer les sauvegardes. Vous devez sauvegarder tout le répertoire de l'application (dans l'onglet «Compte / Sauvegarde») pour restaurer l'état et les données de l'application. L'importation de mots de départ n'est recommandée qu'en cas d'urgence. Si le fichier de base de données et la clé ne sont pas correctement sauvegardés, l'application ne fonctionnera pas! \n\nVoir plus d'informations sur le wiki Bisq: [HYPERLINK:https://bisq.wiki/Backing_up_application_data] +account.seed.warn.noPw.msg=Vous n'avez pas configuré un mot de passe de portefeuille qui protégerait l'affichage des mots composant la seed.\n\nVoulez-vous afficher les mots composant la seed? +account.seed.warn.noPw.yes=Oui, et ne me le demander plus à l'avenir +account.seed.enterPw=Entrer le mot de passe afficher les mots composant la seed +account.seed.restore.info=Veuillez effectuer une sauvegarde avant de procéder à une restauration à partir du mot de passe. Sachez que la restauration d'un portefeuille n'est a faire qu'en cas d'urgence et qu'elle peut causer des problèmes avec la base de données interne du portefeuille.\nCe n'est pas une façon de faire une sauvegarde ! Veuillez utiliser une sauvegarde à partir du répertoire de données de l'application pour restaurer l'état antérieur de l'application.\n\nAprès la restauration, l'application s'arrêtera automatiquement. Après le redémarrage de l'application, elle sera resynchronisée avec le réseau Bitcoin. Cela peut prendre un certain temps et peut consommer beaucoup de puissance sur le CPU, surtout si le portefeuille était plus vieux et contient beaucoup de transactions. Veuillez éviter d'interrompre ce processus, sinon vous devrez peut-être supprimer à nouveau le fichier de chaîne SPV ou répéter le processus de restauration. +account.seed.restore.ok=Ok, effectuer la restauration et arrêter Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Configurer +account.notifications.download.label=Télécharger l'application mobile +account.notifications.waitingForWebCam=En attente de la webcam.... +account.notifications.webCamWindow.headline=Scanner le code QR depuis le téléphone +account.notifications.webcam.label=Utiliser la webcam +account.notifications.webcam.button=Scanner le QR code +account.notifications.noWebcam.button=Je ne possède pas de webcam +account.notifications.erase.label=Effacer les notifications sur le téléphone +account.notifications.erase.title=Effacer les notifications +account.notifications.email.label=Pairing token +account.notifications.email.prompt=Entrez le Pairing token que vous avez reçu par mail +account.notifications.settings.title=Paramètres +account.notifications.useSound.label=Activer l'alerte de notification sur le téléphone +account.notifications.trade.label=Recevoir des messages pour le trade +account.notifications.market.label=Recevoir des alertes sur les ordres +account.notifications.price.label=Recevoir des alertes de prix +account.notifications.priceAlert.title=Alertes de prix +account.notifications.priceAlert.high.label=Me prévenir si le prix du BTC est supérieur à +account.notifications.priceAlert.low.label=Me prévenir si le prix du BTC est inférieur à +account.notifications.priceAlert.setButton=Définir l'alerte de prix +account.notifications.priceAlert.removeButton=Retirer l'alerte de prix +account.notifications.trade.message.title=L'état du trade a été modifié. +account.notifications.trade.message.msg.conf=La transaction de dépôt pour l''échange avec ID {0} est confirmée. Veuillez ouvrir votre application Bisq et initier le paiement. +account.notifications.trade.message.msg.started=L''acheteur de BTC a initié le paiement pour la transaction avec ID {0}. +account.notifications.trade.message.msg.completed=La transaction avec l''ID {0} est terminée. +account.notifications.offer.message.title=Votre ordre a été accepté +account.notifications.offer.message.msg=Votre ordre avec l''ID {0} a été accepté +account.notifications.dispute.message.title=Nouveau message de litige +account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l''ID {0} + +account.notifications.marketAlert.title=Alertes sur les ordres +account.notifications.marketAlert.selectPaymentAccount=Ordres correspondants au compte de paiement +account.notifications.marketAlert.offerType.label=Type d'ordre qui m'intéresse +account.notifications.marketAlert.offerType.buy=Ordres d'achat (je veux vendre des BTC) +account.notifications.marketAlert.offerType.sell=Ordres de vente (je veux acheter des BTC) +account.notifications.marketAlert.trigger=Écart par rapport au prix de l'ordre (%) +account.notifications.marketAlert.trigger.info=Avec la définition d'une distance par rapport au prix, vous ne recevrez une alerte que lorsqu'un odre qui répondra (ou dépassera) vos exigences sera publié. Exemple : vous voulez vendre des BTC, mais vous ne vendrez qu'avec une prime de 2% par rapport au prix actuel du marché. En réglant ce champ à 2%, vous ne recevrez que des alertes pour les ordres dont les prix sont de 2% (ou plus) au dessus du prix actuel du marché. +account.notifications.marketAlert.trigger.prompt=Écart en pourcentage par rapport au prix du marché (par ex. 2,50 %, -0,50 %, etc.) +account.notifications.marketAlert.addButton=Ajouter une alerte pour les ordres +account.notifications.marketAlert.manageAlertsButton=Gérer les alertes des ordres +account.notifications.marketAlert.manageAlerts.title=Gérer les alertes pour les ordres +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Compte de paiement +account.notifications.marketAlert.manageAlerts.header.trigger=Prix de déclenchement +account.notifications.marketAlert.manageAlerts.header.offerType=Type d'ordre +account.notifications.marketAlert.message.title=Alerte d'ordre +account.notifications.marketAlert.message.msg.below=en dessous de +account.notifications.marketAlert.message.msg.above=au dessus de +account.notifications.marketAlert.message.msg=Un nouvel ordre ''{0} {1}''' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement ''{5}'' a été publiée dans le livre des ordres de Bisq.\nID de l''ordre: {6}. +account.notifications.priceAlert.message.title=Alerte de prix pour {0} +account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l''actuel {0} le prix est {1}. {2} +account.notifications.noWebCamFound.warning=Aucune webcam n'a été trouvée.\n\nUtilisez l'option mail pour envoyer le jeton et la clé de cryptage depuis votre téléphone portable vers l'application Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=Le prix le plus élevé doit être supérieur au prix le plus bas. +account.notifications.priceAlert.warning.lowerPriceTooHigh=Le prix le plus bas doit être inférieur au prix le plus élevé. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Faits et chiffres +dao.tab.bsqWallet=Portefeuille BSQ +dao.tab.proposals=Gouvernance +dao.tab.bonding=Bonding +dao.tab.proofOfBurn=Frais d'inscription des actifs/Preuve du burn +dao.tab.monitor=Contrôleur réseau +dao.tab.news=Actualités + +dao.paidWithBsq=payé en BSQ +dao.availableBsqBalance=Disponible à dépenser (vérifiées + sorties non confirmées) +dao.verifiedBsqBalance=Balance de toutes les UTXOs vérifiées +dao.unconfirmedChangeBalance=Solde de toute les transactions de sorties non confirmées +dao.unverifiedBsqBalance=Solde de toutes les transactions non vérifiées (en attente de confirmation du bloc) +dao.lockedForVoteBalance=Utilisé pour le vote +dao.lockedInBonds=Verrouillé en bonds +dao.availableNonBsqBalance=Solde disponible non-BSQ (BTC) +dao.reputationBalance=Score de mérite (non dépensable) + +dao.tx.published.success=Votre transaction a été publiée avec succès. +dao.proposal.menuItem.make=Faire une proposition +dao.proposal.menuItem.browse=Parcourir les demandes en cours +dao.proposal.menuItem.vote=Vote pour les propositions +dao.proposal.menuItem.result=Résultats des votes +dao.cycle.headline=Cycle de vote +dao.cycle.overview.headline=Aperçu du cycle de vote +dao.cycle.currentPhase=Phase actuelle +dao.cycle.currentBlockHeight=Hauteur actuelle de bloc +dao.cycle.proposal=Phase de proposition +dao.cycle.proposal.next=Prochaine étape de proposition +dao.cycle.blindVote=Phase de vote caché +dao.cycle.voteReveal=Phase de dévoilement du vote +dao.cycle.voteResult=Résultat du vote +dao.cycle.phaseDuration={0} blocs (≈{1}); Blocs {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Bloc {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Transaction du dévoilement du vote publiée. +dao.voteReveal.txPublished=Votre transaction de dévoilement du vote avec l''ID de transaction {0} a été publiée avec succès.\n\nCeci se produit automatiquement avec le logiciel si vous avez participé au processus de vote de la DAO. + +dao.results.cycles.header=Cycles +dao.results.cycles.table.header.cycle=Cycle +dao.results.cycles.table.header.numProposals=Propositions +dao.results.cycles.table.header.voteWeight=Poids du vote +dao.results.cycles.table.header.issuance=Émission + +dao.results.results.table.item.cycle=Cycle {0} commencé: {1} + +dao.results.proposals.header=Proposition relative au cycle sélectionné +dao.results.proposals.table.header.nameLink=Nom/lien +dao.results.proposals.table.header.details=Détails +dao.results.proposals.table.header.myVote=Mon vote +dao.results.proposals.table.header.result=Résultat du vote +dao.results.proposals.table.header.threshold=Seuil +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Résultats des votes pour les propositions sélectionnées + +dao.results.exceptions=Exception(s) au résultat du vote + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Indéfini + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQ maker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQ taker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Min. BSQ maker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Min. BSQ taker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTC maker fee +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTC taker fee +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Min. BTC maker fee +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Min. BTC taker fee +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Frais de l'ordre en BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Frais de vote en BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Demande de compensation min. Montant BSQ +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Demande de compensation max. Montant BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Demande de remboursement min. Montant BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Demande de remboursement max. Montant BSQ + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Quorum requis en BSQ pour une proposition standard +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Quorum requis dans BSQ pour une demande d'indemnisation +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Quorum requis dans BSQ pour une demande de remboursement +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Quorum requis dans BSQ pour modifier un paramètre +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Quorum requis dans BSQ pour retirer un actif +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Quorum requis dans BSQ pour une demande de confiscation +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Quorum requis dans BSQ pour les demandes de rôle en bond + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Seuil requis en % pour une proposition standard +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Seuil requis en % pour une demande d'indemnisation +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Seuil requis en % pour une demande de remboursement +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Seuil requis en % pour une modification de paramètre +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Seuil requis en % pour une suppression d'un actif +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Seuil requis en % pour une demande de confiscation +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Seuil requis en % pour les demandes de rôle en bond + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Adresse de réception BTC + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Coût journalier du listing des actifs +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Volume minimal d'échanges pour les actifs + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Temps de verrouillage du tx de versement alternative du trade +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Frais d'arbitrage en BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Montant d'échange max. en BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Bonded role unit factor en BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Limite d'émission par cycle en BSQ + +dao.param.currentValue=Valeur actuelle: {0} +dao.param.currentAndPastValue=Valeur actuelle: {0} (Valeur au moment de l''offre: {1}) +dao.param.blocks={0} blocs + +dao.results.invalidVotes=Il est fait état de votes invalides au cours de ce cycle de vote. Cela peut arriver si un vote n''a pas été bien distribué sur le réseau Bisq.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Phase de proposition +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Interruption 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Phase de vote caché +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Interruption 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Phase de dévoilement du vote +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Interruption 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Phase du résultat + +dao.results.votes.table.header.stakeAndMerit=Poids du vote +dao.results.votes.table.header.stake=Mise +dao.results.votes.table.header.merit=Gagné +dao.results.votes.table.header.vote=Vote + +dao.bond.menuItem.bondedRoles=Bonded roles +dao.bond.menuItem.reputation=Bonded reputation +dao.bond.menuItem.bonds=Bonds + +dao.bond.dashboard.bondsHeadline=Bonded BSQ +dao.bond.dashboard.lockupAmount=Immobiliser les fonds +dao.bond.dashboard.unlockingAmount=Déverrouiller des fonds (veuillez attendre que la période de verrouillage soit terminée). + + +dao.bond.reputation.header=Verrouiller un bond pour la réputation +dao.bond.reputation.table.header=Mes bonds de réputation +dao.bond.reputation.amount=Quantité de BSQ à bloquer +dao.bond.reputation.time=Délai de déverrouillage en blocs +dao.bond.reputation.salt=Salage +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Vérrouillage +dao.bond.reputation.lockup.headline=Confirmer la transaction de verrouillage. +dao.bond.reputation.lockup.details=Montant verrouillé : {0}\nTemps de déverrouillage: {1} block(s) (environ {2})\n\nFrais de minage: {3} ({4} Satoshis/byte)\nTaille virtuelle de la transaction: {5} vKb\n\nÊtes-vous certain de vouloir procéder? +dao.bond.reputation.unlock.headline=Confirmer le déblocage de la transaction +dao.bond.reputation.unlock.details=Montant du déverrouillage: {0}\nTemps de déverrouillage: {1} block(s) (environ {2})\n\nFrais de minage : {3} ({4} Satoshis/vbyte)\nTaille virtuelle de la transaction: {5} vKb\n\nÊtes-vous certain de vouloir procéder ? + +dao.bond.allBonds.header=Tous les bonds + +dao.bond.bondedReputation=Bonded Reputation +dao.bond.bondedRoles=Bonded roles + +dao.bond.details.header=Détails du rôle +dao.bond.details.role=Rôle +dao.bond.details.requiredBond=BSQ requis pour le bond +dao.bond.details.unlockTime=Délai de déverrouillage en blocs +dao.bond.details.link=Lien vers la description des rôles +dao.bond.details.isSingleton=Peut être utilisé par détenteurs de plusieurs rôles +dao.bond.details.blocks={0} blocs + +dao.bond.table.column.name=Nom +dao.bond.table.column.link=Lien +dao.bond.table.column.bondType=Type de Bond +dao.bond.table.column.details=Détails +dao.bond.table.column.lockupTxId=Verrouiller le Tx de l'ID +dao.bond.table.column.bondState=État du bond +dao.bond.table.column.lockTime=Temps de déverrouillage +dao.bond.table.column.lockupDate=Date de verrouillage + +dao.bond.table.button.lockup=Vérrouillage +dao.bond.table.button.unlock=Déverrouiller +dao.bond.table.button.revoke=Révoquer + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Pas encore bonded +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Vérrouillage en attente +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Bond verrouillé +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=En attente de dévérrouillage +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Déverrouiller le tx confirmé +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Déblocage du Bond +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Bond déverrouillé +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Bond confisqué + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Bonded role +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Bonded reputation + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Admin GitHub +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Admin du Forum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Admin Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Administrateur de Keybase +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Admin YouTube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Mainteneur Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Maintainer BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Mainteneur Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Opérateur du site Web +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Opérateur du Forum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Opérateur du nœud de la seed +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Opérateur du prix du noeud +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Opérateur du noeud Bitcoin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Opérateur de marchés +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Opérateur de l'explorateur +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Opérateur relais pour les notifications mobile +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Titulaire du nom de domaine +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Médiateur +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Arbitre +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Propriétaire de l'adresse BTC de donation + +dao.burnBsq.assetFee=Listing des actifs +dao.burnBsq.menuItem.assetFee=Frais d'inscription des actifs +dao.burnBsq.menuItem.proofOfBurn=Preuve du burn +dao.burnBsq.header=Frais pour l'inscription des actifs +dao.burnBsq.selectAsset=Sélectionner un actif +dao.burnBsq.fee=Frais +dao.burnBsq.trialPeriod=Période d'essai +dao.burnBsq.payFee=Payer les frais +dao.burnBsq.allAssets=Tous les actifs +dao.burnBsq.assets.nameAndCode=Nom de l'actif +dao.burnBsq.assets.state=État +dao.burnBsq.assets.tradeVolume=Volume d'échange +dao.burnBsq.assets.lookBackPeriod=Durée de la vérification +dao.burnBsq.assets.trialFee=Frais pour la période d'essai +dao.burnBsq.assets.totalFee=Total des frais payés +dao.burnBsq.assets.days={0} jours +dao.burnBsq.assets.toFewDays=Les frais de l''actif sont trop bas. Le nombre minimum de jours pour la période d''essai est {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Indéterminé +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=En période d'essai +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Activement tradé +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Déréférencé pour cause d'inactivité +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Supprimée par un vote + +dao.proofOfBurn.header=Preuve du burn +dao.proofOfBurn.amount=Montant +dao.proofOfBurn.preImage=Pre-image +dao.proofOfBurn.burn=Burn +dao.proofOfBurn.allTxs=Toutes les preuves des transactions de burn +dao.proofOfBurn.myItems=Ma preuve des transactions de burn +dao.proofOfBurn.date=Date +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transactions +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Signer un message avec la clé provenant de la transaction de burn +dao.proofOfBurn.verify.window.title=Vérifier un message avec la clé provenant de la preuve de la transaction de burn +dao.proofOfBurn.copySig=Copier la signature dans le presse-papiers +dao.proofOfBurn.sign=Signer +dao.proofOfBurn.message=Message +dao.proofOfBurn.sig=Signature +dao.proofOfBurn.verify=Vérifier +dao.proofOfBurn.verificationResult.ok=Vérification réussie +dao.proofOfBurn.verificationResult.failed=Échec de la vérification + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Phase de proposition +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Interrompre avant la période de vote caché +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Phase de vote caché +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Interrompre avant la phase de dévoilement du vote +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Phase de dévoilement du vote +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Interrompre avant la phase de résultat +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Période de résultat du vote + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Phase de proposition +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Cacher le vote +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Dévoilement du vote +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Résultat du vote + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Demande de compensation +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Demande de remboursement +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Demande de bonded role +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Demande de retrait d'un actif +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Demande pour modifier un paramètre +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Demande standard +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Demande de confiscation d'un bond + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Demande de compensation +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Demande de remboursement +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Bonded role +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Retrait d'un altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Modifier un paramètre +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Demande standard +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscation d'un bond + +dao.proposal.details=Détails relatifs à la proposition +dao.proposal.selectedProposal=Proposition sélectionnée +dao.proposal.active.header=Propositions relatives au cycle actuel +dao.proposal.active.remove.confirm=Êtes-vous certain de vouloir retirer cette proposition?\nLes frais de traitement de la proposition déjà payés seront perdus. +dao.proposal.active.remove.doRemove=Oui, retirer ma proposition +dao.proposal.active.remove.failed=Impossible de retirer la proposition +dao.proposal.myVote.title=Vote +dao.proposal.myVote.accept=Accepter la proposition +dao.proposal.myVote.reject=Rejeter la proposition +dao.proposal.myVote.removeMyVote=Ignorer la proposition +dao.proposal.myVote.merit=Poids du vote résultant des BSQ obtenus +dao.proposal.myVote.stake=Poids du vote en fonction de la mise +dao.proposal.myVote.revealTxId=ID de transaction du vote de dévoilement +dao.proposal.myVote.stake.prompt=Solde maximum pour la mise disponible pour le vote: {0} +dao.proposal.votes.header=Fixez une mise pour le vote et publiez vos votes +dao.proposal.myVote.button=Publier les votes +dao.proposal.myVote.setStake.description=Après avoir voté sur toutes les propositions, vous devez fixer votre mise pour le vote en bloquant des BSQ. Plus vous verrouillerez des BSQ, plus votre vote aura de poids. \n\nLes BSQ verrouillés pour le vote seront déverrouillés à nouveau pendant la phase de dévoilement du vote. +dao.proposal.create.selectProposalType=Sélectionner le type de proposition +dao.proposal.create.phase.inactive=Veuillez patienter jusqu'à la prochaine phase de proposition +dao.proposal.create.proposalType=Type de proposition +dao.proposal.create.new=Faire une nouvelle demande +dao.proposal.create.button=Faire une demande +dao.proposal.create.publish=Publier la demande +dao.proposal.create.publishing=La publication de la demande est en cours... +dao.proposal=Demande +dao.proposal.display.type=Type de demande +dao.proposal.display.name=Nom d'utilisateur GitHub exact +dao.proposal.display.link=Lien vers les informations détaillées +dao.proposal.display.link.prompt=Lien vers la proposition +dao.proposal.display.requestedBsq=Montant démandé en BSQ +dao.proposal.display.txId=ID de transaction de la proposition +dao.proposal.display.proposalFee=Frais de la demande +dao.proposal.display.myVote=Mon vote +dao.proposal.display.voteResult=Synthèse des résultats du vote +dao.proposal.display.bondedRoleComboBox.label=Type de rôle Bonded +dao.proposal.display.requiredBondForRole.label=Bond requis pour le rôle +dao.proposal.display.option=Option + +dao.proposal.table.header.proposalType=Type de demande +dao.proposal.table.header.link=Lien +dao.proposal.table.header.myVote=Mon vote +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Enlever +dao.proposal.table.icon.tooltip.removeProposal=Retirer ma demande +dao.proposal.table.icon.tooltip.changeVote=Vote actuel: ''{0}'''. Modifier le vote pour: ''{1}'' + +dao.proposal.display.myVote.accepted=Accepté +dao.proposal.display.myVote.rejected=Rejeté +dao.proposal.display.myVote.ignored=Ignoré +dao.proposal.display.myVote.unCounted=Le vote n'a pas été inclus dans le résultat +dao.proposal.myVote.summary=Voté: {0}; Poids du vote: {1} (gagné: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Le vote n'est pas valide + +dao.proposal.voteResult.success=Accepté +dao.proposal.voteResult.failed=Rejeté +dao.proposal.voteResult.summary=Résultat : {0}; Seuil : {1} (requis> {2}); Quorum: {3} (requis> {4}) + +dao.proposal.display.paramComboBox.label=Sélectionner le paramètre à modifier +dao.proposal.display.paramValue=Valeur du paramètre + +dao.proposal.display.confiscateBondComboBox.label=Choisir le bond +dao.proposal.display.assetComboBox.label=Actif à enlever + +dao.blindVote=Masquer le vote + +dao.blindVote.startPublishing=Publier la transaction du vote caché... +dao.blindVote.success=Votre transaction de vote caché a été publiée avec succès.\n\nVeuillez noter que vous devez être en ligne pendant la phase de dévoilement du vote pour que votre application Bisq puisse publier la transaction de dévoilement du vote. Sans le dévoilement du vote, votre vote serait invalide ! + +dao.wallet.menuItem.send=Envoyer +dao.wallet.menuItem.receive=Recevoir +dao.wallet.menuItem.transactions=Transactions + +dao.wallet.dashboard.myBalance=Mon solde de portefeuille + +dao.wallet.receive.fundYourWallet=Votre adresse de réception BSQ +dao.wallet.receive.bsqAddress=Adresse du portefeuille BSQ (nouvelle adresse non utilisée) + +dao.wallet.send.sendFunds=Envoyer des fonds +dao.wallet.send.sendBtcFunds=Envoyer des fonds non-BSQ (BTC) +dao.wallet.send.amount=Montant en BSQ +dao.wallet.send.btcAmount=Montant en BTC (fonds non-BSQ) +dao.wallet.send.setAmount=Définir le montant à retirer (le montant minimum est {0}) +dao.wallet.send.receiverAddress=Adresse BSQ du destinataire +dao.wallet.send.receiverBtcAddress=Adresse BTC du destinataire +dao.wallet.send.setDestinationAddress=Remplissez votre adresse de destination +dao.wallet.send.send=Envoyer des fonds en BSQ +dao.wallet.send.inputControl=Sélectionner les entrées +dao.wallet.send.sendBtc=Envoyer des fonds en BTC +dao.wallet.send.sendFunds.headline=Confirmer la demande de retrait +dao.wallet.send.sendFunds.details=Envoi: {0}\nVers l'adresse de réception: {1}.\nLes frais de minage requis sont de: {2} ({3} satoshis/byte)\nTaille virtuelle de la transaction: {4} vKb\n\nLe destinataire recevra: {5}\n\nÊtes-vous certain de vouloir retirer ce montant ? +dao.wallet.chainHeightSynced=Dernier bloc vérifié: {0} +dao.wallet.chainHeightSyncing=En attente des blocs.... {0} Blocs vérifiés sur {1}. +dao.wallet.tx.type=Type + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Indéfini +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Non reconnu +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Transaction BSQ non vérifiée +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Transaction BSQ invalide +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Transaction genesis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Transférer des BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ reçu +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ énvoyé +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Frais de transaction +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Frais de demande de compensation +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Frais de demande de remboursement +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Frais de la demande +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Frais du vote caché +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Dévoilement du vote +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Verrouiller le bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Déverrouiller le bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Frais d'inscription des actifs +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Preuve du burn +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irrégulier + +dao.tx.withdrawnFromWallet=BTC prélevé sur le portefeuille +dao.tx.issuanceFromCompReq=Demande de compensation/émission +dao.tx.issuanceFromCompReq.tooltip=La demande de compensation a donné lieu à l''émission de nouveaux BSQ.\nDate d''émission: {0} +dao.tx.issuanceFromReimbursement=Demande de remboursement/émission +dao.tx.issuanceFromReimbursement.tooltip=Demande de remboursement ayant donné lieu à l''émission de nouveaux BSQ.\nDate d''émission : {0}. +dao.proposal.create.missingBsqFunds=Vous ne disposez pas de suffisamment de fonds en BSQ pour créer cette demande. Si vous avez une transaction BSQ non confirmée, vous devez attendre la confirmation de la blockchain car les BSQ ne seront validés que si elle est incluse dans un bloc.\nManquant: {0} + +dao.proposal.create.missingBsqFundsForBond=Vous ne disposez pas d''assez de fonds en BSQ pour ce rôle. Vous pouvez toujours publier cette demande, mais vous aurez besoin du montant total de BSQ requis pour que ce rôle puisse être accepté.\nManquant: {0} + +dao.proposal.create.missingMinerFeeFunds=Vous ne disposez pas des fonds en BTC suffisants pour créer cette demande de transaction. Toutes les transactions BSQ requièrent le paiement des frais BTC pour le minage.\nManquant: {0} + +dao.proposal.create.missingIssuanceFunds=Vous ne disposez pas de fonds BTC suffisants pour créer cette demande de transaction. Toutes les transactions BSQ exigent des frais pour le mineur en BTC, et la création d''une transaction exige également des frais en BTC d''un montant de ({0} Satoshis/BSQ).\nManquant: {1} + +dao.feeTx.confirm=Confirmer {0} transaction +dao.feeTx.confirm.details={0} frais: {1}\nFrais de minage: {2} ({3} Satoshis/byte)\nTaille virtuelle de la transaction: {4} vKb\n\nÊtes-vous certain de vouloir publier la transaction {5}? + +dao.feeTx.issuanceProposal.confirm.details={0}frais: {1}\nBTC nécessaire pour l'émission des BSQ: {2} ({3} Satoshis/BSQ)\nFrais de minage: {4} ({5} Satoshis/byte)\nTaille virtuelle de la transaction: {6} vKb\n\nSi votre demande est approuvée, vous allez reçevoir le montant que vous avez requis, ôté des frais de la demande d'un montant de 2 BSQ.\n\nÊtes-vous sûr de vouloir publier la transaction {7}? + +dao.news.bisqDAO.title=La DAO de BISQ +dao.news.bisqDAO.description=Tout comme la plateforme d'échange Bisq est décentralisée et résistante à la censure, son modèle de gouvernance l'est aussi - ainsi que les jetons de la DAO de Bisq et BSQ sont les outils qui rendent cela possible. +dao.news.bisqDAO.readMoreLink=En savoir plus sur la DAO de Bisq + +dao.news.pastContribution.title=VOUS AVEZ PARTICIPÉ ANTÉRIEUREMENT ? DEMANDEZ DES BSQ +dao.news.pastContribution.description=Si vous avez participé à Bisq, veuillez utiliser l'adresse BSQ ci-dessous et faire une demande pour prendre part à la distribution genesis de BSQ. +dao.news.pastContribution.yourAddress=Adresse de votre portefeuille BSQ +dao.news.pastContribution.requestNow=Demander maintenant + +dao.news.DAOOnTestnet.title=LANCEZ LA DAO DE BISQ SUR NOTRE TESTNET +dao.news.DAOOnTestnet.description=Le mainnet de la DAO de Bisq n'est pas encore lancé mais vous pouvez en savoir plus sur la DAO de Bisq en l'exécutant sur notre testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Passer sur le mode Testnet de la DAO +dao.news.DAOOnTestnet.firstSection.content=Passez au Testnet de la DAO à partir de l'écran des paramètres. +dao.news.DAOOnTestnet.secondSection.title=2. Acquérir des BSQ +dao.news.DAOOnTestnet.secondSection.content=Demander des BSQ sur Slack ou acheter des BSQ sur Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Participer à un cycle de vote +dao.news.DAOOnTestnet.thirdSection.content=Faire des demandes et voter pour des propositions visant à modifier divers aspects de Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Explorez un explorateur de blocs BSQ +dao.news.DAOOnTestnet.fourthSection.content=Dans la mesure où BSQ est comme Bitcoin, vous pouvez voir les transactions en BSQ sur notre explorateur de blocs Bitcoin. +dao.news.DAOOnTestnet.readMoreLink=Lire la documentation complète + +dao.monitor.daoState=Etat de la DAO +dao.monitor.proposals=État des propositions +dao.monitor.blindVotes=État des votes cachés + +dao.monitor.table.peers=Pairs +dao.monitor.table.conflicts=Conflits +dao.monitor.state=Statut +dao.monitor.requestAlHashes=Demander tous les hashes +dao.monitor.resync=Etat de resync de la DAO +dao.monitor.table.header.cycleBlockHeight=Cycle / Hauteur de bloc +dao.monitor.table.cycleBlockHeight=Cycle {0} / bloc {1} +dao.monitor.table.seedPeers=Nœud de la seed: {0} + +dao.monitor.daoState.headline=État de la DAO +dao.monitor.daoState.table.headline=État des hashes de la chaîne DAO +dao.monitor.daoState.table.blockHeight=Hauteur de bloc +dao.monitor.daoState.table.hash=État du hash de la DAO +dao.monitor.daoState.table.prev=Hash précédent +dao.monitor.daoState.conflictTable.headline=État des hashes des pairs de la DAO en situation de conflit +dao.monitor.daoState.utxoConflicts=conflits UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Hauteur de bloc: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Somme de tous les UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Somme de tous les BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=La DAO n'est pas en état de synchronisation avec le réseau. Après redémarrage, la DAO sera resynchronisé. + +dao.monitor.proposal.headline=État des propositions +dao.monitor.proposal.table.headline=Etat du hachage de la chaîne de proposition +dao.monitor.proposal.conflictTable.headline=Etat de la proposition de hachage des pairs en conflit + +dao.monitor.proposal.table.hash=État du hash de la proposition +dao.monitor.proposal.table.prev=Hash précédent +dao.monitor.proposal.table.numProposals=Nombre de propositions + +dao.monitor.isInConflictWithSeedNode=Vos données locales ne font pas consensus avec au moins un nœud de la seed . Veuillez resynchroniser la DAO. +dao.monitor.isInConflictWithNonSeedNode=L'un de vos pairs n'est pas en consensus avec le réseau, mais votre nœud est synchronisé avec les nœuds de la seed. +dao.monitor.daoStateInSync=Votre nœud local est en consensus avec le réseau + +dao.monitor.blindVote.headline=État des votes cachés +dao.monitor.blindVote.table.headline=État du hachage de la chaîne du vote caché +dao.monitor.blindVote.conflictTable.headline=Vote caché de l'état du hash des pairs en conflit +dao.monitor.blindVote.table.hash=État du Hash du vote caché +dao.monitor.blindVote.table.prev=Hash précédent +dao.monitor.blindVote.table.numBlindVotes=Nombre de votes cachés + +dao.factsAndFigures.menuItem.supply=Quantité existante de bsq +dao.factsAndFigures.menuItem.transactions=Transactions BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Moyenne sur 90 jours du prix d'échange BSQ/BTC +dao.factsAndFigures.dashboard.avgPrice30=Moyenne sur 30 jours du prix d'échange BSQ/BTC +dao.factsAndFigures.dashboard.avgUSDPrice90=Moyenne sur 90 jours coefficientée du prix BSQ/USD +dao.factsAndFigures.dashboard.avgUSDPrice30=Moyenne sur 30 jours coefficientée du prix d'échange BSQ/USD +dao.factsAndFigures.dashboard.marketCap=Capitalisation du marché (basée sur la moyenne sur 30 jours du prix d'échange BSQ/USD) +dao.factsAndFigures.dashboard.availableAmount=BSQ disponible au total +dao.factsAndFigures.dashboard.volumeUsd=Volume total du trade en USD +dao.factsAndFigures.dashboard.volumeBtc=Volume total du trade en BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Moyenne du prix de trade BSQ/USD sur la période de temps sélectionnée dans le tableau +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Moyenne du prix de trade BSQ/BTC sur la période de temps sélectionnée dans le tableau + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ émis v. BSQ brûlé + +dao.factsAndFigures.supply.issued=BSQ émis +dao.factsAndFigures.supply.compReq=Requêtes de compensation +dao.factsAndFigures.supply.reimbursement=Demandes de remboursement +dao.factsAndFigures.supply.genesisIssueAmount=BSQ émis lors de la transaction genesis +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ émis pour les demandes de compensation +dao.factsAndFigures.supply.reimbursementAmount=BSQ émis pour les demandes de remboursement +dao.factsAndFigures.supply.totalIssued=BSQ produit au total +dao.factsAndFigures.supply.totalBurned=Total de BSQ brûlé +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ précédemment burn + +dao.factsAndFigures.supply.priceChat=Prix du BSQ +dao.factsAndFigures.supply.volumeChat=Volume d'échange +dao.factsAndFigures.supply.tradeVolumeInUsd=Volume du trade en USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Volume du trade en BTC +dao.factsAndFigures.supply.bsqUsdPrice=Prix BSQ/USD +dao.factsAndFigures.supply.bsqBtcPrice=Prix BSQ/BTC +dao.factsAndFigures.supply.btcUsdPrice=Prix BTC/USD + +dao.factsAndFigures.supply.locked=État global des BSQ verrouillés +dao.factsAndFigures.supply.totalLockedUpAmount=Verrouillé dans les bonds +dao.factsAndFigures.supply.totalUnlockingAmount=Déverrouillage des BSQ en bonds +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ déverrouillés des bonds +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ confisqués en bonds +dao.factsAndFigures.supply.proofOfBurn=Preuve de la destruction +dao.factsAndFigures.supply.bsqTradeFee=Frais de trade du BSQ +dao.factsAndFigures.supply.btcTradeFee=Frais de trade du BTC + +dao.factsAndFigures.transactions.genesis=Transaction genesis +dao.factsAndFigures.transactions.genesisBlockHeight=Hauteur de bloc du bloc genesis +dao.factsAndFigures.transactions.genesisTxId=ID de la transaction genesis +dao.factsAndFigures.transactions.txDetails=Statistiques des transactions en BSQ +dao.factsAndFigures.transactions.allTx=Nombre de transactions en BSQ +dao.factsAndFigures.transactions.utxo=Nombre de transactions de sorties non dépensées +dao.factsAndFigures.transactions.compensationIssuanceTx=Nombre de transactions émises en demande de compensation +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Nombre des transactions émises en demande de remboursement +dao.factsAndFigures.transactions.burntTx=Nombre de transactions ayant occasionné le paiement de frais +dao.factsAndFigures.transactions.invalidTx=Nombre de transactions invalides +dao.factsAndFigures.transactions.irregularTx=Nombre des transactions irrégulières + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Sélectionnez les entrées pour la transaction +inputControlWindow.balanceLabel=Solde disponible + +contractWindow.title=Détails du conflit +contractWindow.dates=Date de l'ordre / date de l'échange +contractWindow.btcAddresses=Adresse Bitcoin BTC acheteur / vendeur BTC +contractWindow.onions=Adresse réseau de l'acheteur de BTC / du vendeur de BTC +contractWindow.accountAge=Âge du compte acheteur BTC / vendeur BTC +contractWindow.numDisputes=Nombre de litiges de l'acheteur de BTC / du vendeur de BTC +contractWindow.contractHash=Contracter le hash + +displayAlertMessageWindow.headline=Information importante! +displayAlertMessageWindow.update.headline=Information de mise à jour importante! +displayAlertMessageWindow.update.download=Télécharger: +displayUpdateDownloadWindow.downloadedFiles=Dossiers: +displayUpdateDownloadWindow.downloadingFile=Téléchargement: {0} +displayUpdateDownloadWindow.verifiedSigs=Signature vérifiée avec les clés : +displayUpdateDownloadWindow.status.downloading=Téléchargement des fichiers en cours... +displayUpdateDownloadWindow.status.verifying=Vérification de la signature.... +displayUpdateDownloadWindow.button.label=Télécharger le programme d'installation et vérifier la signature +displayUpdateDownloadWindow.button.downloadLater=Télécharger plus tard +displayUpdateDownloadWindow.button.ignoreDownload=Ignorer cette version +displayUpdateDownloadWindow.headline=Une nouvelle mise à jour Bisq est disponible ! +displayUpdateDownloadWindow.download.failed.headline=Echec du téléchargement +displayUpdateDownloadWindow.download.failed=Téléchargement échoué. Veuillez télécharger et vérifier via [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Impossible de déterminer le bon programme d'installation. Veuillez télécharger et vérifier manuellement via [HYPERLINK:https://bisq.network/downloads] . +displayUpdateDownloadWindow.verify.failed=Vérification échouée. Veuillez télécharger et vérifier manuellement via [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=La nouvelle version a été téléchargée avec succès et la signature vérifiée.\n\nVeuillez ouvrir le répertoire de téléchargement, fermer l'application et installer la nouvelle version. +displayUpdateDownloadWindow.download.openDir=Ouvrir le répertoire de téléchargement + +disputeSummaryWindow.title=Résumé +disputeSummaryWindow.openDate=Date d'ouverture du ticket +disputeSummaryWindow.role=Rôle du trader +disputeSummaryWindow.payout=Versement du montant de l'opération +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} obtient le montant du versement de la transaction +disputeSummaryWindow.payout.getsAll=Payement maximum en BTC {0} +disputeSummaryWindow.payout.custom=Versement personnalisé +disputeSummaryWindow.payoutAmount.buyer=Montant du versement de l'acheteur +disputeSummaryWindow.payoutAmount.seller=Montant du versement au vendeur +disputeSummaryWindow.payoutAmount.invert=Utiliser le perdant comme publicateur +disputeSummaryWindow.reason=Motif du litige +disputeSummaryWindow.tradePeriodEnd=Fin de la période de trade +disputeSummaryWindow.extraInfo=Informations additionnelles +disputeSummaryWindow.delayedPayoutStatus=Statut du paiement différé + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Utilisabilité +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violation du protocole +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Pas de réponse +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Autre +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banque +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Transaction facultative +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Le trader ne répond pas +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Mauvais compte d'expéditeur +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=La pair a expiré +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=La transaction s'est stabilisée. + +disputeSummaryWindow.summaryNotes=Notes de synthèse +disputeSummaryWindow.addSummaryNotes=Ajouter des notes de synthèse +disputeSummaryWindow.close.button=Fermer le ticket + +# Do no change any line break or order of tokens as the structure is used for signature verification +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.msg=Le ticket a été fermé {0}\n {1} Adresse du noeud: {2} \n\nRésumé: \nID de transaction: {3} \nDevise: {4} \n Montant de la transaction: {5} \nMontant du paiement de l'acheteur BTC: {6} \nMontant du paiement du vendeur BTC: {7} \n\nRaison du litige: {8} \n\nRésumé: {9} \n\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=\n\nÉtape suivante:\nOuvrez la transaction inachevée, acceptez ou rejetez la suggestion du médiateur +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nÉtape suivante: \nAucune autre action n'est requise de votre part. Si l'arbitre rend une décision en votre faveur, vous verrez la transaction «Remboursement d'arbitrage» sur la page Fonds / Transactions +disputeSummaryWindow.close.closePeer=Vous devez également clore le ticket des pairs de trading ! +disputeSummaryWindow.close.txDetails.headline=Publier la transaction de remboursement +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=L''acheteur reçoit {0} à l''adresse: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l''adresse: {1}\n +disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3} ({4} satoshis/vbyte)\nTaille virtuelle de la transaction: {5} vKb\n\nÊtes-vous sûr de vouloir publier cette transaction ? + +disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement +disputeSummaryWindow.close.noPayout.text=Voulez-vous fermer sans paiement ? + +emptyWalletWindow.headline={0} Outil de secours du portefeuille +emptyWalletWindow.info=Veuillez utiliser ceci qu'en cas d'urgence si vous ne pouvez pas accéder à vos fonds à partir de l'interface utilisateur.\n\nVeuillez remarquer que touts les ordres en attente seront automatiquement fermés lors de l'utilisation de cet outil.\n\nAvant d'utiliser cet outil, veuillez sauvegarder votre répertoire de données. Vous pouvez le faire sur \"Compte/sauvegarde\".\n\nVeuillez nous signaler votre problème et déposer un rapport de bug sur GitHub ou sur le forum Bisq afin que nous puissions enquêter sur la source du problème. +emptyWalletWindow.balance=Votre solde disponible sur le portefeuille +emptyWalletWindow.bsq.btcBalance=Solde en Satoshis non-BSQ + +emptyWalletWindow.address=Votre adresse de destination +emptyWalletWindow.button=Envoyer tous les fonds +emptyWalletWindow.openOffers.warn=Vous avez des ordres en cours qui seront supprimées si vous videz votre portefeuille.\nVous êtes certain de vouloir vider votre portefeuille ? +emptyWalletWindow.openOffers.yes=Oui, j'en suis certain +emptyWalletWindow.sent.success=Le solde de votre portefeuille a été transféré avec succès. + +enterPrivKeyWindow.headline=Entrer la clé privée pour l'enregistrement + +filterWindow.headline=Modifier la liste de filtres +filterWindow.offers=Ordres filtrés (séparer avec une virgule) +filterWindow.onions=Banni des addresses de trading (virgule de séparation) +filterWindow.bannedFromNetwork=Banni des addresses réseau (virgule de séparation) +filterWindow.accounts=Données filtrées du compte de trading:\nFormat: séparer par une virgule liste des [ID du mode de paiement | champ de données | valeur]. +filterWindow.bannedCurrencies=Codes des devises filtrées (séparer avec une virgule.) +filterWindow.bannedPaymentMethods=IDs des modes de paiements filtrés (séparer avec une virgule.) +filterWindow.bannedAccountWitnessSignerPubKeys=Clé publique filtrée du signataire du témoin de compte (clé publique hexadécimale séparée par des virgules) +filterWindow.bannedPrivilegedDevPubKeys=Clé publique filtrée de développeur privilégiée (clé publique hexadécimale séparée par des virgules) +filterWindow.arbitrators=Arbitres filtrés (adresses onion séparées par une virgule) +filterWindow.mediators=Médiateurs filtrés (adresses onion sep. par une virgule) +filterWindow.refundAgents=Agents de remboursement filtrés (adresses onion sep. par virgule) +filterWindow.seedNode=Nœuds de seed filtrés (adresses onion séparées par une virgule) +filterWindow.priceRelayNode=Nœuds relais avec prix filtrés (adresses onion séparées par une virgule) +filterWindow.btcNode=Nœuds Bitcoin filtrés (adresses séparées par une virgule + port) +filterWindow.preventPublicBtcNetwork=Empêcher l'utilisation du réseau public Bitcoin +filterWindow.disableDao=Désactiver la DAO +filterWindow.disableAutoConf=Désactiver la confirmation automatique +filterWindow.autoConfExplorers=Explorateur d'auto-confirmations filtrés (addresses à virgule de séparation) +filterWindow.disableDaoBelowVersion=Version minimale requise pour la DAO +filterWindow.disableTradeBelowVersion=Version min. nécessaire pour pouvoir échanger +filterWindow.add=Ajouter le filtre +filterWindow.remove=Retirer le filtre +filterWindow.btcFeeReceiverAddresses=Adresse de réception des frais BTC +filterWindow.disableApi=Désactiver l'API +filterWindow.disableMempoolValidation=Désactiver la validation du Mempool + +offerDetailsWindow.minBtcAmount=Montant BTC min. +offerDetailsWindow.min=(min. {0}) +offerDetailsWindow.distance=(écart par rapport au prix de marché: {0}) +offerDetailsWindow.myTradingAccount=Mon compte de trading +offerDetailsWindow.offererBankId=(ID/BIC/SWIFT de la banque du maker) +offerDetailsWindow.offerersBankName=(nom de la banque du maker) +offerDetailsWindow.bankId=Identifiant bancaire (par ex. BIC ou SWIFT) +offerDetailsWindow.countryBank=Pays de la banque du Maker +offerDetailsWindow.commitment=Engagement +offerDetailsWindow.agree=J'accepte +offerDetailsWindow.tac=Conditions d'utilisation +offerDetailsWindow.confirm.maker=Confirmer: Placer un ordre de {0} Bitcoin +offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} Bitcoin +offerDetailsWindow.creationDate=Date de création +offerDetailsWindow.makersOnion=Adresse onion du maker + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Bisq. +qRCodeWindow.request=Demande de paiement:\n{0} + +selectDepositTxWindow.headline=Sélectionner la transaction de dépôt en cas de litige +selectDepositTxWindow.msg=La transaction de dépôt n'a pas été incluse dans l"échange.\nVeuillez sélectionner l'une des transactions multisig existantes de votre portefeuille qui contient la transaction de dépôt utilisée lors de l'échec de l'échange.\n\nVous trouverez la bonne transaction en ouvrant la fenêtre des détails de la transaction (cliquez sur l'ID de la transaction dans la liste) et en retraçant les frais de transaction de sortie de la prochaine transaction où vous serez en mesure de voir la transaction de dépôt multisig (l'adresse commence par un 3). Cet ID de transaction doit être visible dans la liste ici. Une fois que vous aurez trouvé la bonne transaction, sélectionnez cette transaction la et continuez.\n\nDésolé pour le désagrément mais ce genre d'erreur devrait se produire très rarement et à l'avenir nous trouverons de meilleurs moyens pour le résoudre. +selectDepositTxWindow.select=Sélectionner la transaction de dépôt + +sendAlertMessageWindow.headline=Envoyer une notification globale +sendAlertMessageWindow.alertMsg=Message d'alerte +sendAlertMessageWindow.enterMsg=Entrer le message +sendAlertMessageWindow.isSoftwareUpdate=Notification de téléchargement du logiciel +sendAlertMessageWindow.isUpdate=Est version complète +sendAlertMessageWindow.isPreRelease=Est version pré-complète +sendAlertMessageWindow.version=Nouvelle version N° +sendAlertMessageWindow.send=Envoyer une notification +sendAlertMessageWindow.remove=Supprimer une notification + +sendPrivateNotificationWindow.headline=Envoyer un message privé +sendPrivateNotificationWindow.privateNotification=Notification privée +sendPrivateNotificationWindow.enterNotification=Entrer la notification +sendPrivateNotificationWindow.send=Envoyer une notification privée + +showWalletDataWindow.walletData=Données du portefeuille +showWalletDataWindow.includePrivKeys=Inclure les clés privées + +setXMRTxKeyWindow.headline=La preuve XMR a été envoyée. +setXMRTxKeyWindow.note=Ajoutez les informations tx au-dessous pour confirmer automatiquement les transactions plus rapidement. Plus d'informations: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=ID de transaction (en option) +setXMRTxKeyWindow.txKey=Clé de transaction (en option) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Conditions d'utilisation +tacWindow.agree=Je suis d'accord +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.tradeDate=Date de l'échange +tradeDetailsWindow.txFee=Frais de minage +tradeDetailsWindow.tradingPeersOnion=Adresse onion du pair de trading +tradeDetailsWindow.tradingPeersPubKeyHash=Valeur de hachage de la clé publique du partenaire commercial +tradeDetailsWindow.tradeState=État du trade +tradeDetailsWindow.agentAddresses=Arbitre/Médiateur +tradeDetailsWindow.detailData=Données détaillées + +txDetailsWindow.headline=Détails de la transaction +txDetailsWindow.btc.note=Vous avez envoyé du BTC. +txDetailsWindow.bsq.note=Vous avez encoyé des fonds en BSQ. Le BSQ est du bitcoin coloré, donc la transaction ne s'affichera pas dans un explorateur BSQ tant qu'elle n'est pas confirmée dans un block bitcoin. +txDetailsWindow.sentTo=Envoyé à +txDetailsWindow.txId=ID de transaction + +closedTradesSummaryWindow.headline=Résumé de l'historique de trade +closedTradesSummaryWindow.totalAmount.title=Montant total du trade +closedTradesSummaryWindow.totalAmount.value={0} ({1} avec le prix courant du marché) +closedTradesSummaryWindow.totalVolume.title=Montant total échangé en {0} +closedTradesSummaryWindow.totalMinerFee.title=Somme de tous les frais de mineur +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} du montant total du trade) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Somme de tous les frais de trade payés en BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} du montant total du trade) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Somme de tous les frais de trade payés en BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} du montant total du trade) + +walletPasswordWindow.headline=Entrer le mot de passe pour déverouiller + +torNetworkSettingWindow.header=Paramètres du réseau Tor +torNetworkSettingWindow.noBridges=Ne pas utiliser de bridges. +torNetworkSettingWindow.providedBridges=Se connecter avec les bridges proposés +torNetworkSettingWindow.customBridges=Entrer des bridges personnalisés +torNetworkSettingWindow.transportType=Type de transport +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (recommandé) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Entrer un ou plusieurs relais bridge (un par ligne) +torNetworkSettingWindow.enterBridgePrompt=inscrire l'adresse:port +torNetworkSettingWindow.restartInfo=Vous devez redémarrer pour appliquer les changements +torNetworkSettingWindow.openTorWebPage=Ouvrir la page web du projet Tor +torNetworkSettingWindow.deleteFiles.header=Problèmes de connexion ? +torNetworkSettingWindow.deleteFiles.info=Si vous rencontrez des problèmes de connexion récurrents au démarrage, la suppression des fichiers Tor obsolètes pourrait vous aider. Pour ce faire, cliquez sur le bouton ci-dessous et ensuite redémarrez. +torNetworkSettingWindow.deleteFiles.button=Supprimer les fichiers Tor obsolètes et éteindre +torNetworkSettingWindow.deleteFiles.progress=Arrêt de Tor en cours +torNetworkSettingWindow.deleteFiles.success=Fichiers Tor obsolètes supprimés avec succès. Veuillez redémarrer. +torNetworkSettingWindow.bridges.header=Tor est-il bloqué? +torNetworkSettingWindow.bridges.info=Si Tor est bloqué par votre fournisseur Internet ou dans votre pays, vous pouvez essayer d'utiliser les passerelles Tor.\nVisitez la page web de Tor sur: https://bridges.torproject.org/bridges pour en savoir plus sur les bridges et les pluggable transports. + +feeOptionWindow.headline=Choisissez la devise pour le paiement des frais de transaction +feeOptionWindow.info=Vous pouvez choisir de payer les frais de transaction en BSQ ou en BTC. Si vous choisissez BSQ, vous bénéficierez de frais de transaction réduits. +feeOptionWindow.optionsLabel=Choisissez la devise pour le paiement des frais de transaction +feeOptionWindow.useBTC=Utiliser BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (environ {1}/{2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notification +popup.headline.instruction=Merci de noter: +popup.headline.attention=Attention +popup.headline.backgroundInfo=Information sur les antécédents +popup.headline.feedback=Terminé +popup.headline.confirmation=Confirmation +popup.headline.information=Information +popup.headline.warning=Attention +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 bisq.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} + +error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d''erreur: {0}\n\nVoulez-vous l''effacer et lancer une resynchronisation? +error.deleteAddressEntryListFailed=Impossible de supprimer le dossier AddressEntryList.\nErreur: {0}. +error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l''échange fermé avec l''ID d''échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. +error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l''ID d'échange {0} est nulle.\n\nVeuillez redémarrer l''application pour nettoyer la liste des transactions fermées. + +popup.warning.walletNotInitialized=Le portefeuille n'est pas encore initialisé +popup.warning.osxKeyLoggerWarning=En raison de mesures de sécurité plus strictes dans MacOS 10.14 et dans la version supérieure, le lancement d'une application Java (Bisq utilise Java) provoquera un avertissement pop-up dans MacOS (« Bisq souhaite recevoir les frappes de toute application »). \n\nPour éviter ce problème, veuillez ouvrir «Paramètres MacOS», puis allez dans «Sécurité et confidentialité» -> «Confidentialité» -> «Surveillance des entrées», puis supprimez «Bisq» de la liste de droite. \n\nUne fois les limitations techniques résolues (le packager Java de la version Java requise n'a pas été livré), Bisq effectuera une mise à niveau vers la nouvelle version Java pour éviter ce problème. +popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Bisq sur cet ordinateur.\nL''architecture de votre ordinateur est: {0}.\nLa binary Bisq que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). +popup.warning.incompatibleDB=Nous avons détecté un fichier de base de données incompatible!\n\nCes fichiers de base de données ne sont pas compatibles avec notre base de code actuelle: {0}\n\nNous avons sauvegardé les fichiers endommagés et appliqué les valeurs par défaut à la nouvelle version de la base de données.\n\nLa sauvegarde se trouve dans: \n\n{1} / db / backup_of_corrupted_data. \n\nVeuillez vérifier si vous avez installé la dernière version de Bisq. \n\nVous pouvez télécharger: \n\n[HYPERLINK:https://bisq.network/downloads] \n\nVeuillez redémarrer l'application. +popup.warning.startupFailed.twoInstances=Bisq est déjà lancé. Vous ne pouvez pas lancer deux instances de bisq. +popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n''est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l''état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. +popup.warning.tradePeriod.ended=Votre échange avec l''ID {0} a atteint la période de trading maximale autorisée et n''est pas terminé.\n\nLa période d''échange s''est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. +popup.warning.noTradingAccountSetup.headline=Vous n'avez pas configuré de compte de trading +popup.warning.noTradingAccountSetup.msg=Vous devez configurer une devise nationale ou un compte altcoin avant de pouvoir créer un ordre.\nVoulez-vous configurer un compte ? +popup.warning.noArbitratorsAvailable=Les arbitres ne sont pas disponibles. +popup.warning.noMediatorsAvailable=Il n'y a pas de médiateurs disponibles. +popup.warning.notFullyConnected=Vous devez attendre d'être complètement connecté au réseau.\nCela peut prendre jusqu'à 2 minutes au démarrage. +popup.warning.notSufficientConnectionsToBtcNetwork=Vous devez attendre d''avoir au minimum {0} connexions au réseau Bitcoin. +popup.warning.downloadNotComplete=Vous devez attendre que le téléchargement des blocs Bitcoin manquants soit terminé. +popup.warning.chainNotSynced=La hauteur de la blockchain du portefeuille Bisq n'est pas synchronisée correctement. Si vous avez récemment démarré l'application, veuillez attendre qu'un block de Bitcoin a soit publié.\n\nVous pouvez vérifier la hauteur de la blockchain dans Paramètres/Informations Réseau. Si plus d'un block passe et que ce problème persiste, il est possible que ça soit bloqué, dans ce cas effectuez une resynchronisation SPV [LIEN:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Vous êtes certain de vouloir retirer cet ordre?\nLes frais du maker de {0} seront perdus si vous retirez cet ordre. +popup.warning.tooLargePercentageValue=Vous ne pouvez pas définir un pourcentage de 100% ou plus grand. +popup.warning.examplePercentageValue=Merci de saisir un nombre sous la forme d'un pourcentage tel que \"5.4\" pour 5.4% +popup.warning.noPriceFeedAvailable=Il n'y a pas de flux pour le prix de disponible pour cette devise. Vous ne pouvez pas utiliser un prix basé sur un pourcentage.\nVeuillez sélectionner le prix fixé. +popup.warning.sendMsgFailed=L'envoi du message à votre partenaire d'échange a échoué.\nMerci d'essayer de nouveau et si l'échec persiste merci de reporter le bug. +popup.warning.insufficientBtcFundsForBsqTx=Vous ne disposez pas de suffisamment de fonds BTC pour payer les frais du minage de cette transaction.\nVeuillez approvisionner votre portefeuille BTC.\nFonds manquants: {0} +popup.warning.bsqChangeBelowDustException=Cette transaction crée une BSQ change output qui est inférieure à la dust limit (5,46 BSQ) et serait rejetée par le réseau Bitcoin.\n\nVous devez soit envoyer un montant plus élevé pour éviter la change output (par exemple en ajoutant le montant de dust à votre montant d''envoi), soit ajouter plus de fonds BSQ à votre portefeuille pour éviter de générer une dust output.\n\nLa dust output est {0}. +popup.warning.btcChangeBelowDustException=Cette transaction crée une change output qui est inférieure à la dust limit (546 Satoshi) et serait rejetée par le réseau Bitcoin.\n\nVous devez ajouter la quantité de dust à votre montant envoyé pour éviter de générer une dust output.\n\nLa dust output est {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=Vous avez besoin de plus de BSQ pour effectuer cette transaction - le dernier 5,46 BSQ restant dans le portefeuille ne sera pas utilisé pour payer les frais de transaction en raison de la limite fractionnaire dans l'accord BTC. \n\nVous pouvez acheter plus de BSQ ou utiliser BTC pour payer les frais de transaction\n\nManque de fonds BSQ: {0} +popup.warning.noBsqFundsForBtcFeePayment=Votre portefeuille BSQ ne dispose pas de suffisamment de fonds pour payer les frais de transaction en BSQ. +popup.warning.messageTooLong=Votre message dépasse la taille maximale autorisée. Veuillez l'envoyer en plusieurs parties ou le télécharger depuis un service comme https://pastebin.com. +popup.warning.lockedUpFunds=Vous avez des fonds bloqués d''une transaction qui a échoué.\nSolde bloqué: {0}\nAdresse de la tx de dépôt: {1}\nID de l''échange: {2}.\n\nVeuillez ouvrir un ticket de support en sélectionnant la transaction dans l'écran des transactions ouvertes et en appuyant sur \"alt + o\" ou \"option + o\". + +popup.warning.makerTxInvalid=Cette offre n'est pas valide. Veuillez choisir une autre offre.\n\n +takeOffer.cancelButton=Annuler la prise de l'offre +takeOffer.warningButton=Ignorer et continuer tout de même + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=Un des noeuds {0} a été banni. +# suppress inspection "UnusedProperty" +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 Bisq. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Bisq pour obtenir plus d'informations. +popup.warning.mandatoryUpdate.dao=Veuillez faire une mise à jour vers la dernière version de Bisq. Une mise à jour obligatoire a été publiée, laquelle désactive la DAO de Bisq et BSQ sur les anciennes versions. Veuillez consulter le Forum Bisq pour obtenir plus d'informations. +popup.warning.disable.dao=La DAO de Bisq et BSQ sont désactivés temporairement. Veuillez consulter le Forum Bisq 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 Bisq +popup.warning.burnBTC=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 BTC à 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 Bitcoin.\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 Bisq disposible sur Keybase. + +popup.warning.trade.txRejected.tradeFee=frais de transaction +popup.warning.trade.txRejected.deposit=dépôt +popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Bitcoin.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l'équipe Bisq est disponible sur Keybase. + +popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l''offre avec ID {0} n''est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Bisq est disponible sur Keybase. + +popup.info.securityDepositInfo=Afin de s'assurer que les deux traders suivent le protocole de trading, les deux traders doivent payer un dépôt de garantie.\n\nCe dépôt est conservé dans votre portefeuille d'échange jusqu'à ce que votre transaction soit terminée avec succès, et ensuite il vous sera restitué.\n\nRemarque : si vous créez un nouvel ordre, Bisq doit être en cours d'exécution pour qu'un autre trader puisse l'accepter. Pour garder vos ordres en ligne, laissez Bisq en marche et assurez-vous que cet ordinateur reste en ligne aussi (pour cela, assurez-vous qu'il ne passe pas en mode veille....le mode veille du moniteur ne pose aucun problème). + +popup.info.cashDepositInfo=Veuillez vous assurer d''avoir une succursale de l''établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL''identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. +popup.info.cashDepositInfo.confirm=Je confirme que je peux effectuer le dépôt. +popup.info.shutDownWithOpenOffers=Bisq est en cours de fermeture, mais des ordres sont en attente.\n\nCes ordres ne seront pas disponibles sur le réseau P2P si Bisq est éteint, mais ils seront republiés sur le réseau P2P la prochaine fois que vous lancerez Bisq.\n\nPour garder vos ordres en ligne, laissez Bisq en marche et assurez-vous que cet ordinateur reste aussi en ligne (pour cela, assurez-vous qu'il ne passe pas en mode veille...la veille du moniteur ne pose aucun problème). +popup.info.qubesOSSetupInfo=Il semble que vous exécutez Bisq sous Qubes OS.\n\nVeuillez vous assurer que votre Bisq qube est mis en place de la manière expliquée dans notre guide [LIEN:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=La rétrogradation depuis la version {0} vers la version {1} n'est pas supportée. Veuillez utiliser la dernière version de Bisq. +popup.warn.daoRequiresRestart=Il y'a eu un problème lors de la synchronisation de l'état du DAO. Vous devez redémarrer l'application pour pallier à ce problème. + +popup.privateNotification.headline=Notification privée importante! + +popup.securityRecommendation.headline=Recommendation de sécurité importante +popup.securityRecommendation.msg=Nous vous rappelons d'envisager d'utiliser la protection par mot de passe pour votre portefeuille si vous ne l'avez pas déjà activé.\n\nIl est également fortement recommandé d'écrire les mots de la seed de portefeuille. Ces mots de la seed sont comme un mot de passe principal pour récupérer votre portefeuille Bitcoin.\nVous trouverez plus d'informations à ce sujet dans l'onglet \"seed du portefeuille\".\n\nDe plus, il est recommandé de sauvegarder le dossier complet des données de l'application dans l'onglet \"Sauvegarde". + +popup.bitcoinLocalhostNode.msg=Bisq a détecté un noeud Bitcoin Core en cours d'exécution sur cette machine (sur l'ĥote local)\n\nVeuillez vous assurer que:\n- le noeud est complètement synchronisé avant de lancer Bisq\n- l'élagage est désactivé ('prune=0' dans bitcoin.conf)\n- les filtres de Bloom sont activés ('peerbloomfilters=1' dans bitcoin.conf) + +popup.shutDownInProgress.headline=Fermeture en cours +popup.shutDownInProgress.msg=La fermeture de l'application nécessite quelques secondes.\nVeuillez ne pas interrompre ce processus. + +popup.attention.forTradeWithId=Attention requise la transaction avec l''ID {0} +popup.attention.reasonForPaymentRuleChange=La version 1.5.5 introduit un changement critique de règle de trade concernant le champ \"raison du paiement\" dans les transferts banquaires. Veuillez laisser ce champ vide -- N'UTILISEZ PAS l'ID de trade comme \"raison de paiement\". + +popup.info.multiplePaymentAccounts.headline=Comptes de paiement multiples disponibles +popup.info.multiplePaymentAccounts.msg=Vous disposez de plusieurs comptes de paiement disponibles pour cet ordre. Assurez-vous de choisir le bon. + +popup.accountSigning.selectAccounts.headline=Sélectionner les comptes de paiement +popup.accountSigning.selectAccounts.description=En fonction du mode de paiement et du moment, tous les comptes de paiement qui sont liés à un litige où un paiement à l'acheteur a eu lieu seront sélectionnés afin que vous les signiez. +popup.accountSigning.selectAccounts.signAll=Signer tous les modes de paiement +popup.accountSigning.selectAccounts.datePicker=Sélectionnez le moment jusqu'auquel les comptes seront signés + +popup.accountSigning.confirmSelectedAccounts.headline=Confirmer les comptes de paiement sélectionnés +popup.accountSigning.confirmSelectedAccounts.description=Suite à votre saisie, {0} comptes de paiement seront sélectionnés. +popup.accountSigning.confirmSelectedAccounts.button=Confirmer les comptes de paiement +popup.accountSigning.signAccounts.headline=Confirmer la signature des comptes de paiement +popup.accountSigning.signAccounts.description=Suite à votre sélection, {0} comptes de paiement seront signés. +popup.accountSigning.signAccounts.button=Signer les comptes de paiement +popup.accountSigning.signAccounts.ECKey=Entrez la clé privée d'arbitrage +popup.accountSigning.signAccounts.ECKey.error=Mauvaise ECKey de l'arbitre + +popup.accountSigning.success.headline=Félicitations +popup.accountSigning.success.description=Tous les {0} comptes de paiement ont été signés avec succès ! +popup.accountSigning.generalInformation=Vous trouverez l'état de signature de tous vos comptes dans la section compte.\n\nPour plus d'informations, veuillez consulter [LIEN:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Un de vos comptes de paiement a été vérifié et signé par un arbitre. Echanger avec ce compte signera automatiquement le compte de votre pair de trading après un échange réussi.\n\n{0} +popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d''autres comptes dans les {0} jours à venir.\n\n{1} +popup.accountSigning.peerLimitLifted=La limite initiale pour l''un de vos comptes a été levée.\n\n{0} +popup.accountSigning.peerSigner=Un de vos comptes est suffisamment mature pour signer d'autres comptes de paiement et la limite initiale pour un de vos comptes a été levée.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Importer le témoin non-signé de l'âge du compte +popup.accountSigning.confirmSingleAccount.headline=Confirmer le témoin de l'âge du compte sélectionné +popup.accountSigning.confirmSingleAccount.selectedHash=Hash du témoin sélectionné +popup.accountSigning.confirmSingleAccount.button=Signer le témoin de l'âge du compte +popup.accountSigning.successSingleAccount.description=Le témoin {0} a été signé +popup.accountSigning.successSingleAccount.success.headline=Succès + +popup.accountSigning.unsignedPubKeys.headline=Clés publiques non signées +popup.accountSigning.unsignedPubKeys.sign=Signer les clés publiques +popup.accountSigning.unsignedPubKeys.signed=Les clés publiques ont été signées +popup.accountSigning.unsignedPubKeys.result.signed=Clés publiques signées +popup.accountSigning.unsignedPubKeys.result.failed=Échec de la signature + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notification pour la transaction avec l''ID {0} +notification.ticket.headline=Ticket de support pour l''échange avec l''ID {0} +notification.trade.completed=La transaction est maintenant terminée et vous pouvez retirer vos fonds. +notification.trade.accepted=Votre ordre a été accepté par un BTC {0}. +notification.trade.confirmed=Votre échange avait au moins une confirmation sur la blockchain.\nVous pouvez effectuer le paiement maintenant. +notification.trade.paymentStarted=L'acheteur de BTC a initié le paiement. +notification.trade.selectTrade=Choisir un trade +notification.trade.peerOpenedDispute=Votre pair de trading a ouvert un {0}. +notification.trade.disputeClosed=Le {0} a été fermé +notification.walletUpdate.headline=Mise à jour du portefeuille de trading +notification.walletUpdate.msg=Votre portefeuille de trading est suffisamment approvisionné.\nMontant: {0} +notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d''une précédente tentative d''achat de l'ordre.\nMontant: {0} +notification.tradeCompleted.headline=Le trade est terminé +notification.tradeCompleted.msg=Vous pouvez maintenant retirer vos fonds vers votre portefeuille Bitcoin externe ou les transférer vers le portefeuille Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Montrer la fenêtre de l'application +systemTray.hide=Cacher la fenêtre de l'application +systemTray.info=Informations au sujet de Bisq +systemTray.exit=Sortir +systemTray.tooltip=Bisq: Une plateforme d''échange décentralisée sur le réseau bitcoin + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Veuillez vous assurer que les frais de minage utilisés par votre portefeuille externe sont d'au moins {0} satoshis/vbyte. Le cas échéant les transactions de trade pourraient ne peut être confirmée à temps et le trade aboutirait à une dispute. + +guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l''arborescence:\n{0} +guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation. +guiUtil.accountExport.selectPath=Sélectionner l''arborescence vers {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Compte de trading avec l''ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Nous n''avons pas importé de compte de trading avec l''id {0} car il existe déjà.\n +guiUtil.accountExport.exportFailed=Echec de l''export à CSV à cause d'une erreur.\nErreur = {0} +guiUtil.accountExport.selectExportPath=Sélectionner l'arborescence d'export +guiUtil.accountImport.imported=Compte de trading importé depuis l''arborescence:\n{0}\n\nComptes importés:\n{1} +guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n''a été trouvé sur l''arborescence {0}.\nLe nom du fichier est {1}." +guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n''utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Ouvrir la page web et ne plus me le demander +guiUtil.openWebBrowser.copyUrl=Copier l'URL et annuler +guiUtil.ofTradeAmount=du montant du trade +guiUtil.requiredMinimum=(minimum requis) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Sélectionner la devise +list.currency.showAll=Tout montrer +list.currency.editList=Modifier la liste des devises + +table.placeholder.noItems=Il y a {0} disponible pour le moment +table.placeholder.noData=Il n'y a actuellement aucune donnée disponible +table.placeholder.processingData=Traitement des données en cours... + + +peerInfoIcon.tooltip.tradePeer=Du pair de trading +peerInfoIcon.tooltip.maker=du maker +peerInfoIcon.tooltip.trade.traded={0} adresse onion: {1}\nVous avez déjà échangé a {2} reprise(s) avec ce pair\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n''avez pas échangé avec ce pair jusqu''à présent.\n{2} +peerInfoIcon.tooltip.age=Compte de paiement créé il y a {0}. +peerInfoIcon.tooltip.unknownAge=Ancienneté du compte de paiement inconnue. + +tooltip.openPopupForDetails=Ouvrir le popup pour obtenir des détails +tooltip.invalidTradeState.warning=Le trade est dans un état invalide. Ouvrez la fenêtre des détails pour plus d'informations +tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l''adresse: {0} +tooltip.openBlockchainForTx=Ouvrir un explorateur de blockchain externe pour la transaction: {0} + +confidence.unknown=Statut de transaction inconnu +confidence.seen=Vu par {0} pair(s) / 0 confirmations +confidence.confirmed=Confirmé par {0} bloc(s) +confidence.invalid=La transaction est invalide + +peerInfo.title=info sur le pair +peerInfo.nrOfTrades=Nombre d'opérations effectuées +peerInfo.notTradedYet=Vous n'avez pas encore échangé avec cet utilisateur. +peerInfo.setTag=Définir un tag pour ce pair +peerInfo.age.noRisk=Âge du compte de paiement +peerInfo.age.chargeBackRisk=Temps depuis la signature +peerInfo.unknownAge=Âge inconnu + +addressTextField.openWallet=Ouvrir votre portefeuille Bitcoin par défaut +addressTextField.copyToClipboard=Copier l'adresse dans le presse-papiers +addressTextField.addressCopiedToClipboard=L'adresse a été copiée dans le presse-papier +addressTextField.openWallet.failed=L'ouverture d'un portefeuille Bitcoin par défaut a échoué. Peut-être que vous n'en avez aucun d'installé? + +peerInfoIcon.tooltip={0}\nTag: {1} + +txIdTextField.copyIcon.tooltip=Copier l'ID de la transaction dans le presse-papiers +txIdTextField.blockExplorerIcon.tooltip=Ouvrir l'explorateur de blockchain avec cet ID de transaction +txIdTextField.missingTx.warning.tooltip=Transaction requise manquante + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Compte\" +navigation.account.walletSeed=\"Compte/Seed du portefeuille\" +navigation.funds.availableForWithdrawal=\"Fonds/Envoyer des fonds\" +navigation.portfolio.myOpenOffers=\"Portfolio/Mes ordres en cours\" +navigation.portfolio.pending=\"Portfolio/Échanges en cours\" +navigation.portfolio.closedTrades=\"Portfolio/Historique\" +navigation.funds.depositFunds=\"Fonds/Recevoir des fonds\" +navigation.settings.preferences=\"Paramètres/Préférences\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Fonds/Transactions\" +navigation.support=\"Assistance\" +navigation.dao.wallet.receive=\"DAO/BSQ Portefeuille/Recevoir\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} montant{1} +formatter.makerTaker=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.fiat=Vous êtes en train de créer un ordre pour {0} {1} +formatter.youAreCreatingAnOffer.altcoin=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 + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (obsolète) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=Année +time.month=Mois +time.week=Semaine +time.day=Jour +time.hour=Heure +time.minute10=10 Minutes +time.hours=heures +time.days=jours +time.1hour=1 heure +time.1day=1 jour +time.minute=minute +time.second=seconde +time.minutes=minutes +time.seconds=secondes + + +password.enterPassword=Entrer le mot de passe +password.confirmPassword=Confirmer le mot de passe +password.tooLong=Le mot de passe doit contenir moins de 500 caractères. +password.deriveKey=Récupérer la clé à partir du mot de passe +password.walletDecrypted=Portefeuille décrypté avec succès et protection par mot de passe désactivée. +password.wrongPw=Vous avez entré un mot de passe incorrect.\n\nVeuillez réessayer d'entrer votre mot de passe, en vérifiant soigneusement qu'il ne contient pas de fautes de frappe ou d'orthographe. +password.walletEncrypted=Portefeuille crypté avec succès et protection par mot de passe activée. +password.walletEncryptionFailed=Le mot de passe du portefeuille n'a pas pu être défini. Il est possible que vous ayiez importé des mots sources qui ne correspondent pas à la base de données du portefeuille. Veuillez contacter les développeurs sur Keybase ([LIEN:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=Les 2 mots de passe entrés ne correspondent pas. +password.forgotPassword=Mot de passe oublié? +password.backupReminder=Veuillez noter que lors de la définition d'un mot de passe de portefeuille, toutes les sauvegardes créées automatiquement à partir du portefeuille non crypté seront supprimées.\n\nIl est fortement recommandé de faire une sauvegarde du répertoire de l'application et d'écrire les mots source avant de définir un mot de passe! +password.backupWasDone=J'ai déjà effectué une sauvegarde +password.setPassword=Saisir le mot de passe (J'ai déjà effectué une sauvegarde) +password.makeBackup=Effectuer une sauvegarde + +seed.seedWords=Mots qui composent la seed du portefeuille +seed.enterSeedWords=Entrer les mots qui composent la seed du portefeuille +seed.date=Date du portefeuille +seed.restore.title=Restaurer les portefeuilles à partir des mots de la seed +seed.restore=Restaurer les portefeuilles +seed.creationDate=Date de création +seed.warn.walletNotEmpty.msg=Votre portefeuille Bitcoin n'est pas vide.\n\nVous devez vider ce portefeuille avant d'essayer de restaurer un portefeuille plus ancien, en effet mélanger les portefeuilles peut entraîner l'invalidation des sauvegardes.\n\nVeuillez finaliser vos trades, fermer toutes vos offres ouvertes et aller dans la section Fonds pour retirer votre Bitcoin.\nDans le cas où vous ne pouvez pas accéder à votre bitcoin, vous pouvez utiliser l'outil d'urgence afin de vider votre portefeuille.\nPour ouvrir l'outil d'urgence, pressez \"alt + e\" ou \"Cmd/Ctrl + e\". +seed.warn.walletNotEmpty.restore=Je veux quand même restaurer. +seed.warn.walletNotEmpty.emptyWallet=Je viderai mes portefeuilles en premier. +seed.warn.notEncryptedAnymore=Vos portefeuilles sont cryptés.\n\nAprès la restauration, les portefeuilles ne seront plus cryptés et vous devrez définir un nouveau mot de passe.\n\nSouhaitez-vous continuer ? +seed.warn.walletDateEmpty=Puisque vous n'avez pas spécifié la date du portefeuille, Bisq devra scanner la blockchain après le 09/10/2013 (date de création du BIP39). \n\nLe portefeuille BIP39 a été lancé pour la première fois sur Bisq le 28/06/2017 (version v0.5). Par conséquent, vous pouvez utiliser cette date pour gagner du temps. \n\nIdéalement, vous devez indiquer la date à laquelle la graine de départ du portefeuille est créée. \n\n\nÊtes-vous sûr de vouloir continuer sans spécifier la date du portefeuille? +seed.restore.success=Portefeuilles restaurés avec succès grâce aux nouveaux mots de la seed.\n\nVous devez arrêter et redémarrer l'application. +seed.restore.error=Une erreur est survenue lors de la restauration des portefeuilles avec les mots composant la seed.{0} +seed.restore.openOffers.warn=Vous avez des offres ouvertes qui seront retirées si vous restaurer à partir des mots sources.\nÊtes-vous sûr de vouloir continuer. + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Compte +payment.account.no=N° de compte +payment.account.name=Nom du compte +payment.account.userName=Nom de l'utilisateur +payment.account.phoneNr=Numéro de téléphone +payment.account.owner=Nom et prénoms du propriétaire du compte +payment.account.fullName=Nom complet (prénom, deuxième prénom, nom de famille) +payment.account.state=État/Département/Région +payment.account.city=Ville +payment.bank.country=Pays de la banque +payment.account.name.email=Nom complet du propriétaire du compte / email +payment.account.name.emailAndHolderId=Nom complet du propriétaire du compte / email / {0} +payment.bank.name=Nom de la banque +payment.select.account=Sélectionner le type de compte +payment.select.region=Sélectionner la région +payment.select.country=Sélectionner le pays +payment.select.bank.country=Sélectionner le pays de la banque +payment.foreign.currency=Êtes-vous sûr de vouloir choisir une devise autre que celle du pays par défaut? +payment.restore.default=Non, restaurer la devise par défaut +payment.email=Email +payment.country=Pays +payment.extras=Exigences particulières +payment.email.mobile=Email ou N° de portable +payment.altcoin.address=Adresse Altcoin +payment.altcoin.tradeInstantCheckbox=Échanger instantanément (en 1 heure) avec cet Altcoin +payment.altcoin.tradeInstant.popup=Pour négocier immédiatement, il est nécessaire que les deux pairs de trading soient en ligne afin de pouvoir effectuer l'échange en moins d' 1 heure.\n\nSi vous avez des ordres en cours et que vous n'êtes pas disponible, veuillez désactiver ces ordres sur la page " Portfolio ". +payment.altcoin=Altcoin +payment.select.altcoin=Sélectionner ou chercher l'Altcoin +payment.secret=Question secrète +payment.answer=Réponse +payment.wallet=ID du portefeuille +payment.amazon.site=Acheter la carte cadeau à +payment.ask=Demander dans le chat de trade +payment.uphold.accountId=Nom d'utilisateur ou email ou N° de téléphone +payment.moneyBeam.accountId=Email ou N° de téléphone +payment.venmo.venmoUserName=Nom d'utilisateur Venmo +payment.popmoney.accountId=Email ou N° de téléphone +payment.promptPay.promptPayId=N° de carte d'identité/d'identification du contribuable ou numéro de téléphone +payment.supportedCurrencies=Devises acceptées +payment.supportedCurrenciesForReceiver=Devises pour reçevoir des fonds +payment.limitations=Restrictions +payment.salt=Salage de la vérification de l'âge des comptes +payment.error.noHexSalt=Le salt doit être au format HEX .\nIl est recommandé de modifier le champ du salt uniquement si vous souhaitez effectuer le transfert d'un salt d'un précédent compte pour conserver l'âge du compte. L'âge du compte est vérifié en utilisant le salt du compte et les données d'identification du compte (par exemple l'IBAN). +payment.accept.euro=Accepter les transactions en provenance de ces pays de la zone Euro +payment.accept.nonEuro=Accepter les transactions en provenance de ces pays hors zone Euro +payment.accepted.countries=Pays acceptés +payment.accepted.banks=Banques acceptées (ID) +payment.mobile=N° de téléphone portable +payment.postal.address=Adresse postale +payment.national.account.id.AR=Numéro CBU +shared.accountSigningState=État de la signature du compte + +#new +payment.altcoin.address.dyn={0} adresse +payment.altcoin.receiver.address=Adresse altcoin du destinataire +payment.accountNr=Numéro de compte +payment.emailOrMobile=Email ou N° de portable +payment.useCustomAccountName=Utiliser un nom de compte personnalisé +payment.maxPeriod=Durée d'échange max. autorisée +payment.maxPeriodAndLimit=Durée maximale de l''échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} +payment.maxPeriodAndLimitCrypto=Durée maximale de trade: {0} / Limite maximale de trading {1} +payment.currencyWithSymbol=Devise: {0} +payment.nameOfAcceptedBank=Nom de la banque acceptée +payment.addAcceptedBank=Ajouter une banque acceptée +payment.clearAcceptedBanks=Supprimer des banques acceptées +payment.bank.nameOptional=Nom de la banque (facultatif) +payment.bankCode=Code de la banque +payment.bankId=Identifiant bancaire (BIC/SWIFT) +payment.bankIdOptional=ID de la banque (BIC/SWIFT) (optionnel): +payment.branchNr=N° de la succursale +payment.branchNrOptional=N° de succursale (facultatif) +payment.accountNrLabel=N° de compte (IBAN) +payment.accountType=Type de compte +payment.checking=Vérification +payment.savings=Épargne +payment.personalId=Pièce d'identité +payment.makeOfferToUnsignedAccount.warning=Avec la récente montée en prix du BTC, soyez informés que vendre 0.01 BTC ou moins cause un risque plus élevé qu'avant.\n\nIl est hautement recommandé de:\n- faire des offres au dessus de 0.01 BTC, ainsi vous traiterez uniquement avec des acheteurs signés/de confiance\n- garder les offres pour vendre en desous de 0.01 BTC à une valeur d'environ 100 USD, cette valeur a (historiquement) découragé les arnaqueurs\n\nLes développeurs de Bisq travaillent sur des meilleurs moyens de sécuriser le modèle de compte de paiement pour des trades plus petits. Rejoignez la discussion ici : [LIEN:https://github.com/bisq-network/bisq/discussions/5339]. +payment.takeOfferFromUnsignedAccount.warning=Avec la récente montée en prix du BTC, soyez informés que vendre 0.01 BTC ou moins cause un risque plus élevé qu'avant.\n\nIl est hautement recommandé de:\n- prendre les offres d'acheteurs signés uniquement\n- garder les offres pour vendre en desous de 0.01 BTC à une valeur d'environ 100 USD, cette valeur a (historiquement) découragé les arnaqueurs\n\nLes développeurs de Bisq travaillent sur des meilleurs moyens de sécuriser le modèle de compte de paiement pour des trades plus petits. Rejoignez la discussion ici : [LIEN:https://github.com/bisq-network/bisq/discussions/5339]. +payment.clearXchange.info=Zelle est un service de transfert d'argent, qui fonctionne bien pour transférer de l'argent vers d'autres banques. \n\n1. Consultez cette page pour voir si (et comment) votre banque coopère avec Zelle: \n[HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Faites particulièrement attention à votre limite de transfert - les limites de versement varient d'une banque à l'autre, et les banques spécifient généralement des limites quotidiennes, hebdomadaires et mensuelles. \n\n3. Si votre banque ne peut pas utiliser Zelle, vous pouvez toujours l'utiliser via l'application mobile Zelle, mais votre limite de transfert sera bien inférieure. \n\n4. Le nom indiqué sur votre compte Bisq doit correspondre à celui du compte Zelle / bancaire. \n\nSi vous ne parvenez pas à réaliser la transaction Zelle comme stipulé dans le contrat commercial, vous risquez de perdre une partie (ou la totalité) de votre marge.\n\nComme Zelle présente un risque élevé de rétrofacturation, il est recommandé aux vendeurs de contacter les acheteurs non signés par e-mail ou SMS pour confirmer que les acheteurs ont le compte Zelle spécifié dans Bisq. +payment.fasterPayments.newRequirements.info=Certaines banques ont déjà commencé à vérifier le nom complet du destinataire du paiement rapide. Votre compte de paiement rapide actuel ne remplit pas le nom complet. \n\nPensez à recréer votre compte de paiement rapide dans Bisq pour fournir un nom complet aux futurs {0} acheteurs. \n\nLors de la recréation d'un compte, assurez-vous de copier l'indicatif bancaire, le numéro de compte et le sel de vérification de l'âge de l'ancien compte vers le nouveau compte. Cela garantira que votre âge du compte et état de signature existant sont conservés. +payment.moneyGram.info=Lors de l'utilisation de MoneyGram, l'acheteur de BTC doit envoyer le numéro d'autorisation et une photo du reçu par email au vendeur de BTC. Le reçu doit clairement mentionner le nom complet du vendeur, le pays, la région et le montant. L'email du vendeur sera donné à l'acheteur durant le processus de transaction. +payment.westernUnion.info=Lors de l'utilisation de Western Union, l'acheteur BTC doit envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de BTC. 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 BTC doit envoyer au vendeur de BTC 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 BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut nécessite le 'Nom d'utilisateur' en tant qu'ID de compte et non pas le numéro de téléphone ou l'email comme ça l'était avant. +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.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 + +payment.usPostalMoneyOrder.info=Pour échanger US Postal Money Orders (USPMO) sur Bisq, vous devez comprendre les termes suivants: \n\n- L'acheteur BTC doit écrire le nom du vendeur BTC dans les champs expéditeur et bénéficiaire, et prendre une photo à haute résolution de USPMO et de l'enveloppe avec une preuve de suivi avant l'envoi. \n\n- L'acheteur BTC doit envoyer USPMO avec la confirmation de livraison au vendeur BTC. \n\nSi une médiation est nécessaire, ou s'il y a un différend de transaction, vous devrez envoyer la photo avec le numéro USPMO, le numéro du bureau de poste et le montant de la transaction au médiateur Bisq ou à l'agent de remboursement afin qu'ils puissent vérifier les détails sur le site web de la poste américaine. \n\nSi vous ne fournissez pas les données de transaction requises, vous perdrez directement dans le différend. \n\nDans tous les cas de litige, l'expéditeur de l'USPMO assume à 100% la responsabilité lors de la fourniture de preuves / certification au médiateur ou à l'arbitre. \n\nSi vous ne comprenez pas ces exigences, veuillez ne pas échanger USPMO sur Bisq. + +payment.cashByMail.info=Le trading en cash-by-mail (CBM) sur Bisq nécessite que vous compreniez ce qui suit:\n\n● L'acheteur de BTC doit emballer l'argent liquide dans un sac d'argent inviolable.\n● L'acheteur de BTC doit filmer ou prendre des photos haute résolution du processus d'emballage en espèces avec l'adresse et le numéro de suivi déjà apposés sur l'emballage.\n● L'acheteur de BTC doit envoyer le colis en espèces au vendeur BTC avec une confirmation de livraison et une assurance appropriée.\n● Le vendeur de BTC doit filmer l'ouverture du colis, en s'assurant que le numéro de suivi fourni par l'expéditeur est visible dans la vidéo.\n● Le maker de l'offre doit indiquer toutes les conditions particulières dans le champ «Informations supplémentaires» du compte de paiement.\n● Le preneur de l'offre accepte les conditions générales du maker en acceptant l'offre.\n\nLes transactions CBM imposent la responsabilité d'agir honnêtement pour les deux pairs.\n\n● Les transactions CBM ont des actions moins vérifiables que les autres transactions Fiat. Cela rend la gestion des litiges beaucoup plus difficile.\n● Essayez de résoudre les litiges directement avec votre pair en utilisant le chat de trade. C'est la voie la plus prometteuse pour résoudre tout litige CBM.\n● Les médiateurs peuvent examiner votre cas et faire une suggestion, mais il n'est PAS garanti qu'ils puissent vous aider.\n● Si un médiateur est engagé et si un des pair rejette la suggestion du médiateur, les fonds des deux pairs seront envoyés à une adresse de «don» Bisq [LIEN:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], et le trade sera effectivement terminé.\n● Si un commerçant rejette une suggestion de médiation et ouvre un arbitrage, cela pourrait entraîner une perte à la fois des fonds de négociation et du dépôt.\n● Les arbitres prendront une décision sur la base des preuves qui leur auront été fournies. Par conséquent, veuillez suivre et rédiger un document sur les processus ci-dessus pour avoir des preuves en cas de litige. Pour les transactions Cash by Mail, la décision des arbitres est définitive.\n● Les demandes de remboursement de fonds perdus résultant de transactions Cash By Mail avec le Bisq DAO ne seront PAS prises en compte.\n\nPour être sûr de bien comprendre les exigences des transactions en espèces par courrier, veuillez consulter:[LIEN:https://bisq.wiki/Cash_by_Mail]\n\nSi vous ne comprenez pas ces exigences, n'échangez pas en utilisant CBM sur Bisq. + +payment.cashByMail.contact=information de contact +payment.cashByMail.contact.prompt=Nom ou nym à qui l'enveloppe devrait être addressée +payment.f2f.contact=information de contact +payment.f2f.contact.prompt=Comment voudriez-vous être contacté par le pair de trading? (addresse mail, numéro de téléphone,...) +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.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 BTC 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.bisq.network/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.japan.bank=Banque +payment.japan.branch=Filiale +payment.japan.account=Compte +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 BTC via votre compte Amazon. \n\nBisq indiquera l'adresse e-mail ou le numéro de téléphone du vendeur BTC 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://bisq.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). + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Virement bancaire national +SAME_BANK=Transfert avec la même banque +SPECIFIC_BANKS=Transferts avec des banques spécifiques +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=Dépôt en espèces +CASH_BY_MAIL=Cash via courrier +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Face à face (en personne) +JAPAN_BANK=Banque japonaise Furikomi +AUSTRALIA_PAYID=PayID australien + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Banques nationales +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Banque identique +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Banque spécifiques +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Dépôt en espèces +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=Cash via courrier +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Furikomi japonais +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=ID de paiement + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=eCarte cadeau Amazon +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Paiements plus rapides +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=eCarte cadeau Amazon +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Un champ vide n'est pas autorisé. +validation.NaN=La valeur saisie n'est pas un nombre valide. +validation.notAnInteger=Input is not an integer value. +validation.zero=La saisie d'une valeur égale à 0 n'est pas autorisé. +validation.negative=Une valeur négative n'est pas autorisée. +validation.fiat.toSmall=La saisie d'une valeur plus petite que le montant minimal possible n'est pas autorisée. +validation.fiat.toLarge=La saisie d'une valeur supérieure au montant maximal possible n'est pas autorisée. +validation.btc.fraction=L'entrée résultera dans une valeur bitcoin plus petite qu'1 satoshi +validation.btc.toLarge=La saisie d''une valeur supérieure à {0} n''est pas autorisée. +validation.btc.toSmall=La saisie d''une valeur inférieure à {0} n''est pas autorisée. +validation.passwordTooShort=Le mot de passe que vous avez saisi est trop court. Il doit comporter un minimum de 8 caractères. +validation.passwordTooLong=Le mot de passe que vous avez saisi est trop long. Il ne doit pas contenir plus de 50 caractères. +validation.sortCodeNumber={0} doit être composer de {1} chiffres. +validation.sortCodeChars={0} doit être composer de {1} caractères. +validation.bankIdNumber={0} doit être composer de {1} chiffres. +validation.accountNr=Le numéro du compte doit comporter {0} chiffres. +validation.accountNrChars=Le numéro du compte doit comporter {0} caractères. +validation.btc.invalidAddress=L''adresse n''est pas correcte. Veuillez vérifier le format de l''adresse. +validation.integerOnly=Veuillez seulement entrer des nombres entiers. +validation.inputError=Votre saisie a causé une erreur:\n{0} +validation.bsq.insufficientBalance=Votre solde disponible est {0}. +validation.btc.exceedsMaxTradeLimit=Votre seuil maximum d''échange est {0}. +validation.bsq.amountBelowMinAmount=Le montant minimal est {0} +validation.nationalAccountId={0} doit être composé de {1} nombres. + +#new +validation.invalidInput=La valeur saisie est invalide: {0} +validation.accountNrFormat=Le numéro du compte doit être au format: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=La validation de l''adresse a échoué car elle ne concorde pas avec la structure d''une adresse {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=L'adresse LTZ doit commencer par L. Les adresses commençant par z ne sont pas supportées. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=L'adresse ZEC doit commencer par un t. Les adresses commençant par un z ne sont pas supportées. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Address is not a valid {0} address! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Les addresses segwit natives (celles qui commences par 'lq') ne sont pas supportées. +validation.bic.invalidLength=La longueur de l'entrée doit être de 8 ou 11 +validation.bic.letters=Le code de la banque et le code du pays doivent être constitués de lettres +validation.bic.invalidLocationCode=Le BIC contient un code de localisation invalide +validation.bic.invalidBranchCode=Le BIC contient un branch code invalide. +validation.bic.sepaRevolutBic=Les comptes Sepa de Revolut ne sont pas pris en charge. +validation.btc.invalidFormat=Format invalide pour une addresse Bitcoin. +validation.bsq.invalidFormat=Format invalide pour une addresse BSQ. +validation.email.invalidAddress=Adresse invalide +validation.iban.invalidCountryCode=Code du pays invalide +validation.iban.checkSumNotNumeric=La checksum doit être numérique +validation.iban.nonNumericChars=Caractère non-alphanumérique détecté +validation.iban.checkSumInvalid=La checksum de l'IBAN n'est pas valide +validation.iban.invalidLength=Le nombre doit avoir une longueur de 15 à 34 caractères. +validation.interacETransfer.invalidAreaCode=Indicatif régional non Canadien +validation.interacETransfer.invalidPhone=Veuillez entrer un numéro de téléphone valide à 11 chiffres (par exemple 1-123-456-7890) ou une addresse email +validation.interacETransfer.invalidQuestion=Ne doit contenir que des lettres, des chiffres, des espaces et/ou les symboles ' _ , . ? - +validation.interacETransfer.invalidAnswer=Doit être composé d'un seul mot et contenir que des lettres, des chiffres et/ou le symbole - +validation.inputTooLarge=La valeur saisie ne doit pas être supérieure à {0} +validation.inputTooSmall=La valeur saisie doit être supérieure à {0} +validation.inputToBeAtLeast=La valeur saisie doit être au minimum {0} +validation.amountBelowDust=Un montant en-dessous de la limite de dust de {0} satoshi nest pas autorisé. +validation.length=La longueur doit être comprise entre {0} et {1} +validation.fixedLength=La longueur doit être de {0} +validation.pattern=La valeur saisie doit être au format: {0} +validation.noHexString=La valeur saisie n'est pas au format HEX. +validation.advancedCash.invalidFormat=Doit être un email valide ou un identifiant de portefeuille de type: X000000000000 +validation.invalidUrl=Ceci n'est pas une URL valide +validation.mustBeDifferent=Votre saisie doit être différente de la valeur actuelle. +validation.cannotBeChanged=Le paramètre ne peut pas être modifié +validation.numberFormatException=Number format exception {0} +validation.mustNotBeNegative=La saisie ne doit pas être négative +validation.phone.missingCountryCode=Un code pays à deux lettres est nécessaire pour valider le numéro de téléphone +validation.phone.invalidCharacters=Le numéro de téléphone {0} contient des caractères invalides. +validation.phone.insufficientDigits=Il n'y a pas assez de chiffres dans {0} pour être un numéro de téléphone valide +validation.phone.tooManyDigits=Il y'a trop de chiffres dans {0} pour être un numéro de téléphone valide +validation.phone.invalidDialingCode=L'indicatif de pays du numéro {0} est invalide pour le pays {1}. Le bon indicatif est {2}. +validation.invalidAddressList=Doit être une liste d'addresses valide séparées par des virgules diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties new file mode 100644 index 0000000000..bcf2863926 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Leggi di più +shared.openHelp=Apri la Guida +shared.warning=Attenzione +shared.close=Chiudi +shared.cancel=Annulla +shared.ok=OK +shared.yes=Si +shared.no=No +shared.iUnderstand=Capisco +shared.na=N/A +shared.shutDown=Spegni +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=Acquista bitcoin +shared.sellBitcoin=Vendi bitcoin +shared.buyCurrency=Acquista {0} +shared.sellCurrency=Vendi {0} +shared.buyingBTCWith=acquistando BTC con {0} +shared.sellingBTCFor=vendendo BTC per {0} +shared.buyingCurrency=comprando {0} (vendendo BTC) +shared.sellingCurrency=vendendo {0} (comprando BTC) +shared.buy=compra +shared.sell=vendi +shared.buying=comprando +shared.selling=vendendo +shared.P2P=P2P +shared.oneOffer=offerta +shared.multipleOffers=offerte +shared.Offer=Offerta +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=offerte aperte +shared.trade=scambio +shared.trades=scambi +shared.openTrades=scambi aperti +shared.dateTime=Data/Ora +shared.price=Prezzo +shared.priceWithCur=Prezzo in {0} +shared.priceInCurForCur=Prezzo in {0} per 1 {1} +shared.fixedPriceInCurForCur=Prezzo fissato in {0} per 1 {1} +shared.amount=Importo +shared.txFee=Commissioni di Transazione +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Deposito Acquirente +shared.sellerSecurityDeposit=Deposito Venditore +shared.amountWithCur=Importo in {0} +shared.volumeWithCur=Volume in {0} +shared.currency=Valuta +shared.market=Mercato +shared.deviation=Deviation +shared.paymentMethod=Metodo di pagamento +shared.tradeCurrency=Valuta di scambio +shared.offerType=Tipo di offerta +shared.details=Dettagli +shared.address=Indirizzo +shared.balanceWithCur=Saldo in {0} +shared.utxo=Unspent transaction output +shared.txId=ID Transazione +shared.confirmations=Conferme +shared.revert=Storno Tx +shared.select=Seleziona +shared.usage=Utilizzo +shared.state=Stato +shared.tradeId=ID Scambio +shared.offerId=ID Offerta +shared.bankName=Nome Banca +shared.acceptedBanks=Banche accettate +shared.amountMinMax=Importo (min - max) +shared.amountHelp=Se un'offerta ha un valore massimo e minimo definito, puoi scambiare qualsiasi valore compreso in questo range +shared.remove=Rimuovi +shared.goTo=Vai a {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Rimuovi offerta +shared.dontRemoveOffer=Non rimuovere offerta +shared.editOffer=Modifica offerta +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=Account di scambio +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 Bisq +shared.fundFromExternalWalletButton=Apri il tuo portafoglio esterno per aggiungere fondi +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=Sotto % del prezzo di mercato +shared.aboveInPercent=Sopra % del prezzo di mercato +shared.enterPercentageValue=Immetti il valore % +shared.OR=OPPURE +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=In attesa dei fondi... +shared.depositTransactionId=ID transazione di deposito +shared.TheBTCBuyer=L'acquirente di BTC +shared.You=Tu +shared.sendingConfirmation=Invio della conferma in corso... +shared.sendingConfirmationAgain=Invia nuovamente la conferma +shared.exportCSV=Export to CSV +shared.exportJSON=Esporta in JSON +shared.summary=Show summary +shared.noDateAvailable=Nessuna data disponibile +shared.noDetailsAvailable=Dettagli non disponibili +shared.notUsedYet=Non ancora usato +shared.date=Data +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.copyToClipboard=Copia negli appunti +shared.language=Lingua +shared.country=Paese +shared.applyAndShutDown=Applica e chiudi +shared.selectPaymentMethod=Seleziona il metodo di pagamento +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Vuoi davvero cancellare l'account selezionato? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=Non ci sono ancora account impostati +shared.manageAccounts=Gestisci gli account +shared.addNewAccount=Aggiungi nuovo account +shared.ExportAccounts=Esporta Account +shared.importAccounts=Importa Account +shared.createNewAccount=Crea nuovo account +shared.saveNewAccount=Salva nuovo account +shared.selectedAccount=Account selezionato +shared.deleteAccount=Elimina account +shared.errorMessageInline=\nMessaggio di errore: {0} +shared.errorMessage=Messaggio di errore +shared.information=Informazione +shared.name=Nome +shared.id=ID +shared.dashboard=Dashboard +shared.accept=Accetta +shared.balance=Saldo +shared.save=Salva +shared.onionAddress=Indirizzo onion +shared.supportTicket=ticket di supporto +shared.dispute=disputa +shared.mediationCase=caso di mediazione +shared.seller=venditore +shared.buyer=acquirente +shared.allEuroCountries=Tutti i paesi Euro +shared.acceptedTakerCountries=Paesi accettati dall'acquirente +shared.tradePrice=Prezzo di scambio +shared.tradeAmount=Importo dello scambio +shared.tradeVolume=Volume di scambio +shared.invalidKey=La chiave inserita non è valida. +shared.enterPrivKey=Inserisci la chiave privata per sbloccare +shared.makerFeeTxId=ID Transazione della tassa del creatore dell'offerta +shared.takerFeeTxId=ID Transazione della commissione del creatore dell'offerta +shared.payoutTxId=ID transazione di pagamento +shared.contractAsJson=Contratto in formato JSON +shared.viewContractAsJson=Visualizza il contratto in formato JSON +shared.contract.title=Contratto per lo scambio con ID: {0} +shared.paymentDetails=Dettagli pagamento BTC {0}: +shared.securityDeposit=Deposito di sicurezza +shared.yourSecurityDeposit=Il tuo deposito di sicurezza +shared.contract=Contratto +shared.messageArrived=Messaggio arrivato. +shared.messageStoredInMailbox=Messaggio salvato nella posta. +shared.messageSendingFailed=Invio del messaggio fallito. Errore: {0} +shared.unlock=Sblocca +shared.toReceive=per ricevere +shared.toSpend=da spendere +shared.btcAmount=Importo BTC +shared.yourLanguage=Le tue lingue +shared.addLanguage=Aggiungi lingua +shared.total=Totale +shared.totalsNeeded=Fondi richiesti +shared.tradeWalletAddress=Indirizzo del portafoglio per gli scambi +shared.tradeWalletBalance=Saldo del portafogli per gli scambi +shared.makerTxFee=Maker: {0} +shared.takerTxFee=Taker: {0} +shared.iConfirm=Confermo +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Aperti {0} +shared.fiat=Fiat +shared.crypto=Crypto +shared.all=Tutti +shared.edit=Modifica +shared.advancedOptions=Opzioni avanzate +shared.interval=Intervallo +shared.actions=Azioni +shared.buyerUpperCase=Acquirente +shared.sellerUpperCase=Venditore +shared.new=NUOVO +shared.blindVoteTxId=ID transazione voto cieco +shared.proposal=Proposta +shared.votes=Voti +shared.learnMore=Leggi di più +shared.dismiss=Chiudi +shared.selectedArbitrator=Arbitro selezionato +shared.selectedMediator=Mediatore selezionato +shared.selectedRefundAgent=Arbitro selezionato +shared.mediator=Mediatore +shared.arbitrator=Arbitro +shared.refundAgent=Arbitro +shared.refundAgentForSupportStaff=Agente di rimborso +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=Al momento, hai troppe transazioni non confermate. Per favore riprova più tardi. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Mercato +mainView.menu.buyBtc=Compra BTC +mainView.menu.sellBtc=Vendi BTC +mainView.menu.portfolio=Portafoglio +mainView.menu.funds=Fondi +mainView.menu.support=Supporto +mainView.menu.settings=Impostazioni +mainView.menu.account=Account +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Prezzo di mercato per {0} +mainView.marketPrice.bisqInternalPrice=Prezzo dell'ultimo scambio su Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=Non è disponibile alcun prezzo di mercato da fornitori terzi di feed dei prezzi.\nIl prezzo visualizzato è l'ultimo prezzo di scambio su Bisq per quella valuta. +mainView.marketPrice.tooltip=Il prezzo di mercato é fornito da {0}{1}\nUltimo aggiornamento: {2}\nURL del nodo del provider: {3} +mainView.balance.available=Saldo disponibile +mainView.balance.reserved=Riservati nelle offerte +mainView.balance.locked=Bloccati in scambi +mainView.balance.reserved.short=Riservati +mainView.balance.locked.short=Bloccati + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Connessione alla rete Bitcoin +mainView.footer.bsqInfo.synchronizing=/ Sincronizzando DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Connessione a +mainView.footer.btcInfo.connectionFailed=Connessione fallita +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=Nodo completo DAO + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Connessione alla rete Tor... +mainView.bootstrapState.torNodeCreated=(2/4) Nodo Tor creato +mainView.bootstrapState.hiddenServicePublished=(3/4) Servizio Nascosto pubbblicato +mainView.bootstrapState.initialDataReceived=(4/4) Dati iniziali ricevuti + +mainView.bootstrapWarning.noSeedNodesAvailable=Nessun nodo seme disponibile +mainView.bootstrapWarning.noNodesAvailable=Nessun nodo seme e peer disponibili +mainView.bootstrapWarning.bootstrappingToP2PFailed=Il bootstrap sulla rete Bisq non è riuscito + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Non ci sono nodi seed o peer persistenti disponibili per la richiesta di dati.\nControlla la tua connessione Internet o prova a riavviare l'applicazione. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connessione alla rete Bisq non riuscita (errore segnalato: {0}).\nControlla la tua connessione Internet o prova a riavviare l'applicazione. + +mainView.walletServiceErrorMsg.timeout=Connessione alla rete Bitcoin fallita a causa di un timeout. +mainView.walletServiceErrorMsg.connectionError=Connessione alla rete Bitcoin fallita a causa di un errore: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Una transazione è stata rifiutata dalla rete.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Hai perso la connessione a tutti i {0} peer di rete.\nForse hai perso la connessione a Internet o il computer era in modalità standby. +mainView.networkWarning.localhostBitcoinLost=Hai perso la connessione al nodo Bitcoin in localhost.\nRiavvia l'applicazione Bisq per connetterti ad altri nodi Bitcoin o riavvia il nodo Bitcoin in localhost. +mainView.version.update=(Aggiornamento disponibile) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Registro offerte +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Scambi + +# OfferBookChartView +market.offerBook.buyAltcoin=Compra {0} (vendi {1}) +market.offerBook.sellAltcoin=Vendi {0} (compra {1}) +market.offerBook.buyWithFiat=Acquista {0} +market.offerBook.sellWithFiat=Vendi {0} +market.offerBook.sellOffersHeaderLabel=Vendi {0} a +market.offerBook.buyOffersHeaderLabel=Compra {0} da +market.offerBook.buy=Voglio comprare bitcoin +market.offerBook.sell=Voglio vendere bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Tutte le offerte ({0}) +market.spread.numberOfBuyOffersColumn=Acquista BTC ({0}) +market.spread.numberOfSellOffersColumn=Vendi BTC ({0}) +market.spread.totalAmountColumn=Totale BTC ({0}) +market.spread.spreadColumn=Spread +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Scambi: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Aperti: +market.trades.tooltip.candle.close=Chiusi: +market.trades.tooltip.candle.high=Alto: +market.trades.tooltip.candle.low=Basso: +market.trades.tooltip.candle.average=Media: +market.trades.tooltip.candle.median=Mediana: +market.trades.tooltip.candle.date=Data: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +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.trader=Trader +offerbook.offerersBankId=ID banca del Maker (BIC/SWIFT): {0} +offerbook.offerersBankName=Nome della banca del Maker: {0} +offerbook.offerersBankSeat=Sede del paese bancario del Maker: {0} +offerbook.offerersAcceptedBankSeatsEuro=Sede accettata dei paesi della banca (acquirente): tutti i paesi dell'Euro +offerbook.offerersAcceptedBankSeats=Sede accettata dei paesi bancari (acquirente):\n  {0} +offerbook.availableOffers=Offerte disponibili +offerbook.filterByCurrency=Filtra per valuta +offerbook.filterByPaymentMethod=Filtra per metodo di pagamento +offerbook.matchingOffers=Offers matching my accounts +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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=firmato da un peer e i limiti sono stati alzati +offerbook.timeSinceSigning.info.signer=firmato da un peer e può firmare account peer (limiti alzati) +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.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 +offerbook.timeSinceSigning.notSigned.ageDays={0} giorni +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=N. di offerte: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Crea una nuova offerta per comprare {0} +offerbook.createOfferToSell=Crea una nuova offerta per vendere {0} +offerbook.createOfferToBuy.withFiat=Crea una nuova offerta per acquistare {0} con {1} +offerbook.createOfferToSell.forFiat=Crea una nuova offerta per vendere {0} per {1} +offerbook.createOfferToBuy.withCrypto=Crea una nuova offerta per vendere {0} (acquista {1}) +offerbook.createOfferToSell.forCrypto=Crea una nuova offerta per acquistare {0} (vendi {1}) + +offerbook.takeOfferButton.tooltip=Accetta offera per {0} +offerbook.yesCreateOffer=Sì, crea offerta +offerbook.setupNewAccount=Imposta un nuovo account di scambio +offerbook.removeOffer.success=L'offerta è stata rimossa con successo. +offerbook.removeOffer.failed=Rimozione offerta fallita:\n{0} +offerbook.deactivateOffer.failed=Disattivazione dell'offerta fallita:\n{0} +offerbook.activateOffer.failed=Pubblicazione dell'offerta fallita:\n{0} +offerbook.withdrawFundsHint=Puoi ritirare i fondi versati dalla schermata {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=Questa offerta non può essere accettata a causa di restrizioni di scambio della controparte + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=L'importo di scambio consentito è limitato a {0} a causa delle restrizioni di sicurezza basate sui seguenti criteri:\n- L'account dell'acquirente non è stato firmato da un arbitro o da un pari\n- Il tempo trascorso dalla firma dell'account dell'acquirente non è di almeno 30 giorni\n- Il metodo di pagamento per questa offerta è considerato rischioso per le richieste di storno bancarie\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=L'importo di scambio consentito è limitato a {0} a causa delle restrizioni di sicurezza basate sui seguenti criteri:\n- Il tuo account non è stato firmato da un arbitro o da un pari\n- Il tempo trascorso dalla firma del tuo account non è di almeno 30 giorni\n- Il metodo di pagamento per questa offerta è considerato rischioso per le richieste di storno bancarie\n\n{1} + +offerbook.warning.wrongTradeProtocol=Questa offerta richiede una versione di protocollo diversa da quella utilizzata nella versione del tuo software.\n\nVerifica di aver installato l'ultima versione, altrimenti l'utente che ha creato l'offerta ha utilizzato una versione precedente.\n\nGli utenti non possono effettuare scambi con una versione di protocollo di scambio incompatibile. +offerbook.warning.userIgnored=Hai aggiunto l'indirizzo onion dell'utente al tuo elenco di persone da ignorare. +offerbook.warning.offerBlocked=Tale offerta è stata bloccata dagli sviluppatori Bisq.\nProbabilmente c'è un bug non gestito che causa problemi quando si accetta tale offerta. +offerbook.warning.currencyBanned=La valuta utilizzata in quell'offerta è stata bloccata dagli sviluppatori Bisq.\nPer ulteriori informazioni, visitare il forum di Bisq. +offerbook.warning.paymentMethodBanned=Il metodo di pagamento utilizzato in quell'offerta è stato bloccato dagli sviluppatori Bisq.\nPer ulteriori informazioni, visitare il forum di Bisq. +offerbook.warning.nodeBlocked=L'indirizzo onion di quel trader è stato bloccato dagli sviluppatori Bisq.\nProbabilmente c'è un bug non gestito che causa problemi quando si accettano offerte da quel trader. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=Venderai al prezzo di mercato (aggiornato ogni minuto). +offerbook.info.buyAtMarketPrice=Acquisterai al prezzo di mercato (aggiornato ogni minuto). +offerbook.info.sellBelowMarketPrice=Otterrai {0} in meno del prezzo di mercato corrente (aggiornato ogni minuto). +offerbook.info.buyAboveMarketPrice=Pagherai {0} in più rispetto all'attuale prezzo di mercato (aggiornato ogni minuto). +offerbook.info.sellAboveMarketPrice=Otterrai {0} in più rispetto all'attuale prezzo di mercato (aggiornato ogni minuto). +offerbook.info.buyBelowMarketPrice=Pagherai {0} in meno del prezzo di mercato corrente (aggiornato ogni minuto). +offerbook.info.buyAtFixedPrice=Comprerai a questo prezzo fisso. +offerbook.info.sellAtFixedPrice=Venderai a questo prezzo fisso. +offerbook.info.noArbitrationInUserLanguage=In caso di disputa, si ricorda che l'arbitrato per questa offerta verrà gestito in {0}. La lingua è attualmente impostata su {1}. +offerbook.info.roundedFiatVolume=L'importo è stato arrotondato per aumentare la privacy del tuo scambio. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Inserisci quantità in BTC +createOffer.price.prompt=Inserisci prezzo +createOffer.volume.prompt=Inserisci importo in {0} +createOffer.amountPriceBox.amountDescription=Quantità di BTC a {0} +createOffer.amountPriceBox.buy.volumeDescription=Quantità in {0} da spendere +createOffer.amountPriceBox.sell.volumeDescription=Quantità in {0} da ricevere +createOffer.amountPriceBox.minAmountDescription=Quantità minima di BTC +createOffer.securityDeposit.prompt=Deposito di sicurezza +createOffer.fundsBox.title=Finanzia la tua offerta +createOffer.fundsBox.offerFee=Commissione di scambio +createOffer.fundsBox.networkFee=Commissione di mining +createOffer.fundsBox.placeOfferSpinnerInfo=Pubblicazione dell'offerta in corso ... +createOffer.fundsBox.paymentLabel=Scambio Bisq con ID {0} +createOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione di scambio, {2} commissione di mining) +createOffer.fundsBox.fundsStructure.BSQ=({0} deposito cauzionale, {1} commissione di mining) + {2} commissione di scambio +createOffer.success.headline=La tua offerta è stata pubblicata +createOffer.success.info=Puoi gestire le tue offerte aperte su \"Portafoglio/Le mie offerte aperte\". +createOffer.info.sellAtMarketPrice=Venderai sempre al prezzo di mercato poiché il prezzo della tua offerta verrà continuamente aggiornato. +createOffer.info.buyAtMarketPrice=Acquisterai sempre al prezzo di mercato poiché il prezzo della tua offerta verrà costantemente aggiornato. +createOffer.info.sellAboveMarketPrice=Otterrai sempre il {0}% in più rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. +createOffer.info.buyBelowMarketPrice=Pagherai sempre il {0}% in meno rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. +createOffer.warning.sellBelowMarketPrice=Otterrai sempre il {0}% in meno rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. +createOffer.warning.buyAboveMarketPrice=Pagherai sempre il {0}% in più rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. +createOffer.tradeFee.descriptionBTCOnly=Commissione di scambio +createOffer.tradeFee.descriptionBSQEnabled=Seleziona la valuta della commissione di scambio + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=Revisione: piazza l'offerta a {0} bitcoin +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 Bisq (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. + +# 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. +createOffer.setAmountPrice=Imposta importo e prezzo +createOffer.warnCancelOffer=Hai già finanziato questa offerta.\nSe annulli ora, i tuoi fondi verranno spostati sul tuo portafoglio Bisq locale e saranno disponibili per il prelievo nella schermata "Fondi/Invia fondi".\nSei sicuro di voler annullare? +createOffer.timeoutAtPublishing=Si è verificato un timeout durante la pubblicazione dell'offerta. +createOffer.errorInfo=\n\nLa maker fee è già pagata. Nel peggiore dei casi hai perso quella commissione.\nProva a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. +createOffer.tooLowSecDeposit.warning=Il deposito cauzionale è stato impostato su un valore inferiore rispetto al valore predefinito consigliato di {0}.\nSei sicuro di voler utilizzare un deposito di sicurezza inferiore? +createOffer.tooLowSecDeposit.makerIsSeller=Ti garantisce una minore protezione nel caso in cui il peer di scambio non segua il protocollo di negoziazione. +createOffer.tooLowSecDeposit.makerIsBuyer=Offre una protezione minore per al peer di trading che con cui commerci poiché hai meno depositi a rischio. Altri utenti potrebbero preferire altre offerte anziché le tue. +createOffer.resetToDefault=No, ripristina il valore predefinito +createOffer.useLowerValue=Sì, usa il mio valore più basso +createOffer.priceOutSideOfDeviation=Il prezzo che hai inserito è al di fuori del massimo consentito dalla deviazione dal prezzo di mercato.\nIl massimo consentito per la deviazione è {0} e può essere regolato nelle preferenze. +createOffer.changePrice=Cambia prezzo +createOffer.tac=Con la pubblicazione di questa offerta, accetto di negoziare con qualsiasi operatore che soddisfi le condizioni definite in questa schermata. +createOffer.currencyForFee=Commissione di scambio +createOffer.setDeposit=Imposta il deposito cauzionale dell'acquirente (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Inserisci importo in BTC +takeOffer.amountPriceBox.buy.amountDescription=Importo di BTC da vendere +takeOffer.amountPriceBox.sell.amountDescription=Importo di BTC da acquistare +takeOffer.amountPriceBox.priceDescription=Prezzo per bitcoin in {0} +takeOffer.amountPriceBox.amountRangeDescription=Range di importo possibile +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=L'importo che hai inserito supera il numero di decimali permessi.\nL'importo è stato regolato a 4 decimali. +takeOffer.validation.amountSmallerThanMinAmount=L'importo non può essere più piccolo dell'importo minimo definito nell'offerta. +takeOffer.validation.amountLargerThanOfferAmount=L'importo inserito non può essere più alto dell'importo definito nell'offerta. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Questo importo inserito andrà a creare un resto di basso valore per il venditore di BTC. +takeOffer.fundsBox.title=Finanzia il tuo scambio +takeOffer.fundsBox.isOfferAvailable=Controlla se l'offerta è disponibile ... +takeOffer.fundsBox.tradeAmount=Importo da vendere +takeOffer.fundsBox.offerFee=Commissione di scambio +takeOffer.fundsBox.networkFee=Totale commissioni di mining +takeOffer.fundsBox.takeOfferSpinnerInfo=Accettazione dell'offerta in corso ... +takeOffer.fundsBox.paymentLabel=Scambia Bisq con ID {0} +takeOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione commerciale, {2} commissione mineraria) +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} + +# new entries +takeOffer.takeOfferButton=Rivedi: Accetta l'offerta per {0} bitcoin +takeOffer.noPriceFeedAvailable=Non puoi accettare questa offerta poiché utilizza un prezzo in percentuale basato sul prezzo di mercato ma non è disponibile alcun feed di prezzi. +takeOffer.takeOfferFundWalletInfo.headline=Finanzia il tuo scambio +# suppress inspection "TrailingSpacesInProperty" +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 Bisq (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.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 +takeOffer.alreadyFunded.askCancel=Hai già finanziato questa offerta.\nSe annulli ora, i tuoi fondi verranno spostati sul tuo portafoglio Bisq locale e saranno disponibili per il prelievo nella schermata "Fondi/Invia fondi".\nSei sicuro di voler annullare? +takeOffer.failed.offerNotAvailable=Accettazione dell'offerta non riuscita perché l'offerta non è più disponibile. Nel frattempo, un altro trader potrebbe aver già accettato l'offerta. +takeOffer.failed.offerTaken=Non puoi accettare questa offerta perché l'offerta è già stata presa da un altro trader. +takeOffer.failed.offerRemoved=Non puoi accettare quell'offerta perché nel frattempo l'offerta è stata rimossa. +takeOffer.failed.offererNotOnline=Richiesta di accettazione dell'ooferta non riuscita perché il maker non è più online. +takeOffer.failed.offererOffline=Non puoi accettare l'offerta poiché chi l'ha formulata è offline. +takeOffer.warning.connectionToPeerLost=Hai perso la connessione con il maker.\nPotrebbe essersi scollegato o aver chiuso la connessione verso di te a causa di troppe connessioni aperte.\nSe riesci ancora a vedere l'offerta nel registro offerte, puoi provare a riprenderla. + +takeOffer.error.noFundsLost=\n\nNon è ancora uscito alcun fondo dal tuo portafoglio.\nProva a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nLa transazione di deposito è già stata pubblicata.\nProva a riavviare l'applicazione e verifica la connessione di rete per cercare di risolvere il problema.\nSe il problema persiste, contatta gli sviluppatori per ricevere supporto. +takeOffer.error.payoutPublished=\n\nLa transazione di pagamento è già stata pubblicata.\nProva a riavviare l'applicazione e verifica la connessione di rete per cercare di risolvere il problema.\nSe il problema persiste, contatta gli sviluppatori per ricevere supporto. +takeOffer.tac=Accettando questa offerta, accetto le condizioni commerciali definite in questa schermata. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Prezzo di attivazione +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=Imposta prezzo +editOffer.confirmEdit=Conferma: modifica offerta +editOffer.publishOffer=Pubblica la tua offerta. +editOffer.failed=Modifica dell'offerta fallita:\n{0} +editOffer.success=La tua offerta è stata modificata con successo. +editOffer.invalidDeposit=Il deposito di sicurezza dell'acquirente non rientra nei vincoli definiti dalla DAO di Bisq e non può più essere modificato. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Le mie offerte aperte +portfolio.tab.pendingTrades=Scambi aperti +portfolio.tab.history=Storia +portfolio.tab.failed=Fallita +portfolio.tab.editOpenOffer=Modifica offerta + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Attendi la conferma della blockchain +portfolio.pending.step2_buyer.startPayment=Inizia il pagamento +portfolio.pending.step2_seller.waitPaymentStarted=Attendi fino all'avvio del pagamento +portfolio.pending.step3_buyer.waitPaymentArrived=Attendi fino all'arrivo del pagamento +portfolio.pending.step3_seller.confirmPaymentReceived=Conferma la ricezione del pagamento +portfolio.pending.step5.completed=Completato + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Trasferisci dal tuo portafoglio esterno {0}\n{1} al venditore BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Vai in banca e paga {0} al venditore BTC.\n\n +portfolio.pending.step2_buyer.cash.extra=REQUISITI IMPORTANTI:\nDopo aver effettuato il pagamento scrivi sulla ricevuta cartacea: NESSUN RIMBORSO.\nQuindi strappalo in 2 parti, fai una foto e inviala all'indirizzo e-mail del venditore BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Si prega di pagare {0} al venditore BTC utilizzando MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=REQUISITO IMPORTANTE:\nDopo aver effettuato il pagamento, invia il numero di autorizzazione e una foto della ricevuta via e-mail al venditore BTC.\nLa ricevuta deve mostrare chiaramente il nome completo, il paese, lo stato e l'importo del venditore. L'email del venditore è: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Si prega di pagare {0} al venditore BTC utilizzando Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDopo aver effettuato il pagamento, invia l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail al venditore BTC.\nLa ricevuta deve mostrare chiaramente il nome completo, la città, il paese e l'importo del venditore. L'email del venditore è: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunitense\" al venditore BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Contatta il venditore BTC tramite il contatto fornito e organizza un incontro per pagare {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Inizia il pagamento utilizzando {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Importo da trasferire +portfolio.pending.step2_buyer.sellersAddress=Indirizzo {0} del venditore +portfolio.pending.step2_buyer.buyerAccount=Il tuo conto di pagamento da utilizzare +portfolio.pending.step2_buyer.paymentStarted=Il pagamento è iniziato +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=Non hai ancora effettuato il tuo pagamento {0}!\nSi prega di notare che lo scambio è stato completato da {1}. +portfolio.pending.step2_buyer.openForDispute=Non hai completato il pagamento!\nÈ trascorso il massimo periodo di scambio. Si prega di contattare il mediatore per assistenza. +portfolio.pending.step2_buyer.paperReceipt.headline=Hai inviato la ricevuta cartacea al venditore BTC? +portfolio.pending.step2_buyer.paperReceipt.msg=Ricorda:\nDevi scrivere sulla ricevuta cartacea: NESSUN RIMBORSO.\nQuindi strappala in 2 parti, fai una foto e inviala all'indirizzo e-mail del venditore BTC. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Invia numero di autorizzazione e ricevuta +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=È necessario inviare il numero di Autorizzazione e una foto della ricevuta via e-mail al venditore BTC.\nLa ricevuta deve indicare chiaramente il nome completo, il paese, lo stato e l'importo del venditore. L'email del venditore è: {0}.\n\nHai inviato il numero di Autorizzazione e il contratto al venditore? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Invia MTCN e ricevuta +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Devi inviare l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail al venditore BTC.\nLa ricevuta deve indicare chiaramente il nome completo, la città, il paese e l'importo del venditore. L'email del venditore è: {0}.\n\nHai inviato l'MTCN e la ricevuta al venditore? +portfolio.pending.step2_buyer.halCashInfo.headline=Invia il codice HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=È necessario inviare un messaggio di testo con il codice HalCash e l'ID dello scambio ({0}) al venditore BTC.\nIl numero di cellulare del venditore è {1}.\n\nHai inviato il codice al venditore? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Alcune banche potrebbero richiedere il nome del destinatario. Gli account di Pagamento Veloci creati dai vecchi client Bisq non forniscono il nome del destinatario, quindi (se necessario) utilizza la chat dell scambio per fartelo comunicare. +portfolio.pending.step2_buyer.confirmStart.headline=Conferma di aver avviato il pagamento +portfolio.pending.step2_buyer.confirmStart.msg=Hai avviato il pagamento {0} al tuo partner commerciale? +portfolio.pending.step2_buyer.confirmStart.yes=Sì, ho avviato il pagamento +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=In attesa del pagamento +portfolio.pending.step2_seller.f2fInfo.headline=Informazioni di contatto dell'acquirente +portfolio.pending.step2_seller.waitPayment.msg=La transazione di deposito necessita di almeno una conferma blockchain.\nDevi attendere fino a quando l'acquirente BTC invia il pagamento {0}. +portfolio.pending.step2_seller.warn=L'acquirente BTC non ha ancora effettuato il pagamento {0}.\nDevi aspettare fino a quando non invia il pagamento.\nSe lo scambio non sarà completato il {1}, l'arbitro comincierà ad indagare. +portfolio.pending.step2_seller.openForDispute=L'acquirente BTC non ha ancora inviato il pagamento!\nIl periodo massimo consentito per lo scambio è trascorso.\nPuoi aspettare più a lungo e dare più tempo al partner di scambio oppure puoi contattare il mediatore per ricevere assistenza. +tradeChat.chatWindowTitle=Finestra di chat per scambi con ID '' {0} '' +tradeChat.openChat=Apri la finestra di chat +tradeChat.rules=Puoi comunicare con il tuo peer di trading per risolvere potenziali problemi con questo scambio.\nNon è obbligatorio rispondere nella chat.\nSe un trader viola una delle seguenti regole, apri una controversia ed effettua una segnalazione al mediatore o all'arbitro.\n\nRegole della chat:\n● Non inviare nessun link (rischio di malware). È possibile inviare l'ID transazione e il nome di un block explorer.\n● Non inviare parole del seed, chiavi private, password o altre informazioni sensibili!\n● Non incoraggiare il trading al di fuori di Bisq (non garantisce nessuna sicurezza).\n● Non intraprendere alcuna forma di tentativo di frode di ingegneria sociale.\n● Se un peer non risponde e preferisce non comunicare tramite chat, rispettane la decisione.\n● Limita l'ambito della conversazione allo scambio. Questa chat non è una sostituzione di messenger o un troll-box.\n● Mantieni la conversazione amichevole e rispettosa.\n  + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +message.state.SENT=Messaggio inviato +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Il messaggio è arrivato al peer +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Messaggio di pagamento inviato ma non ancora ricevuto dal peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Il peer ha confermato la ricezione de messaggio +# suppress inspection "UnusedProperty" +message.state.FAILED=Invio del messaggio fallito + +portfolio.pending.step3_buyer.wait.headline=Attendi la conferma del pagamento del venditore BTC +portfolio.pending.step3_buyer.wait.info=In attesa della conferma del venditore BTC per la ricezione del pagamento {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Stato del messaggio di pagamento avviato +portfolio.pending.step3_buyer.warn.part1a=sulla {0} blockchain +portfolio.pending.step3_buyer.warn.part1b=presso il tuo fornitore di servizi di pagamento (ad es. banca) +portfolio.pending.step3_buyer.warn.part2=Il venditore BTC non ha ancora confermato il pagamento. Controlla {0} se l'invio del pagamento è andato a buon fine. +portfolio.pending.step3_buyer.openForDispute=Il venditore BTC non ha confermato il tuo pagamento! Il max. periodo per lo scambio è trascorso. Puoi aspettare più a lungo e dare più tempo al peer di trading o richiedere assistenza al mediatore. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Il tuo partner commerciale ha confermato di aver avviato il pagamento {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=sul tuo {0} blockchain explorer preferito +portfolio.pending.step3_seller.altcoin.wallet=sul tuo portafoglio {0} +portfolio.pending.step3_seller.altcoin={0}Controlla {1} se la transazione è indirizzata correttamente al tuo indirizzo di ricezione\n{2}\nha già sufficienti conferme sulla blockchain.\nL'importo del pagamento deve essere {3}\n\nPuoi copiare e incollare il tuo indirizzo {4} dalla schermata principale dopo aver chiuso questo popup. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Poiché il pagamento viene effettuato tramite deposito in contanti, l'acquirente BTC deve scrivere \"NESSUN RIMBORSO\" sulla ricevuta cartacea, strapparlo in 2 parti e inviarti una foto via e-mail.\n\nPer evitare il rischio di storno, conferma solamente se hai ricevuto l'e-mail e se sei sicuro che la ricevuta cartacea sia valida.\nSe non sei sicuro, {0} +portfolio.pending.step3_seller.moneyGram=L'acquirente deve inviarti il numero di autorizzazione e una foto della ricevuta via e-mail.\nLa ricevuta deve mostrare chiaramente il tuo nome completo, il paese, lo stato e l'importo. Controlla nella tua e-mail se hai ricevuto il numero di autorizzazione.\n\nDopo aver chiuso il popup, vedrai il nome e l'indirizzo dell'acquirente BTC per effettuare il ritiro dell'importo da MoneyGram.\n\nConferma la ricevuta solo dopo aver ricevuto con successo i soldi! +portfolio.pending.step3_seller.westernUnion=L'acquirente deve inviarti l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail.\nLa ricevuta deve mostrare chiaramente il tuo nome completo, la città, il paese e l'importo. Controlla nella tua e-mail se hai ricevuto l'MTCN.\n\nDopo aver chiuso il popup, vedrai il nome e l'indirizzo dell'acquirente BTC per effettuare il ritiro dell'importo da Western Union.\n\nConferma la ricevuta solo dopo aver ricevuto con successo i soldi! +portfolio.pending.step3_seller.halCash=L'acquirente deve inviarti il codice HalCash come messaggio di testo. Riceverai un secondo un messaggio da HalCash con le informazioni richieste per poter ritirare gli EUR da un bancomat supportato da HalCash.\n\nDopo aver ritirato i soldi dal bancomat, conferma qui la ricevuta del pagamento! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nVerifica inoltre che il nome del mittente specificato nel contratto dello scambio corrisponda al nome che appare sul tuo estratto conto bancario:\nNome del mittente, per contratto di scambio: {0}\n\nSe i nomi non sono esattamente gli stessi, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=non confermare la ricevuta del pagamento. Apri una disputa premendo \"alt + o\" oppure \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Conferma pagamento ricevuto +portfolio.pending.step3_seller.amountToReceive=Importo da ricevere +portfolio.pending.step3_seller.yourAddress=Il tuo indirizzo {0} +portfolio.pending.step3_seller.buyersAddress=Indirizzo dell'acquirente {0} +portfolio.pending.step3_seller.yourAccount=Il tuo conto di trading +portfolio.pending.step3_seller.xmrTxHash=ID Transazione +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Conferma pagamento ricevuto +portfolio.pending.step3_seller.buyerStartedPayment=L'acquirente BTC ha avviato il pagamento {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Controlla le conferme blockchain sul tuo portafoglio altcoin o block explorer e conferma il pagamento quando hai sufficienti conferme blockchain +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Controlla sul tuo conto di trading (ad es. Conto bancario) e conferma quando hai ricevuto il pagamento. +portfolio.pending.step3_seller.warn.part1a=sulla {0} blockchain +portfolio.pending.step3_seller.warn.part1b=presso il tuo fornitore di servizi di pagamento (ad es. banca) +portfolio.pending.step3_seller.warn.part2=Non hai ancora confermato la ricevuta del pagamento. Controlla {0} se hai ricevuto il pagamento. +portfolio.pending.step3_seller.openForDispute=Non hai confermato la ricevuta del pagamento!\nIl max. periodo per lo scambio è trascorso.\nConferma o richiedi assistenza al mediatore. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Hai ricevuto il pagamento {0} dal tuo partner commerciale?\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Verifica inoltre che il nome del mittente specificato nel contratto di scambio corrisponda al nome che appare sul tuo estratto conto bancario:\nNome del mittente, per contratto di scambio: {0}\n\nSe i nomi non sono uguali, non confermare la ricevuta del pagamento. Apri invece una disputa premendo \"alt + o\" oppure \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Tieni presente che non appena avrai confermato la ricevuta, l'importo commerciale bloccato verrà rilasciato all'acquirente BTC e il deposito cauzionale verrà rimborsato. +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Conferma di aver ricevuto il pagamento +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Si, ho ricevuto il pagamento +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: confermando la ricezione del pagamento, stai anche verificando il conto della controparte e, di conseguenza, lo stai firmando. Poiché il conto della controparte non è stato ancora firmato, è necessario ritardare la conferma del pagamento il più a lungo possibile per ridurre il rischio di uno storno di addebito. + +portfolio.pending.step5_buyer.groupTitle=Riepilogo degli scambi completati +portfolio.pending.step5_buyer.tradeFee=Commissione di scambio +portfolio.pending.step5_buyer.makersMiningFee=Commissione di mining +portfolio.pending.step5_buyer.takersMiningFee=Totale commissioni di mining +portfolio.pending.step5_buyer.refunded=Deposito di sicurezza rimborsato +portfolio.pending.step5_buyer.withdrawBTC=Preleva i tuoi bitcoin +portfolio.pending.step5_buyer.amount=Importo da prelevare +portfolio.pending.step5_buyer.withdrawToAddress=Ritirare all'indirizzo +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=Ritira verso un portafoglio esterno +portfolio.pending.step5_buyer.alreadyWithdrawn=I tuoi fondi sono già stati ritirati.\nSi prega di controllare la cronologia delle transazioni. +portfolio.pending.step5_buyer.confirmWithdrawal=Conferma richiesta di prelievo +portfolio.pending.step5_buyer.amountTooLow=L'importo da trasferire è inferiore alla commissione di transazione e al min. valore tx possibile (polvere). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Prelievo completato +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Gli scambi completati vengono archiviati in \"Portafoglio/Storia\".\nPuoi rivedere tutte le tue transazioni bitcoin in \"Fondi/Transazioni\" +portfolio.pending.step5_buyer.bought=Hai acquistato +portfolio.pending.step5_buyer.paid=Hai pagato + +portfolio.pending.step5_seller.sold=Hai venduto +portfolio.pending.step5_seller.received=Hai ricevuto + +tradeFeedbackWindow.title=Congratulazioni per aver concluso il tuo scambio +tradeFeedbackWindow.msg.part1=Ci piacerebbe avere notizie sulla tua esperienza. Ci aiuterà a migliorare il software e a correggere eventuali errori. Se desideri fornire un feedback, compila questo breve sondaggio (non è richiesta la registrazione) all'indirizzo: +tradeFeedbackWindow.msg.part2=In caso di domande o problemi, si prega di mettersi in contatto con altri utenti e collaboratori tramite il forum Bisq all'indirizzo: +tradeFeedbackWindow.msg.part3=Grazie per aver usato Bisq! + +portfolio.pending.role=Il mio ruolo +portfolio.pending.tradeInformation=Informazioni sullo scambio +portfolio.pending.remainingTime=Tempo rimanente +portfolio.pending.remainingTimeDetail={0} (fino a {1}) +portfolio.pending.tradePeriodInfo=Dopo la prima conferma della blockchain, inizia il periodo di scambio. In base al metodo di pagamento utilizzato, verrà applicato un diverso periodo di scambio massimo consentito. +portfolio.pending.tradePeriodWarning=Se il periodo viene superato, entrambi i trader possono aprire una disputa. +portfolio.pending.tradeNotCompleted=Scambio non completato in tempo (fino a {0}) +portfolio.pending.tradeProcess=Processo dello scambio +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Apri nuovamente la disputa +portfolio.pending.openSupportTicket.headline=Apri ticket di supporto +portfolio.pending.openSupportTicket.msg=Utilizza questa funzione solo in caso di emergenza se non viene visualizzato il pulsante \"Apri supporto\" o \"Apri disputa\".\n\nQuando apri un ticket di supporto, lo scambio verrà interrotto e gestito da un mediatore o da un arbitro. + +portfolio.pending.timeLockNotOver=Devi aspettare fino a ≈ {0} ({1} più blocchi) prima di poter aprire una controversia arbitrale. +portfolio.pending.error.depositTxNull=La transazione di deposito è nulla. Non è possibile aprire una disputa senza una transazione di deposito valida. Vai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\n\nPer ulteriore assistenza, contatta il canale di supporto Bisq nel team di Bisq Keybase. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=La transazione di deposito non è confermata. Non è possibile aprire una disputa arbitrale con una transazione di deposito non confermata. Attendi fino alla conferma o vai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\n\nPer ulteriore assistenza, contatta il canale di supporto Bisq nel team di Bisq Keybase. + +portfolio.pending.support.headline.getHelp=Ho bisogno di aiuto? +portfolio.pending.support.text.getHelp=In caso di problemi, puoi provare a contattare il peer di scambio tramite la chat oppure puoi chiedere aiuto alla comunità Bisq all'indirizzo https://bisq.community. Se il problema persiste, puoi richiedere ulteriore aiuto ad un mediatore. +portfolio.pending.support.button.getHelp=Apri la chat dello scambio +portfolio.pending.support.headline.halfPeriodOver=Controlla il pagamento +portfolio.pending.support.headline.periodOver=Il periodo di scambio è finito + +portfolio.pending.mediationRequested=Mediazione richiesta +portfolio.pending.refundRequested=Rimborso richiesto +portfolio.pending.openSupport=Apri ticket di supporto +portfolio.pending.supportTicketOpened=Ticket di supporto aperto +portfolio.pending.communicateWithArbitrator=Si prega di comunicare nella schermata \"Supporto\" con l'arbitro. +portfolio.pending.communicateWithMediator=Si prega di comunicare nella schermata \"Supporto\" con il mediatore. +portfolio.pending.disputeOpenedMyUser=Hai già aperto una disputa.\n{0} +portfolio.pending.disputeOpenedByPeer=Il tuo pari commerciale ha aperto una controversia\n{0} +portfolio.pending.noReceiverAddressDefined=Nessun indirizzo del destinatario definito + +portfolio.pending.mediationResult.headline=Pagamento suggerito dalla mediazione +portfolio.pending.mediationResult.info.noneAccepted=Completa lo scambio accettando il suggerimento del mediatore per il pagamento dello stesso. +portfolio.pending.mediationResult.info.selfAccepted=Hai accettato il suggerimento del mediatore. In attesa che anche il peer accetti. +portfolio.pending.mediationResult.info.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore. Accetti anche tu? +portfolio.pending.mediationResult.button=Visualizza la risoluzione proposta +portfolio.pending.mediationResult.popup.headline=Risultato della mediazione per gli scambi con ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore per lo scambio {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Rifiuta e richiedi l'arbitrato +portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Completato +portfolio.closed.ticketClosed=Arbitrato +portfolio.closed.mediationTicketClosed=Mediato +portfolio.closed.canceled=Annullato +portfolio.failed.Failed=Fallito +portfolio.failed.unfail= \nPrima di procedere, assicurati di avere un backup della tua directory dei dati!\nVuoi riportare questo scambio nella sezione degli scambi aperti?\nQuesto è un modo per rientrare in possesso dei fondi bloccati in uno scambio fallito.\n  +portfolio.failed.cantUnfail= \nAl momento questo scambio non può tornare nella sezione degli scambi aperti.\nRiprova dopo il completamento degli scambi {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Ricevi fondi +funds.tab.withdrawal=Invia fondi +funds.tab.reserved=Fondi riservati +funds.tab.locked=Fondi bloccati +funds.tab.transactions=Transazioni + +funds.deposit.unused=Non usato +funds.deposit.usedInTx=Utilizzato in {0} transazioni +funds.deposit.fundBisqWallet=Finanzia portafoglio Bisq +funds.deposit.noAddresses=Non sono stati ancora generati indirizzi di deposito +funds.deposit.fundWallet=Finanzia il tuo portafoglio +funds.deposit.withdrawFromWallet=Invia fondi dal portafoglio +funds.deposit.amount=Importo in BTC (facoltativo) +funds.deposit.generateAddress=Crea nuovo indirizzo +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Seleziona un indirizzo inutilizzato dalla tabella sopra anziché generarne uno nuovo. + +funds.withdrawal.arbitrationFee=Commissione arbitraggio +funds.withdrawal.inputs=Selezione input +funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili +funds.withdrawal.useCustomInputs=Utilizza input personalizzati +funds.withdrawal.receiverAmount=Importo del destinatario +funds.withdrawal.senderAmount=Importo del mittente +funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining +funds.withdrawal.feeIncluded=L'importo include la commissione di mining +funds.withdrawal.fromLabel=Ritirare dall'indirizzo +funds.withdrawal.toLabel=Ritirare all'indirizzo +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Ritira selezionato +funds.withdrawal.noFundsAvailable=Non sono disponibili fondi per il prelievo +funds.withdrawal.confirmWithdrawalRequest=Conferma richiesta di prelievo +funds.withdrawal.withdrawMultipleAddresses=Ritira da più indirizzi ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Ritira da più indirizzi\n({0}) +funds.withdrawal.notEnoughFunds=Non hai abbastanza fondi nel tuo portafoglio. +funds.withdrawal.selectAddress=Seleziona un indirizzo sorgente dalla tabella +funds.withdrawal.setAmount=Imposta l'importo da prelevare +funds.withdrawal.fillDestAddress=Inserisci il tuo indirizzo di destinazione +funds.withdrawal.warn.noSourceAddressSelected=È necessario selezionare un indirizzo di origine nella tabella sopra. +funds.withdrawal.warn.amountExceeds=Non hai fondi sufficienti disponibili presso l'indirizzo selezionato.\nConsidera di selezionare più indirizzi nella tabella sopra o di cambiare la tariffa per includere la commissione del miner. + +funds.reserved.noFunds=Nessun fondo è riservato nelle offerte aperte +funds.reserved.reserved=Riservato nel portafoglio locale per l'offerta con ID: {0} + +funds.locked.noFunds=Nessun fondo è bloccato nelle negoziazioni +funds.locked.locked=Bloccato in multisig per lo scambio con ID: {0} + +funds.tx.direction.sentTo=Inviato a: +funds.tx.direction.receivedWith=Ricevuto con: +funds.tx.direction.genesisTx=Da tx Genesi: +funds.tx.txFeePaymentForBsqTx=Commissione di mining per transazioni BSQ +funds.tx.createOfferFee=Commissione per maker e tx: {0} +funds.tx.takeOfferFee=Commissione per taker e tx: {0} +funds.tx.multiSigDeposit=Deposito multisig: {0} +funds.tx.multiSigPayout=Pagamento Multisig: {0} +funds.tx.disputePayout=Pagamento disputa: {0} +funds.tx.disputeLost=Caso di disputa persa : {0} +funds.tx.collateralForRefund=Garanzia di rimborso: {0} +funds.tx.timeLockedPayoutTx=Tx di pagamento bloccata: {0} +funds.tx.refund=Rimborso dell'arbitrato: {0} +funds.tx.unknown=Motivo sconosciuto: {0} +funds.tx.noFundsFromDispute=Nessun rimborso dalla controversia +funds.tx.receivedFunds=Fondi ricevuti +funds.tx.withdrawnFromWallet=Prelevato dal portafoglio +funds.tx.withdrawnFromBSQWallet=BTC prelevati dal portafoglio BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=Nessuna transazione disponibile +funds.tx.revert=Storna +funds.tx.txSent=Transazione inviata con successo ad un nuovo indirizzo nel portafoglio Bisq locale. +funds.tx.direction.self=Invia a te stesso +funds.tx.daoTxFee=Commissione di mining per transazioni BSQ +funds.tx.reimbursementRequestTxFee=Richiesta di rimborso +funds.tx.compensationRequestTxFee=Richiesta di compenso +funds.tx.dustAttackTx=Polvere ricevuta +funds.tx.dustAttackTx.popup=Questa transazione sta inviando un importo BTC molto piccolo al tuo portafoglio e potrebbe essere un tentativo da parte delle società di chain analysis per spiare il tuo portafoglio.\n\nSe usi quell'output della transazione in una transazione di spesa, scopriranno che probabilmente sei anche il proprietario dell'altro indirizzo (combinazione di monete).\n\nPer proteggere la tua privacy, il portafoglio Bisq ignora tali output di polvere a fini di spesa e nella visualizzazione del saldo. È possibile impostare la soglia al di sotto della quale un output è considerato polvere.\n  + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediazione +support.tab.arbitration.support=Arbitrato +support.tab.legacyArbitration.support=Arbitrato Legacy +support.tab.ArbitratorsSupportTickets=I ticket di {0} +support.filter=Search disputes +support.filter.prompt=Inserisci ID commerciale, data, indirizzo onion o dati dell'account + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Notifica privata +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=Non ci sono ticket aperti +support.sendingMessage=Inviando il messaggio ... +support.receiverNotOnline=Il destinatario non è online. Il messaggio viene salvato nella loro casella di posta. +support.sendMessageError=Invio messaggio non riuscito. Errore: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=L'offerta in quella controversia è stata creata con una versione precedente di Bisq.\nNon puoi chiudere quella controversia con la versione della tua applicazione.\n\nUtilizza una versione precedente con la versione del protocollo {0} +support.openFile=Apri il file da allegare (dimensione massima del file: {0}) +support.attachmentTooLarge=La dimensione dei tuoi allegati è di {0} e supera il massimo consentito di {1} kB. +support.maxSize=La dimensione massima del file permessa è di {0} kB. +support.attachment=Allegato +support.tooManyAttachments=Non è possibile inviare più di 3 allegati in un messaggio. +support.save=Salva il file sul computer +support.messages=Messaggi +support.input.prompt=Inserisci messaggio... +support.send=Invia +support.addAttachments=Aggiungi allegati +support.closeTicket=Chiudi ticket +support.attachments=Allegati: +support.savedInMailbox=Messaggio salvato nella cassetta postale del destinatario +support.arrived=Il messaggio è arrivato al destinatario +support.acknowledged=Arrivo del messaggio confermato dal destinatario +support.error=Il destinatario non ha potuto elaborare il messaggio. Errore: {0} +support.buyerAddress=Indirizzo BTC dell'acquirente +support.sellerAddress=Indirizzo BTC del venditore +support.role=Ruolo +support.agent=Support agent +support.state=Stato +support.chat=Chat +support.closed=Chiuso +support.open=Aperto +support.process=Process +support.buyerOfferer=Acquirente/Maker BTC +support.sellerOfferer=Venditore/Maker BTC +support.buyerTaker=Acquirente/Taker BTC +support.sellerTaker=Venditore/Taker BTC + +support.backgroundInfo=Bisq non è una società, quindi gestisce le controversie in modo diverso.\n\nI trader possono comunicare all'interno dell'applicazione tramite chat sicura nella schermata degli scambi aperti per provare a risolvere le controversie da soli. Se ciò non è sufficiente, un mediatore può intervenire per aiutare. Il mediatore valuterà la situazione e suggerirà un pagamento di fondi commerciali. Se entrambi i trader accettano questo suggerimento, la transazione di pagamento è completata e lo scambio è chiuso. Se uno o entrambi i trader non accettano il pagamento suggerito dal mediatore, possono richiedere l'arbitrato. L'arbitro rivaluterà la situazione e, se garantito, ripagherà personalmente il trader e chiederà il rimborso per questo pagamento dal DAO Bisq. +support.initialInfo=Inserisci una descrizione del tuo problema nel campo di testo qui sotto. Aggiungi quante più informazioni possibili per accelerare i tempi di risoluzione della disputa.\n\nEcco una lista delle informazioni che dovresti fornire:\n● Se sei l'acquirente BTC: hai effettuato il trasferimento Fiat o Altcoin? In tal caso, hai fatto clic sul pulsante "pagamento avviato" nell'applicazione?\n● Se sei il venditore BTC: hai ricevuto il pagamento Fiat o Altcoin? In tal caso, hai fatto clic sul pulsante "pagamento ricevuto" nell'applicazione?\n● Quale versione di Bisq stai usando?\n● Quale sistema operativo stai usando?\n● Se si è verificato un problema con transazioni non riuscite, prendere in considerazione la possibilità di passare a una nuova directory di dati.\n  A volte la directory dei dati viene danneggiata e porta a strani bug.\n  Vedi: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nAcquisire familiarità con le regole di base per la procedura di disputa:\n● È necessario rispondere alle richieste di {0} entro 2 giorni.\n● I mediatori rispondono entro 2 giorni. Gli arbitri rispondono entro 5 giorni lavorativi.\n● Il periodo massimo per una disputa è di 14 giorni.\n● È necessario collaborare con {1} e fornire le informazioni richieste per presentare il proprio caso.\n● Hai accettato le regole delineate nel documento di contestazione nel contratto con l'utente al primo avvio dell'applicazione.\n\nPuoi leggere ulteriori informazioni sulla procedura di contestazione all'indirizzo: {2}\n  +support.systemMsg=Messaggio di sistema: {0} +support.youOpenedTicket=Hai aperto una richiesta di supporto.\n\n{0}\n\nVersione Bisq: {1} +support.youOpenedDispute=Hai aperto una richiesta per una controversia.\n\n{0}\n\nVersione Bisq: {1} +support.youOpenedDisputeForMediation=Hai richiesto la mediazione.\n\n{0}\n\nVersione Bisq: {1} +support.peerOpenedTicket=Il tuo peer di trading ha richiesto supporto a causa di problemi tecnici.\n\n{0}\n\nVersione Bisq: {1} +support.peerOpenedDispute=Il tuo peer di trading ha richiesto una controversia.\n\n{0}\n\nVersione Bisq: {1} +support.peerOpenedDisputeForMediation=Il tuo peer di trading ha richiesto la mediazione.\n\n{0}\n\nVersione Bisq: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Indirizzo nodo del mediatore: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preferenze +settings.tab.network=Informazioni della Rete +settings.tab.about=Circa + +setting.preferences.general=Preferenze generali +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Deviazione massima del prezzo di mercato +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Evita modalità standby +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Non sono ammessi valori superiori a {0}%. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Usa valore personalizzato +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Peer ignorati [indirizzo:porta onion] +setting.preferences.ignoreDustThreshold=Valore minimo di output non-dust +setting.preferences.currenciesInList=Valute nell'elenco dei feed dei prezzi di mercato +setting.preferences.prefCurrency=Valuta preferita +setting.preferences.displayFiat=Mostra valute nazionali +setting.preferences.noFiat=Non ci sono valute nazionali selezionate +setting.preferences.cannotRemovePrefCurrency=Non è possibile rimuovere la valuta di visualizzazione preferita selezionata +setting.preferences.displayAltcoins=Visualizza altcoin +setting.preferences.noAltcoins=Non ci sono altcoin selezionate +setting.preferences.addFiat=Aggiungi valuta nazionale +setting.preferences.addAltcoin=Aggiungi altcoin +setting.preferences.displayOptions=Mostra opzioni +setting.preferences.showOwnOffers=Mostra le mie offerte nel libro delle offerte +setting.preferences.useAnimations=Usa animazioni +setting.preferences.useDarkMode=Usa modalità notte +setting.preferences.sortWithNumOffers=Ordina le liste di mercato con n. di offerte/scambi +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Ripristina tutti i flag \"Non mostrare più\" +settings.preferences.languageChange=Per applicare la modifica della lingua a tutte le schermate è necessario riavviare. +settings.preferences.supportLanguageWarning=In caso di controversia, tenere presente che la mediazione è gestita in {0} e l'arbitrato in {1}. +setting.preferences.daoOptions=Opzioni DAO +setting.preferences.dao.resyncFromGenesis.label=Ricostruisci lo stato della DAO dalla transazione di genesi +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Lancia Bisq come full node DAO +setting.preferences.dao.rpcUser=Username RPC +setting.preferences.dao.rpcPw=Password RPC +setting.preferences.dao.blockNotifyPort=Blocca porta di notifica +setting.preferences.dao.fullNodeInfo=Per eseguire Bisq come nodo DAO completo devi avere Bitcoin Core in esecuzione localmente e RPC abilitato. Tutti i requisiti sono documentati in ''{0}''.\n\nDopo aver modificato la modalità, è necessario riavviare. +setting.preferences.dao.fullNodeInfo.ok=Apri la pagina dei documenti +setting.preferences.dao.fullNodeInfo.cancel=No, continuo ad utilizzare il nodo leggero +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Nome +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Network Bitcoin +settings.net.p2pHeader=Rete Bisq +settings.net.onionAddressLabel=Il mio indirizzo onion +settings.net.btcNodesLabel=Usa nodi Bitcoin Core personalizzati +settings.net.bitcoinPeersLabel=Peer connessi +settings.net.useTorForBtcJLabel=Usa Tor per la rete Bitcoin +settings.net.bitcoinNodesLabel=Nodi Bitcoin Core a cui connettersi +settings.net.useProvidedNodesRadio=Usa i nodi Bitcoin Core forniti +settings.net.usePublicNodesRadio=Usa la rete pubblica di Bitcoin +settings.net.useCustomNodesRadio=Usa nodi Bitcoin Core personalizzati +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=No, utilizza i nodi forniti +settings.net.warn.usePublicNodes.usePublic=Sì, usa la rete pubblica +settings.net.warn.useCustomNodes.B2XWarning=Assicurati che il tuo nodo Bitcoin sia un nodo Bitcoin Core di fiducia!\n\nLa connessione a nodi che non seguono le regole di consenso di Bitcoin Core potrebbe corrompere il tuo portafoglio e causare problemi nel processo di scambio.\n\nGli utenti che si connettono a nodi che violano le regole di consenso sono responsabili per qualsiasi danno risultante. Eventuali controversie risultanti saranno decise a favore dell'altro pari. Nessun supporto tecnico verrà fornito agli utenti che ignorano questo meccanismo di avvertimento e protezione! +settings.net.warn.invalidBtcConfig=Connessione alla rete Bitcoin non riuscita perché la configurazione non è valida.\n\nLa tua configurazione è stata ripristinata per utilizzare invece i nodi Bitcoin forniti. Dovrai riavviare l'applicazione. +settings.net.localhostBtcNodeInfo=Informazioni di base: Bisq cerca un nodo Bitcoin locale all'avvio. Se viene trovato, Bisq comunicherà con la rete Bitcoin esclusivamente attraverso di esso. +settings.net.p2PPeersLabel=Peer connessi +settings.net.onionAddressColumn=Indirizzo onion +settings.net.creationDateColumn=Stabilito +settings.net.connectionTypeColumn=Dentro/Fuori +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Ritorno +settings.net.sentBytesColumn=Inviato +settings.net.receivedBytesColumn=Ricevuto +settings.net.peerTypeColumn=Tipo di peer +settings.net.openTorSettingsButton=Apri impostazioni di Tor + +settings.net.versionColumn=Versione +settings.net.subVersionColumn=Sottoversione +settings.net.heightColumn=Altezza + +settings.net.needRestart=È necessario riavviare l'applicazione per applicare tale modifica.\nVuoi farlo adesso? +settings.net.notKnownYet=Non ancora noto... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[Indirizzo IP:porta | hostname:porta | indirizzo onion:porta] (separato da una virgola). La porta può essere omessa se è usata quella predefinita (8333). +settings.net.seedNode=Nodo seme +settings.net.directPeer=Peer (diretto) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Peer +settings.net.inbound=in entrata +settings.net.outbound=in uscita +settings.net.reSyncSPVChainLabel=Risincronizza la catena SPV +settings.net.reSyncSPVChainButton=Elimina il file SPV e risincronizza +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=Il file della catena SPV è stato eliminato. Per favore sii paziente. La risincronizzazione con la rete può richiedere del tempo. +settings.net.reSyncSPVAfterRestartCompleted=La risincronizzazione è ora completata. Si prega di riavviare l'applicazione. +settings.net.reSyncSPVFailed=Impossibile eliminare il file della catena SPV.\nErrore: {0} +setting.about.aboutBisq=Riguardo Bisq +setting.about.about=Bisq è un software open source che facilita lo scambio di bitcoin con valute nazionali (e altre criptovalute) attraverso una rete peer-to-peer decentralizzata in modo da proteggere fortemente la privacy degli utenti. Leggi di più riguardo Bisq sulla pagina web del progetto. +setting.about.web=Pagina web Bisq +setting.about.code=Codice sorgente +setting.about.agpl=Licenza AGPL +setting.about.support=Supporta Bisq +setting.about.def=Bisq non è un'azienda, è un progetto aperto alla comunità. Se vuoi partecipare o supportare Bisq, segui i link qui sotto. +setting.about.contribute=Contribuisci +setting.about.providers=Fornitori di dati +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Prezzi di mercato forniti da +setting.about.feeEstimation.label=Previsione della commissione di mining fornita da +setting.about.versionDetails=Dettagli versione +setting.about.version=Versione applicazione +setting.about.subsystems.label=Versioni di sottosistemi +setting.about.subsystems.val=Versione di rete: {0}; Versione del messaggio P2P: {1}; Versione DB locale: {2}; Versione del protocollo di scambio: {3} + +setting.about.shortcuts=Scorciatoie +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Naviga il menu principale +setting.about.shortcuts.menuNav.value=Per navigare nel menu principale premere: 'Ctrl' o 'alt' o 'cmd' con un tasto numerico tra '1-9' + +setting.about.shortcuts.close=Chiudi Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Chiudi popup o finestra di dialogo +setting.about.shortcuts.closePopup.value=Tasto 'ESC' + +setting.about.shortcuts.chatSendMsg=Invia messaggio chat al trader +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Apri disputa +setting.about.shortcuts.openDispute.value=Seleziona lo scambio in sospeso e fai clic: {0} + +setting.about.shortcuts.walletDetails=Apri la finestra dei dettagli del portafoglio + +setting.about.shortcuts.openEmergencyBtcWalletTool=Apri lo strumento portafoglio di emergenza per il portafoglio BTC + +setting.about.shortcuts.openEmergencyBsqWalletTool=Apri lo strumento portafoglio di emergenza per il portafoglio BSQ + +setting.about.shortcuts.showTorLogs=Attiva / disattiva il livello di registro per i messaggi Tor tra DEBUG e WARN + +setting.about.shortcuts.manualPayoutTxWindow=Apri la finestra per il pagamento manuale da una transazione di deposito Multisig 2di2 + +setting.about.shortcuts.reRepublishAllGovernanceData=Ripubblicare i dati di governance DAO (proposte, voti) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Registra arbitro (solo mediatore/arbitro) +setting.about.shortcuts.registerArbitrator.value=Passare all'account e premere: {0} + +setting.about.shortcuts.registerMediator=Registra mediatore (solo mediatore/arbitro) +setting.about.shortcuts.registerMediator.value=Passare all'account e premere: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Apri la finestra aperta per firnare l'età dell'account (solo arbitri legacy) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Passare alla visualizzazione dell'arbitro legacy e premere: {0} + +setting.about.shortcuts.sendAlertMsg=Invia avviso o aggiorna messaggio (attività privilegiata) + +setting.about.shortcuts.sendFilter=Imposta Filtro (attività privilegiata) + +setting.about.shortcuts.sendPrivateNotification=Invia notifica privata al peer (attività privilegiata) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Registrazione del mediatore +account.tab.refundAgentRegistration=Registrazione agente di rimborso +account.tab.signing=Signing +account.info.headline=Benvenuto nel tuo Account Bisq +account.info.msg=Qui puoi aggiungere conti di trading per valute nazionali e altcoin e creare un backup dei tuoi dati di portafoglio e conto.\n\nUn nuovo portafoglio Bitcoin è stato creato la prima volta che hai avviato Bisq.\n\nTi consigliamo vivamente di annotare le parole del seme del portafoglio Bitcoin (vedi la scheda in alto) e prendere in considerazione l'aggiunta di una password prima del finanziamento. I depositi e prelievi di bitcoin sono gestiti nella sezione \"Fondi\".\n\nInformativa sulla privacy e sulla sicurezza: poiché Bisq è un exchange decentralizzato, tutti i tuoi dati vengono conservati sul tuo computer. Non ci sono server, quindi non abbiamo accesso alle tue informazioni personali, ai tuoi fondi o persino al tuo indirizzo IP. Dati come numeri di conto bancario, altcoin e indirizzi Bitcoin, ecc. vengono condivisi con il proprio partner commerciale per adempiere alle negoziazioni avviate (in caso di controversia il mediatore o l'arbitro vedrà gli stessi dati del proprio peer di negoziazione). + +account.menu.paymentAccount=Conti in valuta nazionale +account.menu.altCoinsAccountView=Conti altcoin +account.menu.password=Password portafoglio +account.menu.seedWords=Seme portafoglio +account.menu.walletInfo=Wallet info +account.menu.backup=Backup +account.menu.notifications=Notifiche + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Chiave pubblica + +account.arbitratorRegistration.register=Registrare +account.arbitratorRegistration.registration={0} registrazione +account.arbitratorRegistration.revoke=Revoca +account.arbitratorRegistration.info.msg=Nota che è necessario rimanere disponibili per 15 giorni dopo la revoca poiché potrebbero esserci operazioni che ti utilizzano come {0}. Il periodo massimo di scambio consentito è di 8 giorni e il processo di contestazione potrebbe richiedere fino a 7 giorni. +account.arbitratorRegistration.warn.min1Language=Devi impostare almeno 1 lingua.\nAbbiamo aggiunto la lingua predefinita per te. +account.arbitratorRegistration.removedSuccess=Hai rimosso con successo la tua registrazione dalla rete Bisq. +account.arbitratorRegistration.removedFailed=Impossibile rimuovere la registrazione. {0} +account.arbitratorRegistration.registerSuccess=Ti sei registrato correttamente alla rete Bisq. +account.arbitratorRegistration.registerFailed=Impossibile completare la registrazione. {0} + +account.altcoin.yourAltcoinAccounts=I tuoi conti altcoin +account.altcoin.popup.wallet.msg=Assicurati di seguire i requisiti per l'uso di {0} portafogli come descritto nella pagina web {1}.\nL'uso di portafogli da exchange centralizzati in cui (a) non controlli le tue chiavi o (b) che non usano software di portafoglio compatibile è rischioso: può portare alla perdita dei fondi scambiati!\nIl mediatore o l'arbitro non è uno specialista {2} e non può essere d'aiuto in questi casi. +account.altcoin.popup.wallet.confirm=Capisco e confermo di sapere quale portafoglio devo usare. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Il trading di ARQ su Bisq richiede di comprendere e soddisfare i seguenti requisiti:\n\nPer inviare ARQ, è necessario utilizzare il portafoglio GUI ArQmA ufficiale o il portafoglio CLI ArQmA con il flag store-tx-info abilitato (impostazione predefinita nelle nuove versioni). Assicurati di poter accedere alla chiave tx come sarebbe richiesto in caso di controversia.\narqma-wallet-cli (utilizzare il comando get_tx_key)\narqma-wallet-gui (vai allo storico trnsazioni e fai clic sul pulsante (P) per la prova del pagamento)\n\nNegli explorer di blocchi normali il trasferimento non è verificabile.\n\nÈ necessario fornire al mediatore o all'arbitro i seguenti dati in caso di controversia:\n- La chiave privata tx\n- L'hash della transazione\n- L'indirizzo pubblico del destinatario\n\nIl mancato conferimento dei dati di cui sopra o l'utilizzo di un portafoglio incompatibile comporterà la perdita del caso di contestazione. Il mittente ARQ è responsabile di fornire la verifica del trasferimento ARQ al mediatore o all'arbitro in caso di controversia.\n\nNon è richiesto un ID di pagamento, ma solo il normale indirizzo pubblico.\nSe non si è sicuri di tale processo, visitare il canale discord ArQmA (https://discord.gg/s9BQpJT) o il forum ArQmA (https://labs.arqma.com) per trovare ulteriori informazioni.\n  +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Il trading di Dragonglass su Bisq richiede di comprendere e soddisfare i seguenti requisiti:\n\nA causa della privacy fornita da Dragonglass, una transazione non è verificabile sulla blockchain pubblica. Se necessario, puoi provare il tuo pagamento utilizzando la tua chiave privata TXN.\nLa chiave privata TXN è una chiave una tantum generata automaticamente per ogni transazione a cui è possibile accedere solo dal proprio portafoglio DRGL.\nOppore dalla GUI del wallet DRGL (nella finestra di dialogo dei dettagli della transazione) o dalla semplice interfaccia CLI di Dragonglass (usando il comando "get_tx_key").\n\nPer entrambi è richiesta la versione DRGL 'Oathkeeper' e successive.\n\nIn caso di controversia, è necessario fornire al mediatore o all'arbitro i seguenti dati:\n- La chiave privata TXN\n- L'hash della transazione\n- L'indirizzo pubblico del destinatario\n\nLa verifica del pagamento può essere effettuata utilizzando i dati sopra riportati come input su (http://drgl.info/#check_txn).\n\nIl mancato conferimento dei dati di cui sopra o l'utilizzo di un portafoglio incompatibile comporterà la perdita del caso di disputa. Il mittente Dragonglass è responsabile di fornire la verifica del trasferimento DRGL al mediatore o all'arbitro in caso di controversia. L'uso di PaymentID non è richiesto.\n\nIn caso di dubbi su qualsiasi parte di questo processo, visitare Dragonglass su Discord (http://discord.drgl.info) per assistenza.\n  +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Quando si utilizza Zcash è possibile utilizzare solo gli indirizzi trasparenti (a partire da t), non gli indirizzi z (privati), poiché il mediatore o l'arbitro non sarebbero in grado di verificare la transazione con gli indirizzi z. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Quando si utilizza Zcoin è possibile utilizzare solo gli indirizzi trasparenti (tracciabili), non quelli non rintracciabili, poiché il mediatore o l'arbitro non sarebbero in grado di verificare la transazione con indirizzi non rintracciabili in un explorer di blocchi. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN richiede un processo interattivo tra il mittente e il destinatario per creare la transazione. Assicurati di seguire le istruzioni dalla pagina web del progetto GRIN per inviare e ricevere in modo affidabile GRIN (il ricevitore deve essere online o almeno essere online durante un certo periodo di tempo).\n\nBisq supporta solo il formato URL del portafoglio Grinbox (Wallet713).\n\nIl mittente GRIN è tenuto a dimostrare di aver inviato GRIN correttamente. Se il portafoglio non è in grado di fornire tale prova, una potenziale disputa verrà risolta a favore del destinatario GRIN. Assicurati di utilizzare il software Grinbox più recente che supporti la prova delle transazioni e di comprendere il processo di trasferimento e ricezione di GRIN e come creare la prova.\n\nVedi https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only per ulteriori informazioni sullo strumento Grinbox proof. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM richiede un processo interattivo tra il mittente e il destinatario per creare la transazione.\n\nAssicurati di seguire le istruzioni dalla pagina web del progetto BEAM per inviare e ricevere in modo affidabile BEAM (il ricevitore deve essere online o almeno essere online durante un certo periodo di tempo).\n\nIl mittente BEAM è tenuto a fornire la prova di aver inviato BEAM correttamente. Assicurati di utilizzare il software del portafoglio che può produrre tale prova. Se il portafoglio non è in grado di fornire la prova, una potenziale disputa verrà risolta a favore del destinatario BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Il trading di L-BTC su Bisq richiede la comprensione di quanto segue:\n\nQuando ricevi L-BTC per uno scambio su Bisq, non puoi utilizzare l'applicazione mobile Blockstream Green Wallet o un portafoglio di custodia/scambio. Devi ricevere L-BTC solo nel portafoglio Liquid Elements Core o in un altro portafoglio L-BTC che ti consenta di ottenere la chiave per il tuo indirizzo L-BTC.\n\nNel caso in cui sia necessaria la mediazione o in caso di disputa nello scambio, è necessario divulgare la chiave di ricezione per il proprio indirizzo L-BTC al mediatore Bisq o all'agente di rimborso in modo che possano verificare i dettagli della propria Transazione riservata sul proprio full node Elements Core.\n\nLa mancata fornitura delle informazioni richieste dal mediatore o dall'agente di rimborso comporterà la perdita della disputa. In tutti i casi di disputa, il ricevente L-BTC si assume al 100% l'onere della responsabilità nel fornire prove crittografiche al mediatore o all'agente di rimborso.\n\nSe non comprendi i sopracitati requisiti, non scambiare L-BTC su Bisq. + +account.fiat.yourFiatAccounts=I tuoi conti in valuta nazionale + +account.backup.title=Portafoglio di backup +account.backup.location=Posizione di backup +account.backup.selectLocation=Seleziona la posizione di backup +account.backup.backupNow=Esegui il backup ora (il backup non è crittografato!) +account.backup.appDir=Cartella dei dati dell'applicazione +account.backup.openDirectory=Apri cartella +account.backup.openLogFile=Apri il file di registro +account.backup.success=Backup salvato correttamente in:\n{0} +account.backup.directoryNotAccessible=La cartella che hai scelto non è accessibile. {0} + +account.password.removePw.button=Rimuovi password +account.password.removePw.headline=Rimuovi la protezione con password per il portafoglio +account.password.setPw.button=Imposta password +account.password.setPw.headline=Imposta la protezione con password per il portafoglio +account.password.info=Con la protezione con password dovrai inserire la password all'avvio dell'applicazione, quando ritiri bitcoin dal tuo portafoglio e quando ripristini il tuo portafoglio dalle parole chiave. + +account.seed.backup.title=Effettua il backup delle parole del seed dei tuoi portafogli +account.seed.info=Si prega di scrivere sia le parole del seed del portafoglio che la data! Puoi recuperare il tuo portafoglio in qualsiasi momento con le parole del seed e la data.\nLe stesse parole del seed vengono utilizzate per il portafoglio BTC e BSQ.\n\nDovresti scrivere le parole del seed su un foglio di carta. Non salvarli sul tuo computer.\n\nSi noti che le parole del seed NON sostituiscono un backup.\nÈ necessario creare un backup dell'intera directory dell'applicazione dalla schermata \"Account/Backup\" per ripristinare lo stato e i dati dell'applicazione.\nL'importazione delle parole del seed è consigliata solo in casi di emergenza. L'applicazione non funzionerà senza un corretto backup dei file del database e delle chiavi del seed! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Non hai impostato una password per il portafoglio che protegga la visualizzazione delle parole del seed.\n\nVuoi visualizzare le parole del seed? +account.seed.warn.noPw.yes=Sì, e non chiedermelo più +account.seed.enterPw=Immettere la password per visualizzare le parole chiave +account.seed.restore.info=Effettuare un backup prima di cominciare il ripristino dalle parole del seed. Tenere presente che il ripristino del portafoglio è solo per casi di emergenza e potrebbe causare problemi con il database del portafoglio interno.\nNon è un modo per effettuare un backup! Utilizzare un backup della directory dei dati dell'applicazione per ripristinare uno stato dell'applicazione precedente.\n\nDopo aver ripristinato, l'applicazione si spegnerà automaticamente. Dopo aver riavviato l'applicazione, si risincronizzerà con la rete Bitcoin. Questo può richiedere del tempo e può consumare molta CPU, soprattutto se il portafoglio era vecchio e aveva molte transazioni. Evita di interrompere tale processo, altrimenti potrebbe essere necessario eliminare nuovamente il file della catena SPV o ripetere il processo di ripristino. +account.seed.restore.ok=Ok, fai il ripristino e spegni Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Setup +account.notifications.download.label=Scarica app mobile +account.notifications.waitingForWebCam=In attesa della webcam... +account.notifications.webCamWindow.headline=Scansiona il codice QR dal telefono +account.notifications.webcam.label=Utilizza webcam +account.notifications.webcam.button=Scansiona il codice QR +account.notifications.noWebcam.button=Non ho una webcam +account.notifications.erase.label=Cancella notifiche sul telefono +account.notifications.erase.title=Cancella notifiche +account.notifications.email.label=Token di associazione +account.notifications.email.prompt=Inserisci il token di associazione che hai ricevuto via e-mail +account.notifications.settings.title=Impostazioni +account.notifications.useSound.label=Riproduci l'audio di notifica sul telefono +account.notifications.trade.label=Ricevi messaggi commerciali +account.notifications.market.label=Ricevi avvisi sulle offerte +account.notifications.price.label=Ricevi avvisi sui prezzi +account.notifications.priceAlert.title=Avvisi sui prezzi +account.notifications.priceAlert.high.label=Notifica se il prezzo BTC è superiore +account.notifications.priceAlert.low.label=Notifica se il prezzo BTC è inferiore +account.notifications.priceAlert.setButton=Imposta un avviso di prezzo +account.notifications.priceAlert.removeButton=Rimuovi avviso di prezzo +account.notifications.trade.message.title=Lo stato dello scambio è cambiato +account.notifications.trade.message.msg.conf=La transazione di deposito per lo scambio con ID {0} è confermata. Si prega di aprire l'applicazione Bisq e avviare il pagamento. +account.notifications.trade.message.msg.started=L'acquirente BTC ha avviato il pagamento per lo scambio con ID {0}. +account.notifications.trade.message.msg.completed=Lo scambio con ID {0} è completato. +account.notifications.offer.message.title=La tua offerta è stata presa +account.notifications.offer.message.msg=La tua offerta con ID {0} è stata accettata +account.notifications.dispute.message.title=Nuovo messaggio di contestazione +account.notifications.dispute.message.msg=Hai ricevuto un messaggio di contestazione per lo scambio con ID {0} + +account.notifications.marketAlert.title=Offri avvisi +account.notifications.marketAlert.selectPaymentAccount=Offre un account di pagamento corrispondente +account.notifications.marketAlert.offerType.label=Tipo di offerta che mi interessa +account.notifications.marketAlert.offerType.buy=Acquista offerte (voglio vendere BTC) +account.notifications.marketAlert.offerType.sell=Offerte di vendita (Voglio comprare BTC) +account.notifications.marketAlert.trigger=Distanza prezzo offerta (%) +account.notifications.marketAlert.trigger.info=Con una distanza di prezzo impostata, riceverai un avviso solo quando viene pubblicata un'offerta che soddisfa (o supera) i tuoi requisiti. Esempio: vuoi vendere BTC, ma venderai solo con un premio del 2% dal prezzo di mercato attuale. Se si imposta questo campo su 2%, si riceveranno avvisi solo per offerte con prezzi superiori del 2% (o più) dal prezzo di mercato corrente.\n  +account.notifications.marketAlert.trigger.prompt=Distanza percentuale dal prezzo di mercato (ad es. 2,50%, -0,50%, ecc.) +account.notifications.marketAlert.addButton=Aggiungi avviso offerta +account.notifications.marketAlert.manageAlertsButton=Gestisci avvisi di offerta +account.notifications.marketAlert.manageAlerts.title=Gestisci avvisi di offerta +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conto di Pagamento +account.notifications.marketAlert.manageAlerts.header.trigger=Prezzo di attivazione +account.notifications.marketAlert.manageAlerts.header.offerType=Tipo di offerta +account.notifications.marketAlert.message.title=Avviso di offerta +account.notifications.marketAlert.message.msg.below=sotto +account.notifications.marketAlert.message.msg.above=sopra +account.notifications.marketAlert.message.msg=Una nuova ''{0} {1}'' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento ''{5}'' è stata pubblicata sulla pagina delle offerte Bisq.\nID offerta: {6}. +account.notifications.priceAlert.message.title=Avviso di prezzo per {0} +account.notifications.priceAlert.message.msg=Il tuo avviso di prezzo è stato attivato. L'attuale prezzo {0} è {1} {2} +account.notifications.noWebCamFound.warning=Nessuna webcam trovata.\n\nUtilizzare l'opzione e-mail per inviare il token e la chiave di crittografia dal telefono cellulare all'applicazione Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=Il prezzo più alto deve essere maggiore del prezzo più basso. +account.notifications.priceAlert.warning.lowerPriceTooHigh=Il prezzo più basso deve essere inferiore al prezzo più alto. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Fatti e cifre +dao.tab.bsqWallet=Portafoglio BSQ +dao.tab.proposals=Governance +dao.tab.bonding=Bonding +dao.tab.proofOfBurn=Commissione di quotazione delle attività/Proof of burn +dao.tab.monitor=Monitor di rete +dao.tab.news=Notizie + +dao.paidWithBsq=pagato con BSQ +dao.availableBsqBalance=Disponibile per la spesa (verificati + output di resti non confermati) +dao.verifiedBsqBalance=Saldo di tutte le UTXO verificate +dao.unconfirmedChangeBalance=Saldo di tutte gli output di resto non confermati +dao.unverifiedBsqBalance=Saldo di tutte le transazioni non verificate (in attesa di conferma del blocco) +dao.lockedForVoteBalance=Utilizzato per il voto +dao.lockedInBonds=Bloccati in bond +dao.availableNonBsqBalance=Saldo disponibile non-BSQ (BTC) +dao.reputationBalance=Valore di merito (non spendibile) + +dao.tx.published.success=La tua transazione è stata pubblicata con successo. +dao.proposal.menuItem.make=Fai una proposta +dao.proposal.menuItem.browse=Sfoglia le proposte aperte +dao.proposal.menuItem.vote=Vota le proposte +dao.proposal.menuItem.result=Risultati del voto +dao.cycle.headline=Ciclo di votazione +dao.cycle.overview.headline=Panoramica del ciclo di votazione +dao.cycle.currentPhase=Fase attuale +dao.cycle.currentBlockHeight=Altezza attuale del blocco +dao.cycle.proposal=Fase della proposta +dao.cycle.proposal.next=Prossima fase della proposta +dao.cycle.blindVote=Fase di voto alla cieca +dao.cycle.voteReveal=Fase di rivelazione dei voti +dao.cycle.voteResult=Risultato del voto +dao.cycle.phaseDuration={0} blocchi (≈{1}); Blocco {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Blocco {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Transazione del voto di rivelazione pubblicata +dao.voteReveal.txPublished=Il tuo voto ha rivelato che la transazione con identificativo transazione {0} è stata pubblicata correttamente.\n\nCiò accade automaticamente, attraverso il software, se hai partecipato al voto DAO. + +dao.results.cycles.header=Cicli +dao.results.cycles.table.header.cycle=Ciclo +dao.results.cycles.table.header.numProposals=Proposte +dao.results.cycles.table.header.voteWeight=Peso del voto +dao.results.cycles.table.header.issuance=Emissione + +dao.results.results.table.item.cycle=Ciclo {0} avviato: {1} + +dao.results.proposals.header=Proposte del ciclo selezionato +dao.results.proposals.table.header.nameLink=Nome/link +dao.results.proposals.table.header.details=Dettagli +dao.results.proposals.table.header.myVote=Il mio voto +dao.results.proposals.table.header.result=Risultato del voto +dao.results.proposals.table.header.threshold=Soglia +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Risultati del voto per la proposta selezionata + +dao.results.exceptions=Eccezioni del risultato del voto + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Non definito + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=Commissione maker BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=Commissione taker BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Commissione in BSQ minima per il maker +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Commissione in BSQ minima per il taker +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Commissione maker BTC +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Commissione taker BTC +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Commissione in BTC minima per il maker +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Commissione in BSQ minima per il taker +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Commissione di proposta in BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Commissione di voto in BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Richiesta di compenso min. importo BSQ +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Richiesta di compenso max. importo BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Richiesta di rimborso min. importo BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Richiesta di rimborso max. importo BSQ + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Quorum richiesto in BSQ per proposta generica +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Quorum richiesto in BSQ per la richiesta di compenso +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Quorum richiesto in BSQ per la richiesta di rimborso +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Quorum richiesto in BSQ per la modifica di un parametro +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Quorum richiesto in BSQ per la rimozione di un asset +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Quorum richiesto in BSQ per una richiesta di confisca +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Quorum richiesto in BSQ per le proposte di ruoli vincolati + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Soglia richiesta in% per la proposta generica +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Soglia richiesta in% per la richiesta di compenso +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Soglia richiesta in% per la richiesta di rimborso +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Soglia richiesta in% per modificare un parametro +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Soglia obbligatoria in% per la rimozione di un asset +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Soglia richiesta in% per una richiesta di confisca +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Soglia richiesta in % per le proposte di ruolo vincolati + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Indirizzo BTC del destinatario + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Commissione di quotazione giornaliera degli asset +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Volume di scambi minimo degli asset + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Tempo di blocco per pagamento alternativo di scambio tx +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Commissione arbitrale in BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Limite di scambio massimo in BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Ruolo legato all’unità di fattore in BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Limite di emissione per ciclo in BSQ + +dao.param.currentValue=Valore corrente: {0} +dao.param.currentAndPastValue=Valore corrente: {0} (valore al momento della proposta: {1}) +dao.param.blocks={0} blocchi + +dao.results.invalidVotes=Abbiamo avuto voti non validi in quel ciclo di votazione. Ciò può accadere se un voto non è stato distribuito bene nella rete Bisq.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Fase della proposta +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Pausa 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Fase di voto alla cieca +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Pausa 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Fase di rivelazione dei voti +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Pausa 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Fase del risultato + +dao.results.votes.table.header.stakeAndMerit=Peso del voto +dao.results.votes.table.header.stake=Stake +dao.results.votes.table.header.merit=Guadagnato +dao.results.votes.table.header.vote=Votazione + +dao.bond.menuItem.bondedRoles=Ruoli vincolati +dao.bond.menuItem.reputation=Reputazione vincolata +dao.bond.menuItem.bonds=Bond + +dao.bond.dashboard.bondsHeadline=BSQ vincolati +dao.bond.dashboard.lockupAmount=Fondi bloccati +dao.bond.dashboard.unlockingAmount=Sblocco fondi (attendere fino al termine del tempo di blocco) + + +dao.bond.reputation.header=Blocca un deposito per la reputazione +dao.bond.reputation.table.header=I miei vincoli di reputazione +dao.bond.reputation.amount=Quantità di BSQ da bloccare +dao.bond.reputation.time=Tempo di sblocco in blocchi +dao.bond.reputation.salt=Sale +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Blocco +dao.bond.reputation.lockup.headline=Conferma transazione di blocco +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Conferma transazione di sblocco +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=Tutti bond + +dao.bond.bondedReputation=Reputazione Vincolata +dao.bond.bondedRoles=Ruoli vincolati + +dao.bond.details.header=Dettagli ruolo +dao.bond.details.role=Ruolo +dao.bond.details.requiredBond=Bond in BSQ richiesto +dao.bond.details.unlockTime=Tempo di sblocco in blocchi +dao.bond.details.link=Link alla descrizione del ruolo +dao.bond.details.isSingleton=Può essere assunto da più detentori di ruoli +dao.bond.details.blocks={0} blocchi + +dao.bond.table.column.name=Nome +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Tipo di bond +dao.bond.table.column.details=Dettagli +dao.bond.table.column.lockupTxId=ID Tx di blocco +dao.bond.table.column.bondState=Stato bond +dao.bond.table.column.lockTime=Tempo di sblocco +dao.bond.table.column.lockupDate=Data di blocco + +dao.bond.table.button.lockup=Blocco +dao.bond.table.button.unlock=Sbocca +dao.bond.table.button.revoke=Revoca + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Non ancora legato +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Blocco in sospeso +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Bond bloccato +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Sblocco in sospeso +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Tx di sblocco confermata +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Sbloccando il bond +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Bond bloccato +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Bond confiscato + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Ruolo vincolato +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Reputazione vincolata + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Admin GitHub +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Admin forum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Admin Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Admin YouTube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Maintainer Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Maintainer BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Maintainer Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Operatore sito web +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Operatore forum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Operatore nodo seme +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Operatore nodo prezzi +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Operatore nodo Bitcoin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Operatore mercati +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Operatore di inoltro delle notifiche mobile +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Titolare del nome di dominio +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Admin DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediatore +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Arbitro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Proprietario dell'indirizzo di donazione BTC + +dao.burnBsq.assetFee=Listaggio asset +dao.burnBsq.menuItem.assetFee=Commissione listing dell'asset +dao.burnBsq.menuItem.proofOfBurn=Proof of burn +dao.burnBsq.header=Commissione per listaggio asset +dao.burnBsq.selectAsset=Seleziona Asset +dao.burnBsq.fee=Commissione +dao.burnBsq.trialPeriod=Periodo di prova +dao.burnBsq.payFee=Paga commissione +dao.burnBsq.allAssets=Tutti gli asset +dao.burnBsq.assets.nameAndCode=Nome asset +dao.burnBsq.assets.state=Stato +dao.burnBsq.assets.tradeVolume=Volume di scambio +dao.burnBsq.assets.lookBackPeriod=Periodo di verifica +dao.burnBsq.assets.trialFee=Commissione per il periodo di prova +dao.burnBsq.assets.totalFee=Commissioni totali pagate +dao.burnBsq.assets.days={0} giorni +dao.burnBsq.assets.toFewDays=La commissione è troppo bassa. Il numero minimo di giorni per il periodo di prova è {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Nel periodo di prova +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Scambiato attivamente +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=De-listato per inattività +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Rimosso votando + +dao.proofOfBurn.header=Proof of burn +dao.proofOfBurn.amount=Importo +dao.proofOfBurn.preImage=Pre-immagine +dao.proofOfBurn.burn=Burn +dao.proofOfBurn.allTxs=Tutte le transazioni proof of burn +dao.proofOfBurn.myItems=Le mie transazioni proof of burn +dao.proofOfBurn.date=Data +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transazioni +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Firma un messaggio con la chiave dalla transazione proof of burn +dao.proofOfBurn.verify.window.title=Verifica un messaggio con la chiave dalla transazione proof of burn +dao.proofOfBurn.copySig=Copia la firma negli appunti +dao.proofOfBurn.sign=Firma +dao.proofOfBurn.message=Messaggio +dao.proofOfBurn.sig=Firma +dao.proofOfBurn.verify=Verifica +dao.proofOfBurn.verificationResult.ok=Verifica riuscita +dao.proofOfBurn.verificationResult.failed=Verifica fallita + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Fase della proposta +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Pausa prima della fase di voto alla cieca +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Fase di voto alla cieca +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Pausa prima della fase di rivelazione dei voti +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Fase di rivelazione dei voti +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Pausa prima della fase del risultato +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Fase del risultato della votazione + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Fase della proposta +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Voto cieco +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Rivelazione voto +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Risultato del voto + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Richiesta di compenso +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Richiesta di rimborso +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Proposta per un ruolo vincolato +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Proposta per la rimozione di un asset +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Proposta di modifica di un parametro +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Proposta generica +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Proposta di confisca di un bond + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Richiesta di compenso +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Richiesta di rimborso +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Ruolo bloccato +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Rimuovere un altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Cambiare un parametro +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Proposta generica +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscare un bond + +dao.proposal.details=Dettagli della proposta +dao.proposal.selectedProposal=Proposta selezionata +dao.proposal.active.header=Proposte del ciclo attuale +dao.proposal.active.remove.confirm=Sei sicuro di voler rimuovere questa proposta?\nLa commissione di proposta già pagata verrà persa. +dao.proposal.active.remove.doRemove=Sì, rimuovi la mia proposta +dao.proposal.active.remove.failed=Impossibile rimuovere la proposta. +dao.proposal.myVote.title=Voto +dao.proposal.myVote.accept=Accetta la proposta +dao.proposal.myVote.reject=Rifiuta la proposta +dao.proposal.myVote.removeMyVote=Ignora la proposta +dao.proposal.myVote.merit=Peso del voto dai BSQ guadagnati +dao.proposal.myVote.stake=Peso del voto dallo stake +dao.proposal.myVote.revealTxId=ID transazione di rivelazione del voto +dao.proposal.myVote.stake.prompt=Valore massimo disponibile per la votazione: {0} +dao.proposal.votes.header=Imposta il valore per la votazione e pubblica i tuoi voti +dao.proposal.myVote.button=Pubblica voti +dao.proposal.myVote.setStake.description=Dopo aver votato su tutte le proposte devi impostare your stake per il voto bloccando BSQ. Più BSQ bloccati, più peso avrà il tuo voto.\n\nI BSQ bloccati per il voto verranno nuovamente sbloccati durante la fase di rivelazione del voto. +dao.proposal.create.selectProposalType=Seleziona il tipo di proposta +dao.proposal.create.phase.inactive=Attendere fino alla fase della proposta successiva +dao.proposal.create.proposalType=Tipo di proposta +dao.proposal.create.new=Fai una nuova proposta +dao.proposal.create.button=Fai una proposta +dao.proposal.create.publish=Pubblica proposta +dao.proposal.create.publishing=La pubblicazione della proposta è in corso ... +dao.proposal=proposta +dao.proposal.display.type=Tipo di proposta +dao.proposal.display.name=Nome utente GitHub esatto +dao.proposal.display.link=Link a informazioni dettagliate +dao.proposal.display.link.prompt=Link alla proposta +dao.proposal.display.requestedBsq=Importo richiesto in BSQ +dao.proposal.display.txId=ID transazione proposta +dao.proposal.display.proposalFee=Commissione proposta +dao.proposal.display.myVote=Il mio voto +dao.proposal.display.voteResult=Riepilogo dei risultati della votazione +dao.proposal.display.bondedRoleComboBox.label=Tipo di ruolo vincolato +dao.proposal.display.requiredBondForRole.label=Obbligazione richiesta per ruolo +dao.proposal.display.option=Opzione + +dao.proposal.table.header.proposalType=Tipo di proposta +dao.proposal.table.header.link=Link +dao.proposal.table.header.myVote=Il mio voto +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Rimuovi +dao.proposal.table.icon.tooltip.removeProposal=Rimuovi la mia proposta +dao.proposal.table.icon.tooltip.changeVote=Voto attuale: ''{0}''. Cambia voto in: ''{1}'' + +dao.proposal.display.myVote.accepted=Accettato +dao.proposal.display.myVote.rejected=Respinto +dao.proposal.display.myVote.ignored=Ignorato +dao.proposal.display.myVote.unCounted=Il voto non è stato incluso nel risultato +dao.proposal.myVote.summary=Votato: {0}; Peso del voto: {1} (guadagnato: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Il voto non è valido + +dao.proposal.voteResult.success=Accettato +dao.proposal.voteResult.failed=Respinto +dao.proposal.voteResult.summary=Risultato: {0}; Soglia: {1} (richiesto> {2}); Quorum: {3} (richiesto> {4}) + +dao.proposal.display.paramComboBox.label=Seleziona il parametro da modificare +dao.proposal.display.paramValue=Valore del parametro + +dao.proposal.display.confiscateBondComboBox.label=Scegli bond +dao.proposal.display.assetComboBox.label=Asset da rimuovere + +dao.blindVote=voto cieco + +dao.blindVote.startPublishing=Pubblicando la transazione di voto alla cieca... +dao.blindVote.success=La tua transazione di voto alla cieca è stata pubblicata con successo.\n\nSi noti che è necessario essere online nella fase di rivelazione dei voti in modo che la propria applicazione Bisq possa pubblicare la transazione di rivelazione del voto. Senza la transazione di rivelazione del voto, il tuo voto non sarebbe valido! + +dao.wallet.menuItem.send=Invia +dao.wallet.menuItem.receive=Ricevi +dao.wallet.menuItem.transactions=Transazioni + +dao.wallet.dashboard.myBalance=Saldo del mio portafoglio + +dao.wallet.receive.fundYourWallet=Il tuo indirizzo di ricezione BSQ +dao.wallet.receive.bsqAddress=Indirizzo del portafoglio BSQ (Indirizzo non ancora utilizzato) + +dao.wallet.send.sendFunds=Invia fondi +dao.wallet.send.sendBtcFunds=Invia fondi non BSQ (BTC) +dao.wallet.send.amount=Importo in BSQ +dao.wallet.send.btcAmount=Importo in BTC (fondi non BSQ) +dao.wallet.send.setAmount=Imposta l'importo da prelevare (l'importo minimo è {0}) +dao.wallet.send.receiverAddress=Indirizzo BSQ del destinatario +dao.wallet.send.receiverBtcAddress=Indirizzo BTC del destinatario +dao.wallet.send.setDestinationAddress=Inserisci il tuo indirizzo di destinazione +dao.wallet.send.send=Invia fondi BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Invia fondi BTC +dao.wallet.send.sendFunds.headline=Conferma richiesta di prelievo +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Ultimo blocco verificato: {0} +dao.wallet.chainHeightSyncing=In attesa di blocchi... Verificati {0} blocchi su {1} +dao.wallet.tx.type=Tipo + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Non definito +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Non riconosciuto +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Transazione BSQ non verificata +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Transazione BSQ non valida +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Transazione genesi +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Trasferisci BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ ricevuti +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ inviati +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Commissione di scambio +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Commissione per richiesta di compenso +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Commissione per richiesta di rimborso +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Commissione per la proposta +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Commissione per voto alla cieca +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Rivelazione voto +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Blocca bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Sblocca bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Commissione listing dell'asset +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Proof of burn +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregolare + +dao.tx.withdrawnFromWallet=BTC prelevati dal portafoglio +dao.tx.issuanceFromCompReq=Richiesta di compensazione +dao.tx.issuanceFromCompReq.tooltip=Richiesta di compenso che ha portato all'emissione di nuovi BSQ.\nData di emissione: {0} +dao.tx.issuanceFromReimbursement=Richiesta di rimborso/emissione +dao.tx.issuanceFromReimbursement.tooltip=Richiesta di rimborso che ha portato all'emissione di nuovi BSQ.\nData di emissione: {0} +dao.proposal.create.missingBsqFunds=Non hai fondi BSQ sufficienti per creare la proposta. Se hai una transazione BSQ non confermata, devi attendere una conferma sulla blockchain perché BSQ è validato solo se è incluso in un blocco.\nMancante: {0} + +dao.proposal.create.missingBsqFundsForBond=Non hai fondi BSQ sufficienti per questo ruolo. Puoi comunque pubblicare questa proposta, ma avrai bisogno dell'intero importo BSQ richiesto per questo ruolo se viene accettato.\nMancante: {0} + +dao.proposal.create.missingMinerFeeFunds=Non hai fondi BTC sufficienti per creare la transazione di proposta. Tutte le transazioni BSQ richiedono una commissione di mining in BTC.\nMancante: {0} + +dao.proposal.create.missingIssuanceFunds=Non hai fondi BTC sufficienti per creare la transazione di proposta. Tutte le transazioni BSQ richiedono una commissione di mining in BTC e anche le transazioni di emissione richiedono BTC per l'importo BSQ richiesto ({0} Satoshi/BSQ).\nMancante: {1} + +dao.feeTx.confirm=Conferma transazione {0} +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=LA DAO BISQ +dao.news.bisqDAO.description=Proprio come lo scambio di Bisq è decentralizzato e resistente alla censura, così è il suo modello di governance - e il token Bisq DAO e BSQ sono gli strumenti che lo rendono possibile. +dao.news.bisqDAO.readMoreLink=Ulteriori informazioni sulla Bisq DAO + +dao.news.pastContribution.title=HAI EFFETTUATO CONTRIBUTI IN PASSATO? RICHIEDI BSQ +dao.news.pastContribution.description=Se hai contribuito a Bisq, utilizza l'indirizzo BSQ qui sotto e fai una richiesta per prendere parte alla distribuzione genesi BSQ. +dao.news.pastContribution.yourAddress=L'indirizzo del tuo wallet BSQ +dao.news.pastContribution.requestNow=Richiedi ora + +dao.news.DAOOnTestnet.title=ESEGUI LA BISQ DAO NEL NOSTRO TESTNET +dao.news.DAOOnTestnet.description=La mainnet Bisq DAO non è ancora stata lanciata, ma puoi conoscere Bisq DAO eseguendolo sulla nostra testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Passa alla modalità Testnet DAO +dao.news.DAOOnTestnet.firstSection.content=Passa a DAO Testnet dalla schermata Impostazioni. +dao.news.DAOOnTestnet.secondSection.title=2. Acquista un po' di BSQ +dao.news.DAOOnTestnet.secondSection.content=Richiedi BSQ su Slack o Acquista BSQ su Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Partecipa a un Ciclo di Votazione +dao.news.DAOOnTestnet.thirdSection.content=Fare proposte e votare proposte per cambiare vari aspetti di Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Esplora un BSQ Block Explorer +dao.news.DAOOnTestnet.fourthSection.content=Poiché BSQ è solo bitcoin, puoi vedere le transazioni BSQ sul nostro explorer di blocchi bitcoin. +dao.news.DAOOnTestnet.readMoreLink=Leggi la documentazione completa + +dao.monitor.daoState=Stato DAO +dao.monitor.proposals=Stato delle proposte +dao.monitor.blindVotes=Stato dei voti ciechi + +dao.monitor.table.peers=Peers +dao.monitor.table.conflicts=Conflitti +dao.monitor.state=Stato +dao.monitor.requestAlHashes=Richiedi tutti gli hash +dao.monitor.resync=Risincronizza lo stato della DAO +dao.monitor.table.header.cycleBlockHeight=Altezza ciclo / blocco +dao.monitor.table.cycleBlockHeight=Ciclo {0} / blocco {1} +dao.monitor.table.seedPeers=Nodo seme: {0} + +dao.monitor.daoState.headline=Stato DAO +dao.monitor.daoState.table.headline=Catena di hash di stato DAO +dao.monitor.daoState.table.blockHeight=Altezza del blocco +dao.monitor.daoState.table.hash=Hash dello stato DAO +dao.monitor.daoState.table.prev=Hash precedente +dao.monitor.daoState.conflictTable.headline=Hash di stato DAO da peer in conflitto +dao.monitor.daoState.utxoConflicts=Conflitti UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Altezza blocco: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Somma di tutti gli UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Somma di tutti i BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=Lo stato DAO non è sincronizzato con la rete. Dopo il riavvio, lo stato DAO verrà risincronizzato. + +dao.monitor.proposal.headline=Stato delle proposte +dao.monitor.proposal.table.headline=Catena di hash dello stato della proposta +dao.monitor.proposal.conflictTable.headline=Hash di stato della proposta da peer in conflitto + +dao.monitor.proposal.table.hash=Hash dello stato della proposta +dao.monitor.proposal.table.prev=Hash precedente +dao.monitor.proposal.table.numProposals=N. di proposte + +dao.monitor.isInConflictWithSeedNode=I dati locali non sono in accordo con almeno un nodo seme. Risincronizzare nuovamente lo stato DAO. +dao.monitor.isInConflictWithNonSeedNode=Uno dei tuoi peer non è d'accordo con la rete ma il tuo nodo è sincronizzato con i nodi seed. +dao.monitor.daoStateInSync=Il nodo locale è in accordo con la rete + +dao.monitor.blindVote.headline=Stato dei voti ciechi +dao.monitor.blindVote.table.headline=Catena di hash dello stato del voto cieco +dao.monitor.blindVote.conflictTable.headline=Hash dello stato del voto alla cieca da peer in conflitto +dao.monitor.blindVote.table.hash=Hash dello stato di voto cieco +dao.monitor.blindVote.table.prev=Hash precedente +dao.monitor.blindVote.table.numBlindVotes=N. di voti ciechi + +dao.factsAndFigures.menuItem.supply=Supply BSQ +dao.factsAndFigures.menuItem.transactions=Transazioni BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Prezzo di scambio medio BSQ/BTC di 90 giorni +dao.factsAndFigures.dashboard.avgPrice30=Prezzo di scambio medio BSQ/BTC di 30 giorni +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=BSQ totale disponibile +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ emessi v. BSQ bruciati + +dao.factsAndFigures.supply.issued=BSQ coniati +dao.factsAndFigures.supply.compReq=Compensation requests +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ coniati nella trasazione di genesi +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ coniati per le richieste di compensazione +dao.factsAndFigures.supply.reimbursementAmount=BSQ coniati per le richieste di rimborso +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ bruciati + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Volume di scambio +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Stato globale dei BSQ bloccati +dao.factsAndFigures.supply.totalLockedUpAmount=Bloccati in bond +dao.factsAndFigures.supply.totalUnlockingAmount=Sbloccando i BSQ dai vincoli +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ sbloccati dalle obbligazioni +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ confiscati dalle obbligazioni +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Transazione genesi +dao.factsAndFigures.transactions.genesisBlockHeight=Altezza del blocco genesi +dao.factsAndFigures.transactions.genesisTxId=ID transazione genesi +dao.factsAndFigures.transactions.txDetails=Statistiche transazioni BSQ +dao.factsAndFigures.transactions.allTx=Numero di tutte le transazioni BSQ +dao.factsAndFigures.transactions.utxo=Numero di tutti gli output di transazione non spesi +dao.factsAndFigures.transactions.compensationIssuanceTx=Numero di tutte le transazioni di richieste di compensazione +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Numero di tutte le transazioni di richieste di rimborso +dao.factsAndFigures.transactions.burntTx=Numero di tutte le transazioni di pagamento delle commissioni +dao.factsAndFigures.transactions.invalidTx=N. di tutte le transazioni invalide +dao.factsAndFigures.transactions.irregularTx=N. di tutte le transazioni irregolari + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Saldo disponibile + +contractWindow.title=Dettagli disputa +contractWindow.dates=Data dell'offerta / Data di scambio +contractWindow.btcAddresses=Indirizzo Bitcoin acquirente BTC / venditore BTC +contractWindow.onions=Indirizzo di rete acquirente BTC / venditore BTC +contractWindow.accountAge=Età account acquirente BTC / venditore BTC +contractWindow.numDisputes=Numero di controversie acquirente BTC / venditore BTC +contractWindow.contractHash=Hash contratto + +displayAlertMessageWindow.headline=Informazioni importanti! +displayAlertMessageWindow.update.headline=Informazioni importanti sull'aggiornamento! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=File: +displayUpdateDownloadWindow.downloadingFile=Download: {0} +displayUpdateDownloadWindow.verifiedSigs=Firma verificata con chiavi: +displayUpdateDownloadWindow.status.downloading=Download file... +displayUpdateDownloadWindow.status.verifying=Verifica firma... +displayUpdateDownloadWindow.button.label=Scarica il programma di installazione e verifica la firma +displayUpdateDownloadWindow.button.downloadLater=Scarica più tardi +displayUpdateDownloadWindow.button.ignoreDownload=Ignora questa versione +displayUpdateDownloadWindow.headline=È disponibile un nuovo aggiornamento di Bisq! +displayUpdateDownloadWindow.download.failed.headline=Download fallito +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=La nuova versione è stata scaricata correttamente e la firma è stata verificata.\n\nAprire la cartella di download, chiudere l'applicazione e installare la nuova versione. +displayUpdateDownloadWindow.download.openDir=Apri la cartella di download + +disputeSummaryWindow.title=Sommario +disputeSummaryWindow.openDate=Data di apertura del ticket +disputeSummaryWindow.role=Ruolo del trader +disputeSummaryWindow.payout=Pagamento dell'importo di scambio +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} ottiene il pagamento dell'importo commerciale +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Pagamento personalizzato +disputeSummaryWindow.payoutAmount.buyer=Importo pagamento dell'acquirente +disputeSummaryWindow.payoutAmount.seller=Importo pagamento del venditore +disputeSummaryWindow.payoutAmount.invert=Utilizza perdente come editore +disputeSummaryWindow.reason=Motivo della disputa +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Errore +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilità +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violazione del protocollo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Nessuna risposta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Truffa +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Altro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banca +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Note di sintesi +disputeSummaryWindow.addSummaryNotes=Aggiungi note di sitensi +disputeSummaryWindow.close.button=Chiudi ticket + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=Devi chiudere anche il ticket dei peer di trading! +disputeSummaryWindow.close.txDetails.headline=Pubblica transazione di rimborso +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=L'acquirente riceve {0} all'indirizzo: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Il venditore riceve {0} all'indirizzo: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline={0} strumento portafoglio di emergenza +emptyWalletWindow.info=Utilizzalo solo in caso di emergenza se non puoi accedere al tuo fondo dall'interfaccia utente.\n\nSi noti che tutte le offerte aperte verranno chiuse automaticamente quando si utilizza questo strumento.\n\nPrima di utilizzare questo strumento, eseguire il backup della directory dei dati. Puoi farlo in \"Account/Backup\".\n\nTi preghiamo di segnalarci il tuo problema e di presentare una segnalazione di bug su GitHub o sul forum Bisq in modo da poter esaminare la causa del problema. +emptyWalletWindow.balance=Il saldo disponibile del tuo portafoglio +emptyWalletWindow.bsq.btcBalance=Saldo di satoshi non-BSQ + +emptyWalletWindow.address=Il tuo indirizzo di destinazione +emptyWalletWindow.button=Invia tutti i fondi +emptyWalletWindow.openOffers.warn=Hai offerte aperte che verranno rimosse se svuoti il portafoglio.\nSei sicuro di voler svuotare il tuo portafoglio? +emptyWalletWindow.openOffers.yes=Sì, ne sono sicuro +emptyWalletWindow.sent.success=Il saldo del tuo portafoglio è stato trasferito correttamente. + +enterPrivKeyWindow.headline=Immettere la chiave privata per la registrazione + +filterWindow.headline=Modifica elenco filtri +filterWindow.offers=Offerte filtrate (separate con una virgola) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Dati dell'account di trading filtrati:\nFormato: virgola sep. elenco di [ID metodo di pagamento | campo dati | valore] +filterWindow.bannedCurrencies=Codici valuta filtrati (separati con una virgola) +filterWindow.bannedPaymentMethods=ID dei metodi di pagamento filtrati (separati con una virgola) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Arbitri filtrati (indirizzi onion separati con una virgola) +filterWindow.mediators=Mediatori filtrati (indirizzi onion separati con una virgola) +filterWindow.refundAgents=Agenti di rimborso filtrati (virgola sep. indirizzi onion) +filterWindow.seedNode=Nodi seme filtrati (separati con una virgola) +filterWindow.priceRelayNode=Prezzo filtrato dai nodi relay (virgola sep. indirizzi onion) +filterWindow.btcNode=Nodi Bitcoin filtrati (indirizzo + porta separati con una virgola) +filterWindow.preventPublicBtcNetwork=Impedisci l'utilizzo della rete pubblica Bitcoin +filterWindow.disableDao=Disabilita DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Versione minima richiesta per la DAO +filterWindow.disableTradeBelowVersion=Versione minima richiesta per il trading +filterWindow.add=Aggiungi filtro +filterWindow.remove=Rimuovi filtro +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Importo BTC minimo +offerDetailsWindow.min=(min. {0}) +offerDetailsWindow.distance=(distanza dal prezzo di mercato: {0}) +offerDetailsWindow.myTradingAccount=Il mio account di scambio +offerDetailsWindow.offererBankId=(ID banca del produttore/BIC/SWIFT) +offerDetailsWindow.offerersBankName=(nome della banca del maker) +offerDetailsWindow.bankId=ID Banca (es. BIC o SWIFT) +offerDetailsWindow.countryBank=Paese della banca del maker +offerDetailsWindow.commitment=Impegno +offerDetailsWindow.agree=Accetto +offerDetailsWindow.tac=Termini e condizioni +offerDetailsWindow.confirm.maker=Conferma: Piazza l'offerta a {0} bitcoin +offerDetailsWindow.confirm.taker=Conferma: Accetta l'offerta a {0} bitcoin +offerDetailsWindow.creationDate=Data di creazione +offerDetailsWindow.makersOnion=Indirizzo .onion del maker + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Richiesta di pagamento:\n{0} + +selectDepositTxWindow.headline=Seleziona la transazione di deposito per la disputa +selectDepositTxWindow.msg=La transazione di deposito non è stata archiviata nello scambio.\nSeleziona una delle transazioni multisig esistenti dal tuo portafoglio che era la transazione di deposito utilizzata nello scambio fallito.\n\nPuoi trovare la transazione corretta aprendo la finestra dei dettagli dello scambio (fai clic sull'ID dello scambio nell'elenco) e seguendo l'output della commissione di scambio della transazione successiva in cui vedi la transazione di deposito multisig (l'indirizzo inizia con 3). Tale ID transazione dovrebbe essere visibile nell'elenco presentato qui. Una volta trovata la transazione corretta, seleziona quella transazione e continua.\n\nCi scusiamo per l'inconveniente, ma questo caso di errore dovrebbe accadere molto raramente e in futuro cercheremo di trovare modi migliori per risolverlo.\n  +selectDepositTxWindow.select=Seleziona la transazione di deposito + +sendAlertMessageWindow.headline=Invia notifica globale +sendAlertMessageWindow.alertMsg=Messaggio d'avvertimento +sendAlertMessageWindow.enterMsg=Inserisci messaggio +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=Nuova versione numero +sendAlertMessageWindow.send=Invia notifica +sendAlertMessageWindow.remove=Rimuovi notifica + +sendPrivateNotificationWindow.headline=Invia messaggio privato +sendPrivateNotificationWindow.privateNotification=Notifica privata +sendPrivateNotificationWindow.enterNotification=Inserisci notifica +sendPrivateNotificationWindow.send=Invia notifica privata + +showWalletDataWindow.walletData=Dati portafoglio +showWalletDataWindow.includePrivKeys=Includi chiavi private + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Accordo per gli utenti +tacWindow.agree=Accetto +tacWindow.disagree=Non accetto ed esco +tacWindow.arbitrationSystem=Risoluzione disputa + +tradeDetailsWindow.headline=Scambio +tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato: +tradeDetailsWindow.tradeDate=Data di scambio +tradeDetailsWindow.txFee=Commissione di mining +tradeDetailsWindow.tradingPeersOnion=Indirizzi onion peer di trading +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Stato di scambio +tradeDetailsWindow.agentAddresses=Arbitro/Mediatore +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Inserisci la password per sbloccare + +torNetworkSettingWindow.header=Impostazioni rete Tor +torNetworkSettingWindow.noBridges=Non usare bridge +torNetworkSettingWindow.providedBridges=Connetti con i bridge forniti +torNetworkSettingWindow.customBridges=Inserisci bridge personalizzati +torNetworkSettingWindow.transportType=Tipo di trasporto +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (consigliato) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Immettere uno o più bridge relays (uno per riga) +torNetworkSettingWindow.enterBridgePrompt=tipo indirizzo:porta +torNetworkSettingWindow.restartInfo=Devi riavviare per applicare i cambiamenti +torNetworkSettingWindow.openTorWebPage=Apri il sito web del progetto Tor +torNetworkSettingWindow.deleteFiles.header=Problemi di connessione? +torNetworkSettingWindow.deleteFiles.info=Se si verificano ripetuti problemi di connessione all'avvio, l'eliminazione di file Tor obsoleti potrebbe essere d'aiuto. Per fare ciò, fai clic sul pulsante in basso e riavvia in seguito. +torNetworkSettingWindow.deleteFiles.button=Rimuovi i file Tor obsoleti e spegni +torNetworkSettingWindow.deleteFiles.progress=Spegnimento Tor in corso +torNetworkSettingWindow.deleteFiles.success=File obsoleti di Tor eliminati con successo. Riavvia. +torNetworkSettingWindow.bridges.header=Tor è bloccato? +torNetworkSettingWindow.bridges.info=Se Tor è bloccato dal tuo provider di servizi Internet o dal tuo paese, puoi provare a utilizzare i bridge Tor.\nVisitare la pagina Web Tor all'indirizzo: https://bridges.torproject.org/bridges per ulteriori informazioni sui bridge e sui trasporti collegabili.\n  + +feeOptionWindow.headline=Scegli la valuta per il pagamento delle commissioni commerciali +feeOptionWindow.info=Puoi scegliere di pagare la commissione commerciale in BSQ o in BTC. Se scegli BSQ approfitti della commissione commerciale scontata. +feeOptionWindow.optionsLabel=Scegli la valuta per il pagamento delle commissioni commerciali +feeOptionWindow.useBTC=Usa BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notifica +popup.headline.instruction=Nota bene: +popup.headline.attention=Attenzione +popup.headline.backgroundInfo=Informazioni di base +popup.headline.feedback=Completato +popup.headline.confirmation=Conferma +popup.headline.information=Informazione +popup.headline.warning=Attenzione +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 bisq.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} + +error.spvFileCorrupted=Si è verificato un errore durante la lettura del file della catena SPV.\nÈ possibile che il file della catena SPV sia danneggiato.\n\nMessaggio di errore: {0}\n\nVuoi cancellarlo ed iniziare una nuova sincronizzazione? +error.deleteAddressEntryListFailed=Impossibile eliminare il file AddressEntryList.\nErrore: {0} +error.closedTradeWithUnconfirmedDepositTx=La transazione di deposito dello scambio chiuso con ID {0} non è ancora confermata.\n\nEffettuare una risincronizzazione SPV in \"Setting/Network info\" per verificare se la transazione è valida.\n  +error.closedTradeWithNoDepositTx=La transazione di deposito dello scambio chiuso con ID {0} è nulla.\n\nRiavvia l'applicazione per ripulire l'elenco delle transazioni chiuse. + +popup.warning.walletNotInitialized=Il portafoglio non è ancora inizializzato +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=Probabilmente hai la versione Bisq sbagliata per questo computer.\nL'architettura del tuo computer è: {0}.\nIl binario Bisq che hai installato è: {1}.\nChiudere e reinstallare la versione corretta ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq è già in esecuzione. Non è possibile eseguire due istanze di Bisq. +popup.warning.tradePeriod.halfReached=Il tuo scambio con ID {0} ha raggiunto la metà del massimo periodo di negoziazione consentito e non è ancora completato.\n\nIl periodo di scambio termina il {1}\n\nPer ulteriori informazioni, controllare lo stato dello scambio in \"Portafoglio/Scambi aperti\". +popup.warning.tradePeriod.ended= \nIl tuo scambio con ID {0} ha raggiunto il limite massimo del periodo di scambio consentito e non è stato completato.\n\nIl periodo di scambio è terminato il {1}\n\nPer favore verifica il tuo trade su \"Portafoglio/Scambi aperti\" per contattare il mediatore. +popup.warning.noTradingAccountSetup.headline=Non hai impostato un account di trading +popup.warning.noTradingAccountSetup.msg=È necessario impostare un conto in valuta nazionale o altcoin prima di poter creare un'offerta.\nVuoi configurare un conto? +popup.warning.noArbitratorsAvailable=Non ci sono arbitri disponibili. +popup.warning.noMediatorsAvailable=Non ci sono mediatori disponibili. +popup.warning.notFullyConnected=È necessario attendere fino a quando non si è completamente connessi alla rete.\nQuesto potrebbe richiedere fino a circa 2 minuti all'avvio. +popup.warning.notSufficientConnectionsToBtcNetwork=Devi aspettare fino a quando non hai almeno {0} connessioni alla rete Bitcoin. +popup.warning.downloadNotComplete=Devi aspettare fino al completamento del download dei blocchi Bitcoin mancanti. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Sei sicuro di voler rimuovere quell'offerta?\nLa commissione del maker di {0} andrà persa se rimuovi quell'offerta. +popup.warning.tooLargePercentageValue=Non è possibile impostare una percentuale del 100% o superiore. +popup.warning.examplePercentageValue=Inserisci un numero percentuale come \"5.4\" per il 5,4% +popup.warning.noPriceFeedAvailable=Non è disponibile alcun feed di prezzi per la valuta. Non è possibile utilizzare un prezzo basato su percentuale.\nSeleziona il prezzo fisso. +popup.warning.sendMsgFailed=Invio del messaggio al tuo partner commerciale non riuscito.\nTi preghiamo di riprovare e se continua a fallire segnalare un bug. +popup.warning.insufficientBtcFundsForBsqTx=Non hai fondi BTC sufficienti per pagare la commissione di mining per quella transazione.\nPer favore finanzia il tuo portafoglio BTC.\nFondi mancanti: {0} +popup.warning.bsqChangeBelowDustException=Questa transazione crea un output di cambio BSQ che è inferiore al limite di polvere (5,46 BSQ) e verrebbe rifiutato dalla rete Bitcoin.\n\nÈ necessario inviare una quantità maggiore per evitare l'output di cambio (ad es. aggiungendo la quantità di polvere alla quantità di invio) o aggiungere più fondi BSQ al portafoglio in modo da evitare di generare una produzione di polvere.\n\nL'output della polvere è {0}. +popup.warning.btcChangeBelowDustException=Questa transazione crea un output di cambio che è al di sotto del limite di polvere (546 Satoshi) e verrebbe rifiutato dalla rete Bitcoin.\n\nÈ necessario aggiungere la quantità di polvere alla quantità di invio per evitare di generare un output di polvere.\n\nL'output della polvere è {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Il tuo portafoglio BSQ non ha fondi sufficienti per pagare la commissione commerciale in BSQ. +popup.warning.messageTooLong=Il tuo messaggio supera la dimensione massima consentita. Si prega di inviarlo in più parti o caricarlo su un servizio come https://pastebin.com. +popup.warning.lockedUpFunds=Hai bloccato i fondi da uno scambio fallito.\nSaldo bloccato: {0}\nIndirizzo tx deposito: {1}\nID scambio: {2}.\n\nApri un ticket di supporto selezionando lo scambio nella schermata degli scambi aperti e premendo \"alt + o\" o \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=ripetitore di prezzo +popup.warning.seed=seme +popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Bisq all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Bisq. +popup.warning.mandatoryUpdate.dao=Si prega di aggiornare Bisq all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita Bisq DAO e BSQ per le vecchie versioni. Per ulteriori informazioni, consultare il forum Bisq.\n  +popup.warning.disable.dao=Bisq DAO e BSQ sono temporaneamente disabilitati. Per ulteriori informazioni, consultare il forum Bisq. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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ù BTC da trasferire. + +popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Bitcoin.\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 Bisq nel team di Bisq Keybase. + +popup.warning.trade.txRejected.tradeFee=commissione di scambio +popup.warning.trade.txRejected.deposit=deposita +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=La commissione della transazione del creatore dell'offerta con ID {0} non è valida.\nTransazione ID={1}.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Bisq nel team di Bisq Keybase. + +popup.info.securityDepositInfo=Per garantire che i trader seguano il protocollo di scambio, entrambi devono pagare un deposito cauzionale.\n\nQuesto deposito viene conservato nel tuo portafoglio di scambio fino a quando la tua transazione non è stata completata con successo, quindi ti viene rimborsato.\n\nNota: se stai creando una nuova offerta, Bisq deve essere in esecuzione per essere accettato da un altro trader. Per mantenere le tue offerte online, mantieni Bisq in funzione e assicurati che anche questo computer rimanga online (ad esempio, assicurati che non passi alla modalità standby ... monitor standby va bene). + +popup.info.cashDepositInfo=Assicurati di avere una filiale bancaria nella tua zona per poter effettuare il deposito in contanti.\nL'ID bancario (BIC/SWIFT) della banca del venditore è: {0}. +popup.info.cashDepositInfo.confirm=Confermo di poter effettuare il deposito +popup.info.shutDownWithOpenOffers=Bisq viene chiuso, ma ci sono offerte aperte.\n\nQueste offerte non saranno disponibili sulla rete P2P mentre Bisq rimane chiuso, ma verranno ripubblicate sulla rete P2P al prossimo avvio di Bisq.\n\nPer mantenere le tue offerte attive è necessario che Bisq rimanga in funzione ed il computer online (assicurati che non vada in modalità standby. Il solo monitor in standby non è un problema). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Notifica privata importante! + +popup.securityRecommendation.headline=Raccomandazione di sicurezza importante +popup.securityRecommendation.msg=Vorremmo ricordarti di prendere in considerazione l'utilizzo della protezione con password per il tuo portafoglio se non l'avessi già abilitato.\n\nSi consiglia inoltre di annotare le parole seme del portafoglio. Le parole seme sono come una password principale per recuperare il tuo portafoglio Bitcoin.\nNella sezione \"Wallet Seed\" trovi ulteriori informazioni.\n\nInoltre, è necessario eseguire il backup della cartella completa dei dati dell'applicazione nella sezione \"Backup\". + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Arresto in corso +popup.shutDownInProgress.msg=La chiusura dell'applicazione può richiedere un paio di secondi.\nNon interrompere il processo. + +popup.attention.forTradeWithId=Attenzione richiesta per gli scambi con ID {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Disponibili più conti di pagamento +popup.info.multiplePaymentAccounts.msg=Hai più account di pagamento disponibili per questa offerta. Assicurati di aver scelto quello giusto. + +popup.accountSigning.selectAccounts.headline=Seleziona conti di pagamento +popup.accountSigning.selectAccounts.description=In base al metodo di pagamento e al momento in cui verranno selezionati tutti i conti di pagamento collegati a una controversia in cui si è verificato un pagamento +popup.accountSigning.selectAccounts.signAll=Firma tutti i metodi di pagamento +popup.accountSigning.selectAccounts.datePicker=Seleziona il momento in cui verranno firmati gli account + +popup.accountSigning.confirmSelectedAccounts.headline=Conferma account di pagamento selezionati +popup.accountSigning.confirmSelectedAccounts.description=In base ai dati inseriti, verranno selezionati {0} account di pagamento. +popup.accountSigning.confirmSelectedAccounts.button=Conferma conti di pagamento +popup.accountSigning.signAccounts.headline=Conferma la firma dei conti di pagamento +popup.accountSigning.signAccounts.description=In base alla tua selezione, verranno firmati {0} account di pagamento. +popup.accountSigning.signAccounts.button=Firma conti di pagamento +popup.accountSigning.signAccounts.ECKey=Immettere la chiave dell'arbitro privato +popup.accountSigning.signAccounts.ECKey.error=ECKey dell'arbitro errata + +popup.accountSigning.success.headline=Congratulazioni +popup.accountSigning.success.description=Tutti gli account di pagamento {0} sono stati firmati correttamente! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Uno dei tuoi conti di pagamento è stato verificato e firmato da un arbitro. Il trading con questo account firmerà automaticamente l'account del tuo peer di trading dopo una negoziazione di successo.\n\n{0} +popup.accountSigning.signedByPeer=Uno dei tuoi conti di pagamento è stato verificato e firmato da un peer di trading. Il limite di trading iniziale verrà revocato e sarai in grado di firmare altri account tra {0} giorni da adesso.\n\n{1} +popup.accountSigning.peerLimitLifted=Il limite iniziale per uno dei tuoi account è stato revocato.\n\n{0} +popup.accountSigning.peerSigner=Uno dei tuoi account è abbastanza maturo per firmare altri account di pagamento e il limite iniziale per uno dei tuoi account è stato revocato.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notifica per scambi con ID {0} +notification.ticket.headline=Biglietto di supporto per scambi con ID {0} +notification.trade.completed=Il commercio è ora completato e puoi ritirare i tuoi fondi. +notification.trade.accepted=La tua offerta è stata accettata da un BTC {0}. +notification.trade.confirmed=Il tuo trade ha almeno una conferma blockchain.\nPuoi iniziare il pagamento ora. +notification.trade.paymentStarted=L'acquirente BTC ha avviato il pagamento. +notification.trade.selectTrade=Seleziona scambio +notification.trade.peerOpenedDispute=Il tuo peer di trading ha aperto un {0}. +notification.trade.disputeClosed={0} è stato chiuso. +notification.walletUpdate.headline=Aggiornamento del portafoglio di trading +notification.walletUpdate.msg=Il tuo portafoglio di trading è sufficientemente finanziato.\nImporto: {0} +notification.takeOffer.walletUpdate.msg=Il tuo portafoglio di trading era già sufficientemente finanziato da un precedente tentativo di offerta.\nImporto: {0} +notification.tradeCompleted.headline=Scambio completato +notification.tradeCompleted.msg=Puoi ritirare i tuoi fondi sul tuo portafoglio Bitcoin esterno o trasferirlo sul portafoglio Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Mostra la finestra dell'applicazione +systemTray.hide=Nascondi la finestra dell'applicazione +systemTray.info=Informazioni su Bisq +systemTray.exit=Esci +systemTray.tooltip=Bisq: una rete di scambio decentralizzata di bitcoin + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=Account di trading salvati nel percorso:\n{0} +guiUtil.accountExport.noAccountSetup=Non hai account di trading impostati per l'esportazione. +guiUtil.accountExport.selectPath=Seleziona il percorso per {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Account di trading con id {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Non abbiamo importato un account di trading con ID {0} perché esiste già. +guiUtil.accountExport.exportFailed=L'esportazione CSV non è andata a buon fine a causa di un errore.\nErrore = {0} +guiUtil.accountExport.selectExportPath=Seleziona il percorso di esportazione +guiUtil.accountImport.imported=Conto di trading importato dalla cartella:\n{0}\n\nAccount importati:\n{1} +guiUtil.accountImport.noAccountsFound=Non è stato trovato alcun conto di trading esportato nella cartella: {0}.\nIl nome del file è {1}." +guiUtil.openWebBrowser.warning=Stai per aprire una pagina web nel tuo browser.\nVuoi aprire la pagina web adesso?\n\nSe non si utilizza \"Tor Browser\" come Web browser predefinito, ci si collegherà alla pagina Web in chiaro.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Apri la pagina web e non chiedere più +guiUtil.openWebBrowser.copyUrl=Copia URL e annulla +guiUtil.ofTradeAmount=di importo commerciale +guiUtil.requiredMinimum=(minimo richiesto) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Seleziona valuta +list.currency.showAll=Mostra tutto +list.currency.editList=Modifica elenco valuta + +table.placeholder.noItems=Attualmente non ci sono {0} disponibili +table.placeholder.noData=Attualmente non ci sono dati disponibili +table.placeholder.processingData=Elaborazione dei dati... + + +peerInfoIcon.tooltip.tradePeer=Peer di scambio +peerInfoIcon.tooltip.maker=Del Maker +peerInfoIcon.tooltip.trade.traded={0} Indirizzo onion: {1}\nHai già scambiato {2} volte in quel peer\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} Indirizzo onion: {1}\nFinora non hai commerciato con quel peer.\n{2} +peerInfoIcon.tooltip.age=Conto di pagamento creato {0} fa. +peerInfoIcon.tooltip.unknownAge=Età dell'account di pagamento non nota. + +tooltip.openPopupForDetails=Apri il popup per i dettagli +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Apri Explorer blockchain esterno per indirizzo: {0} +tooltip.openBlockchainForTx=Apri Explorer blockchain esterno per la transazione: {0} + +confidence.unknown=Stato della transazione sconosciuto +confidence.seen=Visto da {0} conferme peer / 0 +confidence.confirmed=Confermato in {0} blocco (chi) +confidence.invalid=La transazione non è valida + +peerInfo.title=Info peer +peerInfo.nrOfTrades=Numero di scambi effettuati +peerInfo.notTradedYet=Finora non hai commerciato con quell'utente. +peerInfo.setTag=Imposta tag per questo peer +peerInfo.age.noRisk=Età del conto di pagamento +peerInfo.age.chargeBackRisk=Tempo dall'iscrizione +peerInfo.unknownAge=Età sconosciuta + +addressTextField.openWallet=Apri il tuo portafoglio Bitcoin predefinito +addressTextField.copyToClipboard=Copia l'indirizzo negli appunti +addressTextField.addressCopiedToClipboard=L'indirizzo è stato copiato negli appunti +addressTextField.openWallet.failed=Il tentativo di aprire un portafoglio bitcoin predefinito è fallito. Forse non ne hai installato uno? + +peerInfoIcon.tooltip={0}\nTag: {1} + +txIdTextField.copyIcon.tooltip=Copia l'ID transazione negli appunti +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Conto\" +navigation.account.walletSeed=\"Conto/Seed wallet\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"Portfolio/Le mie offerte aperte\" +navigation.portfolio.pending=\"Portafoglio/Scambi aperti\" +navigation.portfolio.closedTrades=\"Portafoglio/Storia\" +navigation.funds.depositFunds=\"Fondi/Ricevifondi\" +navigation.settings.preferences=\"Impostazioni/Preferenze\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Fondi/Transazioni\" +navigation.support=\"Supporto\" +navigation.dao.wallet.receive=\"Portafoglio DAO/BSQ/Ricevi\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} importo{1} +formatter.makerTaker=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.fiat=Stai creando un'offerta per {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Stai creando un'offerta per {0} {1} ({2} {3}) +formatter.asMaker={0} {1} come maker +formatter.asTaker={0} {1} come taker + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Mainnet Bitcoin +# suppress inspection "UnusedProperty" +BTC_TESTNET=Testnet Bitcoin +# suppress inspection "UnusedProperty" +BTC_REGTEST=Regtest Bitcoin +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Testnet DAO Bitcoin (deprecata) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Betanet Bisq DAO (Mainnet Bitcoin) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Regtest DAO Bitcoin + +time.year=Anno +time.month=Mese +time.week=Settimana +time.day=Giorno +time.hour=Ora +time.minute10=10 Minuti +time.hours=ore +time.days=giorni +time.1hour=1 ora +time.1day=1 giorno +time.minute=minuto +time.second=secondo +time.minutes=minuti +time.seconds=secondi + + +password.enterPassword=Inserisci password +password.confirmPassword=Conferma password +password.tooLong=La password deve contenere meno di 500 caratteri. +password.deriveKey=Deriva la chiave dalla password +password.walletDecrypted=Portafoglio decodificato correttamente e protezione con password rimossa. +password.wrongPw=Hai inserito la password errata.\n\nProva a inserire nuovamente la password, verificando attentamente errori di battitura o errori di ortografia. +password.walletEncrypted=Portafoglio crittografato correttamente e protezione con password abilitata. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=Le 2 password inserite non corrispondono. +password.forgotPassword=Password dimenticata? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Parole seed del portafoglio +seed.enterSeedWords=Inserisci le parole del seed del portafoglio +seed.date=Data portafoglio +seed.restore.title=Ripristina portafogli dalle parole del seed +seed.restore=Ripristina portafogli +seed.creationDate=Data di creazione +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=Voglio comunque effettuare il ripristino +seed.warn.walletNotEmpty.emptyWallet=Prima svuoterò i miei portafogli +seed.warn.notEncryptedAnymore=I tuoi portafogli sono crittografati.\n\nDopo il ripristino, i portafogli non saranno più crittografati ed è necessario impostare una nuova password.\n\nVuoi procedere? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Portafogli ripristinati con successo con le nuove parole mnemoniche.\n\nÈ necessario arrestare e riavviare l'applicazione. +seed.restore.error=Si è verificato un errore durante il ripristino dei portafogli con le parole del seme. {0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Account +payment.account.no=Account n° +payment.account.name=Nome conto +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Nome completo del proprietario del conto +payment.account.fullName=Nome completo (nome, secondo nome, cognome) +payment.account.state=Stato/Provincia/Regione +payment.account.city=Città +payment.bank.country=Paese della banca +payment.account.name.email=Nome completo / email del proprietario dell'account +payment.account.name.emailAndHolderId=Nome completo del proprietario dell'account / email / {0} +payment.bank.name=Nome Banca +payment.select.account=Seleziona il tipo di account +payment.select.region=Seleziona regione +payment.select.country=Seleziona nazione +payment.select.bank.country=Seleziona il paese della banca +payment.foreign.currency=Sei sicuro di voler scegliere una valuta diversa dalla valuta predefinita del paese? +payment.restore.default=No, ripristina valuta predefinita +payment.email=Email +payment.country=Paese +payment.extras=Requisiti extra +payment.email.mobile=Email o numero di telefono cellulare +payment.altcoin.address=Indirizzo altcoin +payment.altcoin.tradeInstantCheckbox=Fai trading istantaneo (entro 1 ora) con questa Altcoin +payment.altcoin.tradeInstant.popup=Per il trading istantaneo è necessario che entrambi i peer di trading siano online per poter completare lo scambio in meno di 1 ora.\n\nSe le tue offerte sono aperte ma non sei momentaneamente disponibile, disabilita tali offerte nella schermata "Portafoglio". +payment.altcoin=Altcoin +payment.select.altcoin=Select or search Altcoin +payment.secret=Domanda segreta +payment.answer=Risposta +payment.wallet=ID portafoglio +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Nome utente o e-mail o n. di telefono +payment.moneyBeam.accountId=Email o numero di telefono fisso +payment.venmo.venmoUserName=Nome utente Venmo +payment.popmoney.accountId=Email o numero di telefono fisso +payment.promptPay.promptPayId=Codice fiscale/P.IVA o n. di telefono +payment.supportedCurrencies=Valute supportate +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Limitazioni +payment.salt=Sale per la verifica dell'età dell'account +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Accetta operazioni da questi paesi dell'Euro +payment.accept.nonEuro=Accetta operazioni da questi paesi non Euro +payment.accepted.countries=Paesi accettati +payment.accepted.banks=Banche accettate (ID) +payment.mobile=N. di cellulare +payment.postal.address=Indirizzo postale +payment.national.account.id.AR=Numero CBU +shared.accountSigningState=Stato della firma dell'account + +#new +payment.altcoin.address.dyn={0} indirizzi +payment.altcoin.receiver.address=Indirizzo altcoin del destinatario +payment.accountNr=Numero conto +payment.emailOrMobile=Email o numero di telefono cellulare +payment.useCustomAccountName=Usa nome dell'account personalizzato +payment.maxPeriod=Max. periodo di scambio consentito +payment.maxPeriodAndLimit=Max. durata dello scambio: {0} / Max. acquisto: {1} / Max. vendita: {2} / Età dell'account: {3} +payment.maxPeriodAndLimitCrypto=Max. durata commerciale: {0} / Max. limite commerciale: {1} +payment.currencyWithSymbol=Valuta: {0} +payment.nameOfAcceptedBank=Nome della banca accettata +payment.addAcceptedBank=Aggiungi banca accettata +payment.clearAcceptedBanks=Cancella banche accettate +payment.bank.nameOptional=Nome della banca (opzionale) +payment.bankCode=Codice bancario +payment.bankId=ID banca (BIC/SWIFT) +payment.bankIdOptional=ID banca (BIC/SWIFT) (opzionale) +payment.branchNr=Filiale n. +payment.branchNrOptional=Filiale n. (opzionale) +payment.accountNrLabel=Conto n. (IBAN) +payment.accountType=Tipologia conto +payment.checking=Verifica +payment.savings=Risparmi +payment.personalId=ID personale +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 BTC deve inviare al venditore BTC 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 BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Informazioni di contatto +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=Informazioni di contatto +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Banca +payment.japan.branch=Filiale +payment.japan.account=Account +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Bonifico bancario nazionale +SAME_BANK=Trasferimento con la stessa banca +SPECIFIC_BANKS=Trasferimenti con banche specifiche +US_POSTAL_MONEY_ORDER=Vaglia Postale USA +CASH_DEPOSIT=Deposito contanti +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Faccia a faccia (di persona) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Banche nazionali +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Stessa banca +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Banche specifiche +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Deposito contanti +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=Pagamenti istantanei SEPA +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoin +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoin Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Istantaneo +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoin +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoin Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Un input vuoto non è consentito. +validation.NaN=L'input non è un numero valido. +validation.notAnInteger=L'input non è un valore intero. +validation.zero=Un input di 0 non è consentito. +validation.negative=Un valore negativo non è consentito. +validation.fiat.toSmall=Non è consentito un input inferiore al minimo possibile. +validation.fiat.toLarge=Non è consentito un input maggiore del massimo possibile. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=L'immissione maggiore di {0} non è consentita. +validation.btc.toSmall=L'immissione inferiore a {0} non è consentita. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=La password inserita è troppo lunga. Non può contenere più di 50 caratteri. +validation.sortCodeNumber={0} deve essere composto da {1} numeri. +validation.sortCodeChars={0} deve essere composto da {1} caratteri. +validation.bankIdNumber={0} deve essere composto da {1} numeri. +validation.accountNr=Il numero di conto deve essere composto da {0} numeri. +validation.accountNrChars=Il numero di conto deve contenere {0} caratteri. +validation.btc.invalidAddress=L'indirizzo non è corretto Si prega di controllare il formato dell'indirizzo. +validation.integerOnly=Inserisci solo numeri interi. +validation.inputError=Il tuo input ha causato un errore:\n{0} +validation.bsq.insufficientBalance=Il saldo disponibile è {0}. +validation.btc.exceedsMaxTradeLimit=Il tuo limite commerciale è {0}. +validation.bsq.amountBelowMinAmount=L'importo minimo è di {0} +validation.nationalAccountId={0} deve essere composto da {1} numeri. + +#new +validation.invalidInput=Input non valido: {0} +validation.accountNrFormat=Il numero di conto deve essere nel formato: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Convalida dell'indirizzo non riuscita perché non corrisponde alla struttura di un indirizzo {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=L'indirizzo non è un indirizzo {0} valido! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Gli indirizzi segwit nativi (quelli che iniziano con 'lq') non sono supportati. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Il codice bancario e quello nazionale devono essere lettere +validation.bic.invalidLocationCode=BIC contiene un codice di posizione non valido +validation.bic.invalidBranchCode=BIC contiene un codice di filiale non valido +validation.bic.sepaRevolutBic=Gli account Revolut Sepa non sono supportati. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Indirizzo non valido +validation.iban.invalidCountryCode=Codice paese non valido +validation.iban.checkSumNotNumeric=Il checksum deve essere numerico +validation.iban.nonNumericChars=Rilevato carattere non alfanumerico +validation.iban.checkSumInvalid=Il checksum IBAN non è valido +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Prefisso non canadese +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Deve contenere solo lettere, numeri, spazi e / o i simboli ' _ , . ? - +validation.interacETransfer.invalidAnswer=Deve essere una parola e contenere solo lettere, numeri e/o il simbolo - +validation.inputTooLarge=L'input non deve essere maggiore di {0} +validation.inputTooSmall=L'input deve essere maggiore di {0} +validation.inputToBeAtLeast=L'input deve essere almeno di {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=La lunghezza deve essere compresa tra {0} e {1} +validation.fixedLength=Length must be {0} +validation.pattern=L'input deve essere nel formato: {0} +validation.noHexString=L'input non è in formato HEX. +validation.advancedCash.invalidFormat=Deve essere un ID e-mail o portafoglio valido del formato: X000000000000 +validation.invalidUrl=Questo URL non è valido +validation.mustBeDifferent=L'input deve essere diverso dal valore corrente +validation.cannotBeChanged=Il parametro non può essere modificato +validation.numberFormatException=Eccezione formato numero {0} +validation.mustNotBeNegative=L'input non deve essere negativo +validation.phone.missingCountryCode=È necessario un codice paese di due lettere per convalidare il numero di telefono +validation.phone.invalidCharacters=Il numero di telefono {0} contiene caratteri non validi +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Deve essere un elenco separato da virgole di indirizzi validi diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties new file mode 100644 index 0000000000..ade6e5964b --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=続きを読む +shared.openHelp=ヘルプ +shared.warning=注意 +shared.close=閉じる +shared.cancel=キャンセル +shared.ok=OK +shared.yes=はい +shared.no=いいえ +shared.iUnderstand=了解 +shared.na=N/A +shared.shutDown=終了 +shared.reportBug=Githubでバグを報告 +shared.buyBitcoin=ビットコインを買う +shared.sellBitcoin=ビットコインを売る +shared.buyCurrency={0}を買う +shared.sellCurrency={0}を売る +shared.buyingBTCWith=BTCを{0}で買う +shared.sellingBTCFor=BTCを{0}で売る +shared.buyingCurrency={0}を購入中 (BTCを売却中) +shared.sellingCurrency={0}を売却中 (BTCを購入中) +shared.buy=買う +shared.sell=売る +shared.buying=購入中 +shared.selling=売却中 +shared.P2P=P2P +shared.oneOffer=オファー +shared.multipleOffers=オファー +shared.Offer=オファー +shared.offerVolumeCode={0} オファー量 +shared.openOffers=オープンオファー +shared.trade=トレード +shared.trades=トレード +shared.openTrades=オープントレード +shared.dateTime=日付/時間 +shared.price=価格 +shared.priceWithCur={0}の価格 +shared.priceInCurForCur=1{1}当りの{0}の価格 +shared.fixedPriceInCurForCur=1 {1}あたりの{0}で価格を固定 +shared.amount=金額 +shared.txFee=トランザクション手数料 +shared.tradeFee=トレード手数料 +shared.buyerSecurityDeposit=買い手敷金 +shared.sellerSecurityDeposit=売り手敷金 +shared.amountWithCur={0}の金額 +shared.volumeWithCur={0}取引高 +shared.currency=通貨 +shared.market=相場 +shared.deviation=偏差 +shared.paymentMethod=支払い方法 +shared.tradeCurrency=取引通貨 +shared.offerType=オファーの種類 +shared.details=詳細 +shared.address=アドレス +shared.balanceWithCur={0}での残高 +shared.utxo=Unspent transaction output +shared.txId=トランザクションID +shared.confirmations=確認 +shared.revert=トランザクションを取り消す +shared.select=選択 +shared.usage=使用量 +shared.state=ステータス +shared.tradeId=トレードID +shared.offerId=オファーID +shared.bankName=銀行名 +shared.acceptedBanks=利用可能な銀行 +shared.amountMinMax=金額(下限 - 上限) +shared.amountHelp=オファーに最小金額と最大金額が設定されている場合は、この範囲内の任意の金額で取引できます。 +shared.remove=取り消す +shared.goTo={0} へ +shared.BTCMinMax=BTC (下限 - 上限) +shared.removeOffer=オファー取消 +shared.dontRemoveOffer=オファー取り消さない +shared.editOffer=オファーを編集 +shared.openLargeQRWindow=大きいQRコードウィンドウを開く +shared.tradingAccount=取引アカウント +shared.faq=FAQを参照する +shared.yesCancel=はい、取り消します +shared.nextStep=次へ +shared.selectTradingAccount=取引アカウントを選択 +shared.fundFromSavingsWalletButton=Bisqウォレットから資金を移動する +shared.fundFromExternalWalletButton=外部のwalletを開く +shared.openDefaultWalletFailed=ビットコインウォレットのアプリを開けませんでした。インストールされているか確認して下さい。 +shared.belowInPercent=市場価格から%以下 +shared.aboveInPercent=市場価格から%以上 +shared.enterPercentageValue=%を入力 +shared.OR=または +shared.notEnoughFunds=このトランザクションには、Bisqウォレットに資金が足りません。\n{0}が必要ですが、Bisqウォレットには{1}しかありません。\n\n外部のビットコインウォレットから入金するか、または「資金 > 資金の受取」でBisqウォレットに入金してください。 +shared.waitingForFunds=資金を待っています +shared.depositTransactionId=入金トランザクションID +shared.TheBTCBuyer=BTC買い手 +shared.You=あなた +shared.sendingConfirmation=承認を送信中 +shared.sendingConfirmationAgain=もう一度承認を送信してください +shared.exportCSV=CSVにエクスポート +shared.exportJSON=JSONにエクスポート +shared.summary=Show summary +shared.noDateAvailable=日付がありません +shared.noDetailsAvailable=詳細不明 +shared.notUsedYet=未使用 +shared.date=日付 +shared.sendFundsDetailsWithFee=送金中: {0}\n送金元アドレス: {1}\n入金先アドレス: {2}\n必要なマイニング手数料: {3} ({4} Satoshis/byte)\nトランザクションvサイズ: {5} vKb\n\n入金先の受け取る金額: {6}\n\n本当にこの金額を出金しますか? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisqがこのトランザクションはダストの最小閾値以下のおつりアウトプットを生じることを検出しました(それにしたがって、ビットコインのコンセンサス・ルールによって許されない)。代わりに、その ({0} satoshi{1}) のダストはマイニング手数料に追加されます。\n\n\n +shared.copyToClipboard=クリップボードにコピー +shared.language=言語 +shared.country=国 +shared.applyAndShutDown=適用して終了 +shared.selectPaymentMethod=支払い方法を選ぶ +shared.accountNameAlreadyUsed=そのアカウント名は既に使用されています。\n別の名前を使用してください。 +shared.askConfirmDeleteAccount=選択したアカウントを本当に削除しますか? +shared.cannotDeleteAccount=このアカウントはトレード中(それともオファーを入れている中)ため消去することはできません。 +shared.noAccountsSetupYet=アカウントが設定されていません +shared.manageAccounts=アカウント管理 +shared.addNewAccount=アカウントを追加 +shared.ExportAccounts=アカウントをエクスポート +shared.importAccounts=アカウントをインポート +shared.createNewAccount=新しいアカウントを作る +shared.saveNewAccount=新しいアカウントを保存する +shared.selectedAccount=選択したアカウント +shared.deleteAccount=アカウントを削除 +shared.errorMessageInline=\nエラーメッセージ: {0} +shared.errorMessage=エラーメッセージ +shared.information=情報 +shared.name=名前 +shared.id=ID +shared.dashboard=ダッシュボード +shared.accept=同意 +shared.balance=残高 +shared.save=保存 +shared.onionAddress=Onionアドレス +shared.supportTicket=サポートチケット +shared.dispute=係争 +shared.mediationCase=調停事件 +shared.seller=売り手 +shared.buyer=買い手 +shared.allEuroCountries=ユーロ全諸国 +shared.acceptedTakerCountries=取引可能なテイカーの国 +shared.tradePrice=取引価格 +shared.tradeAmount=取引額 +shared.tradeVolume=取引量 +shared.invalidKey=入力されたキーが正しくありません +shared.enterPrivKey=アンロックの為にプライベートキーを入力 +shared.makerFeeTxId=メイカー手数料トランザクションID +shared.takerFeeTxId=テイカー手数料トランザクションID +shared.payoutTxId=支払いトランザクションID +shared.contractAsJson=JSON形式の契約 +shared.viewContractAsJson=JSON形式で見る +shared.contract.title=次のIDとのトレードの契約: {0} +shared.paymentDetails=BTC {0} 支払い詳細 +shared.securityDeposit=セキュリティデポジット +shared.yourSecurityDeposit=あなたのセキュリティデポジット +shared.contract=契約 +shared.messageArrived=メッセージが来ました。 +shared.messageStoredInMailbox=メッセージが受信箱に入っています +shared.messageSendingFailed=メッセージ送信失敗。エラー: {0} +shared.unlock=ロック解除 +shared.toReceive=受け取る +shared.toSpend=費やす +shared.btcAmount=BTC金額 +shared.yourLanguage=あなたの言語 +shared.addLanguage=言語を追加 +shared.total=合計 +shared.totalsNeeded=必要な資金 +shared.tradeWalletAddress=トレードウォレットアドレス +shared.tradeWalletBalance=トレードウォレット残高 +shared.makerTxFee=メイカー: {0} +shared.takerTxFee=テイカー: {0} +shared.iConfirm=確認します +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL={0} をオープン +shared.fiat=法定通貨 +shared.crypto=暗号通貨 +shared.all=全て +shared.edit=編集 +shared.advancedOptions=高度なオプション +shared.interval=間隔 +shared.actions=アクション +shared.buyerUpperCase=買い手 +shared.sellerUpperCase=売り手 +shared.new=新 +shared.blindVoteTxId=秘密投票トランザクションID +shared.proposal=提案 +shared.votes=投票 +shared.learnMore=もっと詳しく知る +shared.dismiss=却下する +shared.selectedArbitrator=選択された調停人 +shared.selectedMediator=選択された調停者 +shared.selectedRefundAgent=選択された仲裁者 +shared.mediator=調停者 +shared.arbitrator=仲裁者 +shared.refundAgent=仲裁者 +shared.refundAgentForSupportStaff=仲裁者 +shared.delayedPayoutTxId=遅延支払いトランザクションID +shared.delayedPayoutTxReceiverAddress=遅延支払いトランザクション送り先 +shared.unconfirmedTransactionsLimitReached=現在、非確認されたトランザクションが多すぎます。しばらく待ってからもう一度試して下さい。 +shared.numItemsLabel=記載事項の数: {0} +shared.filter=フィルター +shared.enabled=有効されました + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=相場 +mainView.menu.buyBtc=BTCを購入 +mainView.menu.sellBtc=BTCを売却 +mainView.menu.portfolio=ポートフォリオ +mainView.menu.funds=資金 +mainView.menu.support=サポート +mainView.menu.settings=設定 +mainView.menu.account=アカウント +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label={0} による市場価格 +mainView.marketPrice.bisqInternalPrice=Bisqにおける最新の取引価格 +mainView.marketPrice.tooltip.bisqInternalPrice=利用可能な外部価格フィードプロバイダーからの市場価格がありません。\n表示されている価格は、その通貨の最新のBisq取引価格です。 +mainView.marketPrice.tooltip=市場価格は{0}{1}に提供されています\n最終更新: {2}\n提供者のノードのURL: {3} +mainView.balance.available=利用可能残高 +mainView.balance.reserved=オファーのために予約済み +mainView.balance.locked=トレードにロック中 +mainView.balance.reserved.short=予約済 +mainView.balance.locked.short=ロック中 + +mainView.footer.usingTor=(Tor経由で) +mainView.footer.localhostBitcoinNode=(ローカルホスト) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ 手数料率: {0} サトシ/vB +mainView.footer.btcInfo.initializing=ビットコインネットワークに接続中 +mainView.footer.bsqInfo.synchronizing=/ DAOと同期中 +mainView.footer.btcInfo.synchronizingWith={0}と同期中、ブロック: {1} / {2} +mainView.footer.btcInfo.synchronizedWith={0}と同期されています、ブロック{1}に +mainView.footer.btcInfo.connectingTo=接続中: +mainView.footer.btcInfo.connectionFailed=接続失敗 +mainView.footer.p2pInfo=ビットコインネットワークピア: {0} / Bisqネットワークピア: {1} +mainView.footer.daoFullNode=DAOのフルノード + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Torネットワークに接続中... +mainView.bootstrapState.torNodeCreated=(2/4) Torノードが作成されました +mainView.bootstrapState.hiddenServicePublished=(3/4) 秘匿サービスを公開しました +mainView.bootstrapState.initialDataReceived=(4/4) 初期データを受信しました + +mainView.bootstrapWarning.noSeedNodesAvailable=シードノードが見つかりません +mainView.bootstrapWarning.noNodesAvailable=シードノードとピアが見つかりません +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bisqネットワークとの同期に失敗しました + +mainView.p2pNetworkWarnMsg.noNodesAvailable=データを要求するためのシードノードと永続ピアが見つかりません。\nインターネット接続を確認するか、アプリケーションを再起動してみてください。 +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Bisqネットワークへの接続に失敗しました(報告されたエラー: {0})。\nインターネット接続を確認するか、アプリケーションを再起動してみてください。 + +mainView.walletServiceErrorMsg.timeout=タイムアウトのためビットコインネットワークへの接続に失敗しました +mainView.walletServiceErrorMsg.connectionError=次のエラーのためビットコインネットワークへの接続に失敗しました: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=トランザクションはネットワークに拒否されました。\n\n{0} + +mainView.networkWarning.allConnectionsLost=全ての{0}のネットワークピアへの接続が切断されました。\nインターネット接続が切断されたか、コンピュータがスタンバイモードになった可能性があります。 +mainView.networkWarning.localhostBitcoinLost=ローカルホストビットコインノードへの接続が切断されました。\nBisqアプリケーションを再起動して他のビットコインノードに接続するか、ローカルホストのビットコインノードを再起動してください。 +mainView.version.update=(更新が利用可能) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=オファーブック +market.tabs.spreadCurrency=通貨別のオファー +market.tabs.spreadPayment=支払い方法別のオファー +market.tabs.trades=取引 + +# OfferBookChartView +market.offerBook.buyAltcoin={0}を買う({1}を売る) +market.offerBook.sellAltcoin={0}を売る({1}を買う) +market.offerBook.buyWithFiat={0}を買う +market.offerBook.sellWithFiat={0}を売る +market.offerBook.sellOffersHeaderLabel=以下に{0}を売る +market.offerBook.buyOffersHeaderLabel=以下から{0}を買う +market.offerBook.buy=ビットコインを買いたい +market.offerBook.sell=ビットコインを売りたい + +# SpreadView +market.spread.numberOfOffersColumn=全てのオファー ({0}) +market.spread.numberOfBuyOffersColumn=BTCを買う ({0}) +market.spread.numberOfSellOffersColumn=BTCを売る ({0}) +market.spread.totalAmountColumn=BTC合計 ({0}) +market.spread.spreadColumn=スプレッド +market.spread.expanded=拡張された表示 + +# TradesChartsView +market.trades.nrOfTrades=取引: {0} +market.trades.tooltip.volumeBar=取引量: {0} / {1}\n取引数: {2}\n日付: {3} +market.trades.tooltip.candle.open=オープン: +market.trades.tooltip.candle.close=クローズ: +market.trades.tooltip.candle.high=最高: +market.trades.tooltip.candle.low=最低: +market.trades.tooltip.candle.average=平均: +market.trades.tooltip.candle.median=中央値: +market.trades.tooltip.candle.date=日付: +market.trades.showVolumeInUSD=米ドル建ての貿易量を表示 + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=オファーを作る +offerbook.takeOffer=オファーを受ける +offerbook.takeOfferToBuy={0}購入オファーを受ける +offerbook.takeOfferToSell={0}売却オファーを受ける +offerbook.trader=取引者 +offerbook.offerersBankId=メイカーの銀行ID (BIC/SWIFT): {0} +offerbook.offerersBankName=メーカーの銀行名: {0} +offerbook.offerersBankSeat=メーカーの銀行の国名: {0} +offerbook.offerersAcceptedBankSeatsEuro=利用可能な銀行の国名(テイカー): 全ユーロ諸国 +offerbook.offerersAcceptedBankSeats=利用可能な銀行の国名(テイカー):\n{0} +offerbook.availableOffers=利用可能なオファー +offerbook.filterByCurrency=通貨でフィルター +offerbook.filterByPaymentMethod=支払い方法でフィルター +offerbook.matchingOffers=アカウントと一致するオファー +offerbook.timeSinceSigning=アカウント情報 +offerbook.timeSinceSigning.info=このアカウントは認証されまして、{0} +offerbook.timeSinceSigning.info.arbitrator=調停人に署名されました。ピアアカウントも署名できます +offerbook.timeSinceSigning.info.peer=ピアが署名しました。%d日間後に制限の解除を待ち中 +offerbook.timeSinceSigning.info.peerLimitLifted=ピアが署名しました。制限は解除されました +offerbook.timeSinceSigning.info.signer=ピアが署名しました。ピアアカウントも署名できます(制限は解除されました) +offerbook.timeSinceSigning.info.banned=このアカウントは禁止されました +offerbook.timeSinceSigning.daysSinceSigning={0}日 +offerbook.timeSinceSigning.daysSinceSigning.long=署名する後から {0} +offerbook.xmrAutoConf=自動確認は有効されますか? + +offerbook.timeSinceSigning.help=署名された支払いアカウントを持っているピアと成功にトレードすると、自身の支払いアカウントも署名されることになります。\n{0} 日後に、{1} という初期の制限は解除され、他のピアの支払いアカウントを署名できるようになります。 +offerbook.timeSinceSigning.notSigned=まだ署名されていません +offerbook.timeSinceSigning.notSigned.ageDays={0}日 +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=このアカウントはまだ署名されていない。{0} 日間前に作成されました +shared.notSigned.noNeed=この種類のアカウントは署名を必要しません +shared.notSigned.noNeedDays=この種類のアカウントは署名を必要しません。{0} 日間前に作成されました +shared.notSigned.noNeedAlts=アルトコインのアカウントには署名や熟成という機能がありません + +offerbook.nrOffers=オファー数: {0} +offerbook.volume={0} (下限 - 上限) +offerbook.deposit=BTCの敷金(%) +offerbook.deposit.help=トレードを保証するため、両方の取引者が支払う敷金。トレードが完了されたら、返還されます。 + +offerbook.createOfferToBuy={0} を購入するオファーを作成 +offerbook.createOfferToSell={0} を売却するオファーを作成 +offerbook.createOfferToBuy.withFiat={1} で {0} を購入するオファーを作成 +offerbook.createOfferToSell.forFiat={1} で {0} を売却するオファーを作成 +offerbook.createOfferToBuy.withCrypto={0} を売却する({1}購入)オファーを作成 +offerbook.createOfferToSell.forCrypto={0} を購入する({1}売却)オファーを作成 + +offerbook.takeOfferButton.tooltip={0} のオファーを受ける +offerbook.yesCreateOffer=はい、オファーを作成します +offerbook.setupNewAccount=新しいトレードアカウントを設定 +offerbook.removeOffer.success=オファーの削除に成功しました。 +offerbook.removeOffer.failed=オファー削除に失敗:\n{0} +offerbook.deactivateOffer.failed=オファー無効化に失敗:\n{0} +offerbook.activateOffer.failed=オファー公開に失敗:\n{0} +offerbook.withdrawFundsHint=あなたが支払った資金を{0}画面から出金できます。 + +offerbook.warning.noTradingAccountForCurrency.headline=指定の通貨では支払いアカウントがありません +offerbook.warning.noTradingAccountForCurrency.msg=選択した通貨の支払いアカウントがありません。\n\n他の通貨でオファーを作成しますか? +offerbook.warning.noMatchingAccount.headline=一致する支払いアカウントがありません +offerbook.warning.noMatchingAccount.msg=このオファーは、まだ設定されない支払い方法を利用します。\n\n今すぐ新しい支払いアカウントを設定しますか? + +offerbook.warning.counterpartyTradeRestrictions=相手方のトレード制限のせいでこのオファーを受けることができません + +offerbook.warning.newVersionAnnouncement=このバージョンのソフトウェアでは、トレードするピアがお互いの支払いアカウントを署名・検証でき、信頼できる支払いアカウントのネットワークを作れるようにします。\n\n検証されたアカウントと成功にトレードしたら、自身の支払いアカウントも署名されることになり、一定の時間が過ぎたらトレード制限は解除されます(時間の長さは検証方法によって異なります)。\n\nアカウント署名について詳しくは、ドキュメンテーションを参照して下さい:[HYPERLINK:https://docs.bisq.network/payment-methods#account-signing] + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=許可されたトレード金額は以下のセキュリティ基準に基づいて {0} に制限されました:\n- 買い手のアカウントは調停人やピアに署名されていません\n- 買い手のアカウントが署名された時から30日未満がたちました\n- このオファーの支払い方法は、銀行のチャージバックのリスクが高いと考えられます\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=許可されたトレード金額は以下のセキュリティ基準に基づいて {0} に制限されました:\n- このアカウントは調停人やピアに署名されていません\n- このアカウントが署名された時から30日未満がたちました\n- このオファーの支払い方法は、銀行のチャージバックのリスクが高いと考えられます\n\n{1} + +offerbook.warning.wrongTradeProtocol=そのオファーには、ご使用のソフトウェアのバージョンで使用されているものとは異なるプロトコルバージョンが必要です。\n\n最新バージョンがインストールされているかどうかを確認してください。そうでなければ、オファーを作成したユーザーが古いバージョンを使用しています。\n\nユーザーは、互換性のないトレードプロトコルバージョンと取引することはできません。 +offerbook.warning.userIgnored=そのユーザのonionアドレスを無視リストに追加しました。 +offerbook.warning.offerBlocked=そのオファーはBisq開発者によってブロックされました。\nおそらくそのオファーを受けるときに問題が引きおこさる未処理のバグがあります。 +offerbook.warning.currencyBanned=そのオファーで使用されている通貨はBisq開発者によってブロックされています。\n詳しくはBisqフォーラムをご覧ください。 +offerbook.warning.paymentMethodBanned=そのオファーで使用されている支払い方法はBisq開発者によってブロックされています。\n詳しくはBisqフォーラムをご覧ください。 +offerbook.warning.nodeBlocked=そのonionアドレスはBisq開発者によってブロックされました。\nおそらくその取引者からのオファーを受けるときに問題が引きおこさる未処理のバグがあります。 +offerbook.warning.requireUpdateToNewVersion=このBisqのバージョンはもはやトレードする互換性がありません。\n[HYPERLINK:https://bisq.network/downloads] で最新のBisqバージョンに更新してください。 +offerbook.warning.offerWasAlreadyUsedInTrade=このオファーを以前に受けましたせいで、現在受けることができません。以前のオファー受け入り試みは失敗トレードに終わりましたかもしれません。 + +offerbook.info.sellAtMarketPrice=市場価格で売却されるでしょう(毎分更新されます)。 +offerbook.info.buyAtMarketPrice=市場価格で購入されるでしょう(毎分更新されます)。 +offerbook.info.sellBelowMarketPrice=現在の市場価格よりも{0}以下で入手するでしょう(毎分更新されます)。 +offerbook.info.buyAboveMarketPrice=現在の市場価格よりも{0}以上で支払いされるでしょう(毎分更新されます)。 +offerbook.info.sellAboveMarketPrice=現在の市場価格よりも{0}以上で入手するでしょう(毎分更新されます)。 +offerbook.info.buyBelowMarketPrice=現在の市場価格よりも{0}以下で支払いされるでしょう(毎分更新されます)。 +offerbook.info.buyAtFixedPrice=固定された価格で購入します。 +offerbook.info.sellAtFixedPrice=固定された価格で売却します。 +offerbook.info.noArbitrationInUserLanguage=係争が発生した場合、このオファーの仲裁は{0}で処理されます。 言語は現在{1}に設定されています。 +offerbook.info.roundedFiatVolume=金額は取引のプライバシーを高めるために四捨五入されました。 + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=BTCの金額を入力 +createOffer.price.prompt=価格を入力 +createOffer.volume.prompt={0}の金額を入力 +createOffer.amountPriceBox.amountDescription=以下の金額でBTCを{0} +createOffer.amountPriceBox.buy.volumeDescription=支払う{0}の金額 +createOffer.amountPriceBox.sell.volumeDescription=受け取る{0}の金額 +createOffer.amountPriceBox.minAmountDescription=BTCの最小額 +createOffer.securityDeposit.prompt=セキュリティデポジット +createOffer.fundsBox.title=あなたのオファーへ入金 +createOffer.fundsBox.offerFee=取引手数料 +createOffer.fundsBox.networkFee=マイニング手数料 +createOffer.fundsBox.placeOfferSpinnerInfo=オファー公開の処理中 ... +createOffer.fundsBox.paymentLabel=次のIDとのBisqトレード: {0} +createOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) +createOffer.fundsBox.fundsStructure.BSQ=({0} セキュリティデポジット, {1} 取引手数料) + {2}マイニング手数料 +createOffer.success.headline=オファーが公開されました +createOffer.success.info=自分のオファーは「ポートフォリオ/私のオープンオファー」で管理できます +createOffer.info.sellAtMarketPrice=オファーの価格は継続的に更新されるため、常に市場価格で売却するでしょう。 +createOffer.info.buyAtMarketPrice=オファーの価格は継続的に更新されるため、常に市場価格で購入するでしょう。 +createOffer.info.sellAboveMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以上で入手するでしょう。 +createOffer.info.buyBelowMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以下で支払いするでしょう。 +createOffer.warning.sellBelowMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以下で入手するでしょう。 +createOffer.warning.buyAboveMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以上で支払いするでしょう。 +createOffer.tradeFee.descriptionBTCOnly=取引手数料 +createOffer.tradeFee.descriptionBSQEnabled=トレード手数料通貨を選択 + +createOffer.triggerPrice.prompt=任意選択価格トリガーを設定する +createOffer.triggerPrice.label=市場価格が{0}になる場合、オファーを無効にする +createOffer.triggerPrice.tooltip=価格の著しい変化から保護するため、市場価格が特定の価値に達する時にオファーは無効される価格トリガーを設定できます。 +createOffer.triggerPrice.invalid.tooLow=価値は{0}より高くなければなりません +createOffer.triggerPrice.invalid.tooHigh=価値は{0}より低くなければなりません + +# new entries +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- Bisqウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 + +# 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アプリケーションを再起動してネットワーク接続を確認してください。 +createOffer.setAmountPrice=取引額と価格を入力して下さい +createOffer.warnCancelOffer=そのオファーは既に入金済みです。\n今すぐキャンセルした場合、あなたの資金はあなたのローカルBisqウォレットに移動され、「資金/送金する」画面で出金することができます。\n本当にキャンセルしてもよろしいですか? +createOffer.timeoutAtPublishing=オファーの公開中にタイムアウトが発生しました。 +createOffer.errorInfo=\n\nメイカー手数料は既に支払い済みです。 最悪の場合、あなたはその手数料を失っています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 +createOffer.tooLowSecDeposit.warning=セキュリティデポジットが推奨されるデフォルト値{0}よりも低い値に設定されました。\nより低いセキュリティデポジットを使用してよろしいですか? +createOffer.tooLowSecDeposit.makerIsSeller=取引相手がトレードプロトコルに従わない場合、あなたへの保護は少なくなります。 +createOffer.tooLowSecDeposit.makerIsBuyer=リスクに対してセキュリティデポジットが少ないため、あなたのトレードプロトコルでは取引相手への保護が少なくなります。 他のユーザーはあなたの代わりに他のオファーを選ぶかもしれません。 +createOffer.resetToDefault=いいえ、既定の値に戻します +createOffer.useLowerValue=はい、私の低い値を使用します +createOffer.priceOutSideOfDeviation=入力した価格は、市場価格からの最大許容偏差を超えています。\n最大許容偏差は{0}で、設定で調整できます。 +createOffer.changePrice=価格を変更 +createOffer.tac=このオファーを公開することで、この画面で定義された条件を満たす取引者と取引することに同意します。 +createOffer.currencyForFee=取引手数料 +createOffer.setDeposit=買い手のセキュリティデポジット (%) +createOffer.setDepositAsBuyer=購入時のセキュリティデポジット (%) +createOffer.setDepositForBothTraders=両方の取引者の保証金を設定する(%) +createOffer.securityDepositInfo=あなたの買い手のセキュリティデポジットは{0}です +createOffer.securityDepositInfoAsBuyer=あなたの購入時のセキュリティデポジットは{0}です +createOffer.minSecurityDepositUsed=最小値の買い手の保証金は使用されます + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=BTCの金額を入力 +takeOffer.amountPriceBox.buy.amountDescription=BTC売却額 +takeOffer.amountPriceBox.sell.amountDescription=BTC購入額 +takeOffer.amountPriceBox.priceDescription={0}のビットコインあたりの価格 +takeOffer.amountPriceBox.amountRangeDescription=可能な金額の範囲 +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=入力した金額が、許容される小数点以下の桁数を超えています。\n金額は小数点以下第4位に調整されています。 +takeOffer.validation.amountSmallerThanMinAmount=金額はオファーで示された下限額を下回ることができません +takeOffer.validation.amountLargerThanOfferAmount=オファーで示された上限額を上回る金額は入力できません +takeOffer.validation.amountLargerThanOfferAmountMinusFee=その入力額はBTCの売り手にダストチェンジを引き起こします。 +takeOffer.fundsBox.title=あなたのトレードへ入金 +takeOffer.fundsBox.isOfferAvailable=オファーが有効か確認中... +takeOffer.fundsBox.tradeAmount=売却額 +takeOffer.fundsBox.offerFee=取引手数料 +takeOffer.fundsBox.networkFee=合計マイニング手数料 +takeOffer.fundsBox.takeOfferSpinnerInfo=オファー受け入れ処理中 ... +takeOffer.fundsBox.paymentLabel=次のIDとのBisqトレード: {0} +takeOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) +takeOffer.success.headline=オファー受け入れに成功しました +takeOffer.success.info=あなたのトレード状態は「ポートフォリオ/オープントレード」で見られます +takeOffer.error.message=オファーの受け入れ時にエラーが発生しました。\n\n{0} + +# new entries +takeOffer.takeOfferButton=再確認: ビットコインを{0}オファーを申し込む +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- Bisqウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\nまたは\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 +takeOffer.alreadyPaidInFunds=あなたがすでに資金を支払っている場合は「資金/送金する」画面でそれを出金することができます。 +takeOffer.paymentInfo=支払い情報 +takeOffer.setAmountPrice=金額を設定 +takeOffer.alreadyFunded.askCancel=そのオファーは既に入金済みです。\n今すぐキャンセルした場合、あなたの資金はあなたのローカルBisqウォレットに移動され、「資金/送金する」画面で出金ができます。\n本当にキャンセルしてもよろしいですか? +takeOffer.failed.offerNotAvailable=オファーが利用できなくなったため、オファー受け入れに失敗しました。 この間に別のトレーダーがオファーを受けた可能性があります。 +takeOffer.failed.offerTaken=そのオファーは既に別の取引者によって受け取られため、そのオファーは受け取れません。 +takeOffer.failed.offerRemoved=そのオファーはこの間に削除されたため、そのオファーは受け取れません +takeOffer.failed.offererNotOnline=メイカーがオンラインになっていないため、オファー受け入れに失敗しました。 +takeOffer.failed.offererOffline=このオファーはメーカーがオフラインのため受け取れません +takeOffer.warning.connectionToPeerLost=メイカーとの接続が切れました。\n相手がオフラインになったか、オープンな接続が多すぎるため、あなたへの接続を閉じた可能性があります。\n\nまだオファーブックに相手のオファーが表示されている場合は、もう一度そのオファーを受け取って下さい。 + +takeOffer.error.noFundsLost=\n\nあなたのウォレットにはまだ資金がありません\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nデポジットトランザクションは既に公開されています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。\nそれでも問題が解決しない場合は、開発者に連絡してください。 +takeOffer.error.payoutPublished=\n\n支払いトランザクションは既に公開されています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。\nそれでも問題が解決しない場合は、開発者に連絡してください。 +takeOffer.tac=このオファーを受けることで、この画面で定義されている取引条件に同意します。 + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=価格トリガー +openOffer.triggerPrice=価格トリガー{0} +openOffer.triggered=市場価格は価格トリガーに達しましたため、オファーが無効にされました。\nオファーには新しい価格トリガーを設定して下さい。 + +editOffer.setPrice=価格設定 +editOffer.confirmEdit=承認: オファーを編集 +editOffer.publishOffer=あなたのオファーの公開。 +editOffer.failed=オファー編集に失敗:\n{0} +editOffer.success=オファー編集に成功しました +editOffer.invalidDeposit=買い手のセキュリティデポジットはBisq DAOによって定義された制約の範囲内ではなく、もう編集することはできません。 + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=私のオープンなオファー +portfolio.tab.pendingTrades=オープンなトレード +portfolio.tab.history=履歴 +portfolio.tab.failed=失敗 +portfolio.tab.editOpenOffer=オファーを編集 + +portfolio.closedTrades.deviation.help=市場からの割合価格偏差 + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=ブロックチェーンの承認をお待ち下さい +portfolio.pending.step2_buyer.startPayment=支払い開始 +portfolio.pending.step2_seller.waitPaymentStarted=支払いが始まるまでお待ち下さい +portfolio.pending.step3_buyer.waitPaymentArrived=支払いが到着するまでお待ち下さい +portfolio.pending.step3_seller.confirmPaymentReceived=支払いを受領したことを確認して下さい +portfolio.pending.step5.completed=完了 + +portfolio.pending.step3_seller.autoConf.status.label=自動確認のステータス +portfolio.pending.autoConf=自動確認されました +portfolio.pending.autoConf.blocks=XMR承認: {0} / 必要: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=トランザクション・キーは再利用されました。係争を開始して下さい。 +portfolio.pending.autoConf.state.confirmations=XMR承認: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=トランザクションはまだメモリプールに見られません +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=有効なトランザクションID/トランザクション・キーはありません +portfolio.pending.autoConf.state.filterDisabledFeature=開発者により無効されました + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=自動確認機能は無効されました。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=トレード金額は自動確認の金額制限を越えます +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=ピアは無効データを提供しました。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支払いトランザクションはすでに公開されました。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=係争は開始されました。そのトレードでは自動確認が無効にされました。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=トランザクション証明依頼を開始しました +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=成功の成果: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=全てのサービスでは、証明が成功に終わりました +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=サービスリクエストにはエラーが生じました。自動確認できません。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FAILED=サービスは失敗を返しました。自動確認できません。 + +portfolio.pending.step1.info=デポジットトランザクションが発行されました。\n{0}は、支払いを開始する前に少なくとも1つのブロックチェーンの承認を待つ必要があります。 +portfolio.pending.step1.warn=デポジットトランザクションがまだ承認されていません。外部ウォレットからの取引者の資金調達手数料が低すぎるときには、例外的なケースで起こるかもしれません。 +portfolio.pending.step1.openForDispute=デポジットトランザクションがまだ承認されていません。もう少し待つか、助けを求めて調停人に連絡できます。 + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=トレードは少なくとも1つのブロックチェーン承認に達しました。\n\n + +portfolio.pending.step2_buyer.refTextWarn=注意点:支払う時に、\"支払理由\"のフィールドを空白にしておいて下さい。いかなる場合でも、トレードIDそれとも「ビットコイン」、「BTC」、「Bisq」などを入力しないで下さい。両者にとって許容できる別の\"支払理由\"があれば、自由に取引者チャットで話し合いをして下さい。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees=銀行口座振替を行うには手数料がある場合、その手数料を払う責任があります。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.altcoin=外部{0}ウォレットから転送してください\nBTCの売り手へ{1}。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=銀行に行き、BTCの売り手へ{0}を支払ってください。\n\n +portfolio.pending.step2_buyer.cash.extra=重要な要件:\n支払いが完了したら、領収書に「返金無し(NO REFUNDS)」と記載してください。\nそれからそれを2部に分け、写真を撮り、そしてBTCの売り手のEメールアドレスへそれを送ってください。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=MoneyGramを使用してBTC売り手へ{0}をお支払いください。\n\n +portfolio.pending.step2_buyer.moneyGram.extra=重要な要件: \n支払いが完了したら、認証番号と領収書の写真を電子メールでBTCの売り手へ送信して下さい。\n領収書には、売り手の氏名、国、都道府県、および金額を明確に表示する必要があります。売り手のメールアドレス: {0} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Western Unionを使用してBTCの売り手へ{0}をお支払いください。\n\n +portfolio.pending.step2_buyer.westernUnion.extra=重要な要件: \n支払いが完了したら、MTCN(追跡番号)と領収書の写真を電子メールでBTCの売り手へ送信して下さい。\n領収書には、売り手の氏名、市区町村、国、金額が明確に示されている必要があります。売り手のメールアドレス: {0} + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal={0}を「米国の郵便為替」でBTCの売り手に送付してください。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=\"郵送で現金\"で、{0}をBTC売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはBisqのWikiを参照:[HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=特定された支払い方法で{0}をBTCの売り手に支払ってお願いします。売り手のアカウント詳細は次の画面に表示されます。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=提供された連絡先でBTCの売り手に連絡し、{0}を支払うためのミーティングを準備してください。\n\n +portfolio.pending.step2_buyer.startPaymentUsing={0}を使用して支払いを開始 +portfolio.pending.step2_buyer.recipientsAccountData=受領者 {0} +portfolio.pending.step2_buyer.amountToTransfer=振替金額 +portfolio.pending.step2_buyer.sellersAddress=売り手の{0}アドレス +portfolio.pending.step2_buyer.buyerAccount=使用されるあなたの支払いアカウント +portfolio.pending.step2_buyer.paymentStarted=支払いが開始されました +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn={0}の支払いはまだ完了していません!\nトレードは{1}までに完了しなければなりません。 +portfolio.pending.step2_buyer.openForDispute=支払いを完了していません!\nトレードの最大期間が経過しました。助けを求めるには調停人に連絡してください。 +portfolio.pending.step2_buyer.paperReceipt.headline=領収書をBTCの売り手へ送付しましたか? +portfolio.pending.step2_buyer.paperReceipt.msg=覚えておいてください:\n領収書に「返金無し(NO REFUNDS)」と記載してください。\nそれからそれを2部に分け、写真を撮り、そしてBTCの売り手のEメールアドレスへそれを送ってください。 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=認証番号と領収書を送信 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=認証番号と領収書の写真を電子メールでBTCの売り手へ送信する必要があります。\n領収書には、売り手の氏名、国、都道府県、および金額を明確に表示する必要があります。売却者のメールアドレス: {0}\n\n認証番号と契約書を売り手へ送付しましたか? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCNと領収書を送信 +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=あなたはMTCN(追跡番号)とレシートの写真をBTCの売り手にEメールで送る必要があります。\n領収書には、売り手の氏名、市区町村、国、金額が明確に示されている必要があります。 販売者のメールアドレス: {0}\n\nMTCNと契約書を売り手へ送付しましたか? +portfolio.pending.step2_buyer.halCashInfo.headline=HalCashコードを送信 +portfolio.pending.step2_buyer.halCashInfo.msg=HalCashコードと取引ID({0})を含むテキストメッセージをBTCの売り手に送信する必要があります。\n売り手の携帯電話番号は {1} です。\n\n売り手にコードを送信しましたか? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=銀行によっては、受信者の名前を検証する場合があります。 旧バージョンのBisqクライアントで作成した「Faster Payments」アカウントでは、受信者の名前は提供されませんので、(必要ならば)トレードチャットで尋ねて下さい。 +portfolio.pending.step2_buyer.confirmStart.headline=支払いが開始したことを確認 +portfolio.pending.step2_buyer.confirmStart.msg=トレーディングパートナーへの{0}支払いを開始しましたか? +portfolio.pending.step2_buyer.confirmStart.yes=はい、支払いを開始しました +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=支払証明を提出していません +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=トランザクションIDとトランザクション・キーを入力していません。\n\nこのデータを提供しなければ、ピアはXMRを受取る直後にBTCを解放するため自動確認機能を利用できません。\nその上、係争の場合にBisqはXMRトランザクションの送信者がこの情報を調停者や調停人に送れることを必要とします。\n詳しくはBisqのWikiを参照 [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] 。 +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=入力が32バイトの16進値ではありません。 +portfolio.pending.step2_buyer.confirmStart.warningButton=無視して続ける +portfolio.pending.step2_seller.waitPayment.headline=支払いをお待ちください +portfolio.pending.step2_seller.f2fInfo.headline=買い手の連絡先 +portfolio.pending.step2_seller.waitPayment.msg=デポジットトランザクションには、少なくとも1つのブロックチェーン承認があります。\nBTCの買い手が{0}の支払いを開始するまで待つ必要があります。 +portfolio.pending.step2_seller.warn=BTCの買い手はまだ{0}の支払いを行っていません。\n支払いが開始されるまで待つ必要があります。\n取引が{1}で完了していない場合は、調停人が調査します。 +portfolio.pending.step2_seller.openForDispute=BTCの買い手は支払いを開始していません!\nトレードの許可された最大期間が経過しました。\nもっと長く待ってトレードピアにもっと時間を与えるか、助けを求めるために調停者に連絡することができます。 +tradeChat.chatWindowTitle=トレードID '{0}'' のチャットウィンドウ +tradeChat.openChat=チャットウィンドウを開く +tradeChat.rules=このトレードに対する潜在的な問題を解決するため、トレードピアと連絡できます。\nチャットに返事する義務はありません。\n取引者が以下のルールを破ると、係争を開始して調停者や調停人に報告して下さい。\n\nチャット・ルール:\n\t●リンクを送らないこと(マルウェアの危険性)。トランザクションIDとブロックチェーンエクスプローラの名前を送ることができます。\n\t●シードワード、プライベートキー、パスワードなどの機密な情報を送らないこと。\n\t●Bisq外のトレードを助長しないこと(セキュリティーがありません)。\n\t●ソーシャル・エンジニアリングや詐欺の行為に参加しないこと。\n\t●チャットで返事されない場合、それともチャットでの連絡が断られる場合、ピアの決断を尊重すること。\n\t●チャットの範囲をトレードに集中しておくこと。チャットはメッセンジャーの代わりや釣りをする場所ではありません。\n\t●礼儀正しく丁寧に話すこと。 + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +message.state.SENT=メッセージ送信済 +# suppress inspection "UnusedProperty" +message.state.ARRIVED=相手からのメールが来ました +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=支払いのメッセージは送りしましたが、まだピアに受信されていません。 +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=相手がメッセージ受信を確認 +# suppress inspection "UnusedProperty" +message.state.FAILED=メッセージ送信失敗 + +portfolio.pending.step3_buyer.wait.headline=BTCの売り手の支払い承認をお待ち下さい +portfolio.pending.step3_buyer.wait.info={0}の支払いを受け取るためのBTCの売り手の承認を待っています。 +portfolio.pending.step3_buyer.wait.msgStateInfo.label=支払いはメッセージステータスを開始 +portfolio.pending.step3_buyer.warn.part1a={0} ブロックチェーン上で +portfolio.pending.step3_buyer.warn.part1b=支払いプロバイダ(銀行など)で +portfolio.pending.step3_buyer.warn.part2=BTCの売り手はまだあなたの支払いを確認していません!支払いの送信が成功したかどうか{0}を確認してください。 +portfolio.pending.step3_buyer.openForDispute=BTCの売り手があなたの支払いを確認していません!トレードの最大期間が経過しました。もっと長く待って取引相手にもっと時間を与えるか、調停人から援助を求めることができます。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=あなたのトレード相手は、彼らが{0}の支払いを開始したことを確認しました。\n\n +portfolio.pending.step3_seller.altcoin.explorer=あなたの好きな{0}ブロックチェーンエクスプローラで +portfolio.pending.step3_seller.altcoin.wallet=あなたの{0}ウォレットで +portfolio.pending.step3_seller.altcoin={0}あなたの受け取りアドレスへのトランザクションが{1}かどうかを確認してください\n{2}\nはすでに十分なブロックチェーンの承認があります。\n支払い額は{3}です\n\nポップアップを閉じた後、メイン画面から{4}アドレスをコピーして貼り付けることができます。 +portfolio.pending.step3_seller.postal={0}\"米国の郵便為替\"でBTCの買い手から{1}を受け取ったか確認して下さい。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}\"郵送で現金\"でBTCの買い手から{1}を受け取ったか確認して下さい。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=トレード相手は{0}の支払いを開始した確認をしました。\n\nオンラインバンキングのWebページにアクセスして、BTCの買い手から{1}を受け取ったか確認してください。 +portfolio.pending.step3_seller.cash=支払いは現金入金で行われるので、BTCの買い手は領収書に「返金無し(NO REFUND)」と記入し、2部に分けて写真を電子メールで送ってください。\n\nチャージバックのリスクを回避するために、Eメールを受信したかどうか、および領収書が有効であることが確実であるかどうかを確認してください。\nよくわからない場合は、{0} +portfolio.pending.step3_seller.moneyGram=買い手は承認番号と領収書の写真を電子メールで送信する必要があります。\n領収書には、氏名、国、州、および金額を明確に記載する必要があります。 認証番号を受け取った場合は、メールを確認してください。\n\nそのポップアップを閉じた後、あなたはMoneyGramからお金を得るためのBTC買い手の名前と住所を見られるでしょう。\n\nあなたが正常にお金を得た後にのみ領収書を承認してください! +portfolio.pending.step3_seller.westernUnion=買い手はMTCN(追跡番号)と領収書の写真をEメールで送信する必要があります。\n領収書には、氏名、市区町村、国、金額が明確に記載されている必要があります。 MTCNを受け取った場合は、メールを確認してください。\n\nそのポップアップを閉じた後、あなたはWestern Unionからお金を得るためのBTC買い手の名前と住所を見られるでしょう。\n\nあなたが正常にお金を得た後にのみ領収書を承認してください! +portfolio.pending.step3_seller.halCash=買い手はHalCashコードをテキストメッセージとして送信する必要があります。それに加えて、HalCash対応ATMからEURを出金するために必要な情報を含むメッセージがHalCashから届きます。\n\nあなたはATMからお金を得た後、ここで支払いの領収書を承認して下さい! +portfolio.pending.step3_seller.amazonGiftCard=買い手はEメールアドレス、それともSMSで携帯電話番号までアマゾンeGiftカードを送りました。アマゾンアカウントにeGiftカードを受け取って、済ましたら支払いの受領を確認して下さい。 + +portfolio.pending.step3_seller.bankCheck=\n\nまた、トレード契約書に記載されている送付者の名前が、銀行取引明細書のものと一致することも確認してください:\nトレード契約書のとおり、送信者の名前: {0}\n\n名前がここに表示されているものと同じではない場合、{1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=支払いの受領を確認せず、「alt + o」または「option + o」を入力して係争を開始して下さい。\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=支払い受領を承認 +portfolio.pending.step3_seller.amountToReceive=受取額 +portfolio.pending.step3_seller.yourAddress=あなたの{0} アドレス +portfolio.pending.step3_seller.buyersAddress=買い手の{0} アドレス +portfolio.pending.step3_seller.yourAccount=あなたのトレードアカウント +portfolio.pending.step3_seller.xmrTxHash=トランザクションID +portfolio.pending.step3_seller.xmrTxKey=トランザクション・キー +portfolio.pending.step3_seller.buyersAccount=買い手のアカウント・データ +portfolio.pending.step3_seller.confirmReceipt=支払い受領を確認 +portfolio.pending.step3_seller.buyerStartedPayment=BTCの買い手が{0}の支払いを開始しました。\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=あなたのアルトコインウォレットやブロックエクスプローラーでブロックチェーンの確認を確認し、十分なブロックチェーンの承認があるときに支払いを確認してください。 +portfolio.pending.step3_seller.buyerStartedPayment.fiat=あなたのトレードアカウント(例えば銀行口座)をチェックして、あなたが支払いを受領した時に承認して下さい。 +portfolio.pending.step3_seller.warn.part1a={0} blockchain上で +portfolio.pending.step3_seller.warn.part1b=支払いプロバイダ(銀行など)で +portfolio.pending.step3_seller.warn.part2=あなたはまだ支払いの受領を承認していません。支払い{0}を受け取ったかどうかを確認してください。 +portfolio.pending.step3_seller.openForDispute=支払いの受領を承認していません!\nトレードの最大期間が経過しました。\n確認するか、調停人に助けを求めて下さい。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=あなたの取引相手から{0}の支払いを受けましたか?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=また、銀行取引明細書に記載されている送付者の名前が、トレード契約書のものと一致していることも確認してください:\nトレード契約書とおり、送信者の名前: {0}\n\n送付者の名前がここに表示されているものと異なる場合は、支払いの受領を承認しないで下さい。「alt + o」または「option + o」を入力して係争を開始して下さい。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=領収書の確認が済むとすぐに、ロックされたトレード金額がBTCの買い手に解放され、保証金が返金されます。\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=支払いを受け取ったことを確認 +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=はい、支払いを受け取りました +portfolio.pending.step3_seller.onPaymentReceived.signer=重要:支払いの受け取りを承認すると、相手方のアカウントを検証して署名することになります。相手方のアカウントはまだ署名されていないので、支払取り消しリスクを減らすために支払いの承認をできる限り延期して下さい。 + +portfolio.pending.step5_buyer.groupTitle=完了したトレードのまとめ +portfolio.pending.step5_buyer.tradeFee=取引手数料 +portfolio.pending.step5_buyer.makersMiningFee=マイニング手数料 +portfolio.pending.step5_buyer.takersMiningFee=合計マイニング手数料 +portfolio.pending.step5_buyer.refunded=返金されたセキュリティデポジット +portfolio.pending.step5_buyer.withdrawBTC=ビットコインを出金する +portfolio.pending.step5_buyer.amount=出金額 +portfolio.pending.step5_buyer.withdrawToAddress=出金先アドレス +portfolio.pending.step5_buyer.moveToBisqWallet=資金をBisqウォレットに保管する +portfolio.pending.step5_buyer.withdrawExternal=外部ウォレットに出金する +portfolio.pending.step5_buyer.alreadyWithdrawn=資金はすでに出金されています。\nトランザクション履歴を確認してください。 +portfolio.pending.step5_buyer.confirmWithdrawal=出金リクエストを承認 +portfolio.pending.step5_buyer.amountTooLow=振込金額は、トランザクション手数料および最小可能送信金額(ダスト)よりも少なくなります。 +portfolio.pending.step5_buyer.withdrawalCompleted.headline=引き出し完了 +portfolio.pending.step5_buyer.withdrawalCompleted.msg=完了した取引は「ポートフォリオ/履歴」に保存されます。\nあなたはすべてのあなたのビットコイン取引を「資金/トランザクション」で見直すことができます +portfolio.pending.step5_buyer.bought=購入しました +portfolio.pending.step5_buyer.paid=支払いました + +portfolio.pending.step5_seller.sold=支払いました +portfolio.pending.step5_seller.received=受け取りました + +tradeFeedbackWindow.title=おめでとうございます、トレードが完了しました +tradeFeedbackWindow.msg.part1=私達はあなたの体験についての連絡をお待ちしております。 それは私達のソフトウェアを改良して、荒削りな部分を洗練させる手助けになります。 ご意見をお寄せになりたい場合は、こちらの簡単なアンケートにご記入ください(登録不要): +tradeFeedbackWindow.msg.part2=ご質問がある場合、または問題が発生した場合は、次のBisqフォーラムで他のユーザーや貢献者に連絡してください: +tradeFeedbackWindow.msg.part3=Bisqを使ってくれてありがとう! + +portfolio.pending.role=私の役割 +portfolio.pending.tradeInformation=トレード情報 +portfolio.pending.remainingTime=残り時間 +portfolio.pending.remainingTimeDetail={0} ({1}まで) +portfolio.pending.tradePeriodInfo=最初のブロックチェーンの確認後、トレード期間が始まります。 使用した支払い方法に基づいて、異なる最大許容トレード期間が適用されます。 +portfolio.pending.tradePeriodWarning=この期間を超えた場合、両方の取引者が係争を開始できます。 +portfolio.pending.tradeNotCompleted=時間内に完了してないトレード({0}まで) +portfolio.pending.tradeProcess=トレードプロセス +portfolio.pending.openAgainDispute.msg=調停人や調停者へのメッセージが到着したことに確信が持てない場合(例えば、1日経っても返事がない場合)、「command/ctrl+o」で再度係争を申し立てる、あるいは [HYPERLINK:https://bisq.community] でBisq掲示板からさらにサポートを受けることができます。 +portfolio.pending.openAgainDispute.button=もう一度係争を開始 +portfolio.pending.openSupportTicket.headline=サポートチケットをオープン +portfolio.pending.openSupportTicket.msg=この機能を \"サポートをオープン\" や \"係争を開始\" ボタンが表示されていない緊急の場合のみに利用して下さい。\n\nサポートチケットをオープンすると、トレードは割り込まれ調停人や調停者によって扱われます。 + +portfolio.pending.timeLockNotOver=係争仲裁を開始するには、≈{0} ({1} ブロック) 確認まで待たなければなりません。 +portfolio.pending.error.depositTxNull=入金トランザクションは無効とされました。有効な入金トランザクションがなければ、係争を開始できません。\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\n\nさらにサポートを受けるため、Bisq Keybaseチームのサポートチャンネルに連絡して下さい。 +portfolio.pending.mediationResult.error.depositTxNull=デポジットトランザクションは無効とされました。「失敗トレード」へ送れます。 +portfolio.pending.mediationResult.error.delayedPayoutTxNull=遅延支払いトランザクションは無効とされました。「失敗トレード」へ送れます。 +portfolio.pending.error.depositTxNotConfirmed=入金トランザクションは承認されていません。非確認された入金トランザクションで係争を開始できません。承認まで待つか、\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\n\nさらにサポートを受けるため、Bisq Keybaseチームのサポートチャンネルに連絡して下さい。 + +portfolio.pending.support.headline.getHelp=助けが必要ですか? +portfolio.pending.support.text.getHelp=問題があれば、トレードチャットにトレードピアと連絡してみるか、 https://bisq.community でBisqコミュニティーから助けを求めることができます。それでも問題は解決されない場合、調停者からさらに助けを求めることもできます。 +portfolio.pending.support.button.getHelp=取引者チャットを開く +portfolio.pending.support.headline.halfPeriodOver=支払いを確認 +portfolio.pending.support.headline.periodOver=トレード期間は終了しました + +portfolio.pending.mediationRequested=調停は依頼されました +portfolio.pending.refundRequested=返金は請求されました +portfolio.pending.openSupport=サポートチケットをオープン +portfolio.pending.supportTicketOpened=サポートチケットがオープンされた +portfolio.pending.communicateWithArbitrator=「サポート」画面で調停人と連絡を取ってください。 +portfolio.pending.communicateWithMediator=\"サポート\" 画面で調停者と連絡を取ってください。 +portfolio.pending.disputeOpenedMyUser=あなたは既に係争を開始しています\n{0} +portfolio.pending.disputeOpenedByPeer=あなたのトレード相手は係争を開始しました\n{0} +portfolio.pending.noReceiverAddressDefined=受信者のアドレスが定義されていません + +portfolio.pending.mediationResult.headline=調停から提案された支払い +portfolio.pending.mediationResult.info.noneAccepted=調停者のトレード支払い提案に応じることでトレードを完了する。 +portfolio.pending.mediationResult.info.selfAccepted=調停者の提案を受け入れました。ピアの受け入りを待ち。 +portfolio.pending.mediationResult.info.peerAccepted=トレードピアは調停者の提案を受け入れました。同じく提案に応じますか? +portfolio.pending.mediationResult.button=提案解決法を表示する +portfolio.pending.mediationResult.popup.headline=トレードID {0} の調停の結果 +portfolio.pending.mediationResult.popup.headline.peerAccepted=トレードピアは、トレード {0} に関する調停者の提案を受け入れました。 +portfolio.pending.mediationResult.popup.info=調停者の資金分け提案は以下のとおり:\nあなたの分: {0}\nトレードピアの分: {1}\n\nこの支払い提案を受け取るまたは断ることができます。\n\n受け取ることで、支払い提案のトランザクションを署名します。トレードピアも同じく受け取って署名すると、支払いは完了しトレードは成立されます。\n\n片当事者もしくは両当事者が提案を断ると、2回目の係争を開始するのに{2} (ブロック {3}) まで待つ必要があります。調停人は再びに問題を検討し、調査結果に基づいて支払い提案を申し出ます。\n\n仕事に対する補償として、調停人は手数料を徴収するかもしれない(手数料の上限:取引者のセキュリティデポジット)。両当事者が提案に応じるのは最高の結果です。調停を依頼するのは異例の事態のためです、例えば取引者が調停者の支払い提案は不正だということを確信している場合(それともピアが無反応になる場合)。\n\n新しい仲裁モデルの詳しくは: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=調停者の支払い提案に応じましたが、トレードピアは断りましたそうです。\n\n{0}のロック時間が終わったら(ブロック{1})、2回目の係争を開始できます、そして調停人は再びに問題を検討し調査結果に基づいて支払い提案を申し出るでしょう。\n\n新しい仲裁モデルの詳しくは: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=拒絶して仲裁を求める +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この法定通貨・アルトコイン支払いをBTC売り手に送信しないで下さい。遅延支払い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クリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Bisq調停者からアドバイスを受けることができます。\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.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「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=トレードを「失敗トレード」へ送る +portfolio.pending.failedTrade.warningIcon.tooltip=このトレードに関する問題の詳細を開くのにクリックする +portfolio.failed.revertToPending.popup=このトレードを「オープントレード」に送りますか? +portfolio.failed.revertToPending=トレードを「オープントレード」へ送る + +portfolio.closed.completed=完了 +portfolio.closed.ticketClosed=仲裁をされました +portfolio.closed.mediationTicketClosed=調停をされました +portfolio.closed.canceled=キャンセルされています +portfolio.failed.Failed=失敗 +portfolio.failed.unfail=進む前に、必ずデータディレクトリーをバックアップしといて下さい!\nこのトレードを「オープントレード」へ戻しますか?\n失敗トレードにはまり込まれている資金を解放させる方法の1つです。 +portfolio.failed.cantUnfail=このトレードは現在のところ「オープントレード」へ戻されることができません。\nトレード {0} が完了された後にもう一度試して下さい。 +portfolio.failed.depositTxNull=このトレードは「オープントレード」へ戻されることができません。入金トランザクションは無効とされました。 +portfolio.failed.delayedPayoutTxNull=このトレードは「オープントレード」へ戻されることができません。遅延支払いトランザクションは無効とされました。 + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=資金を受け取る +funds.tab.withdrawal=送金する +funds.tab.reserved=予約された資金 +funds.tab.locked=ロックされた資金 +funds.tab.transactions=トランザクション + +funds.deposit.unused=未使用 +funds.deposit.usedInTx={0}トランザクションで使われています +funds.deposit.fundBisqWallet=Bisqウォレットに入金 +funds.deposit.noAddresses=デポジットアドレスはまだ生成されていません +funds.deposit.fundWallet=あなたのウォレットに入金 +funds.deposit.withdrawFromWallet=ウォレットから資金を送金 +funds.deposit.amount=BTCの金額(オプション) +funds.deposit.generateAddress=新しいアドレスの生成 +funds.deposit.generateAddressSegwit=ネイティブセグウィットのフォーマット(Bech32) +funds.deposit.selectUnused=新しいアドレスを生成するのではなく、上の表から未使用のアドレスを選択してください。 + +funds.withdrawal.arbitrationFee=調停手数料 +funds.withdrawal.inputs=インプット選択 +funds.withdrawal.useAllInputs=全ての利用可能なインプットを使用 +funds.withdrawal.useCustomInputs=任意のインプットを使用 +funds.withdrawal.receiverAmount=受信者の金額 +funds.withdrawal.senderAmount=送信者の金額 +funds.withdrawal.feeExcluded=マイニング手数料を含まない金額 +funds.withdrawal.feeIncluded=マイニング手数料を含む金額 +funds.withdrawal.fromLabel=アドレスから出金 +funds.withdrawal.toLabel=出金先アドレス +funds.withdrawal.memoLabel=出金メモ +funds.withdrawal.memo=任意入力メモ +funds.withdrawal.withdrawButton=選択された出金 +funds.withdrawal.noFundsAvailable=出金のための利用可能な資金がありません +funds.withdrawal.confirmWithdrawalRequest=出金リクエストを承認 +funds.withdrawal.withdrawMultipleAddresses=複数アドレスからの出金({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=複数アドレスからの出金:\n{0} +funds.withdrawal.notEnoughFunds=あなたのウォレットに十分な資金がありません。 +funds.withdrawal.selectAddress=表から送信元アドレスを選択 +funds.withdrawal.setAmount=出金額を設定 +funds.withdrawal.fillDestAddress=あなたの出金先アドレスを記入 +funds.withdrawal.warn.noSourceAddressSelected=上の表で送信元アドレスを選択する必要があります。 +funds.withdrawal.warn.amountExceeds=選択されたアドレスからは十分な利用可能な資金が得られません。\n上の表で複数の住所を選択するか、またはマイニング料金を含むように手数料の切り替えを変更することを検討してください。 + +funds.reserved.noFunds=予約された資金はオープンなオファーにはありません +funds.reserved.reserved=次のIDとのオファーはローカルウォレットで予約されています: {0} + +funds.locked.noFunds=ロックされた資金はトレードにはありません +funds.locked.locked=次のIDとのトレードはマルチシグでロック中です: {0} + +funds.tx.direction.sentTo=送信 to: +funds.tx.direction.receivedWith=次に予約済: +funds.tx.direction.genesisTx=ジェネシスTXから: +funds.tx.txFeePaymentForBsqTx=BSQのTXのマイニング手数料 +funds.tx.createOfferFee=メイカーとTXの手数料: {0} +funds.tx.takeOfferFee=テイカーとTXの手数料: {0} +funds.tx.multiSigDeposit=マルチシグデポジット: {0} +funds.tx.multiSigPayout=マルチシグ支払い: {0} +funds.tx.disputePayout=係争の支払い: {0} +funds.tx.disputeLost=係争事案が消失: {0} +funds.tx.collateralForRefund=担保の払い戻し: {0} +funds.tx.timeLockedPayoutTx=時間ロック支払いtx: {0} +funds.tx.refund=仲裁からの払い戻し: {0} +funds.tx.unknown=不明な理由: {0} +funds.tx.noFundsFromDispute=係争からの返金はありません +funds.tx.receivedFunds=受取済み資金 +funds.tx.withdrawnFromWallet=ウォレットからの出金 +funds.tx.withdrawnFromBSQWallet=BSQウォレットから出金されたBTC +funds.tx.memo=メモ +funds.tx.noTxAvailable=利用できるトランザクションがありません +funds.tx.revert=元に戻す +funds.tx.txSent=トランザクションはローカルBisqウォレットの新しいアドレスに正常に送信されました。 +funds.tx.direction.self=自分自身に送信済み +funds.tx.daoTxFee=BSQのTXのマイニング手数料 +funds.tx.reimbursementRequestTxFee=払い戻しリクエスト +funds.tx.compensationRequestTxFee=報酬リクエスト +funds.tx.dustAttackTx=ダストを受取りました +funds.tx.dustAttackTx.popup=このトランザクションはごくわずかなBTC金額をあなたのウォレットに送っているので、あなたのウォレットを盗もうとするチェーン解析会社による試みかもしれません。\n\nあなたが支払い取引でそのトランザクションアウトプットを使うならば、彼らはあなたが他のアドレスの所有者である可能性が高いことを学びます(コインマージ)。\n\nあなたのプライバシーを保護するために、Bisqウォレットは、支払い目的および残高表示において、そのようなダストアウトプットを無視します。 設定でアウトプットがダストと見なされるときのしきい値を設定できます。 + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=調停 +support.tab.arbitration.support=仲裁 +support.tab.legacyArbitration.support=レガシー仲裁 +support.tab.ArbitratorsSupportTickets={0} のチケット +support.filter=係争を検索 +support.filter.prompt=トレードID、日付、onionアドレスまたはアカウントデータを入力してください + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=DAOに払い戻しリクエストを提出する場合、調停・仲裁プロセスの概要メッセージをGithubで提出された払い戻しリクエストに張り付ける必要があります。検証を可能にするため、ユーザがこのツールで概要メッセージと調停者や調停人の署名が照合するかどうか確かめることができます。 +support.sigCheck.popup.header=係争結果の署名を検証する +support.sigCheck.popup.msg.label=概要メッセージ +support.sigCheck.popup.msg.prompt=係争からの概要メッセージをコピーして貼り付ける +support.sigCheck.popup.result=検証結果 +support.sigCheck.popup.success=有効な署名です +support.sigCheck.popup.failed=署名検証失敗 +support.sigCheck.popup.invalidFormat=メッセージは期待されるフォーマットではありません。係争からの概要メッセージをコピーして貼り付けて下さい。 + +support.reOpenByTrader.prompt=係争を再開しても本当によろしいですか? +support.reOpenButton.label=再開する +support.sendNotificationButton.label=プライベート通知 +support.reportButton.label=報告する +support.fullReportButton.label=全ての係争 +support.noTickets=オープンなチケットはありません +support.sendingMessage=メッセージを送信中 +support.receiverNotOnline=受信者はオンラインではありません。 メッセージは彼のメールボックスに保存されます。 +support.sendMessageError=メッセージ送信失敗。エラー: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=その係争の申し出はBisqの古いバージョンで作成されました。\nあなたのアプリケーションのバージョンではその係争を閉じることはできません。\n\n次のより古いバージョンを使用してください:プロトコルバージョン{0} +support.openFile=添付ファイルを開く(最大ファイルサイズ: {0} kb) +support.attachmentTooLarge=添付ファイルの合計サイズは{0} kbで、最大サイズを超えています。 許容されるメッセージサイズは{1} KBです。 +support.maxSize=許容された最大ファイルサイズは{0} KBです。 +support.attachment=添付ファイル +support.tooManyAttachments=1つのメッセージに3つを超える添付ファイルは送信できません +support.save=ファイルをディスクに保存 +support.messages=メッセージ +support.input.prompt=メッセージを入力... +support.send=送信 +support.addAttachments=添付ファイルを追加 +support.closeTicket=チケットを閉じる +support.attachments=添付ファイル: +support.savedInMailbox=メッセージ受信箱に保存されました +support.arrived=メッセージが受信者へ届きました +support.acknowledged=受信者からメッセージ到着が確認されました +support.error=受信者がメッセージを処理できませんでした。エラー: {0} +support.buyerAddress=BTC買い手のアドレス +support.sellerAddress=BTC売り手のアドレス +support.role=役割 +support.agent=サポート代理人 +support.state=状態 +support.chat=Chat +support.closed=クローズ +support.open=オープン +support.process=Process +support.buyerOfferer=BTC 買い手/メイカー +support.sellerOfferer=BTC 売り手/メイカー +support.buyerTaker=BTC 買い手/テイカー +support.sellerTaker=BTC 売り手/テイカー + +support.backgroundInfo=Bisqは会社ではないので、係争の扱いが異なります。\n\n取引者はアプリ内の「オープントレード」画面からセキュアチャットでお互いに紛争を解決しようと努めれる。その方法は十分でない場合、調停者は間に入って助けることもできます。調停者は状況を判断して、トレード資金の支払い配分を提案します。両当事者は提案に同意する場合、支払いトランザクションは完了され、トレードは閉じられます。片当事者もしくは両当事者は調停者の支払い提案に同意しない場合、仲裁を求めることができます。調停人は再びに問題を検討し、正当な場合は個人的に取引者に払い戻す、そしてBisqのDAOからその分の払い戻し要求を提出します。 +support.initialInfo=下のテキストフィールドに問題の説明を入力してください。係争解決の時間を短縮するために、可能な限り多くの情報を追加してください。\n\n提供する必要がある情報のチェックリストを次に示します:\n\t●BTC買い手の場合:法定通貨またはアルトコインの送金を行いましたか?その場合、アプリケーションの「支払い開始」ボタンをクリックしましたか?\n\t●BTC売り手の場合:法定通貨またはアルトコインの支払いを受け取りましたか?その場合、アプリケーションの「支払いを受け取った」ボタンをクリックしましたか?\n\t●どのバージョンのBisqを使用していますか?\n\t●どのオペレーティングシステムを使用していますか?\n\t●失敗したトランザクションで問題が発生した場合は、新しいデータディレクトリへの切り替えを検討してください。\n\t データディレクトリが破損し、不可解なバグが発生している場合があります。\n\t 参照:https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\n係争プロセスの基本的なルールをよく理解してください:\n\t●2日以内に{0}の要求に応答する必要があります。\n\t●調停者は2日以内に返事をするでしょう。調停人は5営業日以内に返事をするでしょう。\n\t●係争の最大期間は14日間です。\n\t●{1}と協力し、彼らがあなたの主張をするために、要求された情報を提供する必要があります\n\t●あなたは申請を最初に開始したときに、ユーザー契約の係争文書に記載されている規則を受け入れています。\n\n係争プロセスの詳細については、{2} をご覧ください。 +support.systemMsg=システムメッセージ: {0} +support.youOpenedTicket=サポートのリクエスト開始しました。\n\n{0}\n\nBisqバージョン: {1} +support.youOpenedDispute=係争のリクエスト開始しました。\n\n{0}\n\nBisqバージョン: {1} +support.youOpenedDisputeForMediation=調停を求めました。\n\n{0}\n\nBisqバージョン: {1} +support.peerOpenedTicket=トレードピアは技術的な問題によるサポートを要求しました。\n\n{0}\n\nBisqバージョン: {1} +support.peerOpenedDispute=トレードピアは係争を求めました。\n\n{0}\n\nBisqバージョン: {1} +support.peerOpenedDisputeForMediation=トレードピアは調停を求めました。\n\n{0}\n\nBisqバージョン: {1} +support.mediatorsDisputeSummary=システム・メッセージ:調停者の係争概要:\n{0} +support.mediatorsAddress=調停人のノードアドレス: {0} +support.warning.disputesWithInvalidDonationAddress=遅延支払いトランザクションは無効な受信アドレスを利用しました。有効な寄付アドレスに対するDAOパラメーターと合っていません。\n\n詐欺の未遂かもしれません。開発者に報告して、問題が解決される前に事件を閉じないで下さい!\n\nこの係争に利用されたアドレス: {0}\n\nDAOパラメーターに合う寄付アドレス: {1}\n\nトレードID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\n係争を閉じても本当によろしいですか? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n支払いを送信してはいけません。 +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=設定 +settings.tab.network=ネットワーク情報 +settings.tab.about=About + +setting.preferences.general=一般設定 +setting.preferences.explorer=ビットコインのエクスプローラ +setting.preferences.explorer.bsq=Bisqのエクスプローラ +setting.preferences.deviation=市場価格からの最大偏差 +setting.preferences.bsqAverageTrimThreshold=BSQ率の外れ閾値 +setting.preferences.avoidStandbyMode=スタンバイモードを避ける +setting.preferences.autoConfirmXMR=XMR自動確認 +setting.preferences.autoConfirmEnabled=有効されました +setting.preferences.autoConfirmRequiredConfirmations=必要承認 +setting.preferences.autoConfirmMaxTradeSize=最大トレード金額(BTC) +setting.preferences.autoConfirmServiceAddresses=モネロエクスプローラURL(localhost、LANのIPアドレス、または*.localのホストネーム以外はTorを利用します) +setting.preferences.deviationToLarge={0}%以上の値は許可されていません。 +setting.preferences.txFee=出金トランザクション手数料 (satoshis/vbyte) +setting.preferences.useCustomValue=任意の値を使う +setting.preferences.txFeeMin=トランザクション手数料は少なくとも{0} satoshis/vbyte でなければなりません +setting.preferences.txFeeTooLarge=あなたの入力は妥当な値(> 5000 satoshis / vbyte)を超えています。トランザクション手数料は通常 50-400 satoshis/vbyteの範囲です。 +setting.preferences.ignorePeers=無視されたピア [onion アドレス:ポート] +setting.preferences.ignoreDustThreshold=最小の非ダストアウトプット値 +setting.preferences.currenciesInList=市場価格フィードリストの通貨 +setting.preferences.prefCurrency=希望する通貨 +setting.preferences.displayFiat=各国通貨の表示 +setting.preferences.noFiat=各国通貨が選択されていません +setting.preferences.cannotRemovePrefCurrency=選択した希望の表示通貨は削除できません +setting.preferences.displayAltcoins=アルトコインの表示 +setting.preferences.noAltcoins=選択されたアルトコインがありません +setting.preferences.addFiat=各国通貨を追加する +setting.preferences.addAltcoin=アルトコインを追加する +setting.preferences.displayOptions=表示設定 +setting.preferences.showOwnOffers=オファーブックに自分のオファーを表示 +setting.preferences.useAnimations=アニメーションを使用 +setting.preferences.useDarkMode=ダークモードを利用 +setting.preferences.sortWithNumOffers=市場リストをオファー/トレードの数で並び替える +setting.preferences.onlyShowPaymentMethodsFromAccount=サポートされていない支払い方法を非表示にする +setting.preferences.denyApiTaker=APIを使用するテイカーを拒否する +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=「次回から表示しない」フラグを全てリセット +settings.preferences.languageChange=言語の変更をすべての画面に適用するには再起動が必要です。 +settings.preferences.supportLanguageWarning=係争が発生した場合、調停は{0}で、仲裁は{1}で処理されることに注意して下さい。 +setting.preferences.daoOptions=DAO設定 +setting.preferences.dao.resyncFromGenesis.label=ジェネシスTXからDAO状態を再構築 +setting.preferences.dao.resyncFromResources.label=リソースからDAO状態を再構築 +setting.preferences.dao.resyncFromResources.popup=アプリケーションの再起動後、Bisqネットワークガバナンスデータはシードノードから再ロードされ、BSQのコンセンサス状態は最新リソースファイルから再構築されます。 +setting.preferences.dao.resyncFromGenesis.popup=ジェネシストランザクションからの再同期はかなりの時間をかけて、かなりのCPUリソースを消費します。本当によろしいですか?大抵の場合は最新リソースファイルからの再同期は十分、より早い選択です。\n\n進めたら、アプリケーションの再起動後、Bisqネットワークガバナンスデータはシードノードから再ロードされ、BSQのコンセンサス状態はジェネシストランザクションから再構築されるでしょう。 +setting.preferences.dao.resyncFromGenesis.resync=ジェネシスから再同期して終了 +setting.preferences.dao.isDaoFullNode=BisqをDAOのフルノードで実行 +setting.preferences.dao.rpcUser=RPCユーザ名 +setting.preferences.dao.rpcPw=RPCパスワード +setting.preferences.dao.blockNotifyPort=通知ポートをブロック +setting.preferences.dao.fullNodeInfo=DAOフルノードとしてBisqを実行するには、Bitcoin Coreをローカルで実行し、RPCを有効にする必要があります。すべての要件は '' {0} ''に文書化されています。\n\nモードを変更した後は、再起動する必要があります。 +setting.preferences.dao.fullNodeInfo.ok=ドキュメントページを開く +setting.preferences.dao.fullNodeInfo.cancel=いいえ、ライトノードモードを使い続けます +settings.preferences.editCustomExplorer.headline=エクスプローラー設定 +settings.preferences.editCustomExplorer.description=左のリストからシステム定義エクスプローラを選択、それともニーズや好みに合わせてカスタマイズする。 +settings.preferences.editCustomExplorer.available=利用可能なエクスプローラ +settings.preferences.editCustomExplorer.chosen=選択したエクスプローラ設定 +settings.preferences.editCustomExplorer.name=名義 +settings.preferences.editCustomExplorer.txUrl=トランザクションURL +settings.preferences.editCustomExplorer.addressUrl=アドレスURL + +settings.net.btcHeader=ビットコインのネットワーク +settings.net.p2pHeader=Bisqネットワーク +settings.net.onionAddressLabel=私のonionアドレス +settings.net.btcNodesLabel=任意のビットコインノードを使う +settings.net.bitcoinPeersLabel=接続されたピア +settings.net.useTorForBtcJLabel=BitcoinネットワークにTorを使用 +settings.net.bitcoinNodesLabel=接続するBitcoin Coreノード: +settings.net.useProvidedNodesRadio=提供されたBitcoin Core ノードを使う +settings.net.usePublicNodesRadio=ビットコインの公共ネットワークを使用 +settings.net.useCustomNodesRadio=任意のビットコインノードを使う +settings.net.warn.usePublicNodes=パブリックなビットコインネットワークを使用する場合、BitcoinJ(Bisqで使用)のようなSPVウォレットに使用される破損したブルームフィルターの設計と実装によって、重大なプライバシー問題にさらされます。接続しているすべてのノードは、すべてのウォレットアドレスが1つのエンティティに属していることがわかります。\n\n詳細については、[HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare] をご覧ください。\n\nパブリックノードを使用しても本当によろしいですか? +settings.net.warn.usePublicNodes.useProvided=いいえ、提供されたノードを使用します +settings.net.warn.usePublicNodes.usePublic=はい、公共ネットワークを使います +settings.net.warn.useCustomNodes.B2XWarning=あなたのBitcoinノードが信頼できるBitcoin Coreノードであることを確認してください!\n\nBitcoin Coreのコンセンサスルールに従わないノードに接続すると、ウォレットが破損し、トレードプロセスに問題が生じる可能性があります。\n\nコンセンサスルールに違反するノードへ接続したユーザーは、引き起こされるいかなる損害に対しても責任を負います。 結果として生じる係争は、他のピアによって決定されます。この警告と保護のメカニズムを無視しているユーザーには、テクニカルサポートは提供されません! +settings.net.warn.invalidBtcConfig=無効な設定によりビットコインネットワークとの接続は失敗しました。\n\n代りに提供されたビットコインノードを利用するのに設定はリセットされました。アプリを再起動する必要があります。 +settings.net.localhostBtcNodeInfo=バックグラウンド情報:Bisqが起動時に、ローカルビットコインノードを探します。見つかれば、Bisqはそのノードを排他的に介してビットコインネットワークと接続します。 +settings.net.p2PPeersLabel=接続されたピア +settings.net.onionAddressColumn=Onionアドレス +settings.net.creationDateColumn=既定 +settings.net.connectionTypeColumn=イン/アウト +settings.net.sentDataLabel=通信されたデータ統計 +settings.net.receivedDataLabel=受信されたデータ統計 +settings.net.chainHeightLabel=BTCの最新ブロック高さ +settings.net.roundTripTimeColumn=往復 +settings.net.sentBytesColumn=送信済 +settings.net.receivedBytesColumn=受信済 +settings.net.peerTypeColumn=ピアタイプ +settings.net.openTorSettingsButton=Torの設定を開く + +settings.net.versionColumn=バージョン +settings.net.subVersionColumn=サブバージョン +settings.net.heightColumn=高さ + +settings.net.needRestart=その変更を適用するには、アプリケーションを再起動する必要があります。\n今すぐ行いますか? +settings.net.notKnownYet=まだわかりません... +settings.net.sentData=通信されたデータ: {0}, {1} メッセージ、 {2} メッセージ/秒 +settings.net.receivedData=受信されたデータ: {0}, {1} メッセージ、 {2} メッセージ/秒 +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[IPアドレス:ポート | ホスト名:ポート | onionアドレス:ポート](コンマ区切り)。デフォルト(8333)が使用される場合、ポートは省略できます。 +settings.net.seedNode=シードノード +settings.net.directPeer=ピア (ダイレクト) +settings.net.initialDataExchange={0} [ ブートストラップ 中] +settings.net.peer=ピア +settings.net.inbound=インバウンド +settings.net.outbound=アウトバウンド +settings.net.reSyncSPVChainLabel=SPVチェーンを再同期 +settings.net.reSyncSPVChainButton=SPVファイルを削除してを再同期 +settings.net.reSyncSPVSuccess=SPVの再同期を実行してもよろしいですか?実行すると、SPVチェーンファイルは次回の起動時に削除されます。\n\n再起動後、ネットワークとの再同期に時間がかかることがあり、再同期が完了するとすべてのトランザクションのみが表示されます。\n\nトランザクションの数そしてウォレットの時代によって、再同期は数時間かかり、CPUのリソースを100%消費します。再同期プロセスを割り込みしないで下さい。さもなければやり直す必要があります。 +settings.net.reSyncSPVAfterRestart=SPVチェーンファイルが削除されました。しばらくお待ちください。ネットワークとの再同期には時間がかかる場合があります。 +settings.net.reSyncSPVAfterRestartCompleted=再同期が完了しました。アプリケーションを再起動してください。 +settings.net.reSyncSPVFailed=SPVチェーンファイルを削除できませんでした。\nエラー: {0} +setting.about.aboutBisq=Bisqについて +setting.about.about=Bisqは、ユーザーのプライバシーを強力に保護する方法で、非中央集権型のピアツーピアネットワークを介して各国通貨(およびその他の暗号通貨)とのビットコインの交換を容易にするオープンソースソフトウェアです。 Bisqの詳細については、プロジェクトのWebページをご覧ください。 +setting.about.web=Bisqホームページ +setting.about.code=ソースコード +setting.about.agpl=AGPLライセンス +setting.about.support=Bisqをサポートする +setting.about.def=Bisqは会社ではなく、開かれたコミュニティのプロジェクトです。Bisqにサポートしたい時は下のURLをチェックしてください。 +setting.about.contribute=貢献 +setting.about.providers=データプロバイダー +setting.about.apisWithFee=Bisqは、法定通貨とアルトコインの市場価格の推定にBisq物価指数を利用し、マイニング手数料の推定にMempoolノードを使用します。 +setting.about.apis=Bisqは、法定通貨とアルトコインの市場価格の推定にBisq物価指数を利用します。 +setting.about.pricesProvided=市場価格を提供している: +setting.about.feeEstimation.label=推定マイニング手数料の提供: +setting.about.versionDetails=バージョン詳細 +setting.about.version=アプリのバージョン +setting.about.subsystems.label=サブシステムのバージョン +setting.about.subsystems.val=ネットワークバージョン: {0}; P2Pメッセージバージョン: {1}; ローカルDBバージョン: {2}; トレードプロトコルバージョン: {3} + +setting.about.shortcuts=ショートカット +setting.about.shortcuts.ctrlOrAltOrCmd=「Ctrl + {0}」または「Alt + {0}」それとも「Cmd + {0}」 + +setting.about.shortcuts.menuNav=メインメニューをナビゲートする +setting.about.shortcuts.menuNav.value=メインメニューをナビゲートするのに:「Ctrl」または「Alt」それとも「Cmd」キーと1-9の数字キーを押して下さい。 + +setting.about.shortcuts.close=Bisqを閉じる +setting.about.shortcuts.close.value=「Ctrl + {0}」か「cmd + {0}」、それとも「Ctrl + {1}」か「cmd + {1}」 + +setting.about.shortcuts.closePopup=ポップアップやダイアログ・ウィンドウを閉じる +setting.about.shortcuts.closePopup.value=エスケープキー + +setting.about.shortcuts.chatSendMsg=取引者チャットメッセージを送る +setting.about.shortcuts.chatSendMsg.value=「Ctrl + ENTER」または「Alt + ENTER」それとも「cmd + ENTER」 + +setting.about.shortcuts.openDispute=係争を開始 +setting.about.shortcuts.openDispute.value=未決トレードを選択してクリックする:{0} + +setting.about.shortcuts.walletDetails=ウォレット詳細ウィンドウを開く + +setting.about.shortcuts.openEmergencyBtcWalletTool=BTCウォレットに対する緊急ウォレットツールを開く + +setting.about.shortcuts.openEmergencyBsqWalletTool=BSQウォレットに対する緊急ウォレットツールを開く + +setting.about.shortcuts.showTorLogs=TorメッセージのログレベルをDEBUGとWARNを切り替える + +setting.about.shortcuts.manualPayoutTxWindow=2of2マルチシグ入金txから手動支払いのウィンドウを開く + +setting.about.shortcuts.reRepublishAllGovernanceData=DAOガバナンスデータ(提案、票)を再発行する + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=調停人を登録(調停者/調停人のみ) +setting.about.shortcuts.registerArbitrator.value=アカウントへナビゲートして押す: {0} + +setting.about.shortcuts.registerMediator=調停者を登録(調停者/調停人のみ) +setting.about.shortcuts.registerMediator.value=アカウントへナビゲートして押す: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=アカウント年齢署名のウィンドウを開く(レガシー調停人のみ) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=レガシー調停人表示へナビゲートして押す: {0} + +setting.about.shortcuts.sendAlertMsg=アラートまたはアップデートメッセージを送る(特権的行為) + +setting.about.shortcuts.sendFilter=フィルターを設定する(特権的行為) + +setting.about.shortcuts.sendPrivateNotification=ピアにプライベート通知を送る(特権的行為) +setting.about.shortcuts.sendPrivateNotification.value=アバターからピア情報を開いて押す:{0} + +setting.info.headline=新しいXMR自動確認の機能 +setting.info.msg=BTC売ってXMR買う場合、Bisqが自動的にトレードを完了としてマークできるように自動確認機能で適正量のXMRはウォレットに送られたかを検証できます。その際、皆にトレードをより早く完了できるようにします。\n\n自動確認はXMR送信者が提供するプライベート・トランザクション・キーを利用して少なくとも2つのXMRエクスプローラノードでXMRトランザクションを確認します。デフォルト設定でBisqは貢献者に管理されるエクスプローラノードを利用しますが、最大のプライバシーやセキュリティーのため自分のXMRエクスプローラノードを管理するのをおすすめします。\n\n1つのトレードにつき自動確認する最大額のBTC、そして必要承認の数をこの画面で設定できます。\n\nBisqのWikiから詳細(自分のエクスプローラノードを管理する方法も含めて)を参照できます: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=調停者登録 +account.tab.refundAgentRegistration=仲裁人登録 +account.tab.signing=署名中 +account.info.headline=あなたのBisqアカウントへようこそ! +account.info.msg=ここでは、各国通貨とアルトコインのトレードアカウントを追加したり、ウォレットやアカウントデータのバックアップを作成することができます。\n\nBisqは最初に起動した時、新しいビットコインウォレットが自動的に作られました。\n\nビットコインウォレットのシードワードを書き留めて(上部のタブを参照)、入金の前にパスワードを追加することを検討することを強くお勧めします。 ビットコインの入出金は「資金」セクションで管理されます。\n\nプライバシーとセキュリティに関するメモ: Bisqは非中央集権型の交換であるため、すべてのデータはコンピュータに保存されています。 サーバーがないので、私たちはあなたの個人情報、あなたの資金、あるいはあなたのIPアドレスにさえもアクセスできません。 銀行口座番号、アルトコイン&ビットコインのアドレスなどのデータは、あなたが開始したトレードを遂行するためにあなたのトレード相手とだけ共有されます(係争の場合には調停人があなたのトレードピアと同じデータを見るでしょう)。 + +account.menu.paymentAccount=各国通貨口座 +account.menu.altCoinsAccountView=アルトコインアカウント +account.menu.password=ウォレットのパスワード +account.menu.seedWords=ウォレットシード +account.menu.walletInfo=ウォレット情報 +account.menu.backup=バックアップ +account.menu.notifications=通知 + +account.menu.walletInfo.balance.headLine=ウォレット残高 +account.menu.walletInfo.balance.info=非確認されたトランザクションも含めて、内部ウォレット残高を表示します。\nBTCの場合、下に表示される「内部ウォレット残高」はこのウィンドウの右上に表示される「利用可能」と「予約済」の和に等しいはずです。 +account.menu.walletInfo.xpub.headLine=ウォッチキー(xpubキー) +account.menu.walletInfo.walletSelector={0} {1} ウォレット +account.menu.walletInfo.path.headLine=HDキーチェーンのパス +account.menu.walletInfo.path.info=他のウォレット(例えばElectrum)にシードワードをインポートすると、パスを設定しなければなりません。 Bisqウォレットとデータディレクトリーをアクセスできないような緊急事態の場合のみにして下さい。\nBisqでないウォレットからその資金を遣うと、ウォレットデータと繋がっているBisq内部データ構造は破損される可能性があり、失敗トレードにつながる可能性があります。\n\n絶対にBisqでないウォレットからBSQを遣わないで下さい。無効なトランザクションになる可能性が高い、そしてBSQを失うことになるでしょう。 + +account.menu.walletInfo.openDetails=生ウォレット詳細、秘密鍵を表示する + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=パブリックキー + +account.arbitratorRegistration.register=登録する +account.arbitratorRegistration.registration={0} 登録 +account.arbitratorRegistration.revoke=取り消し +account.arbitratorRegistration.info.msg={0} としてあなたを使用しているトレードが存在する可能性があるので、取り消し後の15日間にかけて必ず待機しておいて下さい。許可されるトレード期間は8日間で、係争処理には最長で7日間かかる場合があります。 +account.arbitratorRegistration.warn.min1Language=少なくとも1つの言語を設定する必要があります。\nデフォルトの言語を追加しました。 +account.arbitratorRegistration.removedSuccess=Bisqネットワークから登録を正常に削除しました。 +account.arbitratorRegistration.removedFailed=登録を削除できませんでした。{0} +account.arbitratorRegistration.registerSuccess=Bisqネットワークに正常に登録しました。 +account.arbitratorRegistration.registerFailed=登録を完了できませんでした。{0} + +account.altcoin.yourAltcoinAccounts=あなたのアルトコインアカウント +account.altcoin.popup.wallet.msg={1} のWebページに記載されているように、{0}ウォレットの使用に関する要件に必ず従ってください。\n(a)あなたが自分で鍵を管理していない、または(b)互換性のあるウォレットソフトウェアを使用していないような、中央集権化された取引所でウォレットを使用することは危険です。トレード資金の損失につながる可能性があります!\n調停者や調停人は{2}スペシャリストではなく、そのような場合には手助けできません。 +account.altcoin.popup.wallet.confirm=どのウォレットを使うべきか理解しており、承認する +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=BisqでARQをトレードするには、次の要件を理解し、満たす必要があります。\n\nARQを送信するには、store-tx-infoフラグを有効(新しいバージョンではデフォルト)にした公式のArQmA GUIウォレットまたはArQmA CLIウォレットのいずれかを使用する必要があります。係争が発生した場合に必要になるため、txキーにアクセスできることを確認してください。\narqma-wallet-cli(コマンドget_tx_keyを使用)\narqma-wallet-gui(履歴タブに移動し、支払い証明のために(P)ボタンをクリックします)\n\n通常のブロックエクスプローラーでは、転送は検証できません。\n\n係争の場合、調停人に次のデータを提供する必要があります。\n-txプライベートキー\n-トランザクションハッシュ\n-受信者のパブリックアドレス\n\n上記のデータを提供しない場合、または互換性のないウォレットを使用した場合は、係争のケースが失われます。 ARQ送信者は、係争の場合にARQ転送の検証を調停人に提供する責任があります。\n\n支払いIDは不要で、通常のパブリックアドレスのみです。\nこのプロセスがわからない場合は、ArQmA Discordチャンネル( https://discord.gg/s9BQpJT )またはArQmAフォーラム( https://labs.arqma.com )にアクセスして、詳細を確認してください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=BisqでXMRをトレードするには、以下の要件を理解し、満たす必要があります。\n\nXMRを売る場合、係争を解決するため調停者や調停人に以下の情報を提供できる必要があります:\n- トランザクションキー(Txキー、Tx秘密キー、Txプライベートキー)\n- トランザクションID(TxID、Txハッシュ)\n- 宛先アドレス(受領者のアドレス)\n\n人気のモネロウォレットからこういう情報を見つける方法について、BisqのWikiを参照して下さい [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments]\n必要のトランザクションデータを提供しなければ、係争で不利な裁定を下されます。\n\nBisqではXMRトランザクションに自動確認機能を提供しますが、設で有効にする必要があります。\n\n自動確認機能について詳しくはWikiで参照して下さい:\n[HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=BisqでBLURをトレードするには、次の要件を理解し、満たす必要があります。\n\nBLURを送信するには、Blur Network CLIまたはGUI ウォレットを使用する必要があります。\n\nCLIウォレットを使用している場合、転送の送信後にトランザクションハッシュ(tx ID)が表示されます。この情報を保存する必要があります。転送を送信した直後に、コマンド「get_tx_key」を使用してトランザクションプライベートキーを取得する必要があります。この手順を実行しないと、後でキーを取得できない場合があります。\n\nBlur Network GUIウォレットを使用している場合、トランザクションのプライベートキーとトランザクションIDは「履歴」タブで簡単に見つけることができます。送信後すぐに、目的のトランザクションを見つけてください。このトランザクションを含むボックスの右下隅にある「?」記号をクリックしてください。この情報を保存する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、3) 受信者のアドレス を調停人に提示する必要があります。調停人は、Blur Transaction Viewer( https://blur.cash/#tx-viewer )を使用してBLUR転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、BLUR送信者は、調停人に対する取引を確認する責任の100%を負担します。\n\nこれらの要件を理解していない場合は、Bisqで取引しないでください。まず、Blur Network Discord( https://discord.gg/dMWaqVW )で助けを求めてください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=BisqでCASH2をトレードするには、次の要件を理解し、満たす必要があります。\n\nCASH2を送信するには、Cash2 Walletバージョン3以降を使用する必要があります。\n\nトランザクションが送信された後、トランザクションIDが表示されます。この情報を保存する必要があります。トランザクションを送信した直後に、simplewalletのコマンド「getTxKey」を使用して、トランザクションのプライベートキーを取得する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、および 3) 受信者のCash2アドレス を調停人に提示する必要があります。その後、調停人は、Cash2 Block Explorer( https://blocks.cash2.org )を使用してCASH2転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、CASH2の送信者が、仲裁人への取引を確認する責任を100%負います。\n\nこれらの要件を理解していない場合は、Bisqで取引しないでください。まず、Cash2 Discord( https://discord.gg/FGfXAYN )で助けを求めてください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=BisqでQwertycoinをトレードするには、次の要件を理解し、満たす必要があります。\n\nQWCを送信するには、公式のQWCウォレットバージョン5.1.3以降を使用する必要があります。\n\nトランザクションが送信された後、トランザクションIDが表示されます。この情報を保存する必要があります。トランザクションを送信した直後に、simplewalletのコマンド「get_Tx_Key」を使用してトランザクションのプライベートキーを取得する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、3)受信者のQWCアドレス を調停人に提示する必要があります。その後、調停人はQWC Block Explorer( https://explorer.qwertycoin.org )を使用してQWC転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、QWCの送信者が、調停人のトレードの検証における100%の責任を負います。\n\nこれらの要件を理解していない場合は、Bisqで取引しないでください。まず、QWC Discord( https://discord.gg/rUkfnpC )で助けを求めてください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=BisqでDragonglassをトレードするには、次の要件を理解し、満たす必要があります。\n\nDragonglassが提供するプライバシーのため、トランザクションはパブリックブロックチェーンでは検証できません。必要に応じて、TXN-Private-Keyを使用して支払いを証明できます。\nTXN-Private-Keyは、DRGLウォレット内からのみアクセスできるトランザクションごとに自動的に生成されるワンタイムキーです。\nDRGLウォレットGUI(トランザクション詳細ダイアログ内)またはDragonglass CLIシンプルウォレット(コマンド「get_tx_key」を使用)のいずれか。\n\nDRGLバージョン「Oathkeeper」以降には両方が「必要」です。\n\n係争が発生した場合、調停人に次のデータを提供する必要があります。\n-TXN-Private-Key\n-トランザクションハッシュ\n-受信者のパブリックアドレス\n\n上記のデータを( http://drgl.info/#check_txn )の入力として使用して、支払いの検証を行うことができます。\n\n上記のデータを提供しない場合、または互換性のないウォレットを使用した場合は、係争のケースが失われます。 Dragonglassの送信者は、係争の場合にDRGL転送の調停人に検証を提供する責任があります。 PaymentIDの使用は必要ありません。\n\nこのプロセスについて不明な点がある場合は、Dragonglass on Discord( http://discord.drgl.info )にアクセスしてください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Zcashを使用する場合、調停者や調停人はzアドレスを持つトランザクションを検証できないため、zアドレス(プライベート)ではなく、透過アドレス(tで始まる)のみを使用できます。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Zcoinを使用する場合、調停者や調停人はブロックエクスプローラーで追跡不可能なアドレスを持つトランザクションを検証できないため、追跡不可能なアドレスではなく、透過(追跡可能な)アドレスのみを使用できます。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRINでは、トランザクションを作成するために送信者と受信者の間の対話型プロセスが必要です。 GRINプロジェクトのWebページの指示に従って、GRINを確実に送受信してください(受信者はオンラインであるか、特定の時間枠内で少なくともオンラインである必要があります)。\n\nBisqは、Grinbox(Wallet713)ウォレットURL形式のみをサポートします。\n\nGRIN送信者は、GRINが正常に送信されたことを証明する必要があります。ウォレットがその証拠を提供できない場合、起こり得る係争はGRIN受信者に有利に解決されるでしょう。トランザクションプルーフをサポートする最新のGrinboxソフトウェアを使用し、GRINの送受信プロセスとプルーフの作成方法を理解してください。\n\nGrinboxプルーフツールの詳細については、https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only を参照してください。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAMではトランザクションを作成するために、送信者と受信者の間で対話型プロセスが必要です。\n\n必ずBEAMプロジェクトのWebページの指示に従って、BEAMを確実に送受信してください(受信者はオンラインであるか、特定の時間枠で少なくともオンラインである必要があります)。\n\nBEAM送信者は、BEAMが正常に送信されたことを証明する必要があります。そのような証拠を作成できるウォレットソフトウェアを使用してください。ウォレットが証拠を提供できない場合、起こり得る論争はBEAM受信者に有利に解決されるでしょう。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=ParsiCoinをBisqでトレードするには、次の要件を理解し、満たす必要があります。\n\nPARSを送信するには、公式のParsiCoin Walletバージョン3.0.0以降を使用する必要があります。\n\nGUIウォレット(ParsiPay)のTransactionsセクションでトランザクションハッシュとトランザクションキーを確認できます。Transactionを右クリックして、show detailsをクリックします。\n\n調停が必要な場合は、1) トランザクションハッシュ、2) トランザクションキー、および3) 受信者のPARSア​​ドレス を調停人に提示する必要があります。その後、調停人は、ParsiCoin Block Explorer( http://explorer.parsicoin.net/#check_payment )を使用してPARS転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、ParsiCoinの送信者が、係争人のトランザクションの検証における100%の責任を負います。\n\nこれらの要件を理解していない場合は、Bisqで取引しないでください。まず、ParsiCoin Discord( https://discord.gg/c7qmFNh )で助けを求めてください。 + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=BisqでL-BTCをトレードするには、以下を理解する必要があります:\n\nBisqでのトレードにL-BTCを受け取る場合、モバイル用「Blockstream Green」ウォレットアプリそれとも取引場などの第三者によって保管されるウォレットの利用は不可能です。「Liquid Elements Core」ウォレット、あるいは機密L-BTCアドレスの「blindingキー」が入手可能のウォレットのみにL-BTCを受け取って下さい。\n\n調停が必要になる場合、あるいはトレード係争が開始される場合、調停者や調停人が「Elements Core」フルノードで機密トランザクションを検証できるように、受取アドレスのblindingキーを明かす必要があります。\n\n調停者や調停人に必要な情報を提供しなければ、係争で不利な裁定を下されます。全ての係争には、調停者や調停人に暗号証明を提供するのは100%受信者の責任です。\n\n以上の条件を理解しない場合、BisqでL-BTCのトレードをしないで下さい。 + +account.fiat.yourFiatAccounts=あなたの各国通貨口座 + +account.backup.title=ウォレットのバックアップ +account.backup.location=バックアップの場所 +account.backup.selectLocation=バックアップの場所を選択 +account.backup.backupNow=今すぐバックアップ(バックアップは暗号化されていません!) +account.backup.appDir=アプリケーションデータディレクトリー +account.backup.openDirectory=ディレクトリーを開く +account.backup.openLogFile=ログファイルを開く +account.backup.success=次の場所へのバックアップに成功しました:\n{0} +account.backup.directoryNotAccessible=選択されたディレクトリーにはアクセスできません。{0} + +account.password.removePw.button=パスワードを削除 +account.password.removePw.headline=ウォレットのパスワード保護を削除 +account.password.setPw.button=パスワードをセット +account.password.setPw.headline=ウォレットのパスワード保護をセット +account.password.info=パスワード保護を使用すると、アプリケーションの起動時、ウォレットからビットコインを出金するとき、およびシードワードからウォレットを復元するときにパスワードを入力する必要があります。 + +account.seed.backup.title=あなたのウォレットのシードワードをバックアップ +account.seed.info=ウォレットのシードワードと日付の両方を書き留めてください!あなたはシードワードと日付でいつでもウォレットを復元することができます。\nBTCおよびBSQウォレットには同じシードワードが使用されています。\n\nあなたは一枚の紙にシードワードを書き留めるべきです。コンピュータに保存しないでください。\n\nシードワードはバックアップの代わりにはならないことに気をつけて下さい。\nアプリケーションの状態とデータを復元するには「アカウント/バックアップ」画面からアプリケーションディレクトリ全体のバックアップを作成する必要があります。\nシードワードのインポートは緊急の場合にのみ推奨されます。データベースファイルとキーの適切なバックアップがなければ、アプリケーションは機能しません! +account.seed.backup.warning=ここで留意すべきはシードワードがバックアップの代りとして機能を果たさないことです。\nアプリケーションステートとデータを復元するのに、「アカウント/バックアップ」画面からアプリケーションディレクトリの完全バックアップを作成する必要があります。\nシードワードのインポートは緊急の場合のみにおすすめします。データベースファイルとキーの正当なバックアップがなければ、アプリケーションは機能するようになれません!\n\n詳しくはWikiのページから: [HYPERLINK:https://bisq.wiki/Backing_up_application_data] +account.seed.warn.noPw.msg=シードワードの表示を保護するためのウォレットパスワードを設定していません。 \n\nシードワードを表示しますか? +account.seed.warn.noPw.yes=はい、そして次回から確認しないで下さい +account.seed.enterPw=シードワードを見るためにパスワードを入力 +account.seed.restore.info=シードワードから復元を適用する前に、バックアップを作成してください。ウォレットの復元は緊急時のみであり、内部ウォレットデータベースに問題を引き起こす可能性があることに注意してください。 \nこれはバックアップを適用する方法ではありません!以前のアプリケーションの状態を復元するには、アプリケーションデータディレクトリのバックアップを使用してください。\n\nアプリケーションを復元すると、自動的にシャットダウンします。アプリケーションを再起動すると、ビットコインネットワークと再同期します。特にウォレットが古く、多くのトランザクションがあった場合、これには時間がかかり、CPUを多く消費する可能性があります。このプロセスは中断しないでください。さもなければ、SPVチェーンファイルを再度削除するか、復元プロセスを繰り返す必要があります。 +account.seed.restore.ok=わかりました、復元してBisqをシャットダウンしてください + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=セットアップ +account.notifications.download.label=モバイルアプリをダウンロードする +account.notifications.waitingForWebCam=webcamを待っています... +account.notifications.webCamWindow.headline=携帯でQRコードをスキャン +account.notifications.webcam.label=ウェブカメラを使う +account.notifications.webcam.button=QRコードをスキャンする +account.notifications.noWebcam.button=ウェブカメラを持ってない +account.notifications.erase.label=スマホの通知を削除 +account.notifications.erase.title=通知を削除 +account.notifications.email.label=ペア通貨 +account.notifications.email.prompt=メールで受け取るペア通貨を入力 +account.notifications.settings.title=設定 +account.notifications.useSound.label=スマホの通知音を再生 +account.notifications.trade.label=トレードメッセージを受信 +account.notifications.market.label=オファーアラートを受信 +account.notifications.price.label=価格アラートを受信 +account.notifications.priceAlert.title=価格アラート +account.notifications.priceAlert.high.label=BTC価格が次を上回ったら通知 +account.notifications.priceAlert.low.label=BTC価格が次を下回ったら通知 +account.notifications.priceAlert.setButton=価格アラートをセット +account.notifications.priceAlert.removeButton=価格アラートを削除 +account.notifications.trade.message.title=トレード状態が変わった +account.notifications.trade.message.msg.conf=ID {0}とのトレードのデポジットトランザクションが承認されました。 Bisqアプリケーションを開いて支払いを開始してください。 +account.notifications.trade.message.msg.started=BTCの買い手がID {0}とのトレードの支払いを開始しました。 +account.notifications.trade.message.msg.completed=ID {0}とのトレードが完了しました。 +account.notifications.offer.message.title=オファーが受け入れられました +account.notifications.offer.message.msg=ID {0}とのオファーが受け入れられました +account.notifications.dispute.message.title=新しい係争メッセージ +account.notifications.dispute.message.msg=ID {0}とのトレードに関する係争メッセージを受け取りました + +account.notifications.marketAlert.title=オファーのアラート +account.notifications.marketAlert.selectPaymentAccount=支払いアカウントと一致するオファー +account.notifications.marketAlert.offerType.label=興味のあるオファータイプ +account.notifications.marketAlert.offerType.buy=購入のオファー(BTCを売りたい) +account.notifications.marketAlert.offerType.sell=売却のオファー(BTCを買いたい) +account.notifications.marketAlert.trigger=オファー価格の乖離 (%) +account.notifications.marketAlert.trigger.info=価格乖離を設定すると、要件を満たす(または超える)オファーが公開されたときにのみアラートを受信します。例:BTCを売りたい時、現在の市場価格に対して2%のプレミアムでのみ販売。このフィールドを2%に設定すると、現在の市場価格よりも2%(またはそれ以上)高い価格のオファーのアラートのみを受け取るようになります。 +account.notifications.marketAlert.trigger.prompt=市場価格からの乖離の割合(例:2.50%、-0.50%など) +account.notifications.marketAlert.addButton=オファーアラートを追加 +account.notifications.marketAlert.manageAlertsButton=オファーアラートを管理 +account.notifications.marketAlert.manageAlerts.title=オファーアラートを管理 +account.notifications.marketAlert.manageAlerts.header.paymentAccount=支払いアカウント +account.notifications.marketAlert.manageAlerts.header.trigger=価格トリガー +account.notifications.marketAlert.manageAlerts.header.offerType=オファーの種類 +account.notifications.marketAlert.message.title=オファーのアラート +account.notifications.marketAlert.message.msg.below=以下 +account.notifications.marketAlert.message.msg.above=以上 +account.notifications.marketAlert.message.msg=新しい「{0} {1}」オファーが{2} ({3} {4}市場価格)の価格、支払い方法[{5}」でBisqオファーブックに発行されました。\nオファーID: {6}。 +account.notifications.priceAlert.message.title={0}で価格アラート +account.notifications.priceAlert.message.msg=価格アラートがトリガーされました。現在の{0}価格は{1} {2}です +account.notifications.noWebCamFound.warning=ウェブカメラが見つかりません。\n\n電子メールオプションを使用して、トークンと暗号化キーを携帯電話からBisqアプリケーションに送信してください。 +account.notifications.priceAlert.warning.highPriceTooLow=最高価格は最低価格よりも高い必要があります +account.notifications.priceAlert.warning.lowerPriceTooHigh=最低価格は最高価格よりも低い必要があります + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=正確な詳細 +dao.tab.bsqWallet=BSQウォレット +dao.tab.proposals=ガバナンス +dao.tab.bonding=担保 +dao.tab.proofOfBurn=アセットの上場手数料/プルーフ・オブ・バーン +dao.tab.monitor=ネットワークモニタ +dao.tab.news=ニュース + +dao.paidWithBsq=BSQで支払いました +dao.availableBsqBalance=支払い利用可能額(検証済+未承認の両替アウトプット) +dao.verifiedBsqBalance=検証された全UTXOの残高 +dao.unconfirmedChangeBalance=未承認の全両替アウトプットの残高 +dao.unverifiedBsqBalance=未検証の全トランザクション残高(ブロック承認待ち) +dao.lockedForVoteBalance=投票に使用済 +dao.lockedInBonds=ロック中の担保 +dao.availableNonBsqBalance=利用可能な非BSQ残高 (BTC) +dao.reputationBalance=メリット値(支出できません) + +dao.tx.published.success=トランザクションの発行に成功しました +dao.proposal.menuItem.make=提案する +dao.proposal.menuItem.browse=全ての提案を見る +dao.proposal.menuItem.vote=提案に投票する +dao.proposal.menuItem.result=投票結果 +dao.cycle.headline=投票サイクル +dao.cycle.overview.headline=投票サイクル概要 +dao.cycle.currentPhase=現在のフェーズ +dao.cycle.currentBlockHeight=現在のブロックの高さ +dao.cycle.proposal=提案フェーズ +dao.cycle.proposal.next=次の提案フェーズ +dao.cycle.blindVote=秘密投票フェーズ +dao.cycle.voteReveal=投票公開フェーズ +dao.cycle.voteResult=投票結果 +dao.cycle.phaseDuration={0} ブロック (≈{1}); ブロック {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=ブロック {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=発行された公開トランザクションに投票する +dao.voteReveal.txPublished=トランザクションID {0}の公開投票トランザクションが正常に発行されました。\n\nこれは、DAO投票に参加している場合、ソフトウェアによって自動的に行われます。 + +dao.results.cycles.header=サイクル +dao.results.cycles.table.header.cycle=サイクル +dao.results.cycles.table.header.numProposals=提案 +dao.results.cycles.table.header.voteWeight=投票の重み +dao.results.cycles.table.header.issuance=発行 + +dao.results.results.table.item.cycle=サイクル {0} 開始: {1} + +dao.results.proposals.header=選択されたサイクルの提案 +dao.results.proposals.table.header.nameLink=名前/リンク +dao.results.proposals.table.header.details=詳細 +dao.results.proposals.table.header.myVote=自分の投票 +dao.results.proposals.table.header.result=投票結果 +dao.results.proposals.table.header.threshold=閾値 +dao.results.proposals.table.header.quorum=定足数 + +dao.results.proposals.voting.detail.header=選択された提案の投票結果 + +dao.results.exceptions=投票結果の例外 + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=未定義 + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQメイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQテイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=最小BSQメイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=最小BSQテイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTCメイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTCテイカー手数料 +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=最小BTCメイカー手数料 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=最小BTCテイカー手数料 +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=BSQの提案手数料 +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=BSQの投票手数料 + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=補償リクエストの最小BSQ量 +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=補償リクエストの最大BSQ量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=払い戻しリクエストの最小BSQ量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=払い戻しリクエストの最大BSQ量 + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=一般的な提案に必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=補償リクエストに必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=払い戻しリクエストに必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=パラメーター変更に必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=アセット削除に必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=没収リクエストに必要なBSQの定足数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=担保された役割に必要なBSQの定足数 + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=一般的な提案に必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=補償リクエストに必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=払い戻しリクエストに必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=パラメーターの変更に必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=アセットを削除するために必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=没収リクエストのために必要なしきい値(%) +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=担保された役割のリクエストに必要なしきい値(%) + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=受信者BTCアドレス + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=1日あたりのアセットの上場手数料 +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=アセットの最小トレード額 + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=代替トレード支払いtxのロック時間 +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=BTCでの調停手数料 + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=BTCでの最大トレード制限 + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=BSQの担保された役割単位係数 +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=BSQのサイクルごとの発行制限 + +dao.param.currentValue=現在の値: {0} +dao.param.currentAndPastValue=現在の値: {0} (提案した時の値: {1} ) +dao.param.blocks={0} ブロック + +dao.results.invalidVotes=その投票サイクルで無効な投票がありました。投票がBisqネットワークでうまく配布されなかった場合、それが起こる可能性があります。\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=提案フェーズ +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=ブレーク1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=秘密投票フェーズ +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=ブレーク2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=投票公開フェーズ +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=ブレーク3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=結果フェーズ + +dao.results.votes.table.header.stakeAndMerit=投票の重み +dao.results.votes.table.header.stake=ステーク +dao.results.votes.table.header.merit=獲得済 +dao.results.votes.table.header.vote=投票 + +dao.bond.menuItem.bondedRoles=担保された役割 +dao.bond.menuItem.reputation=担保された評判 +dao.bond.menuItem.bonds=担保 + +dao.bond.dashboard.bondsHeadline=担保のBSQ +dao.bond.dashboard.lockupAmount=ロックされた資金 +dao.bond.dashboard.unlockingAmount=アンロック中の資金(ロック時間が終わるまで待ってください) + + +dao.bond.reputation.header=評判の為に担保をロック +dao.bond.reputation.table.header=評判担保 +dao.bond.reputation.amount=ロックするBSQ額 +dao.bond.reputation.time=ブロック単位のアンロック時間 +dao.bond.reputation.salt=ソルト +dao.bond.reputation.hash=ハッシュ +dao.bond.reputation.lockupButton=ロック +dao.bond.reputation.lockup.headline=トランザクションのロックを承認 +dao.bond.reputation.lockup.details=ロックされる金額: {0}\nアンロック時間: {1}ブロック(≈{2})\n\nマイニング手数料: {3} ({4} Satoshis/vbyte)\nトランザクションvサイズ: {5} Kb\n\n続行してよろしいですか? +dao.bond.reputation.unlock.headline=トランザクションのアンロックを承認 +dao.bond.reputation.unlock.details=アンロックされる金額: {0}\nアンロック時間: {1}ブロック(≈{2})\n\nマイニング手数料: {3} ({4} Satoshis/vbyte)\nトランザクションvサイズ: {5} Kb\n\n続行してよろしいですか? + +dao.bond.allBonds.header=すべての担保 + +dao.bond.bondedReputation=担保された評判 +dao.bond.bondedRoles=担保された役割 + +dao.bond.details.header=役割の詳細 +dao.bond.details.role=役割 +dao.bond.details.requiredBond=必要なBSQ担保 +dao.bond.details.unlockTime=ブロック単位のアンロック時間 +dao.bond.details.link=役割の説明へのリンク +dao.bond.details.isSingleton=複数の役割保有者が取得できます +dao.bond.details.blocks={0} ブロック + +dao.bond.table.column.name=名前 +dao.bond.table.column.link=リンク +dao.bond.table.column.bondType=担保タイプ +dao.bond.table.column.details=詳細 +dao.bond.table.column.lockupTxId=ロック Tx ID +dao.bond.table.column.bondState=担保状態 +dao.bond.table.column.lockTime=アンロック時間 +dao.bond.table.column.lockupDate=ロック日 + +dao.bond.table.button.lockup=ロック +dao.bond.table.button.unlock=ロック解除 +dao.bond.table.button.revoke=取り消し + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=まだ担保されていない +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=ロック保留中 +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=ロック中の担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=アンロック保留中 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=アンロックTXが承認されました +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=アンロック中の担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=アンロックされた担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=没収された担保 + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=担保された役割 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=担保された評判 + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=GitHub管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=フォーラム管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisqの維持者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-forkの維持者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayerの維持者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=ウェブサイト運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=フォーラム運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=シードノード運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=価格ノード運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=ビットコインノード運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=市場運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=エクスプローラー運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=モバイル通知中継の運営者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=ドメインネーム保有者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS管理人 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=調停者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=調停人 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC寄付アドレスの所有者 + +dao.burnBsq.assetFee=アセットリスティング +dao.burnBsq.menuItem.assetFee=アセットの上場手数料 +dao.burnBsq.menuItem.proofOfBurn=プルーフ・オブ・バーン +dao.burnBsq.header=アセット上場の為の手数料 +dao.burnBsq.selectAsset=アセット選択 +dao.burnBsq.fee=手数料 +dao.burnBsq.trialPeriod=試用期間 +dao.burnBsq.payFee=手数料の支払い +dao.burnBsq.allAssets=全てのアセット +dao.burnBsq.assets.nameAndCode=アセット名 +dao.burnBsq.assets.state=状態 +dao.burnBsq.assets.tradeVolume=取引量 +dao.burnBsq.assets.lookBackPeriod=検証期間 +dao.burnBsq.assets.trialFee=試用期間の手数料 +dao.burnBsq.assets.totalFee=合計支払い手数料 +dao.burnBsq.assets.days={0}日 +dao.burnBsq.assets.toFewDays=アセット手数料が低すぎます。試用期間の最小日数は{0}。 + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=試用期間中 +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=アクティブトレード +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=非アクティブのためリストから除外 +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=投票により削除 + +dao.proofOfBurn.header=プルーフ・オブ・バーン +dao.proofOfBurn.amount=金額 +dao.proofOfBurn.preImage=プリイメージ +dao.proofOfBurn.burn=バーン +dao.proofOfBurn.allTxs=全プルーフ・オブ・バーントランザクション +dao.proofOfBurn.myItems=プルーフ・オブ・バーンのトランザクション +dao.proofOfBurn.date=日付 +dao.proofOfBurn.hash=ハッシュ +dao.proofOfBurn.txs=トランザクション +dao.proofOfBurn.pubKey=パブリックキー +dao.proofOfBurn.signature.window.title=バーントランザクションの証明からのキーでメッセージに署名 +dao.proofOfBurn.verify.window.title=バーントランザクションの証明からのキーでメッセージを検証 +dao.proofOfBurn.copySig=署名をクリップボードにコピーする +dao.proofOfBurn.sign=署名する +dao.proofOfBurn.message=メッセージ +dao.proofOfBurn.sig=署名 +dao.proofOfBurn.verify=検証する +dao.proofOfBurn.verificationResult.ok=検証成功 +dao.proofOfBurn.verificationResult.failed=検証失敗 + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=提案フェーズ +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=秘密投票フェーズの前に中断 +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=秘密投票フェーズ +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=投票公開フェーズ前に中断 +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=投票公開フェーズ +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=結果フェーズの前に中断 +# suppress inspection "UnusedProperty" +dao.phase.RESULT=投票結果フェーズ + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=提案フェーズ +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=秘密投票 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=投票公開 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=投票結果 + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=報酬リクエスト +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=払い戻しリクエスト +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=担保された役割の提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=アセット削除の提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=パラメーターの変更の提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=一般的な提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=担保没収の提案 + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=報酬リクエスト +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=払い戻しリクエスト +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=担保された役割 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=アルトコインを取り除く +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=パラメーターの変更 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=一般的な提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=担保を没収 + +dao.proposal.details=提案の詳細 +dao.proposal.selectedProposal=選択された提案 +dao.proposal.active.header=現在のサイクルの提案 +dao.proposal.active.remove.confirm=その提案を削除してもよろしいですか?\n既に支払われた提案手数料は失われます。 +dao.proposal.active.remove.doRemove=はい、提案を削除します +dao.proposal.active.remove.failed=提案を削除できませんでした +dao.proposal.myVote.title=投票中 +dao.proposal.myVote.accept=提案を承認 +dao.proposal.myVote.reject=提案を拒否 +dao.proposal.myVote.removeMyVote=提案を無視 +dao.proposal.myVote.merit=獲得したBSQからの投票の重み +dao.proposal.myVote.stake=ステークからの投票の重み +dao.proposal.myVote.revealTxId=公開されたトランザクションIDへ投票 +dao.proposal.myVote.stake.prompt=投票に利用可能な最大ステーク: {0} +dao.proposal.votes.header=投票のステークをセットし、投票を発行する +dao.proposal.myVote.button=投票公開 +dao.proposal.myVote.setStake.description=すべての提案に投票した後、BSQをロックして投票のステークを設定する必要があります。ロックするBSQが多いほど、投票の重みが大きくなります。\n\n投票のためにロックされたBSQは、投票公開フェーズ中に再びロック解除されます。 +dao.proposal.create.selectProposalType=提案タイプの選択 +dao.proposal.create.phase.inactive=次の提案フェーズまでお待ち下さい +dao.proposal.create.proposalType=提案タイプ +dao.proposal.create.new=新しい提案を作成 +dao.proposal.create.button=提案する +dao.proposal.create.publish=提案の発行 +dao.proposal.create.publishing=提案の発行が進行中です... +dao.proposal=提案 +dao.proposal.display.type=提案タイプ +dao.proposal.display.name=正確なGithubのユーザ名 +dao.proposal.display.link=詳細情報へのリンク +dao.proposal.display.link.prompt=提案へのリンク +dao.proposal.display.requestedBsq=BSQのリクエスト額 +dao.proposal.display.txId=提案トランザクションID +dao.proposal.display.proposalFee=提案手数料 +dao.proposal.display.myVote=自分の投票 +dao.proposal.display.voteResult=投票結果の概要 +dao.proposal.display.bondedRoleComboBox.label=担保された役割のタイプ +dao.proposal.display.requiredBondForRole.label=役割に必要な担保 +dao.proposal.display.option=オプション + +dao.proposal.table.header.proposalType=提案タイプ +dao.proposal.table.header.link=リンク +dao.proposal.table.header.myVote=自分の投票 +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=取り消す +dao.proposal.table.icon.tooltip.removeProposal=自分の提案を削除 +dao.proposal.table.icon.tooltip.changeVote=現在の投票:「{0}」。 次の投票へ変更:「{1}」 + +dao.proposal.display.myVote.accepted=承認 +dao.proposal.display.myVote.rejected=拒否 +dao.proposal.display.myVote.ignored=無視 +dao.proposal.display.myVote.unCounted=票は結果に含められませんでした +dao.proposal.myVote.summary=投票済: {0}; 投票の重さ: {1} (獲得済み: {2} + ステーク: {3}) {4} +dao.proposal.myVote.invalid=投票が無効でした + +dao.proposal.voteResult.success=承認 +dao.proposal.voteResult.failed=拒否 +dao.proposal.voteResult.summary=結果: {0}; しきい値: {1} (必要量 > {2}); 定足数: {3} (必要量 > {4}) + +dao.proposal.display.paramComboBox.label=変更するパラメーターを選択 +dao.proposal.display.paramValue=パラメーター値 + +dao.proposal.display.confiscateBondComboBox.label=担保を選択 +dao.proposal.display.assetComboBox.label=削除するアセット + +dao.blindVote=秘密投票 + +dao.blindVote.startPublishing=秘密投票トランザクションを発行中... +dao.blindVote.success=秘密投票トランザクションが正常に発行されました。\n\nBisqアプリケーションが投票公開トランザクションを発行できるように、投票公開フェーズではオンラインにする必要があることに注意してください。投票公開トランザクションがなければあなたの投票は無効となります! + +dao.wallet.menuItem.send=送金 +dao.wallet.menuItem.receive=受け取る +dao.wallet.menuItem.transactions=トランザクション + +dao.wallet.dashboard.myBalance=ウォレット残高 + +dao.wallet.receive.fundYourWallet=あなたのBSQ受信アドレス +dao.wallet.receive.bsqAddress=BSQウォレットアドレス(未使用の新しいアドレス) + +dao.wallet.send.sendFunds=送金する +dao.wallet.send.sendBtcFunds=非BSQ残高の送金 (BTC) +dao.wallet.send.amount=BSQの額 +dao.wallet.send.btcAmount=BTCの額(非BSQ残高) +dao.wallet.send.setAmount=出金額を設定(最少額は{0}) +dao.wallet.send.receiverAddress=受信者のBSQアドレス +dao.wallet.send.receiverBtcAddress=受信者のBTCアドレス +dao.wallet.send.setDestinationAddress=あなたの出金先アドレスを記入 +dao.wallet.send.send=BSQ残高の送信 +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=BTC残高の送信 +dao.wallet.send.sendFunds.headline=出金リクエストを承認 +dao.wallet.send.sendFunds.details=送金中: {0}\n入金先アドレス: {1}\n必要なマイニング手数料: {2} ({3} サトシ/vbyte)\nトランザクションvサイズ: {4} vKb\n\n入金先の受け取る金額: {5}\n\n本当にこの金額を出金しますか? +dao.wallet.chainHeightSynced=最新検証済みブロック: {0} +dao.wallet.chainHeightSyncing=ブロック待機中... {1}のうち{0}ブロックを検証済み +dao.wallet.tx.type=タイプ + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=認識されていません +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=未検証のBSQトランザクション +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=無効なBSQのトランザクション +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=ジェネシストランザクション +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=BSQ送金 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=受信済BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=送信済BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=トレード手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=補償リクエストの手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=没収リクエストの手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=提案の手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=秘密投票の手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=投票公開 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=担保をロック +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=担保をアンロック +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=アセットの上場手数料 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=プルーフ・オブ・バーン +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=不整 + +dao.tx.withdrawnFromWallet=ウォレットから出金されたBTC +dao.tx.issuanceFromCompReq=補償 リクエスト/発行 +dao.tx.issuanceFromCompReq.tooltip=新しいBSQの発行につながる補償リクエスト。\n発行日: {0} +dao.tx.issuanceFromReimbursement=払い戻し リクエスト/発行 +dao.tx.issuanceFromReimbursement.tooltip=新しいBSQの発行につながる没収リクエスト。\n発行日: {0} +dao.proposal.create.missingBsqFunds=提案を作成するのに十分なBSQ残高がありません。未承認のBSQトランザクションがある場合、ブロックに含まれている場合にのみBSQが検証されるため、ブロックチェーンの承認を待つ必要があります。 \n不足: {0} + +dao.proposal.create.missingBsqFundsForBond=この役割に十分なBSQ残高がありません。この提案を発行することはできますが、この役割が受け入れられた場合、この役割に必要なBSQ全額が必要です。\n不足: {0} + +dao.proposal.create.missingMinerFeeFunds=提案トランザクションを作成するための十分なBTC残高がありません。すべてのBSQトランザクションには、BTCのマイナニング手数料が必要です。\n不足: {0} + +dao.proposal.create.missingIssuanceFunds=提案トランザクションを作成するための十分なBTC残高がありません。すべてのBSQトランザクションにはBTCのマイニング手数料が必要で、発行トランザクションにも要求されたBSQ金額({0} Satoshis/BSQ)のBTCが必要です。\n不足: {1} + +dao.feeTx.confirm={0}トランザクションの承認 +dao.feeTx.confirm.details={0}手数料: {1}\nマイニング手数料: {2} ({3} Satoshis/vbyte)\nトランザクションvサイズ {4} vKb\n\n本当に{5}トランザクションを発行しますか? + +dao.feeTx.issuanceProposal.confirm.details={0}手数料: {1}\nBSQ発行に必要なBTC: {2}({3} Satoshis/BSQ)\nマイニング手数料: {4} ({5} Satoshis/vbyte)\nトランザクションvサイズ {6} vKb\n\nリクエストが承認されると、あなたは2 BSQの提案料金を差し引いた金額を受け取ります。\n\n本当に{7}トランザクションを発行しますか? + +dao.news.bisqDAO.title=THE BISQ DAO +dao.news.bisqDAO.description=Bisqの交換が非中央集権化され検閲に抵抗するように、そのガバナンスモデルも、つまりBisq DAOとBSQトークンはそれを可能にするツールです。 +dao.news.bisqDAO.readMoreLink=Bisq DAOについてもっと詳しく知る + +dao.news.pastContribution.title=過去に貢献しましたか?BSQをリクエストしましょう +dao.news.pastContribution.description=Bisqに貢献している場合は、以下のBSQアドレスを使用して、BSQジェネシス配布に参加するようにリクエストしてください。 +dao.news.pastContribution.yourAddress=あなたのBSQウォレットアドレス +dao.news.pastContribution.requestNow=今すぐリクエスト + +dao.news.DAOOnTestnet.title=私達のテストネットでBISQ DAOを起動 +dao.news.DAOOnTestnet.description=メインネットBisq DAOはまだ起動されていませんが、私達のテストネットで実行することでBisq DAOについて学ぶことができます。 +dao.news.DAOOnTestnet.firstSection.title=1. DAOテストネットモードに切り替え +dao.news.DAOOnTestnet.firstSection.content=設定画面からDAOテストネットへ切り替え +dao.news.DAOOnTestnet.secondSection.title=2. BSQを取得する +dao.news.DAOOnTestnet.secondSection.content=SlackでBSQをリクエストするか、BisqでBSQを購入してください。 +dao.news.DAOOnTestnet.thirdSection.title=3. 投票サイクルに参加する +dao.news.DAOOnTestnet.thirdSection.content=Bisqのさまざまな側面を変更する提案を作成したり、提案に投票したりする。 +dao.news.DAOOnTestnet.fourthSection.title=4. BSQブロックエクスプローラーを使ってみる +dao.news.DAOOnTestnet.fourthSection.content=BSQはビットコインであるため、ビットコインブロックエクスプローラーでBSQトランザクションを確認できます。 +dao.news.DAOOnTestnet.readMoreLink=完全なドキュメントを読む + +dao.monitor.daoState=DAO状態 +dao.monitor.proposals=提案の状態 +dao.monitor.blindVotes=秘密投票の状態 + +dao.monitor.table.peers=ピア +dao.monitor.table.conflicts=競合 +dao.monitor.state=ステータス +dao.monitor.requestAlHashes=全ハッシュのリクエスト +dao.monitor.resync=DAO状態の再同期 +dao.monitor.table.header.cycleBlockHeight=サイクル / ブロックの高さ +dao.monitor.table.cycleBlockHeight=サイクル {0} / ブロック {1} +dao.monitor.table.seedPeers=シードノード: {0} + +dao.monitor.daoState.headline=DAO状態 +dao.monitor.daoState.table.headline=DAO状態ハッシュのチェーン +dao.monitor.daoState.table.blockHeight=ブロックの高さ +dao.monitor.daoState.table.hash=DAO状態のハッシュ +dao.monitor.daoState.table.prev=前のハッシュ +dao.monitor.daoState.conflictTable.headline=競合するピアからのDAO状態ハッシュ +dao.monitor.daoState.utxoConflicts=UTXOの競合 +dao.monitor.daoState.utxoConflicts.blockHeight=ブロックの高さ: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=全UTXOの合計: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=全BSQの合計: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAOステートはネットワークと同期していません。再起動したらDAOステートは再同期します。 + +dao.monitor.proposal.headline=提案の状態 +dao.monitor.proposal.table.headline=提案状態のハッシュのチェーン +dao.monitor.proposal.conflictTable.headline=競合するピアからの提案状態ハッシュ + +dao.monitor.proposal.table.hash=提案状態のハッシュ +dao.monitor.proposal.table.prev=前のハッシュ +dao.monitor.proposal.table.numProposals=提案数 + +dao.monitor.isInConflictWithSeedNode=ローカルデータが、少なくとも1つのシードノードと一致していません。 DAO状態を再同期してください。 +dao.monitor.isInConflictWithNonSeedNode=ピアの1つがネットワークと一致していませんが、あなたのノードはシードノードと同期しています。 +dao.monitor.daoStateInSync=ローカルノードはネットワークと一致しています + +dao.monitor.blindVote.headline=秘密投票の状態 +dao.monitor.blindVote.table.headline=秘密投票状態ハッシュのチェーン +dao.monitor.blindVote.conflictTable.headline=競合するピアからの秘密投票状態ハッシュ +dao.monitor.blindVote.table.hash=秘密投票状態のハッシュ +dao.monitor.blindVote.table.prev=前のハッシュ +dao.monitor.blindVote.table.numBlindVotes=秘密投票数 + +dao.factsAndFigures.menuItem.supply=BSQ 供給 +dao.factsAndFigures.menuItem.transactions=BSQ トランザクション + +dao.factsAndFigures.dashboard.avgPrice90=90日間の平均BSQ/BTCのトレード価格 +dao.factsAndFigures.dashboard.avgPrice30=30日間の平均BSQ/BTCのトレード価格 +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=合計利用可能BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=発行されたBSQ v. バーンされたBSQ + +dao.factsAndFigures.supply.issued=発行されたBSQ +dao.factsAndFigures.supply.compReq=Compensation requests +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=ジェネシストランザクションで発行されたBSQ +dao.factsAndFigures.supply.compRequestIssueAmount=報酬リクエストの為に発行されたBSQ +dao.factsAndFigures.supply.reimbursementAmount=払い戻しリクエストの為に発行されたBSQ +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=バーン済BSQ + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=取引量 +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=ロックされたBSQのグローバル状態 +dao.factsAndFigures.supply.totalLockedUpAmount=担保でロックされている +dao.factsAndFigures.supply.totalUnlockingAmount=担保からアンロック中のBSQ +dao.factsAndFigures.supply.totalUnlockedAmount=担保からアンロックされたBSQ +dao.factsAndFigures.supply.totalConfiscatedAmount=担保から没収されたBSQ +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=ジェネシストランザクション +dao.factsAndFigures.transactions.genesisBlockHeight=ジェネシスブロックの高さ +dao.factsAndFigures.transactions.genesisTxId=ジェネシストランザクションID +dao.factsAndFigures.transactions.txDetails=BSQトランザクション統計 +dao.factsAndFigures.transactions.allTx=全BSQトランザクション数 +dao.factsAndFigures.transactions.utxo=未支払の全トランザクションアウトプット数 +dao.factsAndFigures.transactions.compensationIssuanceTx=全ての補償リクエスト発行トランザクションの数 +dao.factsAndFigures.transactions.reimbursementIssuanceTx=全ての没収リクエスト発行トランザクションの数 +dao.factsAndFigures.transactions.burntTx=全ての手数料支払いトランザクションの数 +dao.factsAndFigures.transactions.invalidTx=全ての無効なトランザクションの数 +dao.factsAndFigures.transactions.irregularTx=不整なトランザクションの数 + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=利用可能残高 + +contractWindow.title=係争の詳細 +contractWindow.dates=オファーの日付 / トレードの日付 +contractWindow.btcAddresses=ビットコインアドレス BTC買い手 / BTC売り手 +contractWindow.onions=ネットワークアドレス BTC買い手 / BTC売り手 +contractWindow.accountAge=アカウント年齢 BTC買い手 / BTC売り手 +contractWindow.numDisputes=調停人の数 BTCの買い手 / BTCの売り手 +contractWindow.contractHash=契約ハッシュ + +displayAlertMessageWindow.headline=重要な情報! +displayAlertMessageWindow.update.headline=重要な更新情報! +displayAlertMessageWindow.update.download=ダウンロード: +displayUpdateDownloadWindow.downloadedFiles=ファイル: +displayUpdateDownloadWindow.downloadingFile=ダウンロード中: {0} +displayUpdateDownloadWindow.verifiedSigs=署名がキーで検証されました: +displayUpdateDownloadWindow.status.downloading=ファイルダウンロード中… +displayUpdateDownloadWindow.status.verifying=署名を検証中... +displayUpdateDownloadWindow.button.label=インストーラーをダウンロードして署名を検証 +displayUpdateDownloadWindow.button.downloadLater=後でダウンロード +displayUpdateDownloadWindow.button.ignoreDownload=このバージョンを無視 +displayUpdateDownloadWindow.headline=新しいBisqの更新が利用可能です! +displayUpdateDownloadWindow.download.failed.headline=ダウンロードに失敗 +displayUpdateDownloadWindow.download.failed=ダウンロード失敗。\n[HYPERLINK:https://bisq.network/downloads] から手動でダウンロード、確認してください。 +displayUpdateDownloadWindow.installer.failed=正しいインストーラーを判別できません。 [HYPERLINK:https://bisq.network/downloads] から手動でダウンロードして検証してください。 +displayUpdateDownloadWindow.verify.failed=検証失敗。\n[HYPERLINK:https://bisq.network/downloads] から手動でダウンロードして確認してください。 +displayUpdateDownloadWindow.success=新しいバージョンが正常にダウンロードされ、署名が検証されました。\n\nダウンロードディレクトリを開き、アプリケーションを終了して新しいバージョンをインストールしてください。 +displayUpdateDownloadWindow.download.openDir=ダウンロードフォルダを開く + +disputeSummaryWindow.title=概要 +disputeSummaryWindow.openDate=チケットオープン日 +disputeSummaryWindow.role=取引者の役割 +disputeSummaryWindow.payout=トレード金額の支払い +disputeSummaryWindow.payout.getsTradeAmount=BTC {0}はトレード金額の支払いを受け取ります +disputeSummaryWindow.payout.getsAll=BTCへの最高額支払い {0} +disputeSummaryWindow.payout.custom=任意の支払い +disputeSummaryWindow.payoutAmount.buyer=買い手の支払額 +disputeSummaryWindow.payoutAmount.seller=売り手の支払額 +disputeSummaryWindow.payoutAmount.invert=発行者として敗者を使用 +disputeSummaryWindow.reason=係争の理由 +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=バグ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=使いやすさ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=プロトコル違反 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=返信無し +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=詐欺 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=その他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=オプション・トレード +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=間違った送信者アカウント +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=ピアが遅れました +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=トレードはすでに決められました + +disputeSummaryWindow.summaryNotes=概要ノート +disputeSummaryWindow.addSummaryNotes=概要ノートを追加 +disputeSummaryWindow.close.button=チケットを閉じる + +# Do no change any line break or order of tokens as the structure is used for signature verification +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.msg=チケットは {0} に閉じられました\n{1} ノードアドレス: {2}\n\nまとめ\nトレードID: {3}\n通貨: {4}\nトレード金額: {5}\n買い手のBTC支払額: {6}\n売り手のBTC支払額 {7}\n\n係争の理由: {8}\n\n概要ノート:\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=\n次のステップ:\nトレードをオープンして、調停者からの提案を受け入れるまたは拒否する +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n次のステップ:\nこれ以上の行動が必要ありません。調停人があなたに有利に決める場合、「仲裁からの払い戻し」というトランザクションは「資金/トランザクション」に表示されます。 +disputeSummaryWindow.close.closePeer=取引相手のチケットも閉じる必要があります! +disputeSummaryWindow.close.txDetails.headline=払い戻しトランザクションを公開する +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=買い手が {0} を受けます、入金先アドレス: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=売り手が {0} を受けます、入金先アドレス: {1}\n +disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}({4}サトシ/vバイト)\nトランザクションvサイズ: {5} vKb\n\nこのトランザクションを発行してもよろしいですか? + +disputeSummaryWindow.close.noPayout.headline=支払いなしで閉じる +disputeSummaryWindow.close.noPayout.text=支払いなしで閉じてもよろしいですか? + +emptyWalletWindow.headline={0} 緊急ウォレットツール +emptyWalletWindow.info=UIから資金にアクセスできない緊急時にのみ使用してください。\n\nこのツールを使用すると、開いているオファーはすべて自動的に閉じられることに注意してください。\n\nこのツールを使用する前に、データディレクトリをバックアップしてください。これは「アカウント/バックアップ」で行えます。\n\n問題の原因を調査できるように、問題を報告し、GitHubまたはBisqフォーラムにバグレポートを提出してください。 +emptyWalletWindow.balance=あなたの利用可能なウォレット残高 +emptyWalletWindow.bsq.btcBalance=非BSQ Satoshisの残高 + +emptyWalletWindow.address=あなたの宛先アドレス +emptyWalletWindow.button=全ての資金を送る +emptyWalletWindow.openOffers.warn=ウォレット空にすると削除されるオープンオファーがあります。 \n本当にウォレットを空にしますか? +emptyWalletWindow.openOffers.yes=はい、そうです +emptyWalletWindow.sent.success=あなたのウォレットの残高は正常に送金されました。 + +enterPrivKeyWindow.headline=登録のためにプライベートキーを入力 + +filterWindow.headline=フィルターリストを編集 +filterWindow.offers=フィルター済オファー(コンマ区切り) +filterWindow.onions=トレードアドレスから追放された(コンマ区切り) +filterWindow.bannedFromNetwork=ネットワークアドレスから追放された(コンマ区切り) +filterWindow.accounts=フィルター済トレードアカウントデータ:\n形式: コンマ区切りのリスト [支払方法id | データフィールド | 値] +filterWindow.bannedCurrencies=フィルター済通貨コード(コンマ区切り) +filterWindow.bannedPaymentMethods=フィルター済支払方法ID(コンマ区切り) +filterWindow.bannedAccountWitnessSignerPubKeys=フィルター済アカウントWitness署名者パブリックキー(コンマ区切りパブリックキーの16進値) +filterWindow.bannedPrivilegedDevPubKeys=フィルター済特権的開発者パブリックキー(コンマ区切りパブリックキーの16進値) +filterWindow.arbitrators=フィルター済調停人(コンマ区切り onionアドレス) +filterWindow.mediators=フィルター済調停者(コンマ区切り onionアドレス) +filterWindow.refundAgents=フィルター済仲裁人(コンマ区切り onionアドレス) +filterWindow.seedNode=フィルター済シードノード(コンマ区切り onionアドレス) +filterWindow.priceRelayNode=フィルター済価格中継ノード(コンマ区切り onionアドレス) +filterWindow.btcNode=フィルター済ビットコインノード(コンマ区切り アドレス+ポート) +filterWindow.preventPublicBtcNetwork=パブリックビットコインネットワークの使用を防止 +filterWindow.disableDao=DAOを無効化 +filterWindow.disableAutoConf=自動確認を無効にする +filterWindow.autoConfExplorers=フィルター済自動確認エクスプローラ(コンマ区切りアドレス) +filterWindow.disableDaoBelowVersion=DAOに必要な最低バージョン +filterWindow.disableTradeBelowVersion=トレードに必要な最低バージョン +filterWindow.add=フィルターを追加 +filterWindow.remove=フィルターを削除 +filterWindow.btcFeeReceiverAddresses=BTC手数料受信アドレス +filterWindow.disableApi=APIを無効化 +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=最小のBTC金額 +offerDetailsWindow.min=(最小 {0}) +offerDetailsWindow.distance=(市場価格からの乖離: {0}) +offerDetailsWindow.myTradingAccount=私のトレードアカウント +offerDetailsWindow.offererBankId=(メイカーの銀行ID/BIC/SWIFT) +offerDetailsWindow.offerersBankName=(メイカーの銀行名) +offerDetailsWindow.bankId=銀行ID(例:BICまたはSWIFT) +offerDetailsWindow.countryBank=メイカーの銀行の国名 +offerDetailsWindow.commitment=約束 +offerDetailsWindow.agree=同意します +offerDetailsWindow.tac=取引条件 +offerDetailsWindow.confirm.maker=承認: ビットコインを{0}オファーを出す +offerDetailsWindow.confirm.taker=承認: ビットコインを{0}オファーを受ける +offerDetailsWindow.creationDate=作成日 +offerDetailsWindow.makersOnion=メイカーのonionアドレス + +qRCodeWindow.headline=QRコード +qRCodeWindow.msg=外部ウォレットからBisqウォレットへ送金するのに、このQRコードを利用して下さい。 +qRCodeWindow.request=支払いリクエスト:\n{0} + +selectDepositTxWindow.headline=係争の為のデポジットトランザクションを選択 +selectDepositTxWindow.msg=このデポジットトランザクションはトレードに保存されませんでした。\n失敗したトレードで使用されたデポジットトランザクションであった、既存のマルチシグトランザクションのいずれかをウォレットから選択してください。\n\n正しいトランザクションを見つけるためには、トレード詳細ウィンドウを開き(リストのトレードIDをクリック)、マルチシグデポジットトランザクション(アドレスは3で始まります)が表示されている次のトランザクションへの、トレード手数料の支払いトランザクションアウトプットをたどってください。そのトランザクションIDは、ここに表示されているリストに見つかるはずです。正しいトランザクションが見つかったら、ここでそのトランザクションを選択して続行します。\n\nご不便をおかけして申し訳ありませんが、そのエラーのケースはごくまれにしか発生しません。今後、より良い解決方法を探します。 +selectDepositTxWindow.select=デポジットトランザクションを選択 + +sendAlertMessageWindow.headline=グローバル通知を送信 +sendAlertMessageWindow.alertMsg=警告メッセージ +sendAlertMessageWindow.enterMsg=メッセージを入力 +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=新バージョンナンバー +sendAlertMessageWindow.send=通知を送信 +sendAlertMessageWindow.remove=通知を削除 + +sendPrivateNotificationWindow.headline=プライベートメッセージを送信 +sendPrivateNotificationWindow.privateNotification=プライベート通知 +sendPrivateNotificationWindow.enterNotification=通知を入力 +sendPrivateNotificationWindow.send=プライベート通知を送信 + +showWalletDataWindow.walletData=ウォレットデータ +showWalletDataWindow.includePrivKeys=プライベートキーを含む + +setXMRTxKeyWindow.headline=XMR送金を証明 +setXMRTxKeyWindow.note=以下にtx情報を追加すると、より早いトレードのため自動確認を有効にします。詳しくは:https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=トランザクションID(任意) +setXMRTxKeyWindow.txKey=トランザクション・キー(任意) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=ユーザー規約 +tacWindow.agree=同意します +tacWindow.disagree=同意せずにに終了 +tacWindow.arbitrationSystem=紛争解決 + +tradeDetailsWindow.headline=トレード +tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID: +tradeDetailsWindow.tradeDate=取引日 +tradeDetailsWindow.txFee=マイニング手数料 +tradeDetailsWindow.tradingPeersOnion=トレード相手のonionアドレス +tradeDetailsWindow.tradingPeersPubKeyHash=トレードピアのパブリックキーハッシュ +tradeDetailsWindow.tradeState=トレード状態 +tradeDetailsWindow.agentAddresses=仲裁者 / 調停人 +tradeDetailsWindow.detailData=詳細データ + +txDetailsWindow.headline=トランザクション詳細 +txDetailsWindow.btc.note=BTCを送金しました。 +txDetailsWindow.bsq.note=BSQ残高を送信しました。 BSQはカラードビットコインなので、ビットコインのブロックで承認されるまでトランザクションはBSQエクスプローラに表示されません。 +txDetailsWindow.sentTo=送信先 +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=アンロックするためにパスワードを入力してください + +torNetworkSettingWindow.header=Torネットワークの設定 +torNetworkSettingWindow.noBridges=ブリッジを使わない +torNetworkSettingWindow.providedBridges=提供されているブリッジと接続 +torNetworkSettingWindow.customBridges=カスタムブリッジを入力してください +torNetworkSettingWindow.transportType=転送タイプ +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (推奨) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=1つ以上のブリッジリレーを入力してください(1行あたり1つ) +torNetworkSettingWindow.enterBridgePrompt=アドレス:ポート番号を入力してください +torNetworkSettingWindow.restartInfo=変更を適用するために再起動してください +torNetworkSettingWindow.openTorWebPage=TorプロジェクトのWebページを開く +torNetworkSettingWindow.deleteFiles.header=接続に問題がありますか? +torNetworkSettingWindow.deleteFiles.info=起動時に接続の問題が繰り返される場合は、古いTorファイルを削除すると解決する可能性があります。そのためには、下のボタンをクリックしてから再起動してください。 +torNetworkSettingWindow.deleteFiles.button=Torの古いファイルを削除してシャットダウンする +torNetworkSettingWindow.deleteFiles.progress=Torをシャットダウン中 +torNetworkSettingWindow.deleteFiles.success=Torの古いファイルの削除に成功しました。再起動してください。 +torNetworkSettingWindow.bridges.header=Torはブロックされていますか? +torNetworkSettingWindow.bridges.info=Torがあなたのインターネットプロバイダや国にブロックされている場合、Torブリッジによる接続を試みることができます。\nTorのWebページ https://bridges.torproject.org/bridges にアクセスして、ブリッジとプラガブル転送について学べます + +feeOptionWindow.headline=取引手数料の支払いに使用する通貨を選択してください +feeOptionWindow.info=あなたは取引手数料の支払いにBSQまたはBTCを選択できます。 BSQを選択した場合は、割引された取引手数料に気付くでしょう。 +feeOptionWindow.optionsLabel=取引手数料の支払いに使用する通貨を選択してください +feeOptionWindow.useBTC=BTCを使用 +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=通知 +popup.headline.instruction=ご注意ください: +popup.headline.attention=注意 +popup.headline.backgroundInfo=バックグラウンド情報 +popup.headline.feedback=完了 +popup.headline.confirmation=承認 +popup.headline.information=情報 +popup.headline.warning=注意 +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「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるbisq.logファイル含めると、デバッグが容易になります。 + +popup.error.tryRestart=アプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 +popup.error.takeOfferRequestFailed=誰かがあなたのいずれかのオファーを受けようと時にエラーが発生しました:\n{0} + +error.spvFileCorrupted=SPVチェーンファイルの読み込み中にエラーが発生しました。\nSPVチェーンファイルが破損している可能性があります。\n\nエラーメッセージ: {0} \n\n削除して再同期を開始しますか? +error.deleteAddressEntryListFailed=AddressEntryListファイルを削除できませんでした。\nエラー: {0} +error.closedTradeWithUnconfirmedDepositTx=トレードID{0}で識別されるトレードのデポジットトランザクションはまだ承認されていません。\n\nトランザクションは有効かどうかを確認するため、\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。 +error.closedTradeWithNoDepositTx=トレードID{0}で識別されるトレードのデポジットトランザクションは無効とされました。\n\n閉じられたトレードリストを更新するため、アプリケーションを再起動して下さい。 + +popup.warning.walletNotInitialized=ウォレットはまだ初期化されていません +popup.warning.osxKeyLoggerWarning=macOS 10.14以上の厳しいセキュリティー対策のため、Javaアプリケーション(BisqはJavaを利用します)はmacOSで警告用のポップアップ・ウィンドウを生じます(「Bisqは他のアプリからキー操作をアクセスしたい」)。\n\nこの問題を解決するのに、macOS設定を開いて、「セキュリティとプライバシー -> プライバシー -> 入力監視」において右側のリストからBisqを外して下さい。\n\n技術的な限界は克服されたら(必要のJavaバージョンパッケージャーがまだリリースされていません)、問題を避けるためにBisqは新しいJavaバージョンにアップグレードします。 +popup.warning.wrongVersion=このコンピューターのBisqバージョンが間違っている可能性があります。\nコンピューターのアーキテクチャ: {0}\nインストールしたBisqバイナリ: {1}\nシャットダウンして、次の正しいバージョンを再インストールしてください({2})。 +popup.warning.incompatibleDB=互換性のないデータベースファイルが検出されました!\n\nこういうデータベースファイルは、現在のコードベースと互換性がありません:\n{0}\n\n破損したファイルのバックアップを作成し、デフォルト値を新しいデータベースバージョンに適用しました。\n\nバックアップは次の場所にあります。\n{1}/db/backup_of_corrupted_data.\n\nBisqの最新バージョンがインストールされているかどうかを確認してください。\n以下からダウンロードできます。\n[HYPERLINK:https://bisq.network/downloads]\n\nアプリケーションを再起動してください。 +popup.warning.startupFailed.twoInstances=Bisqは既に起動中です。Bisqを2つ起動することはできません。 +popup.warning.tradePeriod.halfReached=ID {0}とのトレードは許可された最大トレード期間の半分に達しましたが、まだ完了していません\n\n取引期間は{1}で終了します\n\n詳細については、「ポートフォリオ/オープントレード」でトレード状態を確認してください。 +popup.warning.tradePeriod.ended=ID {0}とのトレードは許可された最大トレード期間に達しましたが、まだ完了していません。\n\nトレード期間は{1}で終了しました\n\n調停者に連絡するには、「ポートフォリオ/オープントレード」であなたのトレードを確認してください。 +popup.warning.noTradingAccountSetup.headline=トレードアカウントが設定されていません +popup.warning.noTradingAccountSetup.msg=オファーを作成する前に、国内通貨またはアルトコインのアカウントを設定する必要があります。\nアカウントを設定しますか? +popup.warning.noArbitratorsAvailable=利用可能な調停人がいません。 +popup.warning.noMediatorsAvailable=利用可能な調停人がいません。 +popup.warning.notFullyConnected=ネットワークへ完全に接続するまで待つ必要があります。\n起動までに約2分かかります。 +popup.warning.notSufficientConnectionsToBtcNetwork=少なくとも{0}のビットコインネットワークへの接続が確立されるまでお待ちください。 +popup.warning.downloadNotComplete=欠落しているビットコインブロックのダウンロードが完了するまで待つ必要があります。 +popup.warning.chainNotSynced=Bisqウォレットのブロックチェーン高さは正しく同期されていません。アプリを最近起動した場合、1つのビットコインブロックが発行されるまで待って下さい。\n\nブロックチェーン高さは\"設定/ネットワーク情報\"に表示されます。 2つ以上のブロックが発行されても問題が解決されない場合、フリーズしている可能性があります。その場合には、SPV再同期を行って下さい [HYPERLINK: https://bisq.wiki/Resyncing_SPV_file ]。 +popup.warning.removeOffer=本当にオファーを削除しますか?\nオファーを削除する場合、{0}のメイカー手数料が失われます。 +popup.warning.tooLargePercentageValue=100%以上のパーセントを設定できません +popup.warning.examplePercentageValue=パーセントの数字を入力してください。5.4%は「5.4」のように入力します。 +popup.warning.noPriceFeedAvailable=その通貨で利用できる価格フィードはありません。パーセントベースの価格は使用できません。 固定価格を選択してください。 +popup.warning.sendMsgFailed=トレード相手へのメッセージの送信に失敗しました。\nもう一度試してください。失敗し続ける場合はバグを報告してください。 +popup.warning.insufficientBtcFundsForBsqTx=あなたはそのトランザクションのマイニング手数料を支払うのに十分なBTC残高を持っていません。 BTCウォレットに資金を入金してください。 \n不足残高: {0} +popup.warning.bsqChangeBelowDustException=このトランザクションは、ダスト制限(5.46 BSQ)を下回るBSQおつりアウトプットを作成し、ビットコインネットワークによって拒否されます。\n\nおつりアウトプットを回避するために、より高い金額を送信する必要があります(たとえば、送金額にダスト額を追加することによって)、またはダストアウトプットを生成しないようにウォレットにBSQ残高を追加する必要があります。\n\nダストアウトプットは{0}。 +popup.warning.btcChangeBelowDustException=このトランザクションは、ダスト制限(546 Satoshi)を下回るBSQおつりアウトプットを作成し、ビットコインネットワークによって拒否されます。\n\nダストアウトプットを生成しないように、あなたの送金額にダスト額を追加する必要があります。\n\nダストアウトプットは{0}。 + +popup.warning.insufficientBsqFundsForBtcFeePayment=このトランザクションにはBSQが足りません。ビットコインプロトコルのダスト制限によると、ウォレットから最後の5.46BSQはトレード手数料に使われることができません。\n\nもっとBSQを買うか、BTCでトレード手数料を支払うことができます。\n\n不足している資金: {0} +popup.warning.noBsqFundsForBtcFeePayment=BSQウォレットにBSQのトレード手数料を支払うのに十分な残高がありません。 +popup.warning.messageTooLong=メッセージが許容サイズ上限を超えています。いくつかに分けて送信するか、 https://pastebin.com のようなサービスにアップロードしてください。 +popup.warning.lockedUpFunds=失敗したトレードから残高をロックしました。\nロックされた残高: {0} \nデポジットtxアドレス: {1} \nトレードID: {2}。\n\nオープントレード画面でこのトレードを選択し、「alt + o」または「option + o」を押してサポートチケットを開いてください。 + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=無視して続ける + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned={0}ノードの1つが禁止されました。 +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=価格中継 +popup.warning.seed=シード +popup.warning.mandatoryUpdate.trading=最新のBisqバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Bisqフォーラムをご覧ください。 +popup.warning.mandatoryUpdate.dao=最新のBisqバージョンに更新してください。古いバージョンのBisq DAOとBSQを無効にする必須の更新プログラムがリリースされました。詳細については、Bisqフォーラムをご覧ください。 +popup.warning.disable.dao=Bisq DAOとBSQは一時的に無効になっています。詳細については、Bisqフォーラムをご覧ください。 +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するBTCがさらに蓄積されるまでお待ちください。 + +popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Bisq Keybaseチームのサポートチャンネルに連絡して下さい。 + +popup.warning.trade.txRejected.tradeFee=トレード手数料 +popup.warning.trade.txRejected.deposit=デポジット +popup.warning.trade.txRejected=ID{1}で識別されるトレードのための{0}トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {2} \nトレードは「失敗トレード」へ送られました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Bisq Keybaseチームのサポートチャンネルに連絡して下さい。 + +popup.warning.openOfferWithInvalidMakerFeeTx=ID{0}で識別されるオファーのためのメイカー手数料トランザクションが無効とされました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Bisq Keybaseチームのサポートチャンネルに連絡して下さい。 + +popup.info.securityDepositInfo=両方の取引者がトレードプロトコルに従うことを保証するために、両方のトレーダーはセキュリティデポジットを支払う必要があります。\n\nこのデポジットはあなたのトレードがうまく完了するまであなたのトレードウォレットに保管され、それからあなたに返金されます。\n\n注意してください:あなたが新しいオファーを作成しているなら、他の取引者がそれを受けるためにBisqを実行しておく必要があります。オファーをオンラインにしておくには、Bisqを実行したままにして、このコンピュータもオンラインにしたままにします(つまり、スタンバイモードに切り替わらないようにします…モニターのスタンバイは大丈夫です)。 + +popup.info.cashDepositInfo=あなたの地域の銀行支店が現金デポジットが作成できることを確認してください。\n売り手の銀行ID(BIC / SWIFT)は{0}です。 +popup.info.cashDepositInfo.confirm=デポジットを作成できるか確認します +popup.info.shutDownWithOpenOffers=Bisqはシャットダウン中ですが、オファーはあります。\n\nこれらのオファーは、Bisqがシャットダウンされている間はP2Pネットワークでは利用できませんが、次回Bisqを起動したときにP2Pネットワークに再公開されます。\n\nオファーをオンラインに保つには、Bisqを実行したままにして、このコンピュータもオンラインにしたままにします(つまり、スタンバイモードにならないようにしてください。モニタースタンバイは問題ありません)。 +popup.info.qubesOSSetupInfo=Qubes OS内でBisqを実行しているようです。\n\nBisqのqubeはセットアップガイドに従って設定されていることを確かめて下さい: [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes] +popup.warn.downGradePrevention=バージョン{0}からバージョン{1}に戻すことはサポートされていません。最新のBisqバージョンを利用して下さい。 +popup.warn.daoRequiresRestart=DAO状態の同期中に問題が発生しました。解決するにはアプリを再起動する必要があります。 + +popup.privateNotification.headline=重要なプライベート通知! + +popup.securityRecommendation.headline=重要なセキュリティ勧告 +popup.securityRecommendation.msg=ウォレットのパスワード保護をまだ有効にしてない場合は、使用することを検討してください。\n\nウォレットシードワードを書き留めることも強くお勧めします。 これらのシードワードは、あなたのビットコインウォレットを復元するためのマスターパスワードのようなものです。\n「ウォレットシード」セクションにてより詳細な情報を確認できます。\n\nまた、「バックアップ」セクションのアプリケーションデータフォルダ全体をバックアップするべきでしょう。 + +popup.bitcoinLocalhostNode.msg=Bisqはローカルで動作するビットコインコアノード(ローカルホストに)を検出しました。\n\n以下の点を確認して下さい:\n- Bisqを起動する前にノードは完全に同期されていること\n- プルーニングモードは無効にされること(bitcoin.confに'prune=0')\n- ブルームフィルターは有効にされること(bitcoin.confに'peerbloomfilters=1') + +popup.shutDownInProgress.headline=シャットダウン中 +popup.shutDownInProgress.msg=アプリケーションのシャットダウンには数秒かかることがあります。\nこのプロセスを中断しないでください。 + +popup.attention.forTradeWithId=ID {0}とのトレードには注意が必要です +popup.attention.reasonForPaymentRuleChange=バージョン1.5.5から、銀行振込の\"支払理由\"フィールドに関するトレードルールに非常に重要な変更があります。このフィールドを必ず空白にしておいて下さい -- いかなる場合でも、トレードIDを\"支払理由\"に入力しないで下さい。 + +popup.info.multiplePaymentAccounts.headline=複数の支払いアカウントが使用可能です +popup.info.multiplePaymentAccounts.msg=このオファーに使用できる支払いアカウントが複数あります。あなたが正しいものを選んだことを確認してください。 + +popup.accountSigning.selectAccounts.headline=支払いアカウントを選択 +popup.accountSigning.selectAccounts.description=支払い方法そして時点に基づいて、買い手への支払いが起こった係争と繋がっている全てのアカウントは署名されるように選択されます。 +popup.accountSigning.selectAccounts.signAll=全ての支払い方法を署名 +popup.accountSigning.selectAccounts.datePicker=アカウント署名のは終了する時点を選択して下さい。 + +popup.accountSigning.confirmSelectedAccounts.headline=選択された支払いアカウントを確認 +popup.accountSigning.confirmSelectedAccounts.description=入力に基づいて、{0}口の支払いアカウントは選択されます +popup.accountSigning.confirmSelectedAccounts.button=支払いアカウントを確認 +popup.accountSigning.signAccounts.headline=支払いアカウントの署名を確認 +popup.accountSigning.signAccounts.description=選択に基づいて、{0}口の支払いアカウントは署名されます +popup.accountSigning.signAccounts.button=支払いアカウントに署名 +popup.accountSigning.signAccounts.ECKey=プライベート調停人キーを入力 +popup.accountSigning.signAccounts.ECKey.error=不良の調停人ECKey + +popup.accountSigning.success.headline=おめでとう +popup.accountSigning.success.description=全{0}口の支払いアカウントは成功裏に署名されました! +popup.accountSigning.generalInformation=全てのアカウントの署名状態はアカウント画面に表示されます。\n\n詳しくは: [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing] +popup.accountSigning.signedByArbitrator=支払いアカウントの1つは調停人に検証、署名されました。このアカウントからトレードを行ったら、トレードピアと成功にトレードする後に相手のアカウントを自動的に署名します。\n\n{0} +popup.accountSigning.signedByPeer=支払いアカウントの1つはトレードピアに検証、署名されました。{0} 日後に、初期のトレード制限は解除され、他の支払いアカウントを署名できるようになります。\n\n{1} +popup.accountSigning.peerLimitLifted=支払いアカウントの1つにおいて初期の制限は解除されました。\n\n{0} +popup.accountSigning.peerSigner=支払いアカウントの1つは十分に熟成されて、初期の制限は解除されました。\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=署名されないアカウント年齢witnessをインポート +popup.accountSigning.confirmSingleAccount.headline=選択されたアカウント年齢witnessを確認 +popup.accountSigning.confirmSingleAccount.selectedHash=選択されたwitnessのハッシュ +popup.accountSigning.confirmSingleAccount.button=アカウント年齢witnessを署名 +popup.accountSigning.successSingleAccount.description=Witness {0} は署名された +popup.accountSigning.successSingleAccount.success.headline=成功 + +popup.accountSigning.unsignedPubKeys.headline=無署名のパブリックキー +popup.accountSigning.unsignedPubKeys.sign=パブリックキーを署名 +popup.accountSigning.unsignedPubKeys.signed=パブリックキーは署名されました +popup.accountSigning.unsignedPubKeys.result.signed=署名されたパブリックキー +popup.accountSigning.unsignedPubKeys.result.failed=署名が失敗しました + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=ID {0}とのトレードの通知 +notification.ticket.headline=ID {0}とのトレード用サポートチケット +notification.trade.completed=これでトレードは完了し、資金を出金することができます。 +notification.trade.accepted=あなたのオファーはBTC {0}によって承認されました。 +notification.trade.confirmed=あなたのトレードには少なくとも1つのブロックチェーン承認があります。\nあなたは今、支払いを始めることができます。 +notification.trade.paymentStarted=BTCの買い手が支払いを開始しました。 +notification.trade.selectTrade=取引を選択 +notification.trade.peerOpenedDispute=あなたの取引相手は{0}をオープンしました。 +notification.trade.disputeClosed={0}は閉じられました。 +notification.walletUpdate.headline=トレードウォレット更新 +notification.walletUpdate.msg=あなたのトレードウォレットは十分に入金されています。\n金額: {0} +notification.takeOffer.walletUpdate.msg=あなたのトレードウォレットは、以前のオファー受け入れの試みからすでに十分な資金を得ています。\n金額: {0} +notification.tradeCompleted.headline=取引完了 +notification.tradeCompleted.msg=あなたは今、外部のビットコインウォレットにあなたの資金を出金するか、それをBisqウォレットに送金することができます。 + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=アプリケーションウィンドウを表示 +systemTray.hide=アプリケーションウィンドウを隠す +systemTray.info=Bisqについての情報 +systemTray.exit=終了 +systemTray.tooltip=Bisq: 分散的ビットコイン取引ネットワーク + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=外部ウォレットで使用されるマイニング手数料が少なくとも{0} satoshis/vbyte あることを確認してください。 そうでなければ、このトレードトランザクションは承認されない可能性、トレードは係争に終わる可能性があります。 + +guiUtil.accountExport.savedToPath=取引アカウントを下記パスに保存しました:\n{0} +guiUtil.accountExport.noAccountSetup=取引アカウントのエクスポート設定がされていません +guiUtil.accountExport.selectPath={0}のパスを選択 +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=ID {0}の取引アカウント\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=ID {0}の取引アカウントは既に存在するためインポートしませんでした。\n +guiUtil.accountExport.exportFailed=エラーのため、CSVへのエクスポートに失敗しました。\nエラー= {0} +guiUtil.accountExport.selectExportPath=エクスポートパスを選択 +guiUtil.accountImport.imported=パスからインポートされた取引アカウント:\n{0}\n\nインポートされたアカウント:\n{1} +guiUtil.accountImport.noAccountsFound=エクスポートされた取引アカウントは次のパスに見つかりませんでした: {0}。\nファイル名は{1}です。" +guiUtil.openWebBrowser.warning=あなたのシステムウェブブラウザでWebページを開こうとしています。\n今すぐWebページを開きますか?\n\nあなたがデフォルトのシステムウェブブラウザとして「Torブラウザ」を使用していない場合は、クリアネットでWebページに接続します。\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Webページを開き、次回から確認しない +guiUtil.openWebBrowser.copyUrl=URLをコピーしてキャンセル +guiUtil.ofTradeAmount=取引額に対して +guiUtil.requiredMinimum=(必要な最低限) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=通貨を選択 +list.currency.showAll=全て表示する +list.currency.editList=通貨リストを編集する + +table.placeholder.noItems=現在利用可能な{0}がありません +table.placeholder.noData=現在利用可能なデータがありません +table.placeholder.processingData=データ処理中... + + +peerInfoIcon.tooltip.tradePeer=トレード相手 +peerInfoIcon.tooltip.maker=メイカーの +peerInfoIcon.tooltip.trade.traded={0} onionアドレス: {1}\n既にその相手と{2}回トレードしました\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} onionアドレス: {1}\nあなたは今までこの相手とトレードしていません\n{2} +peerInfoIcon.tooltip.age=支払いアカウントが{0}前に作成されました。 +peerInfoIcon.tooltip.unknownAge=支払いアカウントの年齢は不明です。 + +tooltip.openPopupForDetails=詳細についてのポップアップを開く +tooltip.invalidTradeState.warning=このトレードは無効な状態とされました。詳しくは詳細ウィンドウを開く +tooltip.openBlockchainForAddress=外部ブロックチェーンエクスプローラーで次のアドレスを開く: {0} +tooltip.openBlockchainForTx=外部ブロックチェーンエクスプローラーで次のトランザクションを開く: {0} + +confidence.unknown=不明なトランザクションステータス +confidence.seen={0} 人のピアに見られた / 0 承認 +confidence.confirmed={0}ブロックで承認済み。 +confidence.invalid=トランザクションが不正です + +peerInfo.title=ピア情報 +peerInfo.nrOfTrades=完了した取引数 +peerInfo.notTradedYet=あなたは今までそのユーザーと取引していません。 +peerInfo.setTag=そのピアにタグを設定する +peerInfo.age.noRisk=支払いアカウントの年齢 +peerInfo.age.chargeBackRisk=署名する後経過時間 +peerInfo.unknownAge=年齢不明 + +addressTextField.openWallet=既定のビットコインウォレットを開く +addressTextField.copyToClipboard=アドレスをクリップボードにコピー +addressTextField.addressCopiedToClipboard=アドレスをクリップボードにコピーしました +addressTextField.openWallet.failed=既定のビットコインウォレットが開けませんでした。何らかのビットコインウォレットはインストールされていますか? + +peerInfoIcon.tooltip={0}\nタグ: {1} + +txIdTextField.copyIcon.tooltip=トランザクションIDをクリップボードにコピー +txIdTextField.blockExplorerIcon.tooltip=このトランザクションIDをブロックチェーンエクスプローラで開く +txIdTextField.missingTx.warning.tooltip=必要なトランザクションは欠測 + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=「アカウント」 +navigation.account.walletSeed=「アカウント/ウォレットシード」 +navigation.funds.availableForWithdrawal=\"資金/送金する\" +navigation.portfolio.myOpenOffers=「ポートフォリオ/私の公開オファー +navigation.portfolio.pending=「ポートフォリオ/オープントレード」 +navigation.portfolio.closedTrades=「ポートフォリオ/履歴」 +navigation.funds.depositFunds=「資金/資金の受取」 +navigation.settings.preferences=「設定/設定」 +# suppress inspection "UnusedProperty" +navigation.funds.transactions=「資金/トランザクション」 +navigation.support=「サポート」 +navigation.dao.wallet.receive=「DAO/BSQウォレット/受取」 + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} 額{1} +formatter.makerTaker=メイカーは{0} {1} / テイカーは{2} {3} +formatter.youAreAsMaker=あなたは:{1} {0}(メイカー) / テイカーは:{3} {2} +formatter.youAreAsTaker=あなたは:{1} {0}(テイカー) / メイカーは{3} {2} +formatter.youAre=あなたは{0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.fiat=あなたはオファーを{0} {1}に作成中です +formatter.youAreCreatingAnOffer.altcoin=あなたはオファーを{0} {1} ({2} {3})に作成中です +formatter.asMaker={0} {1}のメイカー +formatter.asTaker={0} {1}のテイカー + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=ビットコイン メインネット +# suppress inspection "UnusedProperty" +BTC_TESTNET=ビットコイン テストネット +# suppress inspection "UnusedProperty" +BTC_REGTEST=ビットコイン(Regtest) +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=ビットコインDAOテストネット(非推奨) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=BisqDAOベータネット(ビットコイン メインネット) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=ビットコインDAO Regtest + +time.year=年 +time.month=月 +time.week=週 +time.day=日 +time.hour=時 +time.minute10=10分 +time.hours=時 +time.days=日 +time.1hour=1時間 +time.1day=1日間 +time.minute=分 +time.second=秒 +time.minutes=分 +time.seconds=秒 + + +password.enterPassword=パスワードを入力してください +password.confirmPassword=パスワードを確認してください +password.tooLong=パスワードは500文字以下にしてください +password.deriveKey=パスワードから鍵を引き出す +password.walletDecrypted=ウォレットは正常に復号化され、パスワード保護が解除されました。 +password.wrongPw=間違ったパスワードを入力しています。\n\nパスワードをもう一度入力してみてください。入力ミスやスペルミスがないか慎重に確認してください。 +password.walletEncrypted=ウォレットは正常に暗号化され、パスワード保護が有効になりました。 +password.walletEncryptionFailed=ウォレットのパスワードを設定できませんでした。ウォレットデータベースと一致しないシードワードをインポートした可能性があります。Keybase ([HYPERLINK:https://keybase.io/team/bisq]) で開発者と連絡して下さい。 +password.passwordsDoNotMatch=入力した2つのパスワードが一致しません。 +password.forgotPassword=パスワードを忘れましたか? +password.backupReminder=ウォレットパスワードを設定すると、暗号化されていないウォレットから自動的に作成されたすべてのバックアップが削除されます。\n\nパスワードを設定する前に、アプリケーションディレクトリのバックアップを作成してシードワードを書き留めておくことを強く推奨します。 +password.backupWasDone=私は既にバックアップを取りました +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=ウォレットシードワード +seed.enterSeedWords=ウォレットシードワードを入力してください +seed.date=ウォレットの日付 +seed.restore.title=シードワードからウォレットを復元する +seed.restore=ウォレットを復元する +seed.creationDate=作成日 +seed.warn.walletNotEmpty.msg=あなたのビットコインウォレットは空ではありません。\n\nウォレットを混在させると無効なバックアップになる可能性があるため、古いウォレットを復元する前に、このウォレットを空にする必要があります。\n\nあなたの取引を終了し、あなたの全てのオープンオファーを閉じて、あなたのビットコインを出金するために資金セクションに行ってください。\nあなたが自身のビットコインにアクセスできない場合、ウォレットを空にするために緊急ツールを使うことができます。\n緊急ツールを開くには \"Alt+e\" か \"Cmd/Ctrl+e\" を押してください。 +seed.warn.walletNotEmpty.restore=とにかく復元したい +seed.warn.walletNotEmpty.emptyWallet=最初にウォレットを空にしたい +seed.warn.notEncryptedAnymore=あなたの財布は暗号化されています。\n\n復元後、ウォレットは暗号化されなくなり、新しいパスワードを設定する必要があります。\n\n続行しますか? +seed.warn.walletDateEmpty=ウォレット日を特定しなかったため、Bisqは2013.10.09(BIP39エポック日)からブロックチェーンをスキャンしなければなりません。\n\nBIP39のウォレットは2017.06.28(リリースv0.5)にBisqに使われ始めたので、その日を利用して時間を節約できます。\n\n理想的に、ウォレット・シードが作成された日を特定すべきです。\n\n\nウォレット日を特定せずに続いても本当によろしいですか? +seed.restore.success=ウォレットは、新しいシードワードで正常に復元されました。\n\nアプリケーションをシャットダウンして再起動する必要があります。 +seed.restore.error=シードワードを使用したウォレットの復元中にエラーが発生しました。{0} +seed.restore.openOffers.warn=シードワードから復元すると削除されるオープンオファーがあります。 \n本当に続いてもよろしいですか? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=アカウント +payment.account.no=アカウント番号 +payment.account.name=アカウント名 +payment.account.userName=ユーザ名 +payment.account.phoneNr=電話番号 +payment.account.owner=アカウント所有者の氏名 +payment.account.fullName=氏名(名、ミドルネーム、姓) +payment.account.state=州/県/区 +payment.account.city=市区町村 +payment.bank.country=銀行の国名 +payment.account.name.email=アカウント所有者の氏名/メール +payment.account.name.emailAndHolderId=アカウント所有者の氏名/メール/{0} +payment.bank.name=銀行名 +payment.select.account=アカウントタイプを選択してください +payment.select.region=地域を選択してください +payment.select.country=国を選択してください +payment.select.bank.country=銀行の国名を選択 +payment.foreign.currency=国のデフォルト通貨以外の通貨を選択してもよろしいですか? +payment.restore.default=いいえ、デフォルトの通貨を復元します +payment.email=メール +payment.country=国 +payment.extras=追加要件 +payment.email.mobile=メールまたは携帯電話番号 +payment.altcoin.address=アルトコインアドレス +payment.altcoin.tradeInstantCheckbox=このアルトコインでインスタントトレード(1時間以内) +payment.altcoin.tradeInstant.popup=インスタントトレードでは、両方の取引者が1時間以内に取引を完了できるようにオンラインになっている必要があります。\n\n既にオープンなオファーがあり利用できない場合は「ポートフォリオ」画面でそれらのオファーを無効にしてください。 +payment.altcoin=アルトコイン +payment.select.altcoin=アルトコイン選択、または検索する +payment.secret=秘密の質問 +payment.answer=答え +payment.wallet=ウォレットID +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=ユーザーネームかメールか電話番号 +payment.moneyBeam.accountId=メールか電話番号 +payment.venmo.venmoUserName=Venmo ユーザー名 +payment.popmoney.accountId=メールか電話番号 +payment.promptPay.promptPayId=市民ID/納税者番号または電話番号 +payment.supportedCurrencies=サポートされている通貨 +payment.supportedCurrenciesForReceiver=資金を受け取るための通貨 +payment.limitations=制限事項 +payment.salt=アカウント年齢を検証するためのソルト +payment.error.noHexSalt=ソルトはHEXフォーマットである必要があります。\nアカウントの年齢を維持するために古いアカウントからソルトを送金したい場合は、ソルトフィールドを編集することをお勧めします。 アカウントの年齢は、アカウントソルトおよび識別口座データ(例えば、IBAN)を使用することによって検証されます。 +payment.accept.euro=これらのユーロ圏の利用可能なトレード +payment.accept.nonEuro=これらの非ユーロ圏の利用可能なトレード +payment.accepted.countries=利用可能な国 +payment.accepted.banks=利用可能な銀行 (ID) +payment.mobile=携帯電話番号 +payment.postal.address=郵便住所 +payment.national.account.id.AR=CBU番号 +shared.accountSigningState=アカウント署名状況 + +#new +payment.altcoin.address.dyn={0}アドレス +payment.altcoin.receiver.address=受取人のアルトコインアドレス +payment.accountNr=アカウント番号 +payment.emailOrMobile=メールまたは携帯電話番号 +payment.useCustomAccountName=任意のアカウント名を使う +payment.maxPeriod=許可された最大トレード期間 +payment.maxPeriodAndLimit=最大トレード期間: {0} / 最大買い: {1} / 最大売り: {2}/アカウントの年齢: {3} +payment.maxPeriodAndLimitCrypto=最大トレード期間: {0} / 最大トレード制限: {1} +payment.currencyWithSymbol=通貨: {0} +payment.nameOfAcceptedBank=利用可能な銀行の名前 +payment.addAcceptedBank=利用可能な銀行を追加 +payment.clearAcceptedBanks=利用可能な銀行を削除 +payment.bank.nameOptional=銀行名(オプション) +payment.bankCode=銀行コード +payment.bankId=銀行ID (BIC/SWIFT) +payment.bankIdOptional=銀行ID (BIC/SWIFT)(オプション) +payment.branchNr=支店番号 +payment.branchNrOptional=支店番号(オプション) +payment.accountNrLabel=口座番号 (IBAN) +payment.accountType=口座種別 +payment.checking=当座口座 +payment.savings=普通口座 +payment.personalId=個人ID +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.info=Zelleは他の銀行を介して利用するとよりうまくいく送金サービスです。\n\n1. あなたの銀行がZelleと協力するか(そして利用の方法)をここから確認して下さい: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. 送金制限に注意して下さい。制限は銀行によって異なり、1日、1週、1月当たりの制限に分けられていることが多い。\n\n3. 銀行がZelleと協力しない場合でも、Zelleのモバイルアプリ版を使えますが、送金制限ははるかに低くなります。\n\n4. Bisqアカウントで特定される名前は必ずZelleアカウントと銀行口座に特定される名前と合う必要があります。\n\nトレード契約書とおりにZelleトランザクションを完了できなければ、一部(あるいは全て)のセキュリティデポジットを失う可能性はあります。\n\nZelleにおいてやや高い支払取り消しリスクがあるので、売り手はメールやSMSで無署名買い手に連絡して、Bisqに特定されるZelleアカウントの所有者かどうかを確かめるようにおすすめします。 +payment.fasterPayments.newRequirements.info=「Faster Payments」で送金する場合、銀行が受信者の姓名を確認するケースが最近多くなりました。現在の「Faster Payments」アカウントは姓名を特定しません。\n\nこれからの{0}買い手に姓名を提供するため、Bisq内に新しい「Faster Payments」アカウントを作成するのを検討して下さい。\n\n新しいアカウントを作成すると、完全に同じ分類コード、アカウントの口座番号、そしてアカウント年齢検証ソルト値を古いアカウントから新しいアカウントにコピーして下さい。こうやって現在のアカウントの年齢そして署名状況は維持されます。 +payment.moneyGram.info=MoneyGramを使用する場合、BTCの買い手は認証番号と領収書の写真をEメールでBTCの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 +payment.westernUnion.info=Western Unionを使用する場合、BTCの買い手はMTCN(追跡番号)と領収書の写真をEメールでBTCの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 +payment.halCash.info=HalCashを使用する場合、BTCの買い手は携帯電話からのテキストメッセージを介してBTCの売り手にHalCashコードを送信する必要があります。\n\n銀行がHalCashで送金できる最大額を超えないようにしてください。 1回の出金あたりの最小金額は10EURで、最大金額は600EURです。繰り返し出金する場合は、1日に受取人1人あたり3000EUR、1ヶ月に受取人1人あたり6000EURです。あなたの銀行でも、ここに記載されているのと同じ制限を使用しているか、これらの制限を銀行と照合して確認してください。\n\n出金額は10の倍数EURでなければ、ATMから出金できません。 オファーの作成画面およびオファー受け入れ画面のUIは、EUR金額が正しくなるようにBTC金額を調整します。価格の変化とともにEURの金額は変化するため、市場ベースの価格を使用することはできません。\n\n係争が発生した場合、BTCの買い手はEURを送ったという証明を提出する必要があります。 +# suppress inspection "UnusedMessageFormatParameter" +payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Bisqは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://bisq.wiki/Account_limits] 。 +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Bisqはこの支払いアカウントに下記の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://bisq.wiki/Account_limits] + +payment.cashDeposit.info=あなたの銀行が他の人の口座に現金入金を送ることを許可していることを確認してください。たとえば、Bank of America と Wells Fargo では、こうした預金は許可されなくなりました。 + +payment.revolut.info=以前の場合と違って、Revolutは電話番号やメールアドレスではなく「ユーザ名」をアカウントIDとして要求します。 +payment.account.revolut.addUserNameInfo={0}\n現在の「Revolut」アカウント({1})には「ユーザ名」がありません。 \nアカウントデータを更新するのにRevolutの「ユーザ名」を入力して下さい。\nアカウント年齢署名状況に影響を及ぼしません。 +payment.revolut.addUserNameInfo.headLine=Revolutアカウントをアップデートする + +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=Bisqでアメリカ合衆国郵便為替(USPMO)をトレードするには、以下を理解する必要があります:\n\n-送る前に、BTC買い手は必ずBTC売り手の名前を支払人そして支払先フィールド両方に書いて、追跡証明も含めるUSPMOそして封筒の高解像度写真を取る必要があります。\n-BTC買い手は必ず配達確認を利用してBTC売り手にUSPMOを送る必要があります。\n\n調停が必要になる場合、あるいはトレード係争が開始される場合、調停者や調停人がアメリカ合衆国郵便のサイトで詳細を確認できるように、取った写真、USPMOシリアル番号、郵便局番号、そしてドル金額を送る必要があります。\n\n調停者や調停人に必要な情報を提供しなければ、係争で不利な裁定を下されます。\n\n全ての係争には、調停者や調停人に証明を提供するのは100%USPMO送付者の責任です。\n\n以上の条件を理解しない場合、BisqでUSPMOのトレードをしないで下さい。 + +payment.cashByMail.info=Bisqで、郵送で現金(CBM)を利用してトレードするには、以下を理解する必要があります:\n● BTC買い手は現金を開封明示機構のある袋に入れるべき。\n● 送り先住所とトラッキング番号が袋に貼ってある状態で、包装過程の動画あるいはHD写真を取るべき。\n● 配達確認と保険を掛けて、BTC買い手はBTC売り手に袋を送るべき。\n● BTC売り手は送り手にもらったトラッキング番号が見えるように、袋の開梱の動画を撮影するべき。\n● オファーのメイカーは支払いアカウントの「追加情報」フィールドに特別な契約条件を述べるべき。\n● テイカーはオファーを受けることによってその契約条件に同意することを示す。\n\nCBMでトレードする場合では、正直に行動する責任は完全にトレードピアのみに負わされます。\n\n● 他の法定通貨トレードと比べて、CBTトレードには検証可能な行動は少ない。つまり、係争は処理しにくい。\n● 係争が発生した場合、取引者チャットで解決する方が一番効果的です。\n● 調停者が問題を検討し提案できますが、問題を解決できること保障されるものではない。\n● 調停者は取り組まれる場合、何れのトレードピアが調停者の提案を拒否したら両方のピアの資金はBisqの寄付アドレス [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction] まで送られ、トレードは事実上に終了されることになります。\n● 1人の取引者が調停者の提案を拒否して仲裁を開始したら、両方のトレードピアのトレードとデポジット金額両方は失われる可能性があります。\n● 調停人は提供される証拠に基づいて決定を下すので、前もって係争の場合の準備として、以上のトレードプロセスを記録して下さい。郵送で現金トレードの場合、調停人の決定は最終的なものです。\n● 郵送で現金トレードから失われた金額はBisqのDAOから払い戻しできませんので、払い戻しリクエストは否定されます。\n\n以上の要件を十分に理解することを断言するため、Wikiを参照して下さい:[HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nこれらの要件を理解していない場合は、BisqでCBMを利用してトレードしないで下さい。 + +payment.cashByMail.contact=連絡情報 +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=連絡情報 +payment.f2f.contact.prompt=トレードピアからどのように連絡を受け取りたいのでしょうか?(メールアドレス、電話番号…) +payment.f2f.city=「対面」で会うための市区町村 +payment.f2f.city.prompt=オファーとともに市区町村が表示されます +payment.shared.optionalExtra=オプションの追加情報 +payment.shared.extraInfo=追加情報 +payment.shared.extraInfo.prompt=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 +payment.f2f.info=「対面」トレードには違うルールがあり、オンライントレードとは異なるリスクを伴います。\n\n主な違いは以下の通りです。\n●取引者は、提供される連絡先の詳細を使用して、出会う場所と時間に関する情報を交換する必要があります。\n●取引者は自分のノートパソコンを持ってきて、集合場所で「送金」と「入金」の確認をする必要があります。\n●メイカーに特別な「取引条件」がある場合は、アカウントの「追加情報」テキストフィールドにその旨を記載する必要があります。\n●オファーを受けると、テイカーはメイカーの「トレード条件」に同意したものとします。\n●係争が発生した場合、集合場所で何が起きたのかについての改ざん防止証明を入手することは通常困難であるため、調停者や調停人はあまりサポートをできません。このような場合、BTCの資金は無期限に、または取引者が合意に達するまでロックされる可能性があります。\n\n「対面」トレードでの違いを完全に理解しているか確認するためには、次のURLにある手順と推奨事項をお読みください:[HYPERLINK:https://docs.bisq.network/trading-rules.html#f2f-trading] +payment.f2f.info.openURL=Webページを開く +payment.f2f.offerbook.tooltip.countryAndCity=国と都市: {0} / {1} +payment.f2f.offerbook.tooltip.extra=追加情報: {0} + +payment.japan.bank=銀行 +payment.japan.branch=支店 +payment.japan.account=口座 +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カードをBTC売り手に送る必要があります。\n\nBisqはeGiftカードの送り先になるBTC売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://bisq.wiki/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=国立銀行振替 +SAME_BANK=同じ銀行での送金 +SPECIFIC_BANKS=特定銀行での送金 +US_POSTAL_MONEY_ORDER=米国郵便為替 +CASH_DEPOSIT=現金入金 +CASH_BY_MAIL=郵送で現金 +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=対面(直接) +JAPAN_BANK=日本全銀振込 +AUSTRALIA_PAYID=オーストラリアのPayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=国立銀行 +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=同じ銀行 +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=特定の銀行 +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=米国為替 +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=現金入金 +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=郵送で現金 +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=対面 +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=日本全銀振込 +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPAインスタント支払い +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=アルトコイン +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=アマゾンeGiftカード +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=アルトコイン インスタント + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA インスタント +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=アルトコイン +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=アマゾンeGiftカード +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=アルトコイン インスタント + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=空白の入力は許可されていません。 +validation.NaN=入力が不正な数です。 +validation.notAnInteger=入力が整数値ではありません。 +validation.zero=0の入力は許可されていません。 +validation.negative=負の値は許可されていません。 +validation.fiat.toSmall=可能な最小量より小さい入力は許可されていません。 +validation.fiat.toLarge=可能な最大量より大きい入力は許可されていません。 +validation.btc.fraction=この入力では1サトシ以下のビットコイン値が生成されます。 +validation.btc.toLarge={0}より大きい入力は許可されていません。 +validation.btc.toSmall={0}より小さい入力は許可されていません。 +validation.passwordTooShort=入力したパスワードが短すぎます。最低8文字が必要です。 +validation.passwordTooLong=入力したパスワードが長すぎます。 50文字を超えることはできません。 +validation.sortCodeNumber={0}は{1}個の数字で構成されている必要があります。 +validation.sortCodeChars={0}は{1}文字で構成されている必要があります。 +validation.bankIdNumber={0}は{1}個の数字で構成されている必要があります。 +validation.accountNr=アカウント番号は{0}個の数字で構成されている必要があります。 +validation.accountNrChars=アカウント番号は{0}文字で構成されている必要があります。 +validation.btc.invalidAddress=アドレスが正しくありません。アドレス形式を確認してください。 +validation.integerOnly=整数のみを入力してください。 +validation.inputError=入力エラーを起こしました:\n{0} +validation.bsq.insufficientBalance=あなたの利用可能残高は{0}です。 +validation.btc.exceedsMaxTradeLimit=あなたのトレード制限は{0}です。 +validation.bsq.amountBelowMinAmount=最小金額は{0}です +validation.nationalAccountId={0}は{1}個の数字で構成されている必要があります。 + +#new +validation.invalidInput=不正な入力: {0} +validation.accountNrFormat=アカウント番号は次の形式である必要があります: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure={0}アドレスの構造と一致しないためアドレス検証に失敗しました。 +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZアドレスは必ずLで始まる必要があります。zで始まるアドレスはサポートされていません。 +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZECアドレスは必ずtで始まる必要があります。zで始まるアドレスはサポートされていません。 +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=アドレスが無効な{0}アドレスです!{1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=ネイティブsegwitアドレス(lqで始まるアドレス)はサポートされていません。 +validation.bic.invalidLength=入力長が8か11であるべきです +validation.bic.letters=銀行コードと国コードは英字でなければなりません +validation.bic.invalidLocationCode=BICに不正なロケーションコードが含まれています +validation.bic.invalidBranchCode=BICに不正な支店コードが含まれています +validation.bic.sepaRevolutBic=Revolut Sepaのアカウントはサポートされていません。 +validation.btc.invalidFormat=ビットコインアドレスにとって無効な形式です。 +validation.bsq.invalidFormat=BSQアドレスにとって無効な形式です。 +validation.email.invalidAddress=不正なアドレス +validation.iban.invalidCountryCode=不正な国コード +validation.iban.checkSumNotNumeric=チェックサムは数値でなければなりません +validation.iban.nonNumericChars=英数字以外の文字が検出されました +validation.iban.checkSumInvalid=IBANチェックサムが不正です +validation.iban.invalidLength=数字の長さは15〜34文字でなければなりません。 +validation.interacETransfer.invalidAreaCode=非カナダ圏のコード +validation.interacETransfer.invalidPhone=有効な11桁の電話番号(例えば1-123-456-7890)それともEメールアドレスを入力して下さい。 +validation.interacETransfer.invalidQuestion=文字、数字、スペースおよび/または記号 ' _ , . ? - だけを含める必要があります +validation.interacETransfer.invalidAnswer=1つの単語で、文字、数字、- 記号のみを含む必要があります +validation.inputTooLarge=入力は{0}より大きくてはいけません +validation.inputTooSmall=入力は{0}より大きくなければなりません +validation.inputToBeAtLeast=入力は少なくとも{0}でなければなりません +validation.amountBelowDust=ダスト制限である{0}サトシ未満の金額は許可されていません。 +validation.length=長さは{0}から{1}の間である必要があります +validation.fixedLength=Length must be {0} +validation.pattern=入力は次の形式である必要があります: {0} +validation.noHexString=入力がHEXフォーマットではありません。 +validation.advancedCash.invalidFormat=有効なメールアドレスか次のウォレットID形式である必要があります: X000000000000 +validation.invalidUrl=有効なURLではありません +validation.mustBeDifferent=入力する値は現在の値と異なるべきです。 +validation.cannotBeChanged=パラメーターは変更できません +validation.numberFormatException=例外の数値フォーマット {0} +validation.mustNotBeNegative=負の値は入力できません +validation.phone.missingCountryCode=電話番号を検証するのに2文字国コードが必要です +validation.phone.invalidCharacters=電話番号 {0} には無効な文字が含まれている +validation.phone.insufficientDigits={0} には桁数が不十分で有効電話番号になりません +validation.phone.tooManyDigits={0} には桁数が多過ぎて有効電話番号になりません +validation.phone.invalidDialingCode=電話番号 {0} の国番号は国の {1} にとって間違っています。正しい国番号は {2} です。 +validation.invalidAddressList=有効アドレスのコンマ区切りリストでなければなりません diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties new file mode 100644 index 0000000000..dc7b6d6627 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Leia mais +shared.openHelp=Abrir a Ajuda +shared.warning=Aviso +shared.close=Fechar +shared.cancel=Cancelar +shared.ok=OK +shared.yes=Sim +shared.no=Não +shared.iUnderstand=Eu compreendo +shared.na=N/D +shared.shutDown=Desligar +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=Comprar bitcoin +shared.sellBitcoin=Vender bitcoin +shared.buyCurrency=Comprar {0} +shared.sellCurrency=Vender {0} +shared.buyingBTCWith=comprando BTC com {0} +shared.sellingBTCFor=vendendo BTC por {0} +shared.buyingCurrency=comprando {0} (vendendo BTC) +shared.sellingCurrency=vendendo {0} (comprando BTC) +shared.buy=comprar +shared.sell=vender +shared.buying=comprando +shared.selling=vendendo +shared.P2P=P2P +shared.oneOffer=oferta +shared.multipleOffers=ofertas +shared.Offer=Oferta +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=ofertas abertas +shared.trade=negociação +shared.trades=negociações +shared.openTrades=abrir negociações +shared.dateTime=Data/Hora +shared.price=Preço +shared.priceWithCur=Preço em {0} +shared.priceInCurForCur=Preço em {0} para 1 {1} +shared.fixedPriceInCurForCur=Preço em {0} fixo para 1 {1} +shared.amount=Quantidade +shared.txFee=Taxa de negociação +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Depósito do comprador +shared.sellerSecurityDeposit=Depósito do vendedor +shared.amountWithCur=Quantidade em {0} +shared.volumeWithCur=Volume em {0} +shared.currency=Moeda +shared.market=Mercado +shared.deviation=Deviation +shared.paymentMethod=Método de pagamento +shared.tradeCurrency=Moeda negociada +shared.offerType=Tipo de oferta +shared.details=Detalhes +shared.address=Endereço +shared.balanceWithCur=Saldo em {0} +shared.utxo=Unspent transaction output +shared.txId=ID da Transação +shared.confirmations=Confirmações +shared.revert=Reverter transação +shared.select=Selecionar +shared.usage=Uso +shared.state=Status +shared.tradeId=ID da Negociação +shared.offerId=ID da Oferta +shared.bankName=Nome do banco +shared.acceptedBanks=Bancos aceitos +shared.amountMinMax=Quantidade (min - max) +shared.amountHelp=Se uma oferta possuir uma quantia mínima e máxima definidas, você poderá negociar qualquer quantia dentro dessa faixa. +shared.remove=Remover +shared.goTo=Ir para {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Remover oferta +shared.dontRemoveOffer=Não remover a oferta +shared.editOffer=Editar oferta +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=Conta de negociação +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 Bisq +shared.fundFromExternalWalletButton=Abrir sua carteira externa para prover fundos +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=% abaixo do preço de mercado +shared.aboveInPercent=% acima do preço de mercado +shared.enterPercentageValue=Insira a % +shared.OR=OU +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=Aguardando pagamento... +shared.depositTransactionId=ID da Transação de depósito +shared.TheBTCBuyer=O comprador de BTC +shared.You=Você +shared.sendingConfirmation=Enviando confirmação... +shared.sendingConfirmationAgain=Por favor, envie a confirmação novamente +shared.exportCSV=Export to CSV +shared.exportJSON=Exportar para JSON +shared.summary=Show summary +shared.noDateAvailable=Sem data disponível +shared.noDetailsAvailable=Sem detalhes disponíveis +shared.notUsedYet=Ainda não usado +shared.date=Data +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin 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 +shared.language=Idioma +shared.country=País +shared.applyAndShutDown=Aplicar e desligar +shared.selectPaymentMethod=Selecionar método de pagamento +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Você realmente quer apagar a conta selecionada? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=Ainda não há contas configuradas +shared.manageAccounts=Gerenciar contas +shared.addNewAccount=Adicionar conta nova +shared.ExportAccounts=Exportar Contas +shared.importAccounts=Importar Contas +shared.createNewAccount=Criar nova conta +shared.saveNewAccount=Salvar nova conta +shared.selectedAccount=Conta selecionada +shared.deleteAccount=Apagar conta +shared.errorMessageInline=\nMensagem de erro: {0} +shared.errorMessage=Mensagem de erro +shared.information=Informação +shared.name=Nome +shared.id=ID +shared.dashboard=Painel +shared.accept=Aceitar +shared.balance=Saldo +shared.save=Salvar +shared.onionAddress=Endereço Onion +shared.supportTicket=solicitação de suporte +shared.dispute=disputa +shared.mediationCase=mediação +shared.seller=vendedor +shared.buyer=comprador +shared.allEuroCountries=Todos os países do Euro +shared.acceptedTakerCountries=Países tomadores aceitos +shared.tradePrice=Preço de negociação +shared.tradeAmount=Quantidade negociada +shared.tradeVolume=Volume de negociação +shared.invalidKey=A chave que você inseriu não estava correta +shared.enterPrivKey=Insira a chave privada para destravar +shared.makerFeeTxId=ID de transação da taxa do ofertante +shared.takerFeeTxId=ID de transação da taxa do aceitador +shared.payoutTxId=ID da transação de pagamento +shared.contractAsJson=Contrato em formato JSON +shared.viewContractAsJson=Ver contrato em formato JSON +shared.contract.title=Contrato para negociação com ID: {0} +shared.paymentDetails=Detalhes de pagamento do {0} de BTC +shared.securityDeposit=Depósito de segurança +shared.yourSecurityDeposit=Seu depósito de segurança +shared.contract=Contrato +shared.messageArrived=Chegou mensagem. +shared.messageStoredInMailbox=Mensagem guardada na caixa de correio. +shared.messageSendingFailed=Falha no envio da mensagem. Erro: {0} +shared.unlock=Destravar +shared.toReceive=a receber +shared.toSpend=a ser gasto +shared.btcAmount=Quantidade de BTC +shared.yourLanguage=Seus idiomas +shared.addLanguage=Adicionar idioma +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.makerTxFee=Ofertante: {0} +shared.takerTxFee=Aceitador: {0} +shared.iConfirm=Eu confirmo +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Aberto {0} +shared.fiat=Fiat +shared.crypto=Cripto +shared.all=Todos +shared.edit=Editar +shared.advancedOptions=Opções avançadas +shared.interval=Intervalo +shared.actions=Ações +shared.buyerUpperCase=Comprador +shared.sellerUpperCase=Vendedor +shared.new=NOVO +shared.blindVoteTxId=ID de transação de voto fechado +shared.proposal=Proposta +shared.votes=Votos +shared.learnMore=Saiba mais +shared.dismiss=Dispensar +shared.selectedArbitrator=Árbitro escolhido +shared.selectedMediator=Mediador selecionado +shared.selectedRefundAgent=Árbitro escolhido +shared.mediator=Mediador +shared.arbitrator=Árbitro +shared.refundAgent=Árbitro +shared.refundAgentForSupportStaff=Árbitro +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=No momento, você possui muitas transações não-confirmadas. Tente novamente mais tarde. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Mercado +mainView.menu.buyBtc=Comprar BTC +mainView.menu.sellBtc=Vender BTC +mainView.menu.portfolio=Portfolio +mainView.menu.funds=Fundos +mainView.menu.support=Suporte +mainView.menu.settings=Configurações +mainView.menu.account=Conta +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Preço de mercado por {0} +mainView.marketPrice.bisqInternalPrice=Preço da última negociação Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=Não foi encontrado preço de mercado nos provedores externos.\nO preço exibido corresponde ao último preço de negociação no Bisq para essa moeda. +mainView.marketPrice.tooltip=Preço de Mercado fornecido por {0}{1}\nÚltima atualização: {2}\nURL do provedor: {3} +mainView.balance.available=Saldo disponível +mainView.balance.reserved=Reservado em ofertas +mainView.balance.locked=Travado em negociações +mainView.balance.reserved.short=Reservado +mainView.balance.locked.short=Travado + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Conectando-se à rede Bitcoin +mainView.footer.bsqInfo.synchronizing=/ Sincronizando DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Conectando-se a +mainView.footer.btcInfo.connectionFailed=Falha na conexão à +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=Full node da DAO + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando-se à rede Tor... +mainView.bootstrapState.torNodeCreated=(2/4) Nó da rede Tor criado +mainView.bootstrapState.hiddenServicePublished=(3/4) Serviço Oculto publicado +mainView.bootstrapState.initialDataReceived=(4/4) Dados iniciais recebidos + +mainView.bootstrapWarning.noSeedNodesAvailable=Nenhum nó semente disponível +mainView.bootstrapWarning.noNodesAvailable=Sem nós semente e pares disponíveis +mainView.bootstrapWarning.bootstrappingToP2PFailed=A inicialização para a rede Bisq falhou + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Não há nós semente ou pares persistentes para requisição de dados.\nPor gentileza verifique sua conexão com a internet ou tente reiniciar o programa. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Falha ao conectar com a rede Bisq (erro reportado: {0}).\nPor gentileza verifique sua conexão ou tente reiniciar o programa. + +mainView.walletServiceErrorMsg.timeout=Não foi possível conectar-se à rede Bitcoin, pois o tempo limite expirou. +mainView.walletServiceErrorMsg.connectionError=Não foi possível conectar-se à rede Bitcoin, devido ao seguinte erro: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Uma transação foi rejeitada pela rede.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Você perdeu sua conexão com todos os pontos da rede {0}.\nTalvez você tenha perdido a conexão com a internet ou seu computador estava em modo de espera. +mainView.networkWarning.localhostBitcoinLost=Você perdeu a conexão ao nó Bitcoin do localhost.\nPor favor, reinicie o aplicativo Bisq para conectar-se a outros nós Bitcoin ou reinicie o nó Bitcoin do localhost. +mainView.version.update=(Atualização disponível) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Livro de ofertas +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Negociações + +# OfferBookChartView +market.offerBook.buyAltcoin=Comprar {0} (vender {1}) +market.offerBook.sellAltcoin=Vender {0} (comprar {1}) +market.offerBook.buyWithFiat=Comprar {0} +market.offerBook.sellWithFiat=Vender {0} +market.offerBook.sellOffersHeaderLabel=Vender {0} para +market.offerBook.buyOffersHeaderLabel=Comprar {0} de +market.offerBook.buy=Eu quero comprar bitcoin +market.offerBook.sell=Eu quero vender bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Todas as ofertas ({0}) +market.spread.numberOfBuyOffersColumn=Comprar BTC ({0}) +market.spread.numberOfSellOffersColumn=Vender BTC ({0}) +market.spread.totalAmountColumn=Total de BTC ({0}) +market.spread.spreadColumn=Spread +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Negociações: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Abrir: +market.trades.tooltip.candle.close=Fechar: +market.trades.tooltip.candle.high=Alta: +market.trades.tooltip.candle.low=Baixa: +market.trades.tooltip.candle.average=Média: +market.trades.tooltip.candle.median=Mediana: +market.trades.tooltip.candle.date=Data: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=Criar oferta +offerbook.takeOffer=Aceitar oferta +offerbook.takeOfferToBuy=Comprar {0} +offerbook.takeOfferToSell=Vender {0} +offerbook.trader=Trader +offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} +offerbook.offerersBankName=Nome do banco do ofertante: {0} +offerbook.offerersBankSeat=País da sede do banco do ofertante: {0} +offerbook.offerersAcceptedBankSeatsEuro=Países sedes aceitos pelo banco (tomador): Todos os países da zona do Euro +offerbook.offerersAcceptedBankSeats=Países aceitos como sede bancária (tomador):\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.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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=assinada por um par e limites foram levantados +offerbook.timeSinceSigning.info.signer=assinada por um par e pode assinar contas de pares (limites levantados) +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.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 +offerbook.timeSinceSigning.notSigned.ageDays={0} dias +offerbook.timeSinceSigning.notSigned.noNeed=N/D +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=N.º de ofertas: {0} +offerbook.volume={0} (mín. - máx.) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Criar oferta para comprar {0} +offerbook.createOfferToSell=Criar oferta para vender {0} +offerbook.createOfferToBuy.withFiat=Criar nova oferta para comprar {0} com {1} +offerbook.createOfferToSell.forFiat=Criar nova oferta para vender {0} por {1} +offerbook.createOfferToBuy.withCrypto=Criar oferta para vender {0} (comprar {1}) +offerbook.createOfferToSell.forCrypto=Criar oferta para comprar {0} (vender {1}) + +offerbook.takeOfferButton.tooltip=Aceitar oferta {0} +offerbook.yesCreateOffer=Sim, criar oferta +offerbook.setupNewAccount=Configurar uma nova conta de negociação +offerbook.removeOffer.success=Remoção de oferta bem sucedida +offerbook.removeOffer.failed=Remoção da oferta falhou:\n{0} +offerbook.deactivateOffer.failed=Erro ao desativar oferta:\n{0} +offerbook.activateOffer.failed=Erro ao publicar oferta:\n{0} +offerbook.withdrawFundsHint=Você pode retirar fundos que você pagou da tela {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=Esta oferta não pode ser tomada por restrições de negociação da outra parte + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=A quantia permitida para a negociação está limitada a {0} devido a restrições de segurança baseadas nos seguintes critérios:\n- A conta do comprador não foi assinada por um árbitro ou um par\n- A conta do comprador foi assinada há menos de 30 dias\n- O meio de pagamento para essa oferta é considerado de risco para estornos bancários.\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=A quantia permitida para a negociação está limitada a {0} devido a restrições de segurança baseadas nos seguintes critérios:\n- A sua conta não foi assinada por um árbitro ou um par\n- A sua conta foi assinada há menos de 30 dias\n- O meio de pagamento para essa oferta é considerado de risco para estornos bancários.\n\n{1} + +offerbook.warning.wrongTradeProtocol=Essa oferta requer uma versão do protocolo diferente da usada em sua versão do software.\n\nVerifique se você possui a versão mais nova instalada, caso contrário o usuário que criou a oferta usou uma versão ultrapassada.\n\nUsuários não podem negociar com uma versão incompatível do protocolo. +offerbook.warning.userIgnored=Você adicionou o endereço onion à sua lista de endereços ignorados. +offerbook.warning.offerBlocked=Essa oferta foi bloqueada pelos desenvolvedores do Bisq.\nProvavelmente há um problema não resolvido associado àquela oferta. +offerbook.warning.currencyBanned=A moeda usada nesta oferta foi bloqueada pelos desenvolvedores do Bisq.\nPor favor, visite o Fórum do Bisq para maiores informações. +offerbook.warning.paymentMethodBanned=O método de pagamento usado nesta oferta foi bloqueado pelos desenvolvedores do Bisq.\nPor favor, visite o Fórum do Bisq para maiores informações. +offerbook.warning.nodeBlocked=O endereço onion daquele negociador foi bloqueado pelos desenvolvedores do Bisq.\nProvavelmente há um problema não resolvido associado àquele negociador. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=Você irá vender a preço de mercado (atualizado a cada minuto). +offerbook.info.buyAtMarketPrice=Você irá comprar a preço de mercado (atualizado a cada minuto). +offerbook.info.sellBelowMarketPrice=Você irá receber {0} a menos do que o atual preço de mercado (atualizado a cada minuto). +offerbook.info.buyAboveMarketPrice=Você irá pagar {0} a mais do que o atual preço de mercado (atualizado a cada minuto). +offerbook.info.sellAboveMarketPrice=Você irá receber {0} a mais do que o atual preço de mercado (atualizado a cada minuto). +offerbook.info.buyBelowMarketPrice=Você irá pagar {0} a menos do que o atual preço de mercado (atualizado a cada minuto). +offerbook.info.buyAtFixedPrice=Você irá comprar nesse preço fixo. +offerbook.info.sellAtFixedPrice=Você irá vender neste preço fixo. +offerbook.info.noArbitrationInUserLanguage=Em caso de disputa, a arbitragem para essa oferta será realizada em {0}. O idioma atualmente está definido como {1}. +offerbook.info.roundedFiatVolume=O valor foi arredondado para aumentar a privacidade da sua negociação. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Insira o valor em BTC +createOffer.price.prompt=Insira o preço +createOffer.volume.prompt=Insira o valor em {0} +createOffer.amountPriceBox.amountDescription=Quantia em BTC para {0} +createOffer.amountPriceBox.buy.volumeDescription=Valor em {0} a ser gasto +createOffer.amountPriceBox.sell.volumeDescription=Valor em {0} a ser recebido +createOffer.amountPriceBox.minAmountDescription=Quantia mínima de BTC +createOffer.securityDeposit.prompt=Depósito de segurança +createOffer.fundsBox.title=Financie sua oferta +createOffer.fundsBox.offerFee=Taxa de negociação +createOffer.fundsBox.networkFee=Taxa de mineração +createOffer.fundsBox.placeOfferSpinnerInfo=Sua oferta está sendo publicada... +createOffer.fundsBox.paymentLabel=Negociação Bisq com ID {0} +createOffer.fundsBox.fundsStructure=({0} para o depósito de segurança, {1} para a taxa de transação e {2} para a taxa de mineração) +createOffer.fundsBox.fundsStructure.BSQ=({0} depósito de segurança, {1} taxa de mineração) + {2} taxa de transação +createOffer.success.headline=Sua oferta foi publicada +createOffer.success.info=Você pode gerenciar suas ofertas abertas em \"Portfolio/Minhas ofertas\". +createOffer.info.sellAtMarketPrice=Você irá sempre vender a preço de mercado e o preço de sua oferta será atualizado constantemente. +createOffer.info.buyAtMarketPrice=Você irá sempre comprar a preço de mercado e o preço de sua oferta será atualizado constantemente. +createOffer.info.sellAboveMarketPrice=Você irá sempre receber {0}% a mais do que o atual preço de mercado e o preço de sua oferta será atualizado constantemente. +createOffer.info.buyBelowMarketPrice=Você irá sempre pagar {0}% a menos do que o atual preço de mercado e o preço de sua oferta será atualizado constantemente. +createOffer.warning.sellBelowMarketPrice=Você irá sempre receber {0}% a menos do que o atual preço de mercado e o preço da sua oferta será atualizado constantemente. +createOffer.warning.buyAboveMarketPrice=Você irá sempre pagar {0}% a mais do que o atual preço de mercado e o preço da sua oferta será atualizada constantemente. +createOffer.tradeFee.descriptionBTCOnly=Taxa de negociação +createOffer.tradeFee.descriptionBSQEnabled=Escolha a moeda da taxa de transação + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=Revisar: Criar oferta para {0} bitcoin +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 Bisq (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. + +# 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. +createOffer.setAmountPrice=Definir quantidade e preço +createOffer.warnCancelOffer=Você já havia financiado aquela oferta.\nSeus fundos foram movidos para sua carteira Bisq local e estão disponíveis para retirada na janela \"Fundos/Enviar fundos\".\nTem certeza que deseja cancelar? +createOffer.timeoutAtPublishing=Um erro ocorreu ao publicar a oferta: tempo esgotado. +createOffer.errorInfo=\n\nA taxa já está paga. No pior dos casos, você perdeu essa taxa.\nPor favor, tente reiniciar o seu aplicativo e verifique sua conexão de rede para ver se você pode resolver o problema. +createOffer.tooLowSecDeposit.warning=Você definiu o depósito de segurança para um valor mais baixo do que o valor padrão recomendado de {0}.\nVocê tem certeza de que deseja usar um depósito de segurança menor? +createOffer.tooLowSecDeposit.makerIsSeller=Há menos proteção caso a outra parte da negociação não siga o protocolo de negociação. +createOffer.tooLowSecDeposit.makerIsBuyer=Os seus compradores se sentirão menos seguros, pois você terá menos bitcoins sob risco. Isso pode fazer com que eles prefiram escolher outras ofertas ao invés da sua. +createOffer.resetToDefault=Não, voltar ao valor padrão +createOffer.useLowerValue=Sim, usar meu valor mais baixo +createOffer.priceOutSideOfDeviation=O preço submetido está fora do desvio máximo com relação ao preço de mercado.\nO desvio máximo é {0} e pode ser alterado nas preferências. +createOffer.changePrice=Alterar preço +createOffer.tac=Ao publicar essa oferta, eu concordo em negociar com qualquer trader que preencha as condições definidas nesta tela. +createOffer.currencyForFee=Taxa de negociação +createOffer.setDeposit=Definir o depósito de segurança do comprador (%) +createOffer.setDepositAsBuyer=Definir o meu depósito de segurança como comprador (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Insira a quantia em BTC +takeOffer.amountPriceBox.buy.amountDescription=Quantia de BTC para vender +takeOffer.amountPriceBox.sell.amountDescription=Quantia de BTC para comprar +takeOffer.amountPriceBox.priceDescription=Preço por bitcoin em {0} +takeOffer.amountPriceBox.amountRangeDescription=Quantias permitidas +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=A quantia que você inseriu excede o número máximo de casas decimais permitida.\nA quantia foi ajustada para 4 casas decimais. +takeOffer.validation.amountSmallerThanMinAmount=A quantia não pode ser inferior à quantia mínima definida na oferta. +takeOffer.validation.amountLargerThanOfferAmount=A quantia inserida não pode ser superior à quantia definida na oferta. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Essa quantia inserida criaria um troco pequeno demais para o vendedor de BTC. +takeOffer.fundsBox.title=Financiar sua negociação +takeOffer.fundsBox.isOfferAvailable=Verificando se a oferta está disponível ... +takeOffer.fundsBox.tradeAmount=Quantia a ser vendida +takeOffer.fundsBox.offerFee=Taxa de negociação +takeOffer.fundsBox.networkFee=Total em taxas de mineração +takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitação da oferta em progresso ... +takeOffer.fundsBox.paymentLabel=negociação Bisq com ID {0} +takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de transação, {2} taxa de mineração) +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} + +# new entries +takeOffer.takeOfferButton=Revisar: Aceitar oferta para {0} bitcoin +takeOffer.noPriceFeedAvailable=Você não pode aceitar essa oferta pois ela usa uma porcentagem do preço baseada no preço de mercado, mas o canal de preços está indisponível no momento. +takeOffer.takeOfferFundWalletInfo.headline=Financiar sua negociação +# suppress inspection "TrailingSpacesInProperty" +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- Taxas de mineração: {4}\n\nVocê pode escolher entre duas opções para financiar sua negociação:\n- Usar a sua carteira Bisq (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.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 +takeOffer.alreadyFunded.askCancel=Você já financiou essa oferta.\nSe cancelar agora, seus fundos serão movidos para sua carteira Bisq local e estarão disponíveis para retirada na janela \"Fundos/Enviar fundos\".\nTem certeza que deseja cancelar? +takeOffer.failed.offerNotAvailable=Pedido de aceitar oferta de negociação falhou pois a oferta não está mais disponível. Talvez outro negociador tenha aceitado a oferta neste período. +takeOffer.failed.offerTaken=Não foi possível aceitar a oferta, pois ela já foi aceita por outro negociador. +takeOffer.failed.offerRemoved=Não é possível aceitar a oferta pois ela foi removida. +takeOffer.failed.offererNotOnline=Erro ao aceitar a oferta: o ofertante não está mais online. +takeOffer.failed.offererOffline=Erro ao aceitar a oferta: o ofertante está offline. +takeOffer.warning.connectionToPeerLost=Você perdeu a conexão com o ofertante.\nEle pode ter ficado offline ou teve a conexão com você fechada em decorrência de muitas conexões abertas.\n\nSe você ainda pode ver a oferta dele no livro de ofertas você pode tentar aceitá-la novamente. + +takeOffer.error.noFundsLost=\n\nA sua carteira ainda não realizou o pagamento.\nPor favor, reinicie o programa e verifique a sua conexão com a internet. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nA transação do depósito já foi publicada.\nPor favor, reinicie o programa e verifique sua conexão de internet para tentar resolver o problema.\nSe o problema persistir, entre em contato com os desenvolvedores. +takeOffer.error.payoutPublished=\n\nA transação de pagamento já foi publicada.\nPor favor, reinicie o programa e verifique sua conexão de internet para tentar resolver o problema.\nSe o problema persistir, entre em contato com os desenvolvedores. +takeOffer.tac=Ao aceitar essa oferta, eu concordo com as condições de negociação definidas nesta tela. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Preço gatilho +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=Definir preço +editOffer.confirmEdit=Editar oferta +editOffer.publishOffer=Publicando a sua oferta. +editOffer.failed=Erro ao editar oferta:\n{0} +editOffer.success=A sua oferta foi editada com sucesso. +editOffer.invalidDeposit=O depósito de segurança do comprador não está dentro dos limites definidos pela DAO Bisq e não pode mais ser editado. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Minhas ofertas em aberto +portfolio.tab.pendingTrades=Negociações em aberto +portfolio.tab.history=Histórico +portfolio.tab.failed=Falha +portfolio.tab.editOpenOffer=Editar oferta + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Aguardar confirmação da blockchain +portfolio.pending.step2_buyer.startPayment=Iniciar pagamento +portfolio.pending.step2_seller.waitPaymentStarted=Aguardar início do pagamento +portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar recebimento do pagamento +portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar recebimento do pagamento +portfolio.pending.step5.completed=Concluído + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Transfira com a sua carteira {0} externa\n{1} para o vendedor de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Vá ao banco e pague {0} ao vendedor de BTC.\n\n +portfolio.pending.step2_buyer.cash.extra=IMPORTANTE:\nApós executar o pagamento, escreva no comprovante de depósito: SEM REEMBOLSO\nEntão rasgue-o em 2 partes, tire uma foto e envie-a para o e-mail do vendedor de BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Pague {0} ao vendedor de BTC usando MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANTE:\nApós ter feito o pagamento, envie o número de autorização e uma foto do comprovante por e-mail para o vendedor de BTC.\nO comprovante deve exibir claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Pague {0} ao vendedor de BTC usando Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANTE:\nApós ter feito o pagamento, envie o número de rastreamento (MTCN) e uma foto do comprovante por e-mail para o vendedor de BTC.\nO comprovante deve exibir claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Order\" para o vendedor de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Por favor, entre em contato com o vendedor de BTC através do contato fornecido e combine um encontro para pagá-lo {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Quantia a ser transferida +portfolio.pending.step2_buyer.sellersAddress=Endereço {0} do vendedor +portfolio.pending.step2_buyer.buyerAccount=A sua conta de pagamento a ser usada +portfolio.pending.step2_buyer.paymentStarted=Pagamento iniciado +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=Você ainda não realizou seu pagamento de {0}!\nEssa negociação deve ser completada até {1}. +portfolio.pending.step2_buyer.openForDispute=Você ainda não completou o seu pagamento!\nO período máximo para a negociação já passou. Entre em contato com o mediador para pedir assistência. +portfolio.pending.step2_buyer.paperReceipt.headline=Você enviou o comprovante de depósito para o vendedor de BTC? +portfolio.pending.step2_buyer.paperReceipt.msg=Lembre-se:\nVocê deve escrever no comprovante de depósito: SEM REEMBOLSO\nA seguir, rasgue-o em duas partes, tire uma foto e envie-a para o e-mail do vendedor de BTC. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar o número de autorização e o comprovante de depósito +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Você previsa enviar por-email para o vendedor BTC o número de autorização e uma foto com o comprovante de depósito.\nO comprovante deve mostrar claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o número de autorização e o contrato ao vendedor? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN e comprovante +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Você precisa enviar o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de BTC.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o MTCN e o contrato para o vendedor? +portfolio.pending.step2_buyer.halCashInfo.headline=Enviar código HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Você precisa enviar uma mensagem de texto com o código HalCash, bem como o ID da negociação ({0}) para o vendedor BTC.\nO nº do telefone do vendedor é {1}.\n\nVocê enviou o código para o vendedor? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Alguns bancos podem verificar o nome do destinatário. Contas de pagamento rápido criadas numa versão antiga da Bisq não fornecem o nome do destinatário, então, por favor, use o chat de negociação pra obtê-lo (caso necessário). +portfolio.pending.step2_buyer.confirmStart.headline=Confirme que você iniciou o pagamento +portfolio.pending.step2_buyer.confirmStart.msg=Você iniciou o pagamento {0} para o seu parceiro de negociação? +portfolio.pending.step2_buyer.confirmStart.yes=Sim, iniciei o pagamento +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=Aguardar pagamento +portfolio.pending.step2_seller.f2fInfo.headline=Informações de contato do comprador +portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação blockchain do protocolo.\nVocê precisa aguardar até que o comprador de BTC inicie o pagamento de {0}. +portfolio.pending.step2_seller.warn=O comprador de BTC ainda não fez o pagamento de {0}.\nVocê precisa esperar até que ele inicie o pagamento.\nCaso a negociação não conclua em {1}, o árbitro irá investigar. +portfolio.pending.step2_seller.openForDispute=O comprador de BTC ainda não iniciou o pagamento!\nO período máximo permitido para a negociação expirou.\nVocê pode aguardar mais um pouco, dando mais tempo para o seu parceiro de negociação, ou você pode entrar em contato com o mediador para pedir assistência. +tradeChat.chatWindowTitle=Abrir janela de conversa para a negociação com ID "{0}" +tradeChat.openChat=Abrir janela de conversa +tradeChat.rules=Você pode conversar com seu par da negociação para resolver potenciais problemas desta negociação.\nNão é obrigatório responder no chat.\nSe um negociante violar qualquer das regras abaixo, abra uma disputa e reporte o caso ao mediador ou árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar a ID de transação e o nome de um explorador de blocos.\n\t● Não envie suas palavras-semente, chaves privadas, senhas ou outras informações sensíveis!\n\t● Não encoraje negociações fora da Bisq (sem segurança).\n\t● Não tente aplicar golpes por meio de qualquer forma de engenharia social.\n\t● Se o par não responder e preferir não se comunicar pelo chat, respeite essa decisão.\n\t● Mantenha o escopo da conversa limitado à negociação. Este chat não é um substituto de aplicativos de mensagens ou local para trolagens.\n\t● Mantenha a conversa amigável e respeitosa. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +message.state.SENT=Mensagem enviada +# suppress inspection "UnusedProperty" +message.state.ARRIVED=A mensagem chegou ao destinário +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Mensagem do pagamento enviada mas ainda não recebida pelo par +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=O destinário confirmou o recebimento da mensagem +# suppress inspection "UnusedProperty" +message.state.FAILED=Erro ao enviar a mensagem + +portfolio.pending.step3_buyer.wait.headline=Aguarde confirmação de pagamento do vendedor de BTC. +portfolio.pending.step3_buyer.wait.info=Aguardando o vendedor de BTC confirmar o recebimento do pagamento de {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Status da mensagem de pagamento iniciado +portfolio.pending.step3_buyer.warn.part1a=na blockchain {0} +portfolio.pending.step3_buyer.warn.part1b=no seu provedor de pagamentos (ex: seu banco) +portfolio.pending.step3_buyer.warn.part2=O vendedor de BTC ainda não confirmou o seu pagamento. Por favor, verifique em {0} se o pagamento foi enviado com sucesso. +portfolio.pending.step3_buyer.openForDispute=O vendedor de BTC não confirmou o seu pagamento! O período máximo para essa negociação expirou. Você pode aguardar mais um pouco, dando mais tempo para o seu parceiro de negociação, ou você pode pedir a assistência de um mediador. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Seu parceiro de negociação confirmou que iniciou o pagamento de {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=no seu explorador da blockchain {0} preferido +portfolio.pending.step3_seller.altcoin.wallet=em sua carteira {0} +portfolio.pending.step3_seller.altcoin={0}Verifique em {1} se a transação para o seu endereço de recebimento\n{2}\njá tem confirmações suficientes na blockchain.\nA quantia do pagamento deve ser {3}\n\nVocê pode copiar e colar seu endereço {4} na janela principal, após fechar esse popup. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Como o pagamento é realizado através de depósito de dinheiro em espécie, o comprador de BTC obrigatoriamente deve escrever \"SEM REEMBOLSO\" no comprovante de depósito, rasgá-lo em duas partes e enviar uma foto do comprovante para você por e-mail.\n\nPara reduzir a chance de um reembolso (restituição do valor depositado para o comprador), confirme apenas se você tiver recebido o e-mail e tiver certeza de que o comprovante de depósito é autêntico.\nSe você não tiver certeza, {0} +portfolio.pending.step3_seller.moneyGram=O comprador deve enviar o Número de Autorização e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente o seu nome completo, país, estado e a quantia. Por favor verifique seu e-mail se recebeu o Número de Autorização.\n\nDepois de fechar esse pop-up, verá o nome e o endereço do comprador do BTC para retirar o dinheiro da MoneyGram.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! +portfolio.pending.step3_seller.westernUnion=O comprador deve enviar-lhe o MTCN (número de rastreamento) e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente seu nome completo, cidade, país e a quantia Por favor verifique no seu e-mail se você recebeu o MTCN.\n\nDepois de fechar esse pop-up, você verá o nome e endereço do comprador de BTC para receber o dinheiro da Western Union.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! +portfolio.pending.step3_seller.halCash=O comprador deve-lhe enviar o código HalCash como mensagem de texto. Além disso, você receberá uma mensagem do HalCash com as informações necessárias para sacar o EUR de uma ATM que suporte o HalCash.\n\nDepois de retirar o dinheiro na ATM, confirme aqui o recibo do pagamento! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nVerifique também se o nome de quem envia o pagamento no contrato de negociação é o mesmo que aparece em seu extrato bancário:\nNome do pagante, pelo contrato de negociação: {0}\n\nSe os nomes não forem exatamente iguais, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=não confirme o recebimento do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recebimento do pagamento +portfolio.pending.step3_seller.amountToReceive=Quantia a receber +portfolio.pending.step3_seller.yourAddress=Seu endereço {0} +portfolio.pending.step3_seller.buyersAddress=Endereço {0} do comprador +portfolio.pending.step3_seller.yourAccount=Sua conta de negociação +portfolio.pending.step3_seller.xmrTxHash=ID da Transação +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Confirmar recebimento do pagamento +portfolio.pending.step3_seller.buyerStartedPayment=O comprador de BTC iniciou o pagamento {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Verifique as confirmações de transação em sua carteira altcoin ou explorador de blockchain e confirme o pagamento quando houver confirmações suficientes. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Verifique em sua conta de negociação (ex: sua conta bancária) e confirme que recebeu o pagamento. +portfolio.pending.step3_seller.warn.part1a=na blockchain {0} +portfolio.pending.step3_seller.warn.part1b=no seu provedor de pagamentos (ex: banco) +portfolio.pending.step3_seller.warn.part2=Você ainda não confirmou o recebimento do pagamento. Por favor, verifique em {0} se você recebeu o pagamento. +portfolio.pending.step3_seller.openForDispute=Você ainda não confirmou o recebimento do pagamento!\nO período máximo para a negociação expirou.\nPor favor, confirme o recebimento do pagamento ou entre em contato com o mediador para pedir assistência. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Você recebeu o pagamento de {0} do seu parceiro de negociação?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Verifique também se o nome de quem envia o pagamento no contrato de negociação é o mesmo que aparece em seu extrato bancário:\nNome do pagante, pelo contrato de negociação: {0}\n\nSe os nomes não forem exatamente iguais, não confirme o recebimento do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Assim que você confirmar o recebimento do pagamento, o valor da transação será liberado para o comprador de BTC e o depósito de segurança será devolvido.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmar recebimento do pagamento +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sim, eu recebi o pagamento +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Ao confirmar o recebimento do pagamento, você também estará verificando a conta do seu par e a assinando. Como a conta do seu par ainda não foi assinada, você deve segurar a confirmação do pagamento o máximo de tempo possível para reduzir o risco de estorno. + +portfolio.pending.step5_buyer.groupTitle=Resumo da negociação +portfolio.pending.step5_buyer.tradeFee=Taxa de negociação +portfolio.pending.step5_buyer.makersMiningFee=Taxa de mineração +portfolio.pending.step5_buyer.takersMiningFee=Total em taxas de mineração +portfolio.pending.step5_buyer.refunded=Depósito de segurança devolvido +portfolio.pending.step5_buyer.withdrawBTC=Retirar seus bitcoins +portfolio.pending.step5_buyer.amount=Quantia a ser retirada +portfolio.pending.step5_buyer.withdrawToAddress=Enviar para o endereço +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=Retirar para carteira externa +portfolio.pending.step5_buyer.alreadyWithdrawn=Seus fundos já foram retirados.\nFavor verifique o histórico de transações. +portfolio.pending.step5_buyer.confirmWithdrawal=Confirmar solicitação de retirada +portfolio.pending.step5_buyer.amountTooLow=A quantia a ser transferida é inferior à taxa de transação e o valor mínimo de transação (poeira). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retirada concluída +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Suas negociações concluídas estão salvas em \"Portfolio/Histórico\".\nVocê pode rever todas as suas transações bitcoin em \"Fundos/Transações\" +portfolio.pending.step5_buyer.bought=Você comprou +portfolio.pending.step5_buyer.paid=Você pagou + +portfolio.pending.step5_seller.sold=Você vendeu +portfolio.pending.step5_seller.received=Você recebeu + +tradeFeedbackWindow.title=Parabéns por concluir a negociação +tradeFeedbackWindow.msg.part1=Gostaríamos de saber como está sendo a sua experiência. Ela nos ajuda a corrigir e aperfeiçoar o software. Caso queira nos deixar a sua opinião, preencha nosso pequeno questionário (não é necessário registrar-se) em: +tradeFeedbackWindow.msg.part2=Se você tem dúvidas ou está tendo problemas, por favor entre em contato com outros usuários e contribuidores através do fórum Bisq em: +tradeFeedbackWindow.msg.part3=Obrigado por usar Bisq! + +portfolio.pending.role=Minha função +portfolio.pending.tradeInformation=Informação da negociação +portfolio.pending.remainingTime=Tempo restante +portfolio.pending.remainingTimeDetail={0} (até {1}) +portfolio.pending.tradePeriodInfo=O período de negociação irá se iniciar após a primeira confirmação na blockchain. O período de negociação máximo irá variar de acordo com o método de pagamento utilizado. +portfolio.pending.tradePeriodWarning=Se o período expirar, os dois negociantes poderão abrir uma disputa. +portfolio.pending.tradeNotCompleted=Negociação não completada a tempo (até {0}) +portfolio.pending.tradeProcess=Processo de negociação +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Abrir disputa novamente +portfolio.pending.openSupportTicket.headline=Abrir ticket de suporte +portfolio.pending.openSupportTicket.msg=Por favor, apenas use esta função em casos de emergência quando não houver um botão para "Abrir ticket de suporte" ou "Abrir disputa".\n\nQuando você abrir um ticket de suporte, a negociação será interrompida e tratada por um mediador ou árbitro. + +portfolio.pending.timeLockNotOver=Você precisa aguardar até ≈{0} (mais {1} blocos) para conseguir abrir uma disputa com um árbitro. +portfolio.pending.error.depositTxNull=A transação de depósito está ausente. Você não pode abrir uma disputa sem uma transação de depósito válida. Por favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\n\nPara mais informações, por favor acesse o canal #support do time da Bisq na Keybase. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=A transação de depósito não está confirmada. Você não pode abrir uma disputa com uma transação não-confirmada. Por favor, espere até que a transação seja confirmada ou vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\n\nPara mais informações, por favor acesse o canal #support do time da Bisq na Keybase. + +portfolio.pending.support.headline.getHelp=Precisa de ajuda? +portfolio.pending.support.text.getHelp=Caso tenha problemas, você pode tentar contactar o par de negociação no chat ou solicitar ajuda na comunidade da Bisq em https://bisq.community. Se o problema persistir, você pode solicitar ajuda adicional a um mediador. +portfolio.pending.support.button.getHelp=Abrir Chat de Negociante +portfolio.pending.support.headline.halfPeriodOver=Verifique o pagamento +portfolio.pending.support.headline.periodOver=O período de negociação acabou + +portfolio.pending.mediationRequested=Mediação requerida +portfolio.pending.refundRequested=Reembolso requerido +portfolio.pending.openSupport=Abrir ticket de suporte +portfolio.pending.supportTicketOpened=Ticket de suporte aberto +portfolio.pending.communicateWithArbitrator=Por favor, vá até a seção \"Suporte\" e entre em contato com o árbitro. +portfolio.pending.communicateWithMediator=Por favor, entre em contato com o mediador na seção \"Suporte\". +portfolio.pending.disputeOpenedMyUser=Você já abriu uma disputa.\n{0} +portfolio.pending.disputeOpenedByPeer=Seu parceiro de negociação abriu uma disputa\n{0} +portfolio.pending.noReceiverAddressDefined=Nenhum endereço de recebimento definido + +portfolio.pending.mediationResult.headline=Sugestão de pagamento da mediação +portfolio.pending.mediationResult.info.noneAccepted=Completar a negociação aceitando a sugestão do mediador para o pagamento +portfolio.pending.mediationResult.info.selfAccepted=Você aceitou a sugestão do mediador. Aguardando o parceiro de negociação aceitar também. +portfolio.pending.mediationResult.info.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador. Você também aceita +portfolio.pending.mediationResult.button=Ver solução proposta +portfolio.pending.mediationResult.popup.headline=Resultado da mediação para a negociação com ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador para a negociação {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitramento +portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Concluído +portfolio.closed.ticketClosed=Arbitrado +portfolio.closed.mediationTicketClosed=Mediado +portfolio.closed.canceled=Cancelado +portfolio.failed.Failed=Falha +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Receber fundos +funds.tab.withdrawal=Enviar fundos +funds.tab.reserved=Fundos reservados +funds.tab.locked=Fundos travados +funds.tab.transactions=Transações + +funds.deposit.unused=Não utilizado +funds.deposit.usedInTx=Utilizado em {0} transação(ões) +funds.deposit.fundBisqWallet=Financiar carteira Bisq +funds.deposit.noAddresses=Nenhum endereço de depósito foi gerado ainda +funds.deposit.fundWallet=Financiar sua carteira +funds.deposit.withdrawFromWallet=Enviar fundos da carteira +funds.deposit.amount=Quantia em BTC (opcional) +funds.deposit.generateAddress=Gerar um endereço novo +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Selecione um endereço não utilizado da tabela acima ao invés de gerar um novo. + +funds.withdrawal.arbitrationFee=Taxa de arbitragem +funds.withdrawal.inputs=Escolha dos inputs +funds.withdrawal.useAllInputs=Usar todos inputs disponíveis +funds.withdrawal.useCustomInputs=Usar inputs personalizados +funds.withdrawal.receiverAmount=Quantia do destinatário +funds.withdrawal.senderAmount=Quantia do remetente +funds.withdrawal.feeExcluded=Quantia excluindo a taxa de mineração +funds.withdrawal.feeIncluded=Quantia incluindo a taxa de mineração +funds.withdrawal.fromLabel=Retirar do endereço +funds.withdrawal.toLabel=Enviar para o endereço +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Retirar selecionados +funds.withdrawal.noFundsAvailable=Não há fundos disponíveis para retirada +funds.withdrawal.confirmWithdrawalRequest=Confirmar solicitação de retirada +funds.withdrawal.withdrawMultipleAddresses=Retirar de múltiplos endereços ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Retirar de múltiplos endereços:\n{0} +funds.withdrawal.notEnoughFunds=Você não possui saldo suficiente em sua carteira. +funds.withdrawal.selectAddress=Selecione um endereço de origem da tabela +funds.withdrawal.setAmount=Defina quantia a ser retirada +funds.withdrawal.fillDestAddress=Preencha seu endereço de destino +funds.withdrawal.warn.noSourceAddressSelected=Você precisa selecionar um endereço de origem na tabela acima. +funds.withdrawal.warn.amountExceeds=Você não tem saldo suficiente no endereço selecionado.\nTente selecionar múltiplos endereços na tabela acima ou modificar a opção para incluir a taxa do minerador. + +funds.reserved.noFunds=Não há fundos reservados em ofertas abertas +funds.reserved.reserved=Reservado na carteira local para oferta com ID: {0} + +funds.locked.noFunds=Não há fundos travados em negociações +funds.locked.locked=Travado em multisig para negociação com ID: {0} + +funds.tx.direction.sentTo=Enviado para: +funds.tx.direction.receivedWith=Recebido com: +funds.tx.direction.genesisTx=Da transação gênese: +funds.tx.txFeePaymentForBsqTx=Taxa de mineração para transação de BSQ +funds.tx.createOfferFee=Taxa de oferta e transação: {0} +funds.tx.takeOfferFee=Taxa de aceitação e transação: {0} +funds.tx.multiSigDeposit=Depósito Multisig: {0} +funds.tx.multiSigPayout=Pagamento Multisig: {0} +funds.tx.disputePayout=Pagamento de disputa: {0} +funds.tx.disputeLost=Caso com disputa perdida: {0} +funds.tx.collateralForRefund=Colateral de reembolso: {0} +funds.tx.timeLockedPayoutTx=transação de pagamento com trava temporal: {0} +funds.tx.refund=Reembolso de árbitro: {0} +funds.tx.unknown=Razão desconhecida: {0} +funds.tx.noFundsFromDispute=Nenhum reembolso de disputa +funds.tx.receivedFunds=Fundos recebidos +funds.tx.withdrawnFromWallet=Retirado da carteira +funds.tx.withdrawnFromBSQWallet=BTC retirado da carteira BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=Sem transações disponíveis +funds.tx.revert=Reverter +funds.tx.txSent=Transação enviada com sucesso para um novo endereço em sua carteira Bisq local. +funds.tx.direction.self=Enviar para você mesmo +funds.tx.daoTxFee=Taxa de mineração para transação de BSQ +funds.tx.reimbursementRequestTxFee=Solicitar reembolso +funds.tx.compensationRequestTxFee=Pedido de compensação +funds.tx.dustAttackTx=Poeira recebida +funds.tx.dustAttackTx.popup=Esta transação está enviando uma quantia muito pequena de BTC para a sua carteira e pode ser uma tentativa das empresas de análise da blockchain para espionar a sua carteira.\n\nSe você usar esse output em uma transação eles decobrirão que você provavelmente também é o proprietário de outros endereços (mistura de moedas).\n\nPara proteger sua privacidade a carteira Bisq ignora tais outputs de poeira para fins de consumo e na tela de saldo. Você pode definir a quantia limite a partir da qual um output é considerado poeira nas configurações." + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediação +support.tab.arbitration.support=Arbitragem +support.tab.legacyArbitration.support=Arbitração antiga +support.tab.ArbitratorsSupportTickets=Tickets de {0} +support.filter=Search disputes +support.filter.prompt=Insira ID da negociação. data. endereço onion ou dados da conta + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Notificação privada +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=Não há tickets de suporte abertos +support.sendingMessage=Enviando mensagem... +support.receiverNotOnline=O recipiente não está online. A mensagem será salva na caixa postal dele. +support.sendMessageError=Erro ao enviar a mensagem: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=A oferta nessa disputa foi criada com uma versão anterior do Bisq.\nVocê não pode fechar aquela disputa com a sua versão atual do programa.\n\nFavor utilizar uma versão mais velha com protocolo versão {0} +support.openFile=Abrir arquivo para anexar (tamanho máximo: {0} kb) +support.attachmentTooLarge=O tamanho total de seus arquivos anexados é {0} kb e excede o máximo permitido para mensagens de {1} kb. +support.maxSize=O tamanho máximo permitido é {0} kB. +support.attachment=Anexo +support.tooManyAttachments=Você não pode enviar mais de 3 anexos em uma mensagem. +support.save=Salvar arquivo para o disco +support.messages=Mensagens +support.input.prompt=Insira sua mensagem... +support.send=Enviar +support.addAttachments=Adicionar arquivos +support.closeTicket=Fechar ticket +support.attachments=Anexos: +support.savedInMailbox=Mensagem guardada na caixa de correio do destinatário. +support.arrived=Mensagem chegou no destinatário +support.acknowledged=O destinatário confirmou a chegada da mensagem +support.error=O destinatário não pôde processar a mensagem. Erro: {0} +support.buyerAddress=Endereço do comprador de BTC +support.sellerAddress=Endereço do vendedor de BTC +support.role=Função +support.agent=Support agent +support.state=Estado +support.chat=Chat +support.closed=Fechado +support.open=Aberto +support.process=Process +support.buyerOfferer=Comprador de BTC / Ofetante +support.sellerOfferer=Vendedor de BTC / Ofertante +support.buyerTaker=Comprador de BTC / Aceitador da oferta +support.sellerTaker=Vendedor de BTC / Aceitador da oferta + +support.backgroundInfo=Bisq não é uma empresa, então ela lida com disputas de uma forma diferente.\n\nComerciantes podem se comunicar dentro do aplicativo usando um chat seguro na tela de negociações em aberto para tentar resolver conflitos entre eles mesmos. Se isto não for o suficiente, um mediador pode intervir para ajudar. O mediador irá avaliar a situação e sugerir um pagamento. Se ambos comerciantes aceitarem essa sugestão, a transação de pagamento é finalizada e a negociação é fechada. Se um or ambos os comerciantes não concordarem com o pagamento sugerido pelo mediador, eles podem solicitar arbitragem. O árbitro irá reavaliar a situação e, se justificado, pagará pessoalmente o comerciante e então solicitará reembolso deste pagamento à DAO Bisq. +support.initialInfo=Por favor, entre a descrição do seu problema no campo de texto abaixo. Informe o máximo de informações que puder para agilizar a resolução da disputa.\n\nSegue uma lista com as informações que você deve fornecer:\n\t● Se você está comprando BTC: Você fez a transferência do dinheiro ou altcoin? Caso afirmativo, você clicou no botão 'pagamento iniciado' no aplicativo?\n\t● Se você está vendendo BTC: Você recebeu o pagamento em dinheiro ou em altcoin? Caso afirmativo, você clicou no botão 'pagamento recebido' no aplicativo?\n\t● Qual versão da Bisq você está usando?\n\t● Qual sistema operacional você está usando?\n\t● Se seu problema é com falhas em transações, por favor considere usar um novo diretório de dados.\n\t Às vezes, o diretório de dados pode ficar corrompido, causando bugs estranhos.\n\t Veja mais: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPor favor, familiarize-se com as regras básicas do processo de disputa:\n\t● Você precisa responder às solicitações de {0}' dentro do prazo de 2 dias.\n\t● Mediadores respondem dentro de 2 dias. Ábitros respondem dentro de 5 dias úteis.\n\t● O período máximo para uma disputa é de 14 dias.\n\t● Você deve cooperar com o {1} e providenciar as informações requisitadas para comprovar o seu caso.\n\t● Você aceitou as regras estipuladas nos termos de acordo do usuário que foi exibido na primeira vez em que você iniciou o aplicativo. \nVocê pode saber mais sobre o processo de disputa em: {2} +support.systemMsg=Mensagem do sistema: {0} +support.youOpenedTicket=Você abriu um pedido de suporte.\n\n{0}\n\nBisq versão: {1} +support.youOpenedDispute=Você abriu um pedido para uma disputa.\n\n{0}\n\nBisq versão: {1} +support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVersão do Bisq: {1} +support.peerOpenedTicket=O seu parceiro de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão do Bisq: {1} +support.peerOpenedDispute=O seu parceiro de negociação solicitou uma disputa.\n\n{0}\n\nVersão do Bisq: {1} +support.peerOpenedDisputeForMediation=O seu parceiro de negociação solicitou mediação.\n\n{0}\n\nVersão do Bisq: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Endereço do nó do mediador: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preferências +settings.tab.network=Informações da rede +settings.tab.about=Sobre + +setting.preferences.general=Preferências gerais +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Desvio máx. do preço do mercado +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Impedir modo de economia de energia +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Valores acima de {0}% não são permitidos. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Usar valor personalizado +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Pares ignorados [endereço onion:porta] +setting.preferences.ignoreDustThreshold=Mín. valor de output não-poeira +setting.preferences.currenciesInList=Moedas na lista de preços de mercado +setting.preferences.prefCurrency=Moeda de preferência +setting.preferences.displayFiat=Exibir moedas nacionais +setting.preferences.noFiat=Não há moedas nacionais selecionadas +setting.preferences.cannotRemovePrefCurrency=Você não pode remover a moeda preferencial de exibição selecionada +setting.preferences.displayAltcoins=Exibir altcoins +setting.preferences.noAltcoins=Não há altcoins selecionadas +setting.preferences.addFiat=Adicionar moeda nacional +setting.preferences.addAltcoin=Adicionar altcoin +setting.preferences.displayOptions=Opções de exibição +setting.preferences.showOwnOffers=Exibir minhas ofertas no livro de ofertas +setting.preferences.useAnimations=Usar animações +setting.preferences.useDarkMode=Usar modo escuro +setting.preferences.sortWithNumOffers=Ordenar pelo nº de ofertas/negociações +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Esquecer marcações \"Não exibir novamente\" +settings.preferences.languageChange=Aplicar a mudança de idioma em todas as telas requer uma reinicialização. +settings.preferences.supportLanguageWarning=Em caso de disputa, por favor note que a mediação é feita em {0} e a arbitração em {1}. +setting.preferences.daoOptions=Opções da DAO +setting.preferences.dao.resyncFromGenesis.label=Reconstruir o estado da DAO à partir da tx genesis +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Executar Bisq como nó completo DAO +setting.preferences.dao.rpcUser=Nome de usuário de RPC +setting.preferences.dao.rpcPw=Senha de RPC +setting.preferences.dao.blockNotifyPort=Bloquear porta de notificação +setting.preferences.dao.fullNodeInfo=Para executar o Bisq como nó completo da DAO você precisa ter Bitcoin Core em rodando localmente e RPC ativado. Todos os requisitos estão documentados em '' {0} ''. +setting.preferences.dao.fullNodeInfo.ok=Abrir página de documentos +setting.preferences.dao.fullNodeInfo.cancel=Não, eu fico com o modo nó lite +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Nome +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Rede Bitcoin +settings.net.p2pHeader=Rede Bisq +settings.net.onionAddressLabel=Meu endereço onion +settings.net.btcNodesLabel=Usar nodos personalizados do Bitcoin Core +settings.net.bitcoinPeersLabel=Pares conectados +settings.net.useTorForBtcJLabel=Usar Tor na rede Bitcoin +settings.net.bitcoinNodesLabel=Conexão a nodos do Bitcoin Core +settings.net.useProvidedNodesRadio=Usar nodos do Bitcoin Core fornecidos +settings.net.usePublicNodesRadio=Usar rede pública do Bitcoin +settings.net.useCustomNodesRadio=Usar nodos personalizados do Bitcoin Core +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=Não, usar os nodos fornecidos +settings.net.warn.usePublicNodes.usePublic=Sim, usar rede pública +settings.net.warn.useCustomNodes.B2XWarning=Certifique-se de que o seu nodo Bitcoin é um nodo Bitcoin Core confiável!\n\nAo se conectar a nodos que não estão seguindo as regras de consenso do Bitcoin Core, você pode corromper a sua carteira e causar problemas no processo de negociação.\n\nOs usuários que se conectam a nodos que violam as regras de consenso são responsáveis pelos danos que forem criados por isso. As disputas causadas por esse motivo serão decididas a favor do outro negociante. Nenhum suporte técnico será fornecido para os usuários que ignorarem esse aviso e os mecanismos de proteção! +settings.net.warn.invalidBtcConfig=A conexão com a rede Bitcoin falhou porque suas configurações são inválidas.\n\nSuas configurações foram resetadas para utilizar os nós fornecidos da rede Bitcoin. É necessário reiniciar o aplicativo. +settings.net.localhostBtcNodeInfo=Informações básicas: Bisq busca por um nó Bitcoin local na inicialização. Caso encontre, Bisq irá comunicar com a rede Bitcoin exclusivamente através deste nó. +settings.net.p2PPeersLabel=Pares conectados +settings.net.onionAddressColumn=Endereço onion +settings.net.creationDateColumn=Estabelecida +settings.net.connectionTypeColumn=Entrada/Saída +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Ping +settings.net.sentBytesColumn=Enviado +settings.net.receivedBytesColumn=Recebido +settings.net.peerTypeColumn=Tipo +settings.net.openTorSettingsButton=Abrir configurações do Tor + +settings.net.versionColumn=Versão +settings.net.subVersionColumn=Subversão +settings.net.heightColumn=Altura + +settings.net.needRestart=Você precisa reiniciar o programa para aplicar esta alteração.\nDeseja fazer isso agora? +settings.net.notKnownYet=Ainda desconhecido... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[Endeço IP:porta | nome do host:porta | endereço onion:porta] (seperados por vírgulas). A porta pode ser omitida quando a porta padrão (8333) for usada. +settings.net.seedNode=Nó semente +settings.net.directPeer=Par (direto) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Par +settings.net.inbound=entrada +settings.net.outbound=saída +settings.net.reSyncSPVChainLabel=Ressincronizar SPV chain +settings.net.reSyncSPVChainButton=Remover arquivo SPV e ressincronizar +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=O arquivo SPV chain foi removido. Por favor, tenha paciência, pois a ressincronização com a rede pode demorar. +settings.net.reSyncSPVAfterRestartCompleted=A ressincronização terminou. Favor reiniciar o programa. +settings.net.reSyncSPVFailed=Não foi possível apagar o arquivo da SPV chain.\nErro: {0} +setting.about.aboutBisq=Sobre Bisq +setting.about.about=Bisq é um software de código aberto que facilita a troca de Bitcoin por moedas nacionais (e outras criptomoedas) através de uma rede ponto-a-ponto descentralizada, protegendo a privacidade dos usuários. Descubra mais sobre o Bisq no site do projeto. +setting.about.web=Site do Bisq +setting.about.code=Código fonte +setting.about.agpl=Licença AGPL +setting.about.support=Suporte Bisq +setting.about.def=Bisq não é uma empresa — é um projeto aberto à participação da comunidade. Se você tem interesse em participar do projeto ou apoiá-lo, visite os links abaixo. +setting.about.contribute=Contribuir +setting.about.providers=Provedores de dados +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Preços de mercado fornecidos por +setting.about.feeEstimation.label=Estimativa da taxa de mineração fornecida por +setting.about.versionDetails=Detalhes da versão +setting.about.version=Versão do programa +setting.about.subsystems.label=Versões dos subsistemas +setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagens P2P: {1}; Versão do banco de dados local: {2}; Versão do protocolo de negociação: {3} + +setting.about.shortcuts=atalhos +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navegar para o menu principal +setting.about.shortcuts.menuNav.value=Para ir ao menu principal, pressione: "ctr" ou "alt" ou "cmd" com um botão numérico de 1 a 9 + +setting.about.shortcuts.close=Fechar Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo +setting.about.shortcuts.closePopup.value=botão "Esc" + +setting.about.shortcuts.chatSendMsg=Enviar mensagem de chat ao negociador +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Abrir disputa +setting.about.shortcuts.openDispute.value=Selecione negociação pendente e clique: {0} + +setting.about.shortcuts.walletDetails=Abrir janela de detalhes da carteira + +setting.about.shortcuts.openEmergencyBtcWalletTool=Abrir ferramenta de emergência da carteira BTC + +setting.about.shortcuts.openEmergencyBsqWalletTool=Abrir ferramenta de emergência da carteira BSQ + +setting.about.shortcuts.showTorLogs=Ativar registro de logs para mensagens Tor de níveis entre DEBUG e WARN + +setting.about.shortcuts.manualPayoutTxWindow=Abrir janela de pagamento manual do multisig 2-de-2 da transação de depósito + +setting.about.shortcuts.reRepublishAllGovernanceData=Republicar dados de governança da DAO (propostas, votos) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Registrar árbitro (apenas mediador/árbitro) +setting.about.shortcuts.registerArbitrator.value=Navegue até à conta e pressione: {0} + +setting.about.shortcuts.registerMediator=Registrar mediador (apenas mediador/árbitro) +setting.about.shortcuts.registerMediator.value=Navegue até à conta e pressione: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir janela para assinar idade da conta (apenas para árbitros legados) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navegue até a tela de árbitro legado e pressione: {0} + +setting.about.shortcuts.sendAlertMsg=Enviar alerta ou atualizar mensagem (atividade privilegiada) + +setting.about.shortcuts.sendFilter=Definir Filtro (atividade privilegiada) + +setting.about.shortcuts.sendPrivateNotification=Enviar notificação privada ao par (atividade privilegiada) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Registro de mediador +account.tab.refundAgentRegistration=Registro de agente de reembolsos +account.tab.signing=Signing +account.info.headline=Bem vindo à sua conta Bisq +account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais & altcoins e criar um backup da sua carteira e dados da sua conta.\n\nUma nova carteira Bitcoin foi criada na primeira vez em que você iniciou a Bisq.\nNós encorajamos fortemente que você anote as palavras semente da sua carteira Bitcoin (veja a aba no topo) e considere adicionar uma senha antes de depositar fundos. Depósitos e retiradas de Bitcoin são gerenciados na seção "Fundos".\n\nNota de privacidade & segurança: visto que a Bisq é uma exchange decentralizada, todos os seus dados são mantidos no seu computador. Não existem servidores, então não temos acesso às suas informações pessoais, seus fundos ou até mesmo ao seu endereço IP. Dados como número de conta bancária, endereços de Bitcoin & altcoin, etc apenas são compartilhados com seu parceiro de negociação para completar as negociações iniciadas por você (em caso de disputa, o mediador ou árbitro verá as mesmas informações que seu parceiro de negociação). + +account.menu.paymentAccount=Contas de moedas nacionais +account.menu.altCoinsAccountView=Contas de altcoins +account.menu.password=Senha da carteira +account.menu.seedWords=Semente da carteira +account.menu.walletInfo=Wallet info +account.menu.backup=Backup +account.menu.notifications=Notificações + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Chave pública + +account.arbitratorRegistration.register=Registro +account.arbitratorRegistration.registration=Registro de {0} +account.arbitratorRegistration.revoke=Revogar +account.arbitratorRegistration.info.msg=Por favor, note que você precisa estar disponível por 15 dias após a revogação visto que podem existir negociações em que você é usado como {0}. O período máximo de negociação é de 8 dias e o processo de disputa pode durar até 7 dias. +account.arbitratorRegistration.warn.min1Language=Você precisa escolher pelo menos 1 idioma.\nNós adicionamos o idioma padrão para você. +account.arbitratorRegistration.removedSuccess=O seu registro foi removido com sucesso da rede Bisq. +account.arbitratorRegistration.removedFailed=Não foi possível remover o registro.{0} +account.arbitratorRegistration.registerSuccess=Você se registrou com sucesso na rede Bisq. +account.arbitratorRegistration.registerFailed=Não foi possível completar o registro.{0} + +account.altcoin.yourAltcoinAccounts=Suas contas de altcoins +account.altcoin.popup.wallet.msg=Por favor, certifique-se de seguir os requisitos para uso de carteiras {0} como descritos na página {1}.\nUsar carteiras de exchanges centralizadas onde (a) você não tem controle das suas chaves privadas ou (b) não se usa um software de carteira compatível é arriscado: você pode perder seus fundos negociados!\nO mediador ou árbitro não é um especialista em {2} e não pode ajudar em tais casos. +account.altcoin.popup.wallet.confirm=Eu entendo e confirmo que sei qual carteira preciso usar. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Negociar MSR Bisq requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar MSR, você deve usar uma destas: carteira oficial GUI Masari, carteira CLI Masari com a opção store-tx-info habilitada (habilitada por padrão) ou a carteira web Masari (https://wallet.getmasari.org). Por favor, certifique-se que você tenha acesso à chave da transação pois esta seria necessária em caso de uma disputa.\nmasari-wallet-cli (use o comando get_tx_key)\nmasari-wallet-gui (vá até a aba histórico e clique no botão (P) para prova de pagamento)\n\nCarteira Web Masari (vá até Conta -> histórico de transações e veja os detalhes da sua transação enviada.)\n\nVerificação pode ser feita dentro da carteira.\nmasari-wallet-cli : usando um comando (check_tx_key).\nmasari-wallet-gui : na página Avançado > Comprovar/Verificar.\nVerificação pode ser feita via explorador de blocos.\nAbra o explorador de blocos (https://explorer.getmasari.org) e use a barra de busca para encontrar o hash da sua transação.\nAssim que a transação for encontrada, role até o final da seção 'Comprovar envio' e preencha os detalhes conforme necessário.\nVocê precisa fornecer os seguintes dados ao mediador ou árbitro em caso de uma disputa:\n- Chave privada da transação\n- Hash da transação\n- Endereço público do destinatário\n\nA impossibilidade de fornecer as informações acima ou uso de uma carteira incompatível resultará na perda do caso de disputa. Em caso de uma disputa, o remetente de MSR é responsável por providenciar, ao mediador ou árbitro, a verificação do envio de MSR.\n\nNão é necessário um ID de pagamento, apenas o endereço público convencional.\nCaso tenha dúvidas sobre este processo, solicite ajuda no Discord oficial Masari (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Negociar Solo na Bisq requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar Solo, você deve usar a carteira CLI Solo Network. \n\nSe você estiver usando a carteira CLI, um hash de transação (tx ID) será exibido após o envio de uma transferência. Você deve salvar essa informação. Imediatamente após o envio, você deve utilizar o comando 'get_tx_key' para obter a chave privada da transação. Se você não executar este passo devidamente, você pode não conseguir obter a chave depois.\n\nEm um evento onde uma arbitragem for necessária, você deve apresentar o seguinte para um árbitro ou mediador: 1.) ID da transação, 2.) a chave privada da transação, e 3.) o endereço do destinatário. O mediador ou árbitro irá então verificar a transferência Solo usando o explorador de blocos Solo, buscando pela transação e então usando a função "Provar envio" (https://explorer.minesolo.com/).\n\nA impossibilidade de fornecer as informações requeridas ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, In all cases of dispute, o remetente de Solo arca 100% com a responsabilidade de verificar as transações para um árbitro ou mediador.\n\nCaso não entenda estes requisitos, não negocie na Bisq. Procure ajuda no Discord da Solo Network primeiro. (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Negociar CASH2 na Bisq requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar CASH2, você deve usar a carteira Cash2 versão 3 ou superior. \n\nApós o envio de uma transação, o ID da transação será exibido. Você deve salvar essa informação. Imediatamente após enviar a transação, você deve utilizar o comando 'getTxKey' na simplewallet para obter a chave secreta da transação. Se você não executar este passo devidamente, você pode não conseguir obter a chave depois.\n\nEm um evento onde uma arbitragem for necessária, você deve apresentar o seguinte para um árbitro ou mediador: 1.) ID da transação, 2.) a chave secreta da transação, e 3.) o endereço do destinatário Cash2. O mediador ou árbitro irá então verificar a transferência CASH2 usando o explorador de blocos CASH2 (https://blocks.cash2.org).\n\nA impossibilidade de fornecer as informações requeridas ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, In all cases of dispute, o remetente de CASH2 arca 100% com a responsabilidade de verificar as transações para um árbitro ou mediador.\n\nCaso não entenda estes requisitos, não negocie na Bisq. Procure ajuda no Discord da Cash2 primeiro. (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Ao usar Zcash você só pode usar endereços transparentes(que começam com t), não os z-addresses (privados), pois o mediador e o árbitro não conseguiriam verificar a transação com endereços privados num explorador de blocos. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Ao usar Zcoin você só pode usar endereços transparentes(rastreáveis), não os inrrastreáveis, pois o mediador e o árbitro não conseguiriam verificar a transação com endereços irrastreáveis num explorador de blocos. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requer um processo interativo entre o remetente e o destinatário para criar a transação. Certifique-se de seguir as instruções da página web do projeto GRIN para enviar e receber de forma confiável o GRIN (o destinatário precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nBisq suporta apenas o formato de URL da carteira Grinbox (Wallet713).\n\nO remetente GRIN é obrigado a fornecer prova de que ele enviou GRIN com sucesso. Se a carteira não puder fornecer essa prova, uma potencial disputa será resolvida em favor do destinatário de GRIN. Certifique-se de usar o software Grinbox mais recente, que suporta a prova de transação e que você entende o processo de transferência e receção do GRIN, bem como criar a prova.\n\nConsulte https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para obter mais informações sobre a ferramenta de prova Grinbox. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM requer um processo interativo entre o remetente e o destinatário para criar a transação.\n\nCertifique-se de seguir as instruções da página Web do projeto BEAM para enviar e receber BEAM de forma confiável (o destinatário precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO remetente BEAM é obrigado a fornecer prova de que ele enviou o BEAM com sucesso. Certifique-se de usar uma carteira que possa produzir tal prova. Se a carteira não puder fornecer a prova, uma disputa potencial será resolvida em favor do destinarário do BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Para negociar com L-BTC na Bisq é preciso entender o seguinte:\n\nQuando se recebe L-BTC de uma negociação na Bisq, você não pode usar a carteira móvel Blockstream Green ou uma carteira de exchange. Você só pode receber L-BTC numa carteira Liquid Elements Core, ou outra carteira L-BTC que lhe permita obter a blinding key para o seu endereço blinded de L-BTC.\n\nNo caso de mediação ou se uma disputa acontecer, você precisa divulgar ao mediador, ou agente de reembolsos, a blinding key do seu endereço receptor de L-BTC para que ele possa verificar os detalhes da sua Transação Confidencial no node próprio deles.\n\nCaso essa informação não seja fornecida ao mediador ou agente de reembolsos você corre o risco de perder a disputa. Em todos os casos de disputa o recebedor de L-BTC tem 100% de responsabilidade em fornecer a prova criptográfica ao mediador ou agente de reembolsos.\n\nSe você não entendeu esses requisitos, por favor não negocie L-BTC na Bisq. + +account.fiat.yourFiatAccounts=Suas contas de moeda nacional + +account.backup.title=Backup da carteira +account.backup.location=Local de backup +account.backup.selectLocation=Selecione local para backup +account.backup.backupNow=Fazer backup agora (o backup não é criptografado!) +account.backup.appDir=Pasta de dados do programa +account.backup.openDirectory=Abrir pasta +account.backup.openLogFile=Abrir arquivo de Log +account.backup.success=Backup salvo com sucesso em:\n{0} +account.backup.directoryNotAccessible=A pasta escolhida não está acessível. {0} + +account.password.removePw.button=Remover senha +account.password.removePw.headline=Remover proteção de senha da carteira +account.password.setPw.button=Definir senha +account.password.setPw.headline=Definir proteção de senha da carteira +account.password.info=Ao proteger a carteira com uma senha, você precisará digitá-la sempre que abrir o programa, retirar bitcoins da carteira e restaurar a carteira a partir da semente. + +account.seed.backup.title=Fazer backup das palavras-semente da carteira +account.seed.info=Por favor, anote em um papel a data e as palavras-semente da carteira! Com essas informações, você poderá recuperar sua carteira à qualquer momento.\nA semente exibida é usada tanto para a carteira BTC quanto para a carteira BSQ.\n\nVocê deve anotá-las em uma folha de papel. Jamais anote as palavras em um arquivo no seu computador ou em seu e-mail.\n\nNote que a semente da carteira NÃO substitui um backup.\nPara fazer isso, você precisa fazer backup da pasta do Bisq na seção \"Conta/Backup\".\nA importação da semente da carteira só é recomendada em casos de emergência. O programa não funcionará corretamente se você não recuperá-lo através de um backup! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Você não definiu uma senha para carteira, que protegeria a exibição das palavras-semente.\n\nGostaria de exibir as palavras-semente? +account.seed.warn.noPw.yes=Sim, e não me pergunte novamente +account.seed.enterPw=Digite a senha para ver a semente da carteira +account.seed.restore.info=Faça um backup antes de aplicar a restauração a partir de palavras-semente. Esteja ciente de que a restauração da carteira é apenas para casos de emergência e pode causar problemas com a base de dados interna da carteira.\nNão é uma maneira de aplicar um backup! Por favor, use um backup do diretório de dados do programa para restaurar um estado anterior do programa.\n\nDepois de restaurado, o programa será desligado automaticamente. Após ser reiniciado, o programa será ressincronizado com a rede Bitcoin. Isso pode demorar um pouco e aumenta ro consumo de CPU, especialmente se a carteira for mais antiga e tiver muitas transações. Por favor, evite interromper esse processo, caso contrário, você pode precisar excluir o diretório da corrente SPV novamente ou repetir o processo de restauração. +account.seed.restore.ok=Ok, restaurar e desligar o Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Configurações +account.notifications.download.label=Baixar app móvel +account.notifications.waitingForWebCam=Aguardando webcam... +account.notifications.webCamWindow.headline=Escanear código QR do celular +account.notifications.webcam.label=Usar webcam +account.notifications.webcam.button=Escanear código QR +account.notifications.noWebcam.button=Eu não tenho uma webcam +account.notifications.erase.label=Limpar notificações no celular +account.notifications.erase.title=Limpar notificações +account.notifications.email.label=Token de pareamento +account.notifications.email.prompt=Insira o token de pareamento que você recebeu por e-mail +account.notifications.settings.title=Configurações +account.notifications.useSound.label=Reproduzir som de notificação no celular +account.notifications.trade.label=Receber mensagens de negociação +account.notifications.market.label=Receber alertas de oferta +account.notifications.price.label=Receber alertas de preço +account.notifications.priceAlert.title=Alertas de preço +account.notifications.priceAlert.high.label=Avisar se o preço do BTC estiver acima de +account.notifications.priceAlert.low.label=Avisar se o preço do BTC estiver abaixo de +account.notifications.priceAlert.setButton=Definir alerta de preço +account.notifications.priceAlert.removeButton=Remover alerta de preço +account.notifications.trade.message.title=O estado da negociação mudou +account.notifications.trade.message.msg.conf=A transação de depósito para a negociação com o ID {0} foi confirmada. Por favor, abra o seu aplicativo Bisq e realize o pagamento. +account.notifications.trade.message.msg.started=O comprador de BTC iniciou o pagarmento para a negociação com o ID {0}. +account.notifications.trade.message.msg.completed=A negociação com o ID {0} foi completada. +account.notifications.offer.message.title=A sua oferta foi aceita +account.notifications.offer.message.msg=A sua oferta com o ID {0} foi aceita +account.notifications.dispute.message.title=Nova mensagem de disputa +account.notifications.dispute.message.msg=Você recebeu uma mensagem de disputa pela negociação com o ID {0} + +account.notifications.marketAlert.title=Alertas de oferta +account.notifications.marketAlert.selectPaymentAccount=Ofertas correspondendo à conta de pagamento +account.notifications.marketAlert.offerType.label=Tenho interesse em +account.notifications.marketAlert.offerType.buy=Ofertas de compra (eu quero vender BTC) +account.notifications.marketAlert.offerType.sell=Ofertas de venda (eu quero comprar BTC) +account.notifications.marketAlert.trigger=Distância do preço da oferta (%) +account.notifications.marketAlert.trigger.info=Ao definir uma distância de preço, você só irá receber um alerta quando alguém publicar uma oferta que atinge (ou excede) os seus critérios. Por exemplo: você quer vender BTC, mas você só irá vender a um prêmio de 2% sobre o preço de mercado atual. Ao definir esse campo para 2%, você só irá receber alertas de ofertas cujos preços estão 2% (ou mais) acima do preço de mercado atual. +account.notifications.marketAlert.trigger.prompt=Distância percentual do preço do mercado (ex: 2.50%, -0.50%, etc.) +account.notifications.marketAlert.addButton=Inserir alerta de oferta +account.notifications.marketAlert.manageAlertsButton=Gerenciar alertas de oferta +account.notifications.marketAlert.manageAlerts.title=Gerenciar alertas de oferta +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conta de pagamento +account.notifications.marketAlert.manageAlerts.header.trigger=Preço gatilho +account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta +account.notifications.marketAlert.message.title=Alerta de oferta +account.notifications.marketAlert.message.msg.below=abaixo +account.notifications.marketAlert.message.msg.above=acima +account.notifications.marketAlert.message.msg=Uma nova oferta ''{0} {1}'' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento ''{5}'' foi publicada no livro de ofertas do Bisq.\nID da oferta: {6}. +account.notifications.priceAlert.message.title=Alerta de preço para {0} +account.notifications.priceAlert.message.msg=O seu preço de alerta foi atingido. O preço atual da {0} é {1} {2} +account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor, use a opção e-mail para enviar o token e a chave de criptografia do seu celular para o Bisq +account.notifications.priceAlert.warning.highPriceTooLow=O preço mais alto deve ser maior do que o preço mais baixo +account.notifications.priceAlert.warning.lowerPriceTooHigh=O preço mais baixo deve ser menor do que o preço mais alto + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Fatos & Números +dao.tab.bsqWallet=Carteira BSQ +dao.tab.proposals=Governança +dao.tab.bonding=Vínculo +dao.tab.proofOfBurn=Taxa de listagem de ativos/Prova de destruição +dao.tab.monitor=Monitor da rede +dao.tab.news=Notícias + +dao.paidWithBsq=pago com BSQ +dao.availableBsqBalance=Disponível para gastos (verificado + outputs de trocos não-confirmados) +dao.verifiedBsqBalance=Saldo de todas UTXOs verificadas +dao.unconfirmedChangeBalance=Saldo de todos outputs de troco não confirmados +dao.unverifiedBsqBalance=Saldo de todas as transações não verificadas (aguardando confirmação do bloco) +dao.lockedForVoteBalance=Usado para votação +dao.lockedInBonds=Bloqueado em vínculos +dao.availableNonBsqBalance=Saldo não-BSQ disponível (BTC) +dao.reputationBalance=Valor de mérito (não gastável) + +dao.tx.published.success=Sua transação foi publicada com sucesso. +dao.proposal.menuItem.make=Fazer proposta +dao.proposal.menuItem.browse=Propostas abertas +dao.proposal.menuItem.vote=Votar em propostas +dao.proposal.menuItem.result=Resultados dos votos +dao.cycle.headline=Ciclo de votação +dao.cycle.overview.headline=Visão geral do ciclo de votação +dao.cycle.currentPhase=Fase atual +dao.cycle.currentBlockHeight=Altura do bloco atual +dao.cycle.proposal=Fase de proposta +dao.cycle.proposal.next=Próxima fase de proposta +dao.cycle.blindVote=Fase de voto fechado +dao.cycle.voteReveal=Fase de revelação dos votos +dao.cycle.voteResult=Resultado dos votos +dao.cycle.phaseDuration={0} blocos (≈{1}); Bloco {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Bloco {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=A transação da revelação da votação foi publicada +dao.voteReveal.txPublished=Sua transação de revelação do voto com o ID de transação {0} foi publicada com sucesso.\n\nIsso acontece automaticamente pelo software se você tiver participado na votação da DAO. + +dao.results.cycles.header=Ciclos +dao.results.cycles.table.header.cycle=Ciclo +dao.results.cycles.table.header.numProposals=Propostas +dao.results.cycles.table.header.voteWeight=Peso do voto +dao.results.cycles.table.header.issuance=Emissão + +dao.results.results.table.item.cycle=Ciclo {0} começou: {1} + +dao.results.proposals.header=Propostas do ciclo selecionado +dao.results.proposals.table.header.nameLink=Nome/link +dao.results.proposals.table.header.details=Detalhes +dao.results.proposals.table.header.myVote=Meu voto +dao.results.proposals.table.header.result=Resultado dos votos +dao.results.proposals.table.header.threshold=Limite +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Resultados dos votos para a proposta selecionada + +dao.results.exceptions=Excepção(s) do resultado dos votos + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Indefinido + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=taxa BSQ do ofertante +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=taxa BSQ do aceitador +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Taxa mínima em BSQ do ofertante +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Taxa mínima em BSQ do aceitador +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Taxa em BTC do ofertante +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Taxa em BTC do aceitador +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Taxa mínima em BTC do ofertante +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Taxa mínima em BTC do aceitador +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Taxa de proposta em BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Taxa de votação em BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Quantia mínima de BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Quantia máxima de BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Quantia mínima de BSQ para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Quantia máxima de BSQ para pedido de reembolso + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Quórum necessário em BSQ para proposta genérica +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Quórum necessário em BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Quórum necessário em BSQ para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Quórum necessário em BSQ para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Quórum necessário em BSQ para remover um ativo +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Quórum necessário em BSQ para pedido de confiscação +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Quórum necessário em BSQ para pedidos de cargos vinculados + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Limite necessário em % para proposta genérica +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Limite necessário em % para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Limite necessário em % para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Limite necessário em % para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Limite necessário em % para remover um ativo +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Limite necessário em % para pedido de confiscação +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Limite necessário em % para pedidos de cargos vinculados + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Endereço BTC do destinatário + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Taxa de listagem do ativo por dia +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Volume mínimo de negócio para ativos + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Tempo de bloqueio para tx de pagamento de negociação alternativa +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Taxa de arbitragem em BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Limite máximo de negociação em BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Fator de unidade de cargo vinculado em BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Limite de emissão por ciclo em BSQ + +dao.param.currentValue=Valor atual: {0} +dao.param.currentAndPastValue=Valor atual: {0} (Valor quando a proposta foi feita: {1}) +dao.param.blocks={0} blocos + +dao.results.invalidVotes=Tivemos votos inválidos naquele ciclo de votação. Isso pode acontecer se o voto não foi bem propagado pela rede da Bisq.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Pausa 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Fase de voto fechado +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Pausa 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Fase de revelação dos votos +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Pausa 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Fase de resultado + +dao.results.votes.table.header.stakeAndMerit=Peso do voto +dao.results.votes.table.header.stake=Participação +dao.results.votes.table.header.merit=Ganho +dao.results.votes.table.header.vote=Votar + +dao.bond.menuItem.bondedRoles=Cargos vinculados +dao.bond.menuItem.reputation=Reputação vinculada +dao.bond.menuItem.bonds=Vínculos + +dao.bond.dashboard.bondsHeadline=BSQ vinculado +dao.bond.dashboard.lockupAmount=Bloquear fundos +dao.bond.dashboard.unlockingAmount=Desbloqueando fundos (espere até que o tempo de bloqueio termine) + + +dao.bond.reputation.header=Bloquear um vínculo para reputação +dao.bond.reputation.table.header=Os meus vínculos de reputação +dao.bond.reputation.amount=Quantia de BSQ a bloquear +dao.bond.reputation.time=Tempo de desbloqueio em blocos +dao.bond.reputation.salt=Sal +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Travar +dao.bond.reputation.lockup.headline=Confirmar transação de bloqueio +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Confirmar transação de desbloqueio +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=Todos os vínculos + +dao.bond.bondedReputation=Reputação Vinculada +dao.bond.bondedRoles=Cargos vinculados + +dao.bond.details.header=Detalhes do cargo +dao.bond.details.role=Função +dao.bond.details.requiredBond=Vínculo de BSQ necessário +dao.bond.details.unlockTime=Tempo de desbloqueio em blocos +dao.bond.details.link=Link para descrição do cargo +dao.bond.details.isSingleton=Pode ser aceite por detentores de vários cargos +dao.bond.details.blocks={0} blocos + +dao.bond.table.column.name=Nome +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Tipo de vínculo +dao.bond.table.column.details=Detalhes +dao.bond.table.column.lockupTxId=ID da tx de bloqueio +dao.bond.table.column.bondState=Estado do vínculo +dao.bond.table.column.lockTime=Tempo para destravar +dao.bond.table.column.lockupDate=Travado em + +dao.bond.table.button.lockup=Travar +dao.bond.table.button.unlock=Destravar +dao.bond.table.button.revoke=Revogar + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Ainda não vinculado +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Bloqueio pendente +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Vínculo bloqueado +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Desbloqueio pendente +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Tx de desbloqueio confirmada +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Desbloqueio de vínculo +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Vínculo bloqueado +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Vínculo confiscado + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Cargo vínculado +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Reputação vinculada + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Admin do GitHub +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Admin do Fórum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Admin do Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Admin do YouTube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Mantenedor Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Mantenedor do BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Mantenedor Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Operador do Website +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Operador do Fórum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Operador do nó de semente +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Operador do node de preço +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Operador do nó de BTC +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Operador de mercados +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Operador da transmissão de notificações móveis +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Detentor do nome de domínio +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Admin do DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediador +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Árbitro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Proprietário do endereço BTC de doação + +dao.burnBsq.assetFee=Listagem de ativos +dao.burnBsq.menuItem.assetFee=Taxa de listagem de ativos +dao.burnBsq.menuItem.proofOfBurn=Prova-de-queima +dao.burnBsq.header=Taxa para listagem de ativos +dao.burnBsq.selectAsset=Escolher Ativo +dao.burnBsq.fee=Taxa +dao.burnBsq.trialPeriod=Período de testes +dao.burnBsq.payFee=Pagar taxa +dao.burnBsq.allAssets=Todos os ativos +dao.burnBsq.assets.nameAndCode=Nome do ativo +dao.burnBsq.assets.state=Estado +dao.burnBsq.assets.tradeVolume=Volume de negociação +dao.burnBsq.assets.lookBackPeriod=Período de verificação +dao.burnBsq.assets.trialFee=Taxa para o período de testes +dao.burnBsq.assets.totalFee=Total de taxas pagas +dao.burnBsq.assets.days={0} dias +dao.burnBsq.assets.toFewDays=A taxa do ativo é muito baixa. A quantidade mínima de dias para o período de teste é {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Em período de testes +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Ativamente negociado +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Retirado por inatividade +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Removido por votação + +dao.proofOfBurn.header=Prova-de-queima +dao.proofOfBurn.amount=Quantia +dao.proofOfBurn.preImage=Pré-imagem +dao.proofOfBurn.burn=Queimar +dao.proofOfBurn.allTxs=Todas as transações de Prova-de-queima +dao.proofOfBurn.myItems=Minhas transações de Prova-de-queima +dao.proofOfBurn.date=Data +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transações +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Assinar uma mensagem com a chave da transação de prova-de-queima +dao.proofOfBurn.verify.window.title=Verificar uma mensagem com a chave da transação de prova-de-queima +dao.proofOfBurn.copySig=Copiar assinatura para a área de trabalho +dao.proofOfBurn.sign=Assinar +dao.proofOfBurn.message=Mensagem +dao.proofOfBurn.sig=Assinatura +dao.proofOfBurn.verify=Verificar +dao.proofOfBurn.verificationResult.ok=Verificação realizada com sucesso +dao.proofOfBurn.verificationResult.failed=Erro na verificação + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Pausa antes da fase de votação fechada +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Fase de voto fechado +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Pausa antes da fase de revelação dos votos +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Fase de revelação dos votos +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Pausa antes da fase de resultado +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Fase de resultado da votação + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Voto cego +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Revelar voto +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Resultado dos votos + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Pedido de compensação +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Pedido de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Proposta para um cargo vinculado +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Proposta para remover um ativo +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Proposta para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Proposta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Proposta para confiscação de um vínculo + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Pedido de compensação +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Pedido de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Cargo vínculado +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Removendo uma altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Modificando um parâmetro +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Proposta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscando um titulo de dívida + +dao.proposal.details=Detalhes da proposta +dao.proposal.selectedProposal=Proposta selecionada +dao.proposal.active.header=Propostas do ciclo atual +dao.proposal.active.remove.confirm=Tem certeza de que pretende remover essa proposta?\nA taxa da proposta paga será perdida. +dao.proposal.active.remove.doRemove=Sim, remover minha proposta +dao.proposal.active.remove.failed=Não foi possível remover a proposta. +dao.proposal.myVote.title=Votação +dao.proposal.myVote.accept=Aceitar proposta +dao.proposal.myVote.reject=Rejeitar proposta +dao.proposal.myVote.removeMyVote=Ignorar proposta +dao.proposal.myVote.merit=Peso de voto de BSQ ganho +dao.proposal.myVote.stake=Peso de voto de participação +dao.proposal.myVote.revealTxId=ID de transação de revelação de voto +dao.proposal.myVote.stake.prompt=Máx. participação disponível para votação: {0} +dao.proposal.votes.header=Definir participação para votar e publicar seus votos +dao.proposal.myVote.button=Publicar votos +dao.proposal.myVote.setStake.description=Depois de votar em todas as propostas, você deve definir a sua participação para votação bloqueando BSQ. Quanto mais BSQ você bloquear, mais peso o seu voto terá.\n\nBSQ bloqueado para votação será desbloqueado novamente durante a fase de revelação de voto. +dao.proposal.create.selectProposalType=Escolha o tipo de proposta +dao.proposal.create.phase.inactive=Por favor espere até a próxima fase +dao.proposal.create.proposalType=Tipo de proposta +dao.proposal.create.new=Fazer nova proposta +dao.proposal.create.button=Fazer proposta +dao.proposal.create.publish=Publicar proposta +dao.proposal.create.publishing=Publicação de proposta em progresso ... +dao.proposal=proposta +dao.proposal.display.type=Tipo de proposta +dao.proposal.display.name=Usuário no GitHub +dao.proposal.display.link=Link para mais detalhes +dao.proposal.display.link.prompt=Link para proposta +dao.proposal.display.requestedBsq=Quantia requerida em BSQ +dao.proposal.display.txId=ID de transação de proposta +dao.proposal.display.proposalFee=Taxa de proposta +dao.proposal.display.myVote=Meu voto +dao.proposal.display.voteResult=Resumo do resultado da votação +dao.proposal.display.bondedRoleComboBox.label=Tipo de cargo vinculado +dao.proposal.display.requiredBondForRole.label=Vínculo necessário para cargo +dao.proposal.display.option=Opção + +dao.proposal.table.header.proposalType=Tipo de proposta +dao.proposal.table.header.link=Link +dao.proposal.table.header.myVote=Meu voto +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Remover +dao.proposal.table.icon.tooltip.removeProposal=Remover minha proposta +dao.proposal.table.icon.tooltip.changeVote=Voto atual: ''{0}''. Mudar voto para: ''{1}'' + +dao.proposal.display.myVote.accepted=Aceito +dao.proposal.display.myVote.rejected=Rejeitado +dao.proposal.display.myVote.ignored=Ignorado +dao.proposal.display.myVote.unCounted=Voto não foi incluído no resultado +dao.proposal.myVote.summary=Votado: {0}; Peso do voto: {1} (ganho: {2} + stake {3}) {4} +dao.proposal.myVote.invalid=O voto era inválido + +dao.proposal.voteResult.success=Aceito +dao.proposal.voteResult.failed=Rejeitado +dao.proposal.voteResult.summary=Resultado: {0}; Limite: {1} (necessário > {2}); Quórum: {3} (necessário > {4}) + +dao.proposal.display.paramComboBox.label=Escolha parâmetro para modificar +dao.proposal.display.paramValue=Valor do parâmetro + +dao.proposal.display.confiscateBondComboBox.label=Escolha o vínculo +dao.proposal.display.assetComboBox.label=Ativo para remover + +dao.blindVote=voto fechado + +dao.blindVote.startPublishing=Publicando transação de voto fechado... +dao.blindVote.success=Sua transação de voto fechado foi publicada com sucesso.\n\nNote que você tem que estar online na fase de revelação de votos para que o Bisq possa publicar a transação de revelação de voto. Sem a transação da revelação do voto o seu voto seria inválido! + +dao.wallet.menuItem.send=Enviar +dao.wallet.menuItem.receive=Receber +dao.wallet.menuItem.transactions=Transações + +dao.wallet.dashboard.myBalance=Meu saldo de carteira + +dao.wallet.receive.fundYourWallet=Seu endereço de recebimento BSQ +dao.wallet.receive.bsqAddress=Endereço da carteira BSQ (endereço não utilizado) + +dao.wallet.send.sendFunds=Enviar fundos +dao.wallet.send.sendBtcFunds=Enviar fundos não-BSQ (BTC) +dao.wallet.send.amount=Quantia em BSQ +dao.wallet.send.btcAmount=Quantia em BTC (fundos não-BSQ) +dao.wallet.send.setAmount=Definir quantia a retirar (quantia mínima é {0}) +dao.wallet.send.receiverAddress=Endereço BSQ do destinatário +dao.wallet.send.receiverBtcAddress=Endereço BTC do destinatário +dao.wallet.send.setDestinationAddress=Preencha seu endereço de destino +dao.wallet.send.send=Enviar fundos BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Enviar fundos BTC +dao.wallet.send.sendFunds.headline=Confirmar solicitação de retirada. +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Último bloco verificado: {0} +dao.wallet.chainHeightSyncing=Aguardando blocos... Verificados {0} blocos de {1} +dao.wallet.tx.type=Tipo + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Não reconhecida +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Transação BSQ não verificada +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Transação BSQ inválida +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Transação gênesis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Transferência BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ recebido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ enviado +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Taxa de negociação +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Taxa de pedido de compensação +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Taxa para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Taxa para proposta +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Taxa para voto fechado +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Revelar voto +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Bloquear vínculo +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Desbloquear vínculo +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Taxa de listagem de ativos +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Prova-de-queima +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregular + +dao.tx.withdrawnFromWallet=BTC retirado da carteira +dao.tx.issuanceFromCompReq=Pedido de compensação/emissão +dao.tx.issuanceFromCompReq.tooltip=Pedido de compensação que levou à emissão de novo BSQ.\nData de emissão: {0} +dao.tx.issuanceFromReimbursement=Pedido de reembolso/emissão +dao.tx.issuanceFromReimbursement.tooltip=Solicitação de reembolso que levou à emissão de novo BSQ.\nData de emissão: {0} +dao.proposal.create.missingBsqFunds=Você não tem fundos suficientes para criar a proposta. Se você tem uma transação de BSQ não confirmada, você precisa esperar por uma confirmação da blockchain porque o BSQ é validado somente se estiver incluído num bloco.\nFaltando: {0} + +dao.proposal.create.missingBsqFundsForBond=Você não tem BSQ suficiente para este cargo. Você ainda pode publicar essa proposta, mas precisará do valor completo de BSQ necessário para esse cargo se ela for aceito.\nEm falta: {0} + +dao.proposal.create.missingMinerFeeFunds=Você não tem BTC suficiente para criar a transação da proposta. Todas as transações de BSQ exigem uma taxa de mineração em BTC.\nEm falta: {0} + +dao.proposal.create.missingIssuanceFunds=Você não tem BTC suficiente para criar a transação da proposta. Todas as transações de BSQ exigem uma taxa de mineração em BTC, e as transações de emissão também exigem BTC pela quantia de BSQ solicitado ({0} satoshis/BSQ).\nEm falta: {1} + +dao.feeTx.confirm=Confirmar transação {0} +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=A DAO BISQ +dao.news.bisqDAO.description=O modelo de governança da Bisq é assim como a exchange - descentralizado e resistente à censura. As ferramentas que tornam isso realidade são a DAO da BISQ e o token BSQ. +dao.news.bisqDAO.readMoreLink=Ler mais sobre o DAO da Bisq + +dao.news.pastContribution.title=JÁ FEZ CONTRIBUIÇÕES? SOLICITE BSQ +dao.news.pastContribution.description=Se você já contribuiu para a Bisq, use o endereço BSQ abaixo e faça uma solicitação para fazer parte da distribuição gênese do BSQ. +dao.news.pastContribution.yourAddress=Seu Endereço de Carteira BSQ +dao.news.pastContribution.requestNow=Solicitar agora + +dao.news.DAOOnTestnet.title=RODE O DAO DA BISQ EM NOSSA TESTNET +dao.news.DAOOnTestnet.description=A rede principal da DAO do Bisq ainda não foi lançada, mas você pode aprender sobre a DAO do Bisq executando-a na nossa rede de testes. +dao.news.DAOOnTestnet.firstSection.title=1. Mude para o Modo Testnet da DAO +dao.news.DAOOnTestnet.firstSection.content=Mude para o Testnet da DAO na seção Configurações. +dao.news.DAOOnTestnet.secondSection.title=2. Adquira alguns BSQ +dao.news.DAOOnTestnet.secondSection.content=Solicite BSQ no Slack ou Compre BSQ na Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Participe de um Ciclo de Votação +dao.news.DAOOnTestnet.thirdSection.content=Fazendo propostas e votando em propostas para mudar vários aspetos da Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Use um Explorador de Blocos de BSQ +dao.news.DAOOnTestnet.fourthSection.content=Como o BSQ é bitcoin, você pode ver as transações BSQ em nosso explorador de blocos de bitcoin. +dao.news.DAOOnTestnet.readMoreLink=Leia a documentação completa + +dao.monitor.daoState=Estado da DAO +dao.monitor.proposals=Estado das propostas +dao.monitor.blindVotes=Estado dos votos fechados + +dao.monitor.table.peers=Pares +dao.monitor.table.conflicts=Conflitos +dao.monitor.state=Status +dao.monitor.requestAlHashes=Pedir todos os hashes +dao.monitor.resync=Re-sincronizar estado da DAO +dao.monitor.table.header.cycleBlockHeight=Ciclo / altura do bloco +dao.monitor.table.cycleBlockHeight=Ciclo {0} / bloco {1} +dao.monitor.table.seedPeers=Nó semente: {0} + +dao.monitor.daoState.headline=Estado da DAO +dao.monitor.daoState.table.headline=Corrente de hashes do estado da DAO +dao.monitor.daoState.table.blockHeight=Altura do bloco +dao.monitor.daoState.table.hash=Hash do estado da DAO +dao.monitor.daoState.table.prev=Hash anterior +dao.monitor.daoState.conflictTable.headline=Hashes do estado da DAO de pares em conflito +dao.monitor.daoState.utxoConflicts=Conflitos de UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Altura do bloco: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Soma do total de UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Soma do total de BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=O estado da DAO não está sincronizado com a rede. Após reiniciar o estado da DAO vai resincronizar. + +dao.monitor.proposal.headline=Estado de propostas +dao.monitor.proposal.table.headline=Corrente dos hashes de estado da proposta +dao.monitor.proposal.conflictTable.headline=Hashes de estado da proposta de pares em conflito + +dao.monitor.proposal.table.hash=Hash do estado da proposta +dao.monitor.proposal.table.prev=Hash anterior +dao.monitor.proposal.table.numProposals=Nº de propostas + +dao.monitor.isInConflictWithSeedNode=Os seus dados locais não estão em consenso com pelo menos um nó semente. Por favor, re-sincronizar o estado da DAO. +dao.monitor.isInConflictWithNonSeedNode=Um dos seus pares não está em consenso com a rede, mas o seu nó está em sincronia com os nós semente. +dao.monitor.daoStateInSync=Seu nó local está em consenso com a rede + +dao.monitor.blindVote.headline=Estado de votos fechados +dao.monitor.blindVote.table.headline=Corrente de hashes de estado de voto fechado +dao.monitor.blindVote.conflictTable.headline=Hashes de estado de voto fechado de pares em conflito +dao.monitor.blindVote.table.hash=Hash de estado de voto fechado +dao.monitor.blindVote.table.prev=Hash anterior +dao.monitor.blindVote.table.numBlindVotes=Nº de votos fechados + +dao.factsAndFigures.menuItem.supply=Estoque de BSQ +dao.factsAndFigures.menuItem.transactions=Transações BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Média de 90 dias BSQ/BTC +dao.factsAndFigures.dashboard.avgPrice30=Média de 30 dias BSQ/BTC +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=Total de BSQ disponível +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ emitido vs. BSQ queimado + +dao.factsAndFigures.supply.issued=BSQ emitido +dao.factsAndFigures.supply.compReq=Pedidos de compensação +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ emitido na transação genesis +dao.factsAndFigures.supply.compRequestIssueAmount=BDQ emitido para pedidos de compensação +dao.factsAndFigures.supply.reimbursementAmount=BSQ emitido para pedidos de reembolso +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ destruído + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Volume de negociação +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Estado global de BSQ bloqueado +dao.factsAndFigures.supply.totalLockedUpAmount=Bloqueado em vínculos +dao.factsAndFigures.supply.totalUnlockingAmount=Desbloqueando BSQ de vínculos +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ desbloqueado de vínculos +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ confiscado de vínculos +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Transação gênesis +dao.factsAndFigures.transactions.genesisBlockHeight=Altura do bloco gênese +dao.factsAndFigures.transactions.genesisTxId=ID da transação gênese +dao.factsAndFigures.transactions.txDetails=Estatísticas das transações de BSQ +dao.factsAndFigures.transactions.allTx=Nº de todas as transações BSQ +dao.factsAndFigures.transactions.utxo=Nº de todos os outputs de transações não gastos +dao.factsAndFigures.transactions.compensationIssuanceTx=Nº de todas as transações de emissão de pedido de compensação +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Nº de todas as transações de emissão de pedido de reembolso +dao.factsAndFigures.transactions.burntTx=Nº de todas transações de pagamentos de taxa +dao.factsAndFigures.transactions.invalidTx=Nº de todas as transações inválidas +dao.factsAndFigures.transactions.irregularTx=Nº de todas as transações irregulares + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Saldo disponível + +contractWindow.title=Detalhes da disputa +contractWindow.dates=Data da oferta / Data da negociação +contractWindow.btcAddresses=Endereço bitcoin do comprador de BTC / vendedor de BTC +contractWindow.onions=Endereço de rede comprador de BTC / vendendor de BTC +contractWindow.accountAge=Idade da conta do comprador de BTC / vendedor de BTC +contractWindow.numDisputes=Nº de disputas comprador de BTC / vendedor de BTC: +contractWindow.contractHash=Hash do contrato + +displayAlertMessageWindow.headline=Informação importante! +displayAlertMessageWindow.update.headline=Informação importante de atualização! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=Arquivos: +displayUpdateDownloadWindow.downloadingFile=Baixando: {0} +displayUpdateDownloadWindow.verifiedSigs=Assinatura verificada com as chaves: +displayUpdateDownloadWindow.status.downloading=Baixando arquivos... +displayUpdateDownloadWindow.status.verifying=Verificando assinatura... +displayUpdateDownloadWindow.button.label=Baixar instalador e verificar assinatura +displayUpdateDownloadWindow.button.downloadLater=Baixar depois +displayUpdateDownloadWindow.button.ignoreDownload=Ignorar essa versão +displayUpdateDownloadWindow.headline=Uma nova atualização para o Bisq está disponível! +displayUpdateDownloadWindow.download.failed.headline=Erro no download +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=A nova versão foi baixada com sucesso e teve a sua assinatura verificada.\n\nPara usá-la, abra a pasta de downloads, feche o programa e instale a nova versão. +displayUpdateDownloadWindow.download.openDir=Abrir pasta de download + +disputeSummaryWindow.title=Resumo +disputeSummaryWindow.openDate=Data da abertura do ticket +disputeSummaryWindow.role=Função do negociador +disputeSummaryWindow.payout=Pagamento da quantia negociada +disputeSummaryWindow.payout.getsTradeAmount={0} BTC fica com o pagamento da negociação +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Pagamento personalizado +disputeSummaryWindow.payoutAmount.buyer=Quantia do pagamento do comprador +disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor +disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador +disputeSummaryWindow.reason=Motivo da disputa +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug (problema técnico) +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sem resposta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Golpe (Scam) +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Outro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Notas de resumo +disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo +disputeSummaryWindow.close.button=Fechar ticket + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=Você também precisa fechar o ticket dos parceiros de negociação! +disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Comprador recebe {0} no endereço: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Vendedor recebe {0} no endereço: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline={0} ferramenta de emergência da carteira +emptyWalletWindow.info=Por favor, utilize essa opção apenas em caso de emergência, caso você não consiga acessar seus fundos a partir do programa.\n\nNote que todas as ofertas abertas serão fechadas automaticamente quando você utilizar esta ferramenta.\n\nAntes de usar esta ferramenta, faça um backup da sua pasta de dados. Você pode fazer isso em \"Conta/Backup\".\n\nHavendo qualquer problema, avise-nos através do GitHub ou do fórum Bisq, para que assim possamos investigar o que causou o problema. +emptyWalletWindow.balance=Seu saldo disponível na carteira +emptyWalletWindow.bsq.btcBalance=Saldo de satoshis não-BSQ + +emptyWalletWindow.address=Seu endereço de destino +emptyWalletWindow.button=Enviar todos os fundos +emptyWalletWindow.openOffers.warn=Você possui ofertas abertas que serão removidas se você esvaziar sua carteira.\nTem certeza de que deseja esvaziar sua carteira? +emptyWalletWindow.openOffers.yes=Sim, tenho certeza +emptyWalletWindow.sent.success=O conteúdo da sua carteira foi transferido com sucesso. + +enterPrivKeyWindow.headline=Insira a chave privada para o registro + +filterWindow.headline=Editar lista de filtragem +filterWindow.offers=Ofertas filtradas (sep. por vírgula): +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Dados de conta de negociação filtrados:\nFormato: lista separada por vírgulas de [id do método de pagamento | dados | valor] +filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) +filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) +filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) +filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion separados por vírgula) +filterWindow.seedNode=Nós de semente filtrados (endereços onion sep. por vírgula) +filterWindow.priceRelayNode=Nós de transmissão de preço filtrados (endereços onion sep. por vírgula) +filterWindow.btcNode=Nós de Bitcoin filtrados (endereços + portas sep. por vírgula) +filterWindow.preventPublicBtcNetwork=Prevenir uso da rede de Bitcoin pública +filterWindow.disableDao=Desativar DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Versão mín. necessária para a DAO +filterWindow.disableTradeBelowVersion=Versão mínima necessária para negociação +filterWindow.add=Adicionar filtro +filterWindow.remove=Remover filtro +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Quantia mín. em BTC +offerDetailsWindow.min=(mín. {0}) +offerDetailsWindow.distance=(distância do preço de mercado: {0}) +offerDetailsWindow.myTradingAccount=Minha conta de negociação +offerDetailsWindow.offererBankId=(ID/BIC/SWIFT do banco do ofertante) +offerDetailsWindow.offerersBankName=(nome do banco do ofertante) +offerDetailsWindow.bankId=ID do banco (ex: BIC ou SWIFT) +offerDetailsWindow.countryBank=País do banco do ofertante +offerDetailsWindow.commitment=Compromisso +offerDetailsWindow.agree=Eu concordo +offerDetailsWindow.tac=Termos e condições +offerDetailsWindow.confirm.maker=Criar oferta para {0} bitcoin +offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} bitcoin +offerDetailsWindow.creationDate=Criada em +offerDetailsWindow.makersOnion=Endereço onion do ofertante + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Solicitação de pagamento:\n{0} + +selectDepositTxWindow.headline=Selecionar transação de depósito para disputa +selectDepositTxWindow.msg=A transação do depósito não foi armazenada na negociação.\nPor favor, selecione a transação multisig da sua carteira utilizada como transação de depósito na negociação que falhou.\n\nVocê pode verificar qual foi a transação abrindo a janela de detalhe de negociações (clique no ID da negociação na lista) e seguindo a saída (output) da transação de pagamento da taxa de negociação para a próxima transação onde você verá a transação de depósito multisig (o endereço começa com o número 3). Esse ID de transação deve estar visível na lista apresentada aqui. Uma vez encontrada a transação, selecione-a aqui e continue.\n\nDesculpe o transtorno, este erro deve ocorrer muito pouco e no futuro vamos procurar melhores formas de resolvê-lo. +selectDepositTxWindow.select=Selecionar transação de depósito + +sendAlertMessageWindow.headline=Enviar notificação global +sendAlertMessageWindow.alertMsg=Mensagem de alerta +sendAlertMessageWindow.enterMsg=Digitar mensagem +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=Nº da nova versão +sendAlertMessageWindow.send=Enviar notificação +sendAlertMessageWindow.remove=Remover notificação + +sendPrivateNotificationWindow.headline=Enviar mensagem privada +sendPrivateNotificationWindow.privateNotification=Notificação privada +sendPrivateNotificationWindow.enterNotification=Digite notificação +sendPrivateNotificationWindow.send=Enviar notificação privada + +showWalletDataWindow.walletData=Dados da carteira +showWalletDataWindow.includePrivKeys=Incluir chaves privadas + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Acordo de usuário +tacWindow.agree=Eu concordo +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.tradeDate=Data da negociação +tradeDetailsWindow.txFee=Taxa de mineração +tradeDetailsWindow.tradingPeersOnion=Endereço onion dos parceiros de negociação +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Estado da negociação +tradeDetailsWindow.agentAddresses=Árbitro/Mediador +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Digite senha para abrir: + +torNetworkSettingWindow.header=Configurações de rede do Tor +torNetworkSettingWindow.noBridges=Não usar pontes +torNetworkSettingWindow.providedBridges=Conectar com as pontes fornecidas +torNetworkSettingWindow.customBridges=Adicionar pontes personalizadas +torNetworkSettingWindow.transportType=Tipo de transporte +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (recomendado) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Insira uma ou mais pontes de retransmissão (uma por linha) +torNetworkSettingWindow.enterBridgePrompt=digite endereço:porta +torNetworkSettingWindow.restartInfo=Você precisa reiniciar o programa para aplicar as modificações +torNetworkSettingWindow.openTorWebPage=Abrir site do projeto Tor +torNetworkSettingWindow.deleteFiles.header=Problemas de conexão? +torNetworkSettingWindow.deleteFiles.info=Caso você está tendo problemas de conexão durante a inicialização, tente deletar os arquivos desatualizados do Tor. Para fazer isso, clique no botão abaixo e reinicialize o programa em seguida. +torNetworkSettingWindow.deleteFiles.button=Apagar arquivos desatualizados do Tor e desligar +torNetworkSettingWindow.deleteFiles.progress=Desligando Tor... +torNetworkSettingWindow.deleteFiles.success=Os arquivos desatualizados do Tor foram deletados com sucesso. Por favor, reinicie o aplicativo. +torNetworkSettingWindow.bridges.header=O Tor está bloqueado? +torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu provedor de internet ou em seu país, você pode tentar usar pontes do Tor.\nVisite a página do Tor em https://bridges.torproject.org/bridges para aprender mais sobre pontes e transportadores plugáveis. + +feeOptionWindow.headline=Escolha a moeda para pagar a taxa de negociação +feeOptionWindow.info=Você pode optar por pagar a taxa de negociação em BSQ ou BTC. As taxas de negociação são reduzidas quando pagas com BSQ. +feeOptionWindow.optionsLabel=Escolha a moeda para pagar a taxa de negociação +feeOptionWindow.useBTC=Usar BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notificação +popup.headline.instruction=Favor observar: +popup.headline.attention=Atenção +popup.headline.backgroundInfo=Informação preliminar +popup.headline.feedback=Concluído +popup.headline.confirmation=Confirmação +popup.headline.information=Informação +popup.headline.warning=Aviso +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 bisq.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} + +error.spvFileCorrupted=Houve um erro ao ler o arquivo SPV chain.\nPode ser que o arquivo SPV chain esteja corrompido.\n\nMensagem de erro: {0}\n\nDeseja remover o arquivo e re-sincronizar? +error.deleteAddressEntryListFailed=Não foi possível apagar o arquivo AddressEntryList.\nErro: {0} +error.closedTradeWithUnconfirmedDepositTx=A transação de depósito da negociação já fechada com ID {0} ainda está não-confirmada.\n\nPor favor ressincronize o arquivo SPV em "Configurações/Informações da rede" para verificar se a transação é válida. +error.closedTradeWithNoDepositTx=A transação de depósito da negociação já fechada com ID {0} está ausente.\n\nPor favor, reinicie o aplicativo para atualizar a lista de negociações encerradas. + +popup.warning.walletNotInitialized=A carteira ainda não foi inicializada +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=Você provavelmente está usando a versão incorreta do Bisq para este computador.\nA arquitetura do seu computador é: {0}.\nO binário do Bisq que você instalou é: {1}.\nPor favor, feche o programa e instale a versão correta ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=O Bisq já está sendo executado. Você não pode executar duas instâncias do Bisq ao mesmo tempo. +popup.warning.tradePeriod.halfReached=Sua negociação com ID {0} chegou à metade do período máximo permitido e ainda não foi concluída.\n\nO período de negociação acaba em {1}\n\nFavor verifique o estado de sua negociação em \"Portfolio/Negociações em aberto\" para mais informações. +popup.warning.tradePeriod.ended=Sua negociação com ID {0} atingiu o período máximo de negociação e não foi finalizada.\n\nO período de negociação terminou em {1}.\n\nPor favor, verifique sua negociação em "Portfolio/Negociações em aberto" para contactar o mediador. +popup.warning.noTradingAccountSetup.headline=Você ainda não configurou uma conta para negociação +popup.warning.noTradingAccountSetup.msg=Você precisa criar uma conta em moeda nacional ou altcoin para poder criar uma oferta.\nCriar uma conta? +popup.warning.noArbitratorsAvailable=Não há árbitros disponíveis. +popup.warning.noMediatorsAvailable=Não há mediadores disponíveis. +popup.warning.notFullyConnected=Você precisa aguardar até estar totalmente conectado à rede.\nIsto pode levar até 2 minutos na inicialização do programa. +popup.warning.notSufficientConnectionsToBtcNetwork=Você precisa esperar até ter pelo menos {0} conexões à rede Bitcoin. +popup.warning.downloadNotComplete=Você precisa aguardar até que termine o download dos blocos de Bitcoin restantes +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Tem certeza que deseja remover essa oferta?\nA taxa de oferta de {0} será perdida se você removê-la. +popup.warning.tooLargePercentageValue=Você não pode definir uma porcentagem superior a 100%. +popup.warning.examplePercentageValue=Digite um número percentual, como \"5.4\" para 5.4% +popup.warning.noPriceFeedAvailable=Não há feed de preços disponível para essa moeda. Você não pode usar um preço porcentual.\nPor favor selecione um preço fixo. +popup.warning.sendMsgFailed=O envio da mensagem para seu parceiro de negociação falhou.\nFavor tentar novamente, e se o erro persistir reportar o erro (bug report). +popup.warning.insufficientBtcFundsForBsqTx=Você não possui fundos BTC suficientes para pagar a taxa de mineração para essa transação.\nPor favor, deposite BTC em sua carteira.\nFundos faltando: {0} +popup.warning.bsqChangeBelowDustException=Esta transação cria um troco BSQ menor do que o limite poeira (5.46 BSQ) e seria rejeitada pela rede Bitcoin.\nVocê precisa ou enviar uma quantia maior para evitar o troco (ex: adicionando a quantia poeira ao montante a ser enviado) ou adicionar mais fundos BSQ à sua carteira para evitar gerar uma saída de poeira.\nA saída de poeira é {0}. +popup.warning.btcChangeBelowDustException=Esta transação cria um troco menor do que o limite poeira (546 Satoshi) e seria rejeitada pela rede Bitcoin.\nVocê precisa adicionar a quantia poeira ao montante de envio para evitar gerar uma saída de poeira.\nA saída de poeira é {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Sua carteira BSQ não possui fundos suficientes para pagar a taxa de transação em BSQ. +popup.warning.messageTooLong=Sua mensagem excede o tamanho máximo permitido. Favor enviá-la em várias partes ou utilizando um serviço como https://pastebin.com. +popup.warning.lockedUpFunds=Você possui fundos travados em uma negociação com erro.\nSaldo travado: {0}\nEndereço da transação de depósito: {1}\nID da negociação: {2}.\n\nPor favor, abra um ticket de suporte selecionando a negociação na tela de negociações em aberto e depois pressionando "\alt+o\" ou \"option+o\". + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +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 Bisq. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Bisq para mais informações. +popup.warning.mandatoryUpdate.dao=Faça o update para a última versão do Bisq. Um update obrigatório foi publicado, ele desabilita a DAO Bisq e BSQ em versões anteriores. Por gentileza, veja o Fórum do Bisq para mais informações.\n\n +popup.warning.disable.dao=A DAO Bisq e BSQ estão temporariamente desativados. Verifique o fórum Bisq para mais informações. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 BTC para transferir. + +popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Bitcoin.\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 Bisq na Keybase. + +popup.warning.trade.txRejected.tradeFee=taxa de negociação +popup.warning.trade.txRejected.deposit=depósito +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=A transação de taxa de ofertante para a oferta com ID {0} é inválida.\nID da transação: {1}.\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 Bisq na Keybase. + +popup.info.securityDepositInfo=Para garantir que ambas as partes sigam o protocolo de negociação, tanto o vendedor quanto o comprador precisam fazer um depósito de segurança.\n\nEste depósito permanecerá em sua carteira local até que a negociação seja concluída com sucesso. Depois, ele será devolvido para você.\n\nAtenção: se você está criando uma nova oferta, é necessário que você mantenha o programa aberto, para que outro usuário possa aceitar a sua oferta. Para manter suas ofertas online, mantenha o Bisq sempre aberto e conectado à internet (por exemplo: verifique-se de que as funções de economia de energia do seu computador estão desativadas). + +popup.info.cashDepositInfo=Certifique-se de que você possui uma agência bancária em sua região para poder fazer o depósito em dinheiro.\nO ID (BIC/SWIFT) do banco do vendedor é: {0}. +popup.info.cashDepositInfo.confirm=Eu confirmo que posso fazer o depósito +popup.info.shutDownWithOpenOffers=O Bisq está desligando, mas há ofertas abertas.\n\nEstas ofertas não ficarão disponíveis na rede P2P enquanto o Bisq estiver desligado, mas elas serão republicadas na rede assim que você reiniciar o programa.\n\nPara manter suas ofertas online, mantenha o Bisq aberto e certifique-se de que o seu computador continua online (ex: certifique-se de que o computador não está entrando em modo de hibernação). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Notificação privada importante! + +popup.securityRecommendation.headline=Recomendação de segurança importante +popup.securityRecommendation.msg=Lembre-se de proteger a sua carteira com uma senha, caso você já não tenha criado uma.\n\nRecomendamos que você escreva num papel as palavras da semente de sua carteira. Essas palavras funcionam como uma senha mestra para recuperar a sua carteira Bitcoin, caso o seu computador apresente algum problema.\nVocê irá encontrar mais informações na seção \"Semente da carteira\".\n\nTambém aconselhamos que você faça um backup completo da pasta de dados do programa na seção \"Backup\". + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Desligando +popup.shutDownInProgress.msg=O desligamento do programa pode levar alguns segundos.\nPor favor, não interrompa este processo. + +popup.attention.forTradeWithId=Atenção para a negociação com ID {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Múltiplas contas de pagamento disponíveis +popup.info.multiplePaymentAccounts.msg=Você tem várias contas de pagamento disponíveis para esta oferta. Por favor, verifique se você escolheu a correta. + +popup.accountSigning.selectAccounts.headline=Selecionar contas de pagamento +popup.accountSigning.selectAccounts.description=Baseado no método de pagamento e ponto no tempo, todas as contas de pagamento que estiverem ligadas a um disputa em que o pagamento ocorreu em favor do comprador serão selecionadas para que você assine-as. +popup.accountSigning.selectAccounts.signAll=Assinar todas as contas de pagamento +popup.accountSigning.selectAccounts.datePicker=Selecione o ponto no tempo até o qual as contas serão assinadas + +popup.accountSigning.confirmSelectedAccounts.headline=Confirmar contas de pagamento selecionadas +popup.accountSigning.confirmSelectedAccounts.description=Baseado na sua seleção, {0} contas de pagamento serão selecionadas. +popup.accountSigning.confirmSelectedAccounts.button=Confirmar contas de pagamento +popup.accountSigning.signAccounts.headline=Confirmar assinatura de contas de pagamento. +popup.accountSigning.signAccounts.description=Baseado na sua seleção, {0} contas de pagamento serão assinadas. +popup.accountSigning.signAccounts.button=Assinar contas de pagamento +popup.accountSigning.signAccounts.ECKey=Insira a chave privada de árbitro +popup.accountSigning.signAccounts.ECKey.error=Chave ECKey de árbitro errada. + +popup.accountSigning.success.headline=Parabéns +popup.accountSigning.success.description=Todas as {0} contas de pagamento foram assinadas com sucesso! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Uma de suas contas de pagamento foi verificada e assinada por um árbitro. Ao negociar com essa conta você automaticamente assinará a conta de seu par após uma negociação bem succedida.\n\n{0} +popup.accountSigning.signedByPeer=Uma de suas contas de pagamento foi verificada e assinada por um par de negociação. Seu limite de negociação inicial será aumentado e você poderá assinar outras contas em {0} dias.\n\n{1} +popup.accountSigning.peerLimitLifted=O limite inicial para uma de suas contas acaba de ser aumentado. +popup.accountSigning.peerSigner=Uma das suas contas é antiga o suficiente para assinar outras contas de pagamento e o limite para uma de suas contas acaba de ser aumentado\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notificação para o oferta com ID {0} +notification.ticket.headline=Ticket de suporte para a oferta com ID {0} +notification.trade.completed=A negociação foi concluída e você já pode retirar seus fundos. +notification.trade.accepted=Sua oferta foi aceita por um {0}. +notification.trade.confirmed=Sua negociação tem pelo menos uma confirmação da blockchain.\nVocê já pode iniciar o pagamento. +notification.trade.paymentStarted=O comprador BTC iniciou o pagamento +notification.trade.selectTrade=Selecionar negociação +notification.trade.peerOpenedDispute=Seu parceiro de negociação abriu um {0}. +notification.trade.disputeClosed=A {0} foi fechada. +notification.walletUpdate.headline=Update da carteira de negociação +notification.walletUpdate.msg=Sua carteira Bisq tem saldo suficiente.\nQuantia: {0} +notification.takeOffer.walletUpdate.msg=Sua carteira Bisq já tinha saldo suficiente de uma tentativa anterior de aceitar oferta.\nQuantia: {0} +notification.tradeCompleted.headline=Negociação concluída +notification.tradeCompleted.msg=Você pode retirar seus fundos agora para sua carteira Bitcoin externa ou transferi-los para a carteira Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Mostrar janela do applicativo +systemTray.hide=Esconder janela do applicativo +systemTray.info=Informações sobre Bisq +systemTray.exit=Sair +systemTray.tooltip=Bisq: a rede de exchange decentralizada de bitcoin + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=Contas de negociação salvas na pasta:\n{0} +guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação para exportar. +guiUtil.accountExport.selectPath=Selecione pasta de {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Conta de negociação com ID {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Não importamos a conta de negociação com id {0} pois ela já existe.\n +guiUtil.accountExport.exportFailed=Exportar para CSV falhou pois houve um erro.\nErro = {0} +guiUtil.accountExport.selectExportPath=Selecionar pasta para exportar +guiUtil.accountImport.imported=Conta de negociação importada da pasta:\n{0}\n\nContas importadas:\n{1} +guiUtil.accountImport.noAccountsFound=Nenhuma conta de negociação exportada foi encontrada em: {0}.\nNome do arquivo é {1}." +guiUtil.openWebBrowser.warning=Você abrirá uma página web em seu navegador padrão.\nDeseja abrir a página agora?\n\nSe você não estiver usando o \"Tor Browser\" como seu navegador padrão você conectará à página pela internet aberta (clear net).\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Abrir a página e não perguntar novamente +guiUtil.openWebBrowser.copyUrl=Copiar URL e fechar +guiUtil.ofTradeAmount=da quantia da negociação +guiUtil.requiredMinimum=(mínimo requerido) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Selecione a moeda +list.currency.showAll=Ver todos +list.currency.editList=Editar lista de moedas + +table.placeholder.noItems=Atualmente não há {0} disponíveis +table.placeholder.noData=Não há dados disponíveis no momento +table.placeholder.processingData=Processando dados... + + +peerInfoIcon.tooltip.tradePeer=Parceiro de negociação +peerInfoIcon.tooltip.maker=do ofertante +peerInfoIcon.tooltip.trade.traded={0} endereço onion: {1}\nVocê já negociou {2} vez(es) com esse parceiro\n{3} +peerInfoIcon.tooltip.trade.notTraded=Endereço onion do {0}: {1}\nVocê ainda não negociou com esse usuário.\n{2} +peerInfoIcon.tooltip.age=Conta de pagamento criada {0} atrás. +peerInfoIcon.tooltip.unknownAge=Idade da conta de pagamento desconhecida. + +tooltip.openPopupForDetails=Abrir popup para mais detalhes +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Abrir um explorer de blockchain externo para o endereço: {0} +tooltip.openBlockchainForTx=Abrir um explorer de blockchain externo para a transação: {0} + +confidence.unknown=Transação com estado desconhecido +confidence.seen=Visto por {0} par(es) / 0 confirmações +confidence.confirmed=Confirmado em {0} bloco(s) +confidence.invalid=A transação é inválida + +peerInfo.title=Informação do par +peerInfo.nrOfTrades=Nº de negociações concluídas +peerInfo.notTradedYet=Você ainda não negociou com este usuário. +peerInfo.setTag=Definir um rótulo para este par +peerInfo.age.noRisk=Idade da conta de pagamento +peerInfo.age.chargeBackRisk=Tempo desde a assinatura +peerInfo.unknownAge=Idade desconhecida + +addressTextField.openWallet=Abrir a sua carteira Bitcoin padrão +addressTextField.copyToClipboard=Copiar endereço para área de transferência +addressTextField.addressCopiedToClipboard=Endereço copiado para área de transferência +addressTextField.openWallet.failed=Erro ao abrir a carteira padrão Bitcoin. Talvez você não possua uma instalada. + +peerInfoIcon.tooltip={0}\nRótulo: {1} + +txIdTextField.copyIcon.tooltip=Copiar ID da transação +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Conta\" +navigation.account.walletSeed=\"Conta/Semente da carteira\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"Portfolio/Minhas ofertas\" +navigation.portfolio.pending=\"Portfolio/Negociações em aberto\" +navigation.portfolio.closedTrades=\"Portfólio/Histórico\" +navigation.funds.depositFunds=\"Fundos/Receber fundos\" +navigation.settings.preferences=\"Configurações/Preferências\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Fundos/Transações\" +navigation.support=\"Suporte\" +navigation.dao.wallet.receive=\"DAO/Carteira BSQ/Receber\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} quantia{1} +formatter.makerTaker=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.fiat=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.asMaker={0} {1} como ofertante +formatter.asTaker={0} {1} como aceitador + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Mainnet do Bitcoin +# suppress inspection "UnusedProperty" +BTC_TESTNET=Testnet do Bitcoin +# suppress inspection "UnusedProperty" +BTC_REGTEST=Regtest do Bitcoin +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Testnet da DAO do Bitcoin (descontinuada) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Regtest da DAO do Bitcoin + +time.year=Ano +time.month=Mês +time.week=Semana +time.day=Dia +time.hour=Hora +time.minute10=10 Minutos +time.hours=horas +time.days=dias +time.1hour=1 hora +time.1day=1 dia +time.minute=minuto +time.second=segundo +time.minutes=minutos +time.seconds=segundos + + +password.enterPassword=Insira a senha +password.confirmPassword=Confirme a senha +password.tooLong=A senha deve ter menos de 500 caracteres. +password.deriveKey=Derivando chave a partir da senha +password.walletDecrypted=A carteira foi decifrada com sucesso e a proteção por senha removida +password.wrongPw=Você digitou a senha incorreta.\n\nFavor tentar novamente, verificando com cuidado erros de digitação ou ortografia. +password.walletEncrypted=A carteira foi encriptada e a proteção por senha foi ativada com sucesso. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=As 2 senhas inseridas não são iguais. +password.forgotPassword=Esqueceu a senha? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Semente da carteira +seed.enterSeedWords=Insira a semente da carteira +seed.date=Data da carteira +seed.restore.title=Recuperar carteira a partir das palavras semente +seed.restore=Recuperar carteira +seed.creationDate=Criada em +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=Desejo recuperar mesmo assim +seed.warn.walletNotEmpty.emptyWallet=Esvaziarei as carteiras primeiro +seed.warn.notEncryptedAnymore=Suas carteiras estão encriptadas.\n\nApós a restauração, as carteiras não estarão mais encriptadas e você deverá definir uma nova senha.\n\nDeseja continuar? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Carteiras recuperadas com sucesso com as novas palavras semente.\n\nVocê precisa desligar e reiniciar o aplicativo. +seed.restore.error=Ocorreu um erro ao restaurar as carteiras com palavras semente.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Conta +payment.account.no=Nº da conta +payment.account.name=Nome da conta +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Nome completo do titular da conta +payment.account.fullName=Nome completo (nome e sobrenome) +payment.account.state=Estado/Província/Região +payment.account.city=Cidade +payment.bank.country=País do banco +payment.account.name.email=Nome completo / e-mail do titular da conta +payment.account.name.emailAndHolderId=Nome completo / e-mail / {0} do titular da conta +payment.bank.name=Nome do banco +payment.select.account=Selecione o tipo de conta +payment.select.region=Selecionar região +payment.select.country=Selecionar país +payment.select.bank.country=Selecionar país do banco +payment.foreign.currency=Tem certeza que deseja selecionar uma moeda que não seja a moeda padrão do pais? +payment.restore.default=Não, restaurar para a moeda padrão +payment.email=E-mail +payment.country=País +payment.extras=Requerimentos adicionais +payment.email.mobile=E-mail ou celular +payment.altcoin.address=Endereço altcoin +payment.altcoin.tradeInstantCheckbox=Negócio instantâneo (dentro de 1 hora) com esta Altcoin +payment.altcoin.tradeInstant.popup=Para negociação instantânea, é necessário que os dois pares de negociação estejam online para concluir a negociação em menos de 1 hora.\n\nSe você tem ofertas abertas e você não está disponível, por favor desative essas ofertas na tela 'Portfolio'. +payment.altcoin=Altcoin +payment.select.altcoin=Select or search Altcoin +payment.secret=Pergunta secreta +payment.answer=Resposta +payment.wallet=ID da carteira +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Nome de usuário, e-mail ou nº de telefone +payment.moneyBeam.accountId=E-mail ou nº de telefone +payment.venmo.venmoUserName=Nome do usuário do Venmo +payment.popmoney.accountId=E-mail ou nº de telefone +payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telefone +payment.supportedCurrencies=Moedas disponíveis +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Limitações +payment.salt=Sal para verificação da idade da conta +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Aceitar negociações destes países do Euro +payment.accept.nonEuro=Aceitar negociações desses países fora do Euro +payment.accepted.countries=Países aceitos +payment.accepted.banks=Bancos aceitos (ID) +payment.mobile=Celular +payment.postal.address=CEP +payment.national.account.id.AR=Número CBU +shared.accountSigningState=Status de assinatura da conta + +#new +payment.altcoin.address.dyn=Endereço {0} +payment.altcoin.receiver.address=Endereço altcoin do destinatário +payment.accountNr=Nº da conta +payment.emailOrMobile=E-mail ou celular +payment.useCustomAccountName=Usar nome personalizado +payment.maxPeriod=Período máximo de negociação permitido +payment.maxPeriodAndLimit=Duração máxima da negociação: {0} / Max. compra: {1} / Max. venda: {2} / Idade de conta: {3} +payment.maxPeriodAndLimitCrypto=Duração máxima de negociação: {0} / Limite de negociação: {1} +payment.currencyWithSymbol=Moeda: {0} +payment.nameOfAcceptedBank=Nome do banco aceito +payment.addAcceptedBank=Adicionar banco aceito +payment.clearAcceptedBanks=Limpar bancos aceitos +payment.bank.nameOptional=Nome do banco (opcional) +payment.bankCode=Código do banco +payment.bankId=ID do banco (BIC/SWIFT) +payment.bankIdOptional=ID do banco (BIC/SWIFT) (opcional) +payment.branchNr=Nº da agência +payment.branchNrOptional=Nº da agência (opcional) +payment.accountNrLabel=Nº da conta (IBAN) +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 BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 BTC precisa enviar ao vendedor de BTC 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 BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Informações para contato +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=Informações para contato +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Banco +payment.japan.branch=Ramo +payment.japan.account=Conta +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Transferência bancária nacional +SAME_BANK=Transferência para mesmo banco +SPECIFIC_BANKS=Transferência com bancos específicos +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=Depósito em dinheiro (cash deposit) +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Face a face (pessoalmente) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Bancos nacionais +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Mesmo banco +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Bancos específicos +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Depósito em dinheiro (cash deposit) +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Instant Altcoins + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Obrigatório +validation.NaN=Número inválido +validation.notAnInteger=A quantia não é um valor inteiro. +validation.zero=Número 0 não é permitido +validation.negative=Valores negativos não são permitidos. +validation.fiat.toSmall=Quantia menor do que a mínima permitida. +validation.fiat.toLarge=Quantia maior do que a máxima permitida. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=Quantia máx. permitida: {0} +validation.btc.toSmall=Quantia mín. permitida: {0} +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=A senha inserida é muito longa. Não pode ser maior do que 50 caracteres +validation.sortCodeNumber={0} deve consistir de {1} números. +validation.sortCodeChars={0} deve consistir de {1} caracteres. +validation.bankIdNumber={0} deve consistir de {1} números. +validation.accountNr=O número de conta deve conter {0} números. +validation.accountNrChars=O número da conta deve conter {0} caracteres. +validation.btc.invalidAddress=O endereço está incorreto. Por favor, verifique o formato do endereço. +validation.integerOnly=Por favor, insira apesar números inteiros +validation.inputError=Os dados inseridos causaram um erro:\n{0} +validation.bsq.insufficientBalance=Seu saldo disponível é {0}. +validation.btc.exceedsMaxTradeLimit=Seu limite de negociação é {0}. +validation.bsq.amountBelowMinAmount=A quantia mínima é {0} +validation.nationalAccountId={0} deve consistir de {1} números. + +#new +validation.invalidInput=Entrada inválida: {0} +validation.accountNrFormat=O número da conta deve estar no formato: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Validação do endereço falhou pois este não é compatível com a estrutura de um endereço {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Endereço não é um endereço {0} válido! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Endereços nativos de Segwit (começando com "lq") não são suportados. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Banco e código de país devem ser letras +validation.bic.invalidLocationCode=BIC contém código de localização inválido +validation.bic.invalidBranchCode=BIC contém código da agência inválido +validation.bic.sepaRevolutBic=Contas Revolut Sepa não são suportadas. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Endereço inválido +validation.iban.invalidCountryCode=Código de país inválido +validation.iban.checkSumNotNumeric=Código verificador deve ser numérico +validation.iban.nonNumericChars=Caractere não alfanumérico detectado +validation.iban.checkSumInvalid=Código de verificação IBAN é inválido +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Código de área não é canadense. +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Deve conter somente letras, números, espaços e/ou os símbolos ' _ , . ? - +validation.interacETransfer.invalidAnswer=Deve ser uma palavra e conter apenas letras, números e/ou o símbolo - +validation.inputTooLarge=Não deve ser maior do que {0} +validation.inputTooSmall=Deve ser maior do que {0} +validation.inputToBeAtLeast=O input tem de ser pelo menos {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=Comprimento deve ser entre {0} e {1} +validation.fixedLength=Length must be {0} +validation.pattern=Input deve ser no formato: {0} +validation.noHexString=O input não está no formato hexadecimal +validation.advancedCash.invalidFormat=Deve ser um e-mail válido ou uma ID de carteira no formato: X000000000000 +validation.invalidUrl=Essa URL não é válida +validation.mustBeDifferent=Seu input precisa ser diferente do valor atual. +validation.cannotBeChanged=O parâmetro não pode ser alterado +validation.numberFormatException=Exceção do formato do número {0} +validation.mustNotBeNegative=O input não deve ser negativo +validation.phone.missingCountryCode=Precisa do código do país com duas letras para validar o número de telefone +validation.phone.invalidCharacters=O número de telefone {0} contém caracteres inválidos. +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Precisa ser uma lista delimitada por vírgulas de endereços válidos diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties new file mode 100644 index 0000000000..186f0fdfb0 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Ler mais +shared.openHelp=Abrir a Ajuda +shared.warning=Aviso +shared.close=Fechar +shared.cancel=Cancelar +shared.ok=OK +shared.yes=Sim +shared.no=Não +shared.iUnderstand=Eu compreendo +shared.na=N/D +shared.shutDown=Desligar +shared.reportBug=Reportar erro no GitHub +shared.buyBitcoin=Comprar bitcoin +shared.sellBitcoin=Vender bitcoin +shared.buyCurrency=Comprar {0} +shared.sellCurrency=Vender {0} +shared.buyingBTCWith=comprando BTC com {0} +shared.sellingBTCFor=vendendo BTC por {0} +shared.buyingCurrency=comprando {0} (vendendo BTC) +shared.sellingCurrency=vendendo {0} (comprando BTC) +shared.buy=comprar +shared.sell=vender +shared.buying=comprando +shared.selling=vendendo +shared.P2P=P2P +shared.oneOffer=oferta +shared.multipleOffers=ofertas +shared.Offer=Oferta +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=ofertas abertas +shared.trade=negócio +shared.trades=negócios +shared.openTrades=negócios abertos +shared.dateTime=Data/Hora +shared.price=Preço +shared.priceWithCur=Preço em {0} +shared.priceInCurForCur=Preço em {0} para 1 {1} +shared.fixedPriceInCurForCur=Preço fixo em {0} para 1 {1} +shared.amount=Quantia +shared.txFee=Taxa da transação +shared.tradeFee=Taxa de Negócio +shared.buyerSecurityDeposit=Depósito do comprador +shared.sellerSecurityDeposit=Depósito do vendedor +shared.amountWithCur=Quantia em {0} +shared.volumeWithCur=Volume em {0} +shared.currency=Moeda +shared.market=Mercado +shared.deviation=Deviation +shared.paymentMethod=Método de pagamento +shared.tradeCurrency=Moeda de negócio +shared.offerType=Tipo de oferta +shared.details=Detalhes +shared.address=Endereço +shared.balanceWithCur=Saldo em {0} +shared.utxo=Unspent transaction output +shared.txId=ID de transação +shared.confirmations=Confirmações +shared.revert=Reverter Tx +shared.select=Selecionar +shared.usage=Uso +shared.state=Estado +shared.tradeId=ID do Negócio +shared.offerId=ID de Oferta +shared.bankName=Nome do banco +shared.acceptedBanks=Bancos aceites +shared.amountMinMax=Quantia (mín - máx) +shared.amountHelp=Se a oferta tem uma quantia mínima ou máxima definida, poderá negociar qualquer quantia dentro deste intervalo. +shared.remove=Remover +shared.goTo=Ir para {0} +shared.BTCMinMax=BTC (mín - máx) +shared.removeOffer=Remover oferta +shared.dontRemoveOffer=Não remover a oferta +shared.editOffer=Editar oferta +shared.openLargeQRWindow=Abrir QR-Code em janela grande +shared.tradingAccount=Conta de negociação +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 Bisq +shared.fundFromExternalWalletButton=Abrir sua carteira externa para o financiamento +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=Abaixo % do preço de mercado +shared.aboveInPercent=Acima % do preço de mercado +shared.enterPercentageValue=Insira % do valor +shared.OR=OU +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=Esperando pelos fundos... +shared.depositTransactionId=ID da transação de depósito +shared.TheBTCBuyer=O comprador de BTC +shared.You=Você +shared.sendingConfirmation=Enviando confirmação... +shared.sendingConfirmationAgain=Por favor envia a confirmação de novo +shared.exportCSV=Export to CSV +shared.exportJSON=Exportar para JSON +shared.summary=Show summary +shared.noDateAvailable=Sem dada disponível +shared.noDetailsAvailable=Sem detalhes disponíveis +shared.notUsedYet=Ainda não usado +shared.date=Data +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin 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 +shared.language=Idioma +shared.country=País +shared.applyAndShutDown=Aplicar e desligar +shared.selectPaymentMethod=Selecionar método de pagamento +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Você realmente quer apagar a conta selecionada? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=Ainda não há contas configuradas +shared.manageAccounts=Gerir contas +shared.addNewAccount=Adicionar uma nova conta +shared.ExportAccounts=Exportar Contas +shared.importAccounts=Importar Contas +shared.createNewAccount=Criar nova conta +shared.saveNewAccount=Guardar nova conta +shared.selectedAccount=Conta selecionada +shared.deleteAccount=Apagar conta +shared.errorMessageInline=\nMensagem de erro: {0} +shared.errorMessage=Mensagem de erro +shared.information=Informação +shared.name=Nome +shared.id=ID +shared.dashboard=Painel +shared.accept=Aceitar +shared.balance=Saldo +shared.save=Guardar +shared.onionAddress=Endereço onion +shared.supportTicket=solicitação de suporte +shared.dispute=disputa +shared.mediationCase=caso de mediação +shared.seller=vendedor +shared.buyer=comprador +shared.allEuroCountries=Todos os países do Euro +shared.acceptedTakerCountries=Países aceites para aceitador +shared.tradePrice=Preço de negócio +shared.tradeAmount=Quantia de negócio +shared.tradeVolume=Volume de negócio +shared.invalidKey=A chave que você inseriu não estava correta +shared.enterPrivKey=Coloque a chave privada para desbloquear +shared.makerFeeTxId=ID de transação da taxa de ofertante +shared.takerFeeTxId=ID de transação da taxa de aceitador +shared.payoutTxId=ID de transação de pagamento +shared.contractAsJson=Contrato em formato JSON +shared.viewContractAsJson=Ver contrato em formato JSON +shared.contract.title=Contrato para negócio com ID: {0} +shared.paymentDetails=Detalhes do pagamento do {0} de BTC +shared.securityDeposit=Depósito de segurança +shared.yourSecurityDeposit=O seu depósito de segurança +shared.contract=Contrato +shared.messageArrived=Mensagem chegou. +shared.messageStoredInMailbox=Mensagem guardada na caixa de correio. +shared.messageSendingFailed=Falha no envio da mensagem. Erro: {0} +shared.unlock=Desbloquear +shared.toReceive=a receber +shared.toSpend=a enviar +shared.btcAmount=Quantia de BTC +shared.yourLanguage=Os seus idiomas +shared.addLanguage=Adicionar idioma +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.makerTxFee=Ofertante: {0} +shared.takerTxFee=Aceitador: {0} +shared.iConfirm=Eu confirmo +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Abrir {0} +shared.fiat=Moeda fiduciária +shared.crypto=Cripto +shared.all=Tudo +shared.edit=Editar +shared.advancedOptions=Opções avançadas +shared.interval=Intervalo +shared.actions=Ações +shared.buyerUpperCase=Comprador +shared.sellerUpperCase=Vendedor +shared.new=NOVO +shared.blindVoteTxId=ID de transação de voto cego +shared.proposal=Proposta +shared.votes=Votos +shared.learnMore=Saber mais +shared.dismiss=Ignorar +shared.selectedArbitrator=Árbitro selecionado +shared.selectedMediator=Mediador selecionado +shared.selectedRefundAgent=Árbitro selecionado +shared.mediator=Mediador +shared.arbitrator=Árbitro +shared.refundAgent=Árbitro +shared.refundAgentForSupportStaff=Agente de Reembolso +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Mercado +mainView.menu.buyBtc=Comprar BTC +mainView.menu.sellBtc=Vender BTC +mainView.menu.portfolio=Portefólio +mainView.menu.funds=Fundos +mainView.menu.support=Apoio +mainView.menu.settings=Definições +mainView.menu.account=Conta +mainView.menu.dao=OAD + +mainView.marketPriceWithProvider.label=Preço de mercado por {0} +mainView.marketPrice.bisqInternalPrice=Preço do último negócio do Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=Não há preço de mercado de fornecedores de feed de preço externos disponíveis.\nO preço exibido é o mais recente preço de negócio do Bisq para essa moeda. +mainView.marketPrice.tooltip=O preço de mercado é fornecido por {0} {1}\nÚltima atualização: {2}\nURL do nó do provedor: {3} +mainView.balance.available=Saldo disponível +mainView.balance.reserved=Reservado em ofertas +mainView.balance.locked=Bloqueado em negócios +mainView.balance.reserved.short=Reservado +mainView.balance.locked.short=Bloqueado + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(localhost) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Conectando à rede Bitcoin +mainView.footer.bsqInfo.synchronizing=/ Sincronizando a OAD +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Conectando à +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=Nó completo da OAD + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando à rede Tor.... +mainView.bootstrapState.torNodeCreated=(2/4) Nó da rede Tor criado +mainView.bootstrapState.hiddenServicePublished=(3/4) Serviço Oculto publicado +mainView.bootstrapState.initialDataReceived=(4/4) Dados iniciais recebidos + +mainView.bootstrapWarning.noSeedNodesAvailable=Nenhum nó semente disponível +mainView.bootstrapWarning.noNodesAvailable=Sem nós semente e pares disponíveis +mainView.bootstrapWarning.bootstrappingToP2PFailed=O bootstrap para a rede do Bisq falhou + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Não há nós de semente ou pares persistentes disponíveis para solicitar dados.\nPor favor, verifique a sua conexão de Internet ou tente reiniciar o programa. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=A conexão com a rede do Bisq falhou (erro reportado: {0}).\nPor favor, verifique sua conexão com a Internet ou tente reiniciar o programa. + +mainView.walletServiceErrorMsg.timeout=A conexão com a rede Bitcoin falhou por causa de tempo esgotado. +mainView.walletServiceErrorMsg.connectionError=A conexão com a rede Bitcoin falhou devido ao erro: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=Uma transação foi rejeitada pela rede.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Você perdeu a conexão com todos os pares de rede de {0} .\nTalvez você tenha perdido sua conexão de internet ou o seu computador estivesse no modo de espera. +mainView.networkWarning.localhostBitcoinLost=Perdeu a conexão ao nó Bitcoin do localhost.\nPor favor recomeçar o programa do Bisq para conectar à outros nós Bitcoin ou recomeçar o nó Bitcoin do localhost. +mainView.version.update=(Atualização disponível) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Livro de ofertas +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Negócios + +# OfferBookChartView +market.offerBook.buyAltcoin=Eu quero comprar {0} (vender {1}) +market.offerBook.sellAltcoin=Eu quero vender {0} (comprar {1}) +market.offerBook.buyWithFiat=Comprar {0} +market.offerBook.sellWithFiat=Vender {0} +market.offerBook.sellOffersHeaderLabel=Vender {0} para +market.offerBook.buyOffersHeaderLabel=Comprar {0} de +market.offerBook.buy=Eu quero comprar bitcoin +market.offerBook.sell=Eu quero vender bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Todas as ofertas ({0}) +market.spread.numberOfBuyOffersColumn=Comprar BTC ({0}) +market.spread.numberOfSellOffersColumn=Vender BTC ({0}) +market.spread.totalAmountColumn=Total de BTC ({0}) +market.spread.spreadColumn=Spread +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Negócios: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Abrir: +market.trades.tooltip.candle.close=Fechar: +market.trades.tooltip.candle.high=Alta: +market.trades.tooltip.candle.low=Baixa: +market.trades.tooltip.candle.average=Média: +market.trades.tooltip.candle.median=Mediano: +market.trades.tooltip.candle.date=Data: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=Criar oferta +offerbook.takeOffer=Aceitar oferta +offerbook.takeOfferToBuy=Aceitar oferta para comprar {0} +offerbook.takeOfferToSell=Aceitar oferta para vender {0} +offerbook.trader=Negociador +offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} +offerbook.offerersBankName=Nome do banco do ofertante: {0} +offerbook.offerersBankSeat=Sede do banco do ofertante: {0} +offerbook.offerersAcceptedBankSeatsEuro=Sedes do banco aceites (aceitador): Todos os países do Euro +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.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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=assinada por um par e os limites foram aumentados +offerbook.timeSinceSigning.info.signer=assinada por um par e pode assinar contas de pares (limites aumentados) +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.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 +offerbook.timeSinceSigning.notSigned.ageDays={0} dias +offerbook.timeSinceSigning.notSigned.noNeed=N/D +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=Nº de ofertas: {0} +offerbook.volume={0} (mín - máx) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Criar nova oferta para comprar {0} +offerbook.createOfferToSell=Criar nova oferta para vender {0} +offerbook.createOfferToBuy.withFiat=Criar nova oferta para comprar {0} com {1} +offerbook.createOfferToSell.forFiat=Criar nova oferta para vender {0} por {1} +offerbook.createOfferToBuy.withCrypto=Criar nova oferta para vender {0} (comprar {1}) +offerbook.createOfferToSell.forCrypto=Criar nova oferta para comprar {0} (vender {1}) + +offerbook.takeOfferButton.tooltip=Aceitar oferta por {0} +offerbook.yesCreateOffer=Sim, criar oferta +offerbook.setupNewAccount=Configurar uma nova conta de negociação +offerbook.removeOffer.success=Remoção da oferta bem sucedida +offerbook.removeOffer.failed=Remoção da oferta falhou:\n{0} +offerbook.deactivateOffer.failed=A desativação da oferta falhou:\n{0} +offerbook.activateOffer.failed=A publicação da oferta falhou:\n{0} +offerbook.withdrawFundsHint=Você pode levantar fundos que você pagou a partir do ecrã de {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=Esta oferta não pode ser aceite devido às restrições de negócio da contraparte + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=A quantia de negócio é limitada à {0} devido à restrições de segurança baseadas nos seguinte critérios:\n- A conta do comprador não foi assinada por um árbitro ou um par\n- O tempo decorrido desde a assinatura da conta do comprador não é de pelo menos 30 dias\n- O método de pagamento para esta oferta é considerado arriscado para estornos bancários\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=A quantia de negócio é limitada à {0} devido à restrições de segurança baseadas nos seguinte critérios:\n- A sua conta não foi assinada por um árbitro ou um par\n- O tempo decorrido desde a assinatura da sua conta não é de pelo menos 30 dias\n- O método de pagamento para esta oferta é considerado arriscado para estornos bancários\n\n{1} + +offerbook.warning.wrongTradeProtocol=Essa oferta requer uma versão de protocolo diferente da usada na sua versão do software.\n\nPor favor, verifique se você tem a versão mais recente instalada, caso contrário, o usuário que criou a oferta usou uma versão mais antiga.\n\nOs utilizadores não podem negociar com uma versão de protocolo de negócio incompatível. +offerbook.warning.userIgnored=Você adicionou o endereço onion daquele utilizador à sua lista de endereços ignorados. +offerbook.warning.offerBlocked=Essa oferta foi bloqueada pelos desenvolvedores do Bisq.\nProvavelmente, há um erro não tratado causando problemas ao aceitar a oferta. +offerbook.warning.currencyBanned=A moeda usada nessa oferta foi bloqueada pelos desenvolvedores do Bisq.\nPor favor, visite o Fórum Bisq para mais informações. +offerbook.warning.paymentMethodBanned=O método de pagamento usado nessa oferta foi bloqueado pelos desenvolvedores do Bisq.\nPor favor, visite o Fórum Bisq para mais informações. +offerbook.warning.nodeBlocked=O endereço onion desse negociador foi bloqueado pelos desenvolvedores do Bisq.\nProvavelmente, há um erro não tratado causando problemas ao aceitar ofertas desse negociador. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=Venderá ao preço de mercado (atualizado à cada minuto). +offerbook.info.buyAtMarketPrice=Comprará ao preço de mercado (atualizado à cada minuto). +offerbook.info.sellBelowMarketPrice=Receberá menos {0} do que o atual preço de mercado (atualizado cada minuto). +offerbook.info.buyAboveMarketPrice=Pagará mais {0} do que o atual preço do mercado (atualizado cada minuto). +offerbook.info.sellAboveMarketPrice=Receberá mais {0} do que o atual preço do mercado (atualizado à cada minuto). +offerbook.info.buyBelowMarketPrice=Pagará menos {0} do que o atual preço do mercado (atualizado à cada minuto). +offerbook.info.buyAtFixedPrice=Comprará à este preço fixo. +offerbook.info.sellAtFixedPrice=Venderá à este preço fixo. +offerbook.info.noArbitrationInUserLanguage=Em caso de disputa, saiba que a arbitragem para esta oferta será tratada em {0}. O idioma está atualmente definido para {1}. +offerbook.info.roundedFiatVolume=A quantia foi arredondada para aumentar a privacidade do seu negócio. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Escreva a quantia em BTC +createOffer.price.prompt=Escreva o preço +createOffer.volume.prompt=Escreva a quantia em {0} +createOffer.amountPriceBox.amountDescription=Quantia de BTC para {0} +createOffer.amountPriceBox.buy.volumeDescription=Quantia em {0} a ser gasto +createOffer.amountPriceBox.sell.volumeDescription=Quantia em {0} a ser recebido +createOffer.amountPriceBox.minAmountDescription=Quantia mínima de BTC +createOffer.securityDeposit.prompt=Depósito de segurança +createOffer.fundsBox.title=Financiar sua oferta +createOffer.fundsBox.offerFee=Taxa de negócio +createOffer.fundsBox.networkFee=Taxa de mineração +createOffer.fundsBox.placeOfferSpinnerInfo=Oferta sendo publicada... +createOffer.fundsBox.paymentLabel=negócio do Bisq com ID {0} +createOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) +createOffer.fundsBox.fundsStructure.BSQ=({0} depósito de segurança, {1} taxa de mineração) + {2} taxa de negócio +createOffer.success.headline=Sua oferta foi publicada. +createOffer.success.info=Você pode gerir as suas ofertas abertas em \"Portefólio/As minhas ofertas abertas\". +createOffer.info.sellAtMarketPrice=Venderá sempre ao preço do mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.info.buyAtMarketPrice=Comprará sempre ao preço do mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.info.sellAboveMarketPrice=Receberá sempre mais {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.info.buyBelowMarketPrice=Pagará sempre menos {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.warning.sellBelowMarketPrice=Receberá sempre menos {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.warning.buyAboveMarketPrice=Pagará sempre mais {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. +createOffer.tradeFee.descriptionBTCOnly=taxa de negócio +createOffer.tradeFee.descriptionBSQEnabled=Selecione a moeda da taxa de negócio + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=Rever: Colocar oferta para {0} bitcoin +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 Bisq (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. + +# 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. +createOffer.setAmountPrice=Definir preço e quantia +createOffer.warnCancelOffer=Você já financiou essa oferta.\nSe você cancelar agora, seus fundos serão transferidos para sua carteira Bisq local e estarão disponíveis para levantamento no ecrã \"Fundos/Enviar fundos\".\nVocê tem certeza que deseja cancelar? +createOffer.timeoutAtPublishing=Tempo esgotado durante a publicação da oferta. +createOffer.errorInfo=\n\nA taxa de ofertante já está paga. No pior dos casos você perdeu essa taxa.\nPor favor tente recomeçar o seu programa e verificar a sua conexão de rede para ver se consegue resolver o problema. +createOffer.tooLowSecDeposit.warning=Você definiu o depósito de segurança para um valor inferior ao valor padrão recomendado de {0}.\nTem certeza de que deseja usar um depósito de segurança inferior? +createOffer.tooLowSecDeposit.makerIsSeller=Dá-te menos proteção caso o gteu par de negociação não siga o protocolo de negócio. +createOffer.tooLowSecDeposit.makerIsBuyer=Dá menos proteção para o par de negociação que você segue o protocolo de negócio, pois você tem menos depósito em risco. Outros utilizadores podem preferir receber outras ofertas em vez da sua. +createOffer.resetToDefault=Não, voltar ao valor padrão +createOffer.useLowerValue=Sim, usar meu valor mais baixo +createOffer.priceOutSideOfDeviation=O preço que você inseriu está fora do valor máx. de desvio permitido do preço de mercado.\nO máx. desvio permitido é {0} e pode ser ajustado nas preferências. +createOffer.changePrice=Alterar preço +createOffer.tac=Com a publicação dessa oferta, concordo em negociar com qualquer negociador que preencha as condições definidas nesse ecrã. +createOffer.currencyForFee=Taxa de negócio +createOffer.setDeposit=Definir o depósito de segurança do comprador (%) +createOffer.setDepositAsBuyer=Definir o meu depósito de segurança enquanto comprador (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Insira a quantia de BTC +takeOffer.amountPriceBox.buy.amountDescription=Quantia de BTC a vender +takeOffer.amountPriceBox.sell.amountDescription=Quantia de BTC a comprar +takeOffer.amountPriceBox.priceDescription=Preço por bitcoin em {0} +takeOffer.amountPriceBox.amountRangeDescription=Intervalo de quantia possível +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=A quantia introduzida excede o número de casas décimas permitido.\nA quantia foi ajustada para 4 casas decimais. +takeOffer.validation.amountSmallerThanMinAmount=A quantia não pode ser inferior à quantia mínima definida na oferta. +takeOffer.validation.amountLargerThanOfferAmount=A quantia inserida não pode ser superior à quantia definida na oferta. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Essa quantia inseria criaria troco poeira para o vendedor de BTC. +takeOffer.fundsBox.title=Financiar o seu negócio +takeOffer.fundsBox.isOfferAvailable=Verificar se a oferta está disponível ... +takeOffer.fundsBox.tradeAmount=Quantia para vender +takeOffer.fundsBox.offerFee=Taxa de negócio +takeOffer.fundsBox.networkFee=Total de taxas de mineração +takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitação da oferta em progresso ... +takeOffer.fundsBox.paymentLabel=negócio do Bisq com ID {0} +takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) +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} + +# new entries +takeOffer.takeOfferButton=Rever: Colocar oferta para {0} bitcoin +takeOffer.noPriceFeedAvailable=Você não pode aceitar aquela oferta pois ela utiliza uma percentagem do preço baseada no preço de mercado, mas o feed de preços está indisponível no momento. +takeOffer.takeOfferFundWalletInfo.headline=Financiar seu negócio +# suppress inspection "TrailingSpacesInProperty" +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- Total das taxas de mineração: {4}\n\nVocê pode escolher entre duas opções ao financiar o seu negócio:\n- Use sua carteira Bisq (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.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 +takeOffer.alreadyFunded.askCancel=Você já financiou essa oferta.\nSe você cancelar agora, seus fundos serão transferidos para sua carteira Bisq local e estarão disponíveis para levantamento no ecrã \"Fundos/Enviar fundos\".\nVocê tem certeza que deseja cancelar? +takeOffer.failed.offerNotAvailable=Pedido para aceitar oferta falhou porque a oferta já não está disponível. Talvez um outro negociador aceitou a oferta entretanto. +takeOffer.failed.offerTaken=Não é possível aceitar a oferta pois ela já foi aceita por outro negociador. +takeOffer.failed.offerRemoved=Não é possível aceitar a oferta pois ela foi removida. +takeOffer.failed.offererNotOnline=Aceitação da oferta falhou pois o ofertante já não está online. +takeOffer.failed.offererOffline=Não pode aceitar a oferta porque o ofertante está offline. +takeOffer.warning.connectionToPeerLost=Perdeu a conexão ao ofertante.\nEle pode ter ficado offline ou fechado a conexão consigo devido à demasiadas conexões abertas.\n\nSe ainda consegue ver a sua oferta no livro de ofertas pode tentar aceitar a oferta de novo. + +takeOffer.error.noFundsLost=\n\nAinda não saíram nenhuns fundos da sua carteira.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nA transação de depósito já está publicada.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema.\nSe o problema persistir, por favor contacte os desenvolvedores para obter apoio. +takeOffer.error.payoutPublished=\n\nA transação de depósito já está publicada.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema.\nSe o problema persistir, por favor contacte os desenvolvedores para obter apoio. +takeOffer.tac=Ao aceitar esta oferta, eu concordo com as condições de negócio definidas neste ecrã. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Preço de desencadeamento +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=Definir preço +editOffer.confirmEdit=Confirmar: Editar oferta +editOffer.publishOffer=Publicando sua oferta. +editOffer.failed=A edição da oferta falhou:\n{0} +editOffer.success=Sua oferta foi editada com sucesso. +editOffer.invalidDeposit=O depósito de segurança do comprador não está dentro dos limites definidos pela OAD do Bisq e não pode mais ser editado. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=As minhas ofertas abertas +portfolio.tab.pendingTrades=Negócios abertos +portfolio.tab.history=Histórico +portfolio.tab.failed=Falhou +portfolio.tab.editOpenOffer=Editar oferta + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Esperando confirmação da blockchain +portfolio.pending.step2_buyer.startPayment=Iniciar pagamento +portfolio.pending.step2_seller.waitPaymentStarted=Aguardar até que o pagamento inicie +portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar até que o pagamento chegue +portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar pagamento recebido +portfolio.pending.step5.completed=Concluído + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Por favor transfira da sua carteira externa {0}\n{1} para o vendedor de.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Por favor vá à um banco e pague{0} ao vendedor de BTC.\n\n +portfolio.pending.step2_buyer.cash.extra=REQUERIMENTO IMPORTANTE:\nDepois de ter feito o pagamento escreva no recibo de papel: SEM REEMBOLSOS.\nEm seguida, rasgue-o em 2 partes, tire uma foto e envie-a para o endereço de e-mail do vendedor de BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Por favor pague {0} ao vendedor de BTC usando MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=REQUERIMENTO IMPORTANTE:\nDepois de ter feito o pagamento envie o Número de autorização e uma foto do recibo por email para o vendedor de BTC\nO recibo deve mostrar claramente o nome completo do vendedor, o país, o estado e a quantia. O email do vendedor é: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Por favor pague {0} ao vendedor de BTC usando Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDepois de ter feito o pagamento, envie o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de BTC.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money Order\" para o vendedor de BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Por favor contacte o vendedor de BTC pelo contacto fornecido e marque um encontro para pagar {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Quantia a transferir +portfolio.pending.step2_buyer.sellersAddress=Endereço {0} do vendedor +portfolio.pending.step2_buyer.buyerAccount=A sua conta de pagamento a ser usada +portfolio.pending.step2_buyer.paymentStarted=Pagamento iniciado +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=Você ainda não fez o seu pagamento de {0}!\nSaiba que o negócio tem de ser concluído até {1}. +portfolio.pending.step2_buyer.openForDispute=Você não completou o seu pagamento!\nO período máx. para o negócio acabou. Por favor entre em contacto com o mediador para assistência. +portfolio.pending.step2_buyer.paperReceipt.headline=Você enviou o recibo de papel para o vendedor de BTC? +portfolio.pending.step2_buyer.paperReceipt.msg=Lembre-se:\nPrecisa escrever no recibo de papel: SEM REEMBOLSOS.\nEm seguida, rasgue-o em 2 partes, tire uma foto e envie-a para o endereço de e-mail do vendedor de BTC. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar Número de autorização e recibo +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Você precisa enviar o Número de Autorização e uma foto do recibo por e-mail para o vendedor de BTC.\nO recibo deve mostrar claramente o nome completo do vendedor, o país, o estado e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o Número de Autorização e o contrato para o vendedor? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN e recibo +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Você precisa enviar o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de BTC.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o MTCN e o contrato para o vendedor? +portfolio.pending.step2_buyer.halCashInfo.headline=Enviar o código HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Você precisa enviar uma mensagem de texto com o código HalCash, bem como o ID da negociação ({0}) para o vendedor BTC.\nO nº do telemóvel do vendedor é {1}.\n\nVocê enviou o código para o vendedor? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Bisq clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=Confirme que você iniciou o pagamento +portfolio.pending.step2_buyer.confirmStart.msg=Você iniciou o pagamento de {0} para o seu parceiro de negociação? +portfolio.pending.step2_buyer.confirmStart.yes=Sim, iniciei o pagamento +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=Aguardar o pagamento +portfolio.pending.step2_seller.f2fInfo.headline=Informação do contacto do comprador +portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação da blockchain.\nVocê precisa esperar até que o comprador de BTC inicie o pagamento {0}. +portfolio.pending.step2_seller.warn=O comprador do BTC ainda não efetuou o pagamento de {0}.\nVocê precisa esperar até que eles tenham iniciado o pagamento.\nSe o negócio não for concluído em {1}, o árbitro irá investigar. +portfolio.pending.step2_seller.openForDispute=O comprador de BTC não iniciou o seu pagamento!\nO período máx. permitido para o negócio acabou.\nVocê pode esperar e dar mais tempo ao seu par de negociação ou entrar em contacto com o mediador para assistência. +tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID ''{0}'' +tradeChat.openChat=Abrir janela de chat +tradeChat.rules=Você pode comunicar com o seu par de negociação para resolver problemas com este negócio.\nNão é obrigatório responder no chat.\nSe algum negociante infringir alguma das regras abaixo, abra uma disputa e reporte-o ao mediador ou ao árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar o ID da transação e o nome de um explorador de blocos.\n\t● Não envie as suas palavras-semente, chaves privadas, senhas ou outra informação sensitiva!\n\t● Não encoraje negócios fora do Bisq (sem segurança).\n\t● Não engaje em nenhuma forma de scams de engenharia social.\n\t● Se um par não responde e prefere não comunicar pelo chat, respeite a sua decisão.\n\t● Mantenha o âmbito da conversa limitado ao negócio. Este chat não é um substituto para o messenger ou uma caixa para trolls.\n\t● Mantenha a conversa amigável e respeitosa. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +message.state.SENT=Mensagem enviada +# suppress inspection "UnusedProperty" +message.state.ARRIVED=A mensagem chegou ao par +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Mensagem de pagamento enviada mais ainda não recebida pelo par +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=O par confirmou a recepção da mensagem +# suppress inspection "UnusedProperty" +message.state.FAILED=Falha de envio de mensagem + +portfolio.pending.step3_buyer.wait.headline=Aguarde confirmação de pagamento do vendedor de BTC. +portfolio.pending.step3_buyer.wait.info=Aguardando confirmação do vendedor de BTC para o recibo do pagamento de {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Pagamento iniciado mensagem de estado +portfolio.pending.step3_buyer.warn.part1a=na blockchain {0} +portfolio.pending.step3_buyer.warn.part1b=no seu provedor de pagamentos (ex: banco) +portfolio.pending.step3_buyer.warn.part2=O vendedor de BTC ainda não confirmou o seu pagamento. Por favor confirme se o envio do pagamento de {0} foi bem-sucedido. +portfolio.pending.step3_buyer.openForDispute=O vendedor de Bisq não confirmou o seu pagamento! O período máx. para o negócio acabou. Você pode esperar e dar mais tempo ao seu par de negociação or pedir assistência de um mediador. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=O seu parceiro de negociação confirmou que começou o pagamento de {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=no seu explorador de blockchain de {0} favorito +portfolio.pending.step3_seller.altcoin.wallet=na sua carteira de {0} +portfolio.pending.step3_seller.altcoin={0} Por favor verifique {1} se a transação para o seu endereço recipiente\n{2}\njá possui confirmações suficientes da blockchain.\nA quantia de pagamento deve ser {3}\n\nVocê pode copiar e colar o seu endereço {4} do ecrã principal depois de fechar o pop-up. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Como o pagamento é feito via Depósito em Dinheiro, o comprador do BTC deve escrever "SEM REEMBOLSO" no recibo de papel, rasgá-lo em 2 partes e enviar uma foto por e-mail.\n\nPara evitar o risco de estorno, confirme apenas se você recebeu o e-mail e se tiver certeza de que o recibo de papel é válido.\nSe você não tiver certeza, {0} +portfolio.pending.step3_seller.moneyGram=O comprador deve enviar o Número de Autorização e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente o seu nome completo, país, estado e a quantia. Por favor verifique seu e-mail se recebeu o Número de Autorização.\n\nDepois de fechar esse pop-up, verá o nome e o endereço do comprador do BTC para levantar o dinheiro da MoneyGram.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! +portfolio.pending.step3_seller.westernUnion=O comprador deve enviar-lhe o MTCN (número de rastreamento) e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente seu nome completo, cidade, país e a quantia Por favor verifique no seu e-mail se você recebeu o MTCN.\n\nDepois de fechar esse pop-up, você verá o nome e endereço do comprador de BTC para levantar o dinheiro da Western Union.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! +portfolio.pending.step3_seller.halCash=O comprador deve-lhe enviar o código HalCash como mensagem de texto. Além disso, você receberá uma mensagem do HalCash com as informações necessárias para retirar o EUR de uma ATM que suporte o HalCash.\n\nDepois de levantar o dinheiro na ATM, confirme aqui o recibo do pagamento! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nVerifique também se o nome do remetente especificado no contrato de negócio corresponde ao nome que aparece no seu extrato bancário:\nNome do remetente, por contrato de negócio: {0}\n\nSe os nomes não forem exatamente iguais, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=não confirme a recepção do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" ou \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recibo de pagamento +portfolio.pending.step3_seller.amountToReceive=Quantia a receber +portfolio.pending.step3_seller.yourAddress=Seu endereço de {0} +portfolio.pending.step3_seller.buyersAddress=Endereço de {0} do comprador: +portfolio.pending.step3_seller.yourAccount=Sua conta de negociação +portfolio.pending.step3_seller.xmrTxHash=ID de transação +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Confirmar recibo de pagamento +portfolio.pending.step3_seller.buyerStartedPayment=O comprador de BTC começou o pagamento de {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Verifique as confirmações da blockchain na sua carteira altcoin ou explorador de blocos e confirme o pagamento quando houverem confirmações da blockchain suficientes. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Verifique em sua conta de negociação (por exemplo, sua conta bancária) e confirme que recebeu o pagamento. +portfolio.pending.step3_seller.warn.part1a=na blockchain {0} +portfolio.pending.step3_seller.warn.part1b=em seu provedor de pagamentos (ex: banco) +portfolio.pending.step3_seller.warn.part2=Você ainda não confirmou a receção do pagamento. Por favor verifique {0} se você recebeu o pagamento. +portfolio.pending.step3_seller.openForDispute=Você não confirmou a receção do pagamento!\nO período máx. para o negócio acabou.\nPor favor confirme ou peça assistência do mediador. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Você recebeu o pagamento de {0} do seu parceiro de negociação?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Verifique também se o nome do remetente especificado no contrato de negócio corresponde ao nome que aparece no seu extrato bancário:\nNome do remetente, por contrato de negócio: {0}\n\nSe os nomes não forem exatamente iguais, não confirme a recepção do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" ou \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Observe que, assim que você confirmar a recepção, o valor da transação bloqueada será liberado para o comprador de BTC e o depósito de segurança será reembolsado.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirme que recebeu o pagamento +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sim, eu recebi o pagamento +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Ao confirmar a recepção do pagamento, você também está verificando a conta da contraparte e assinando-a. Como a conta da contraparte ainda não foi assinada, você deve adiar a confirmação do pagamento o máximo possível para reduzir o risco de estorno. + +portfolio.pending.step5_buyer.groupTitle=Resumo do negócio completo +portfolio.pending.step5_buyer.tradeFee=Taxa de negócio +portfolio.pending.step5_buyer.makersMiningFee=Taxa de mineração +portfolio.pending.step5_buyer.takersMiningFee=Total das taxas de mineração +portfolio.pending.step5_buyer.refunded=Depósito de segurança reembolsado +portfolio.pending.step5_buyer.withdrawBTC=Levantar seus bitcoins +portfolio.pending.step5_buyer.amount=Quantia a levantar +portfolio.pending.step5_buyer.withdrawToAddress=Levantar para o endereço +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=Levantar para carteira externa +portfolio.pending.step5_buyer.alreadyWithdrawn=Seus fundos já foram levantados.\nPor favor, verifique o histórico de transações. +portfolio.pending.step5_buyer.confirmWithdrawal=Confirmar solicitação de levantamento +portfolio.pending.step5_buyer.amountTooLow=A quantia a ser transferida é inferior à taxa de transação e o mín. valor de transação possível (poeira). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Levantamento completado +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Os seus negócios concluídos são armazenadas em \ "Portefólio/Histórico\".\nVocê pode analisar todas as suas transações de bitcoin em \"Fundos/Transações\" +portfolio.pending.step5_buyer.bought=Você comprou +portfolio.pending.step5_buyer.paid=Você pagou + +portfolio.pending.step5_seller.sold=Você vendeu +portfolio.pending.step5_seller.received=Você recebeu + +tradeFeedbackWindow.title=Felicitações por completar o seu negócio +tradeFeedbackWindow.msg.part1=Adoraríamos ouvir sobre sua experiência. Isso nos ajudará a aperfeiçoar o software e a suavizar as arestas. Se você gostaria de fornecer feedback, preencha este pequeno questionário (sem necessidade de registo) em: +tradeFeedbackWindow.msg.part2=Se tiver alguma dúvida ou algum problema, entre em contacto com outros usuários e colaboradores através do fórum Bisq em: +tradeFeedbackWindow.msg.part3=Obrigado por usar Bisq! + +portfolio.pending.role=O meu cargo +portfolio.pending.tradeInformation=Informação do negócio +portfolio.pending.remainingTime=Tempo restante +portfolio.pending.remainingTimeDetail={0} (até {1}) +portfolio.pending.tradePeriodInfo=Após a primeira confirmação da blockchain, o período de negócio começa. Com base no método de pagamento usado, um período de negócio máximo permitido é aplicado. +portfolio.pending.tradePeriodWarning=Se o período é excedido ambos os negociadores podem abrir disputa. +portfolio.pending.tradeNotCompleted=Negócio não completo à tempo (até {0}) +portfolio.pending.tradeProcess=Processo de negócio +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Abrir disputa novamente +portfolio.pending.openSupportTicket.headline=Abrir bilhete de apoio +portfolio.pending.openSupportTicket.msg=Por favor, use esta função apenas em casos de emergência, se você não vir o botão \"Abrir apoio\" ou \"Abrir disputa\".\n\nQuando você abre um bilhete de apoio, o negócio será interrompido e tratado por um mediador ou árbitro. + +portfolio.pending.timeLockNotOver=Você deve esperar ≈{0} (mais {1} blocos) antes que você possa abrir uma disputa de arbitragem. +portfolio.pending.error.depositTxNull=A transação de depósito é null. Você não pode abrir a disputa sem uma transação de depósito válida. Por favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\n\nPara mais ajuda por favor contacte o canal de apoio do Bisq na equipa Keybase do Bisq. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=A transação de depósito não foi confirmada. Você pode abrir uma disputa de arbitragem com uma transação de depósito não confirmada. Por favor espere até que seja confirmada ou vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\n\nPara mais ajuda por favor contacte o canal de apoio do Bisq na equipa Keybase do Bisq. + +portfolio.pending.support.headline.getHelp=Precisa de ajuda? +portfolio.pending.support.text.getHelp=Se tiver algum problema você pode tentar contactar o par de negociação no chat do negócio or perguntar à comunidade do Bisq em https://bisq.community. Se o seu problema ainda não for resolvido, você pode pedir mais ajuda à um mediador. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Verificar o pagamento +portfolio.pending.support.headline.periodOver=O período de negócio acabou + +portfolio.pending.mediationRequested=Mediação solicitada +portfolio.pending.refundRequested=Reembolso pedido +portfolio.pending.openSupport=Abrir bilhete de apoio +portfolio.pending.supportTicketOpened=Bilhete de apoio aberto +portfolio.pending.communicateWithArbitrator=Por favor comunique no ecrã \"Apoio\" com o árbitro. +portfolio.pending.communicateWithMediator=Por favor comunique com o mediador no ecrã \"Apoio\". +portfolio.pending.disputeOpenedMyUser=Você já abriu uma disputa.\n{0} +portfolio.pending.disputeOpenedByPeer=O seu par de negociação abriu uma disputa\n{0} +portfolio.pending.noReceiverAddressDefined=Nenhum endereço de recipiente definido + +portfolio.pending.mediationResult.headline=Pagamento sugerido pela mediação +portfolio.pending.mediationResult.info.noneAccepted=Conclua o negócio aceitando a sugestão do mediador para o pagamento do negócio. +portfolio.pending.mediationResult.info.selfAccepted=Você aceitou a sugestão do mediador. À espera que o par também a aceite. +portfolio.pending.mediationResult.info.peerAccepted=O seu par de negócio aceitou a sugestão do mediador. Você também a aceita? +portfolio.pending.mediationResult.button=Ver a resolução proposta +portfolio.pending.mediationResult.popup.headline=Resultado da mediação para o negócio com o ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu par de negócio aceitou a sugestão do mediador para o negócio {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitragem +portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Concluído +portfolio.closed.ticketClosed=Arbitrado +portfolio.closed.mediationTicketClosed=Mediado +portfolio.closed.canceled=Cancelado +portfolio.failed.Failed=Falhado +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Receber fundos +funds.tab.withdrawal=Enviar fundos +funds.tab.reserved=Fundos reservados +funds.tab.locked=Fundos bloqueados +funds.tab.transactions=Transações + +funds.deposit.unused=Não utilizado +funds.deposit.usedInTx=Utilizado em {0} transação(s) +funds.deposit.fundBisqWallet=Financiar carteira Bisq +funds.deposit.noAddresses=Ainda não foi gerado um endereço de depósito +funds.deposit.fundWallet=Financiar sua carteira +funds.deposit.withdrawFromWallet=Enviar fundos da carteira +funds.deposit.amount=Quantia em BTC (opcional) +funds.deposit.generateAddress=Gerar um endereço novo +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Favor selecione um endereço não utilizado da tabela acima ao invés de gerar um novo. + +funds.withdrawal.arbitrationFee=Taxa de arbitragem +funds.withdrawal.inputs=Seleção de inputs +funds.withdrawal.useAllInputs=Usar todos os inputs disponíveis +funds.withdrawal.useCustomInputs=Usar inputs personalizados +funds.withdrawal.receiverAmount=Quantia do recipiente +funds.withdrawal.senderAmount=Quantia do remetente +funds.withdrawal.feeExcluded=A quantia exclui a taxa de mineração +funds.withdrawal.feeIncluded=A quantia inclui a taxa de mineração +funds.withdrawal.fromLabel=Levantar do endereço +funds.withdrawal.toLabel=Levantar para o endereço +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Levantar selecionados +funds.withdrawal.noFundsAvailable=Não há fundos disponíveis para levantamento +funds.withdrawal.confirmWithdrawalRequest=Confirmar pedido de levantamento +funds.withdrawal.withdrawMultipleAddresses=Levantar de múltiplos endereços ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Levantar de múltiplos endereços:\n{0} +funds.withdrawal.notEnoughFunds=Você não possui fundos suficientes na sua carteira. +funds.withdrawal.selectAddress=Selecione um endereço de origem da tabela +funds.withdrawal.setAmount=Defina quantia a levantar +funds.withdrawal.fillDestAddress=Preencha seu endereço de destino +funds.withdrawal.warn.noSourceAddressSelected=Você precisa selecionar um endereço de origem na tabela acima. +funds.withdrawal.warn.amountExceeds=Você não tem fundos suficientes disponíveis no endereço selecionado.\nConsidere selecionar vários endereços na tabela acima ou alterar acima para incluir a taxa do mineiro. + +funds.reserved.noFunds=Não há fundos reservados em ofertas abertas +funds.reserved.reserved=Reservado na carteira local par a oferte com o ID: {0} + +funds.locked.noFunds=Não há fundos bloqueados em negócios +funds.locked.locked=Bloqueado em transação multi-assinatura para o negócio com o ID: {0} + +funds.tx.direction.sentTo=Enviado para: +funds.tx.direction.receivedWith=Recebido com: +funds.tx.direction.genesisTx=Da tx Genesis: +funds.tx.txFeePaymentForBsqTx=Taxa do mineiro para tx de BSQ +funds.tx.createOfferFee=Taxa do ofertante e da tx: {0} +funds.tx.takeOfferFee=Taxa do aceitador e da tx: {0} +funds.tx.multiSigDeposit=Depósito multi-assinatura: {0} +funds.tx.multiSigPayout=Pagamento multi-assinatura: {0} +funds.tx.disputePayout=Pagamento de disputa: {0} +funds.tx.disputeLost=Caso de disputa perdido: {0} +funds.tx.collateralForRefund=Colateral do reembolso: {0} +funds.tx.timeLockedPayoutTx=Tx de pagamento com trava temporal: {0} +funds.tx.refund=Reembolso da arbitragem: {0} +funds.tx.unknown=Razão desconhecida: {0} +funds.tx.noFundsFromDispute=Nenhum reembolso de disputa +funds.tx.receivedFunds=Fundos recebidos +funds.tx.withdrawnFromWallet=Levantado da carteira +funds.tx.withdrawnFromBSQWallet=BTC levantado da carteira BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=Sem transações disponíveis +funds.tx.revert=Reverter +funds.tx.txSent=Transação enviada com sucesso para um novo endereço em sua carteira Bisq local. +funds.tx.direction.self=Enviado à você mesmo +funds.tx.daoTxFee=Taxa do mineiro para tx de BSQ +funds.tx.reimbursementRequestTxFee=Pedido de reembolso +funds.tx.compensationRequestTxFee=Pedido de compensação +funds.tx.dustAttackTx=Poeira recebida +funds.tx.dustAttackTx.popup=Esta transação está enviando uma quantia muito pequena de BTC para a sua carteira e pode ser uma tentativa das empresas de análise da blockchain para espionar a sua carteira.\n\nSe você usar esse output em uma transação eles decobrirão que você provavelmente também é o proprietário de outros endereços (mistura de moedas).\n\nPara proteger sua privacidade a carteira Bisq ignora tais outputs de poeira para fins de consumo e no ecrã de saldo. Você pode definir a quantia limite a partir da qual um output é considerado poeira nas definições." + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediação +support.tab.arbitration.support=Arbitragem +support.tab.legacyArbitration.support=Arbitragem Antiga +support.tab.ArbitratorsSupportTickets=Bilhetes de {0} +support.filter=Search disputes +support.filter.prompt=Insira o ID do negócio, data, endereço onion ou dados da conta + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Notificação privada +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=Não há bilhetes abertos +support.sendingMessage=Enviando mensagem... +support.receiverNotOnline=O recipiente não está online. A mensagem foi guardada na caixa de correio. +support.sendMessageError=Falha de envio de mensagem. Erro: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=A oferta nessa disputa foi criada com uma versão mais antiga do Bisq.\nVocê não pode fechar essa disputa com sua versão do programa.\n\nPor favor, use uma versão mais antiga com a versão do protocolo {0} +support.openFile=Abrir ficheiro para anexar (tamanho máx.: {0} kb) +support.attachmentTooLarge=O tamanho total de seus anexos anexados é {0} kb e excede o máximo permitido para mensagens de {1} kb. +support.maxSize=O tamanho de ficheiro máx. permitido é {0} kB. +support.attachment=Anexo +support.tooManyAttachments=Você não pode enviar mais do que 3 anexos em uma mensagem. +support.save=Guardar ficheiro no disco +support.messages=Mensagens +support.input.prompt=Inserir a mensagem... +support.send=Enviar +support.addAttachments=Adicionar anexos +support.closeTicket=Fechar bilhete +support.attachments=Anexos: +support.savedInMailbox=Mensagem guardada na caixa de correio do recipiente. +support.arrived=Mensagem chegou em seu destino. +support.acknowledged=Chegada de mensagem confirmada pelo recipiente +support.error=O recipiente não pode processar a mensagem. Erro: {0} +support.buyerAddress=Endereço do comprador de BTC +support.sellerAddress=Endereço do vendedor de BTC +support.role=Cargo +support.agent=Support agent +support.state=Estado +support.chat=Chat +support.closed=Fechado +support.open=Aberto +support.process=Process +support.buyerOfferer=Comprador de BTC/Ofertante +support.sellerOfferer=Vendedor de BTC/Ofertante +support.buyerTaker=Comprador de BTC/Aceitador +support.sellerTaker=Vendedor de BTC/Aceitador + +support.backgroundInfo=O Bisq não é uma empresa, por isso as disputas são tratadas diferentemente.\n\nOs negociadores podem se comunicar dentro do programa via chat seguro no ecrã de negócios abertos para tentar resolver disputas por conta própria. Se isso não for suficiente, um mediador pode ajudar. O mediador avaliará a situação e sugerirá um pagamento dos fundos de negócio. Se ambos os negociadores aceitarem essa sugestão, a transação de pagamento será concluída e o negócio será encerrado. Se um ou ambos os negociadores não concordarem com o pagamento sugerido pelo mediador, eles podem solicitar a arbitragem. O árbitro reavaliará a situação e, se justificado, pagará pessoalmente o comerciante de volta e solicitará reembolso à OAD do Bisq. +support.initialInfo=Digite uma descrição do seu problema no campo de texto abaixo. Adicione o máximo de informações possível para acelerar o tempo de resolução da disputa.\n\nAqui está uma lista do que você deve fornecer:\n● Se você é o comprador de BTC: Você fez a transferência da Fiat ou Altcoin? Se sim, você clicou no botão 'pagamento iniciado' no programa?\n● Se você é o vendedor de BTC: Você recebeu o pagamento da Fiat ou Altcoin? Se sim, você clicou no botão 'pagamento recebido' no programa?\n\t● Qual versão do Bisq você está usando?\n\t● Qual sistema operacional você está usando?\n\t ● Se você encontrou um problema com transações com falha, considere mudar para um novo diretório de dados.\n\t Às vezes, o diretório de dados é corrompido e leva a erros estranhos.\n\t Consulte: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nFamiliarize-se com as regras básicas do processo de disputa:\n\t● Você precisa responder às solicitações do {0} dentro de 2 dias.\n\t● Os mediadores respondem entre 2 dias. Os árbitros respondem dentro de 5 dias úteis.\n\t ● O período máximo para uma disputa é de 14 dias.\n\t ● Você precisa cooperar com o {1} e fornecer as informações solicitadas para justificar o seu caso.\n\t● Você aceitou as regras descritas no documento de disputa no contrato do usuário quando iniciou o programa.\n\nVocê pode ler mais sobre o processo de disputa em: {2} +support.systemMsg=Mensagem do sistema: {0} +support.youOpenedTicket=Você abriu um pedido para apoio.\n\n{0}\n\nBisq versão: {1} +support.youOpenedDispute=Você abriu um pedido para uma disputa.\n\n{0}\n\nBisq versão: {1} +support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVersão Bisq: {1} +support.peerOpenedTicket=O seu par de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão Bisq: {1} +support.peerOpenedDispute=O seu par de negociação solicitou uma disputa.\n\n{0}\n\nVersão Bisq: {1} +support.peerOpenedDisputeForMediation=O seu par de negociação solicitou uma mediação.\n\n{0}\n\nVersão Bisq: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Endereço do nó do mediador: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Preferências +settings.tab.network=Informação da rede +settings.tab.about=Sobre + +setting.preferences.general=Preferências gerais +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Máx. desvio do preço de mercado +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Evite o modo espera +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Valores acima de {0}% não são permitidos. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Usar valor personalizado +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Pares ignorados [endereço onion:porta] +setting.preferences.ignoreDustThreshold=Mín. valor de output não-poeira +setting.preferences.currenciesInList=Moedas na lista de feed de preço de mercado +setting.preferences.prefCurrency=Moeda preferrida +setting.preferences.displayFiat=Mostrar moedas nacionais +setting.preferences.noFiat=Não há moedas nacionais selecionadas +setting.preferences.cannotRemovePrefCurrency=Você não pode remover a moeda preferida de exibição selecionada +setting.preferences.displayAltcoins=Mostrar altcoins +setting.preferences.noAltcoins=Não há altcoins selecionadas +setting.preferences.addFiat=Adicionar moeda nacional +setting.preferences.addAltcoin=Adicionar altcoin +setting.preferences.displayOptions=Mostrar opções +setting.preferences.showOwnOffers=Mostrar as minhas próprias ofertas no livro de ofertas +setting.preferences.useAnimations=Usar animações +setting.preferences.useDarkMode=Usar o modo escuro +setting.preferences.sortWithNumOffers=Ordenar listas de mercado por nº de ofertas/negociações: +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Reiniciar todos os marcadores \"Não mostrar novamente\" +settings.preferences.languageChange=Para aplicar a mudança de língua em todas os ecrãs requer uma reinicialização. +settings.preferences.supportLanguageWarning=Em caso de disputa, por favor saiba que a mediação será tratada em {0} e a arbitragem em {1}. +setting.preferences.daoOptions=Opções da OAD +setting.preferences.dao.resyncFromGenesis.label=Reconstruir o estado da OAD à partir da tx genesis +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Executar Bisq como nó completo OAD +setting.preferences.dao.rpcUser=Nome de usuário de RPC +setting.preferences.dao.rpcPw=Senha de RPC +setting.preferences.dao.blockNotifyPort=Bloquear porta de notificação +setting.preferences.dao.fullNodeInfo=Para executar o Bisq como nó completo da OAD você precisa ter Bitcoin Core em execução local e RPC ativado. Todos os requerimentos estão documentados em '' {0} ''. +setting.preferences.dao.fullNodeInfo.ok=Abrir página de documentos +setting.preferences.dao.fullNodeInfo.cancel=Não, eu fico com o modo nó lite +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Nome +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Rede Bitcoin +settings.net.p2pHeader=Rede do Bisq +settings.net.onionAddressLabel=O meu endereço onion +settings.net.btcNodesLabel=Usar nós de Bitcoin Core personalizados +settings.net.bitcoinPeersLabel=Pares conectados +settings.net.useTorForBtcJLabel=Usar Tor para a rede de Bitcoin +settings.net.bitcoinNodesLabel=Nós de Bitcoin Core para conectar +settings.net.useProvidedNodesRadio=Usar nós de Bitcoin Core providenciados +settings.net.usePublicNodesRadio=Usar rede de Bitcoin pública +settings.net.useCustomNodesRadio=Usar nós de Bitcoin Core personalizados +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=Não, usar nós providenciados +settings.net.warn.usePublicNodes.usePublic=Sim, usar a rede pública +settings.net.warn.useCustomNodes.B2XWarning=Por favor, certifique-se de que seu nó Bitcoin é um nó confiável do Bitcoin Core!\n\nConectar-se a nós que não seguem as regras de consenso do Bitcoin Core pode corromper a sua carteira e causar problemas no processo de negócio.\n\nOs usuários que se conectam a nós que violam regras de consenso são responsáveis por qualquer dano resultante. Quaisquer disputas resultantes serão decididas em favor do outro par. Nenhum suporte técnico será dado aos usuários que ignorarem esses alertas e mecanismos de proteção! +settings.net.warn.invalidBtcConfig=A conexão à rede Bitcoin falhou porque sua configuração é inválida.\n\nA sua configuração foi redefinida para usar os nós de Bitcoin fornecidos. Você precisará reiniciar o programa. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=Pares conectados +settings.net.onionAddressColumn=Endereço onion +settings.net.creationDateColumn=Estabelecida +settings.net.connectionTypeColumn=Entrando/Saindo +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Ida-e-volta +settings.net.sentBytesColumn=Enviado +settings.net.receivedBytesColumn=Recebido +settings.net.peerTypeColumn=Tipo de par +settings.net.openTorSettingsButton=Abrir definições de Tor + +settings.net.versionColumn=Versão +settings.net.subVersionColumn=Sub-versão +settings.net.heightColumn=Altura + +settings.net.needRestart=Você precisa reiniciar o programa para aplicar essa alteração.\nVocê quer fazer isso agora?? +settings.net.notKnownYet=Ainda desconhecido... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[endereço IP:porta | nome de host:porta | endereço onion:porta] (separado por vírgula). A porta pode ser omitida se a padrão for usada (8333). +settings.net.seedNode=Nó semente +settings.net.directPeer=Par (direto) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Par +settings.net.inbound=entrante +settings.net.outbound=sainte +settings.net.reSyncSPVChainLabel=Re-sincronizar corrente SPV +settings.net.reSyncSPVChainButton=Remover ficheiro SPV e re-sincronizar +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=O ficheiro da corrente SPV foi apagado. Por favor, seja paciente. Pode demorar um pouco para re-sincronizar com a rede. +settings.net.reSyncSPVAfterRestartCompleted=A resincronização concluiu. Por favor reiniciar o programa. +settings.net.reSyncSPVFailed=Não foi possível remover o ficherio da corrente SPV\nErro: {0} +setting.about.aboutBisq=Sobre Bisq +setting.about.about=O Bisq é um software de código aberto que facilita a troca de bitcoins com moedas nacionais (e outras criptomoedas) por meio de uma rede par-à-par descentralizada, de uma maneira que protege fortemente a privacidade do utilizador. Saiba mais sobre o Bisq na nossa página web do projeto. +setting.about.web=página da web do Bisq +setting.about.code=Código fonte +setting.about.agpl=Licença AGPL +setting.about.support=Apoio Bisq +setting.about.def=O Bisq não é uma empresa - é um projeto aberto à comunidade. Se você quiser participar ou apoiar o Bisq, siga os links abaixo. +setting.about.contribute=Contribuir +setting.about.providers=Provedores de dados +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Preços de mercado fornecidos por +setting.about.feeEstimation.label=Taxa de mineração fornecida por +setting.about.versionDetails=Detalhes da versão +setting.about.version=Versão do programa +setting.about.subsystems.label=Versão de subsistemas +setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagem P2P: {1}; Versão da base de dados local: {2}; Versão do protocolo de negócio: {3} + +setting.about.shortcuts=Atalhos +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigar o menu principal +setting.about.shortcuts.menuNav.value=Para navigar o menu principal pressione:: 'Ctrl' ou 'alt' ou 'cmd' juntamente com uma tecla numérica entre '1-9' + +setting.about.shortcuts.close=Fechar o Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo +setting.about.shortcuts.closePopup.value=Tecla "ESCAPE" + +setting.about.shortcuts.chatSendMsg=Enviar uma mensagem ao negociador +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Abrir disputa +setting.about.shortcuts.openDispute.value=Selecionar negócio pendente e clicar: {0} + +setting.about.shortcuts.walletDetails=Abrir janela de detalhes da carteira + +setting.about.shortcuts.openEmergencyBtcWalletTool=Abrir ferramenta e emergência da carteira para a carteira de BTC + +setting.about.shortcuts.openEmergencyBsqWalletTool=Abrir ferramenta e emergência da carteira para a carteira de BSQ + +setting.about.shortcuts.showTorLogs=Escolher o nível de log para mensagens Tor entre DEBUG e WARN + +setting.about.shortcuts.manualPayoutTxWindow=Abrir janela para pagamento manual da tx de depósito multi-assinatura 2de2 + +setting.about.shortcuts.reRepublishAllGovernanceData=Republicar informação da governação da OAD (propostas, votos) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Registrar árbitro (apenas mediador/árbitro) +setting.about.shortcuts.registerArbitrator.value=Navigar para conta e pressionar: {0} + +setting.about.shortcuts.registerMediator=Registar mediador (apenas medidor/árbitro) +setting.about.shortcuts.registerMediator.value=Navigar para conta e pressionar: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir janela para assinatura de idade da conta (apenas árbitros legacy) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigar para o ecrã de árbitro legacy e pressionar: {0} + +setting.about.shortcuts.sendAlertMsg=Enviar alerta ou mensagem de atualização (atividade privilegiada) + +setting.about.shortcuts.sendFilter=Definir Filtro (atividade privilegiada) + +setting.about.shortcuts.sendPrivateNotification=Enviar notificação privada ao par (atividade privilegiada) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Registo do Mediador +account.tab.refundAgentRegistration=Registro de agente de reembolso +account.tab.signing=Signing +account.info.headline=Bem vindo à sua conta Bisq +account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais e altcoins e criar um backup da sua carteira e dos dados da conta.\n\nUma nova carteira de Bitcoin foi criada na primeira vez que você iniciou o Bisq.\n\nÉ altamente recomendável que você anote as sua palavras-semente da carteira do Bitcoin (consulte a guia na parte superior) e considere adicionar uma senha antes do financiamento. Depósitos e retiradas de Bitcoin são gerenciados na secção \"Fundos\".\n\nNota sobre privacidade e segurança: como o Bisq é uma exchange descentralizada, todos os seus dados são mantidos no seu computador. Como não há servidores, não temos acesso às suas informações pessoais, fundos ou mesmo seu endereço IP. Dados como números de contas bancárias, endereços de altcoin e Bitcoin etc. são compartilhados apenas com seu par de negociação para realizar negociações iniciadas (no caso de uma disputa, o mediador ou o árbitro verá os mesmos dados que o seu parceiro de negociação). + +account.menu.paymentAccount=Contas de moedas nacionais +account.menu.altCoinsAccountView=Contas de altcoins +account.menu.password=Senha da carteira +account.menu.seedWords=Semente da carteira +account.menu.walletInfo=Wallet info +account.menu.backup=Backup +account.menu.notifications=Notificações + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Chave pública + +account.arbitratorRegistration.register=Registrar +account.arbitratorRegistration.registration=Registro de {0} +account.arbitratorRegistration.revoke=Revogar +account.arbitratorRegistration.info.msg=Saiba que precisa estar disponível até 15 dias depois depois da revogação porque podem existir negócios que o estejam a usar como {0}. O período máx. de negócio permitido é de 8 dias e a disputa pode levar até 7 dias. +account.arbitratorRegistration.warn.min1Language=Precisa definir pelo menos 1 idioma.\nAdicionamos o idioma padrão para você. +account.arbitratorRegistration.removedSuccess=Você removeu com sucesso seu registro da rede do Bisq. +account.arbitratorRegistration.removedFailed=Não foi possível remover o registro.{0} +account.arbitratorRegistration.registerSuccess=Você se registrou com sucesso na rede do Bisq. +account.arbitratorRegistration.registerFailed=Não foi possível completar o registro.{0} + +account.altcoin.yourAltcoinAccounts=As suas contas de altcoins +account.altcoin.popup.wallet.msg=Certifique-se de seguir os requisitos para o uso das carteiras de {0}, conforme descrito na página da web de {1}.\nO uso de carteiras de exchanges centralizadas nas quais (a) você não controla suas chaves ou (b) que não utiliza software de carteira compatível é arriscado: pode levar à perda dos fundos negociados!\nO mediador ou árbitro não é um especialista em {2} e não pode ajudar nesses casos. +account.altcoin.popup.wallet.confirm=Eu entendo e confirmo que eu sei qual carteira que preciso usar. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Negociar UPX no Bisq exige que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o UPX, você precisa usar a carteira GUI oficial do uPlexa ou a carteira CLI do uPlexa com o sinalizador store-tx-info ativado (padrão em novas versões). Certifique-se de que você pode acessar a chave da tx, pois isso seria necessário em caso de disputa.\nuplexa-wallet-cli (use o comando get_tx_key)\nuplexa-wallet-gui (vá para a aba do histoórico e clique no botão (P) para prova de pagamento)\n\nEm exploradores de blocos normais, a transferência não é verificável.\n\nVocê precisa fornecer ao árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- Endereço público do destinatário\n\nA falha no fornecimento dos dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso da disputa. O remetente de UPX é responsável por fornecer a verificação da transferência de UPX ao árbitro em caso de disputa.\n\nNão é necessário um ID de pagamento, apenas o endereço público normal.\nSe você não tiver certeza sobre esse processo, visite o canal de discord do uPlexa (https://discord.gg/vhdNSrV) ou o chat do Telegram do uPlexa (https://t.me/uplexaOfficial) para encontrar mais informações. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Negociar o ARQ no Bisq requer que você entenda e atenda aos seguintes requerimentos:\n\nPara enviar o ARQ, você precisa usar a wallet oficial do ArQmA GUI ou a carteira do ArQmA CLI com o marcador store-tx-info ativado (padrão em novas versões). Por favor, certifique-se que você pode acessar a chave da tx porque isso seria necessário em caso de uma disputa.\narqma-wallet-cli (use o comando get_tx_key)\narqma-wallet-gui (vá para a aba do histórico e clique no botão (P) para comprovar o pagamento)\n\nEm exploradores de blocos normais, a transferência não é verificável.\n\nVocê precisa fornecer ao mediador ou árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- o endereço público do destinatário\n\nA falha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso de disputa. O remetente do ARQ é responsável por fornecer a verificação da transferência do ARQ ao mediador ou árbitro em caso de disputa.\n\nNão é necessário um código de pagamento, apenas o endereço público normal.\nSe você não tiver certeza sobre esse processo, visite o canal de discord do ArQmA (https://discord.gg/s9BQpJT) ou o fórum do ArQmA (https://labs.arqma.com) para obter mais informações. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Negociar MSR no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar MSR, você precisa usar a carteira GUI oficial do Masari ou a carteira CLI do Masari com o marcador store-tx-info ativado (ativado por padrão) ou a carteira web do Masari (https://wallet.getmasari.org). Por favor, certifique-se que você pode acessar a chave da tx porque isso seria necessário em caso de uma disputa.\nmasari-wallet-cli (use o comando get_tx_key)\nmasari-wallet-gui (vá para a aba do histórico e clique no botão (P) para comprovar o pagamento)\n\nMasari Web Wallet (vá para Account -> histórico de transação e veja os detalhes da sua transação enviada)\n\nA verificação pode ser realizada na carteira.\nmasari-wallet-cli: usando o comando (check_tx_key).\nmasari-wallet-gui: na aba Advanced > Prove/Check.\nA verificação pode ser realizada no eplorador de blocos\nExplorador de blocos aberto (https://explorer.getmasari.org), use a barra de procurar para encontrar o hash da transação.\nUma que vez que a transação for encontrada, desça até ao baixo da àrea 'Prove Sending' e preencha os detalhes necessários.\nVocê precisa fornecer ao mediador ou ao árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- o endereço público do destinatário\n\nFalha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso de disputa. O remetente da XMR é responsável por fornecer a verificação da transferência da MSR para o mediador ou o árbitro no caso de uma disputa.\n\nNão é necessário um código de pagamento, apenas o endereço público normal.\nSe você não tem certeza sobre o processo, peça ajuda no Discord official do Masari (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Negociar o BLUR no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o BLUR você deve usar a carteira CLI da Blur Network ou a carteira GUI.\n\nSe você estiver usando a carteira CLI, um hash da transação (tx ID) será exibido após uma transferência ser enviada. Você deve guardar esta informação. Imediatamente após o envio da transferência, você deve usar o comando 'get_tx_key' para recuperar a chave privada da transação. Se você não conseguir executar essa etapa, talvez não consiga recuperar a chave mais tarde.\n\nSe você estiver usando a carteira GUI do Blur Network, a chave privada da transação e a ID da transação podem ser encontradas convenientemente na aba "Histórico". Imediatamente após o envio, localize a transação de interesse. Clique no símbolo "?" no canto inferior direito da caixa que contém a transação. Você deve guardar esta informação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1.) a ID da transação, 2.) a chave privada da transação e 3.) o endereço do destinatário. O mediador ou árbitro verificará a transferência do BLUR usando o Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente de BLUR tem 100% de responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos não negocie no Bisq. Primeiro, procure ajuda no Discord da Rede de Blur (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Negociar Solo no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o Solo, você deve usar a carteira CLI do Solo.\n\nSe você está a usar a carteira CLI, um hash da transação (tx ID) aparecerá depois de a transação ser feita. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'get_tx_key' para recuperar a chave secreta da transação. Se você não conseguir executar este passo, talvez não seja possível recuperar a chave mais tarde.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1.) o ID da transação, 2.) a chave privada da transação e 3.) o endereço do recipiente. O mediador ou árbitro então verificará a transferência do Solo usando o Explorador de Blocos Solo (https://explorer.minesolo.com/).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente de Solo tem 100% da responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Bisq. Primeiro, procure ajuda no Discord da Rede do Solo (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Negociar o CASH2 no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar CASH2, você deve usar a versão Cash2 Wallet versão 3 ou superior.\n\nDepois que uma transação é enviada, a ID da transação será exibida. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'getTxKey' no simplewallet para recuperar a chave secreta da transação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1) a ID da transação, 2) a chave secreta da transação e 3) o endereço Cash2 do destinatário. O mediador ou árbitro irá então verificar a transferência do CASH2 usando o Explorador de Blocos do Cash2 (https://blocks.cash2.org).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente do CASH2 tem 100% de responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Bisq. Primeiro procure ajuda no Discord do Cash2 (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Negociar Qwertycoin no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o QWC, você deve usar a versão oficial do QWC Wallet 5.1.3 ou superior.\n\nDepois que uma transação é enviada, o ID da transação será exibida. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'get_Tx_Key' na simplewallet para recuperar a chave secreta da transação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1) o ID da transação, 2) a chave secreta da transação e 3) o endereço QWC do destinatário. O mediador ou árbitro então verificará a transferência do QWC usando o Explorador de Blocos QWC (https://explorer.qwertycoin.org).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente QWC tem 100% da responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Bisq. Primeiro, procure ajuda no QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Negociar Dragonglass no Bisq requer que você entenda e cumpra os seguintes requerimentos:\n\nPor causa da privacidade que a Dragonglass fornece, uma transação não é verificável na blockchain pública. Se necessário, você pode comprovar seu pagamento através do uso de sua chave privada TXN.\nA chave privade TXN é uma chave única gerada automaticamente para cada transação que só pode ser acessada dentro da sua carteira DRGL.\nTanto pela GUI do DRGL-wallet (dentro da janela de detalhes da transação) ou pelo simplewallet da CLI do Dragonglass (usando o comando "get_tx_key").\n\nA versão do DRGL 'Oathkeeper' e superior são NECESSÁRIAS para ambos.\n\nEm caso de disputa, você deve fornecer ao mediador ou árbitro os seguintes dados:\n- A chave privada TXN\n- O hash da transação\n- o endereço público do destinatário\n\nA verificação do pagamento pode ser feita usando os dados acima como inputs em (http://drgl.info/#check_txn).\n\nA falha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda disputa. O remetente da Dragonglass é responsável por fornecer a verificação da transferência do DRGL para o mediador ou árbitro em caso de disputa. O uso de PaymentID não é obrigatório.\n\nSe você não tiver certeza sobre qualquer parte deste processo, visite Dragonglass on Discord (http://discord.drgl.info) para obter ajuda. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=Ao usar o Zcash você só pode usar os endereços transparentes (começando com t), e não os endereços z (privados), porque o mediador ou árbitro não seria capaz de verificar a transação com endereços z. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=Ao usar a Zcoin, você só pode usar os endereços transparentes (rastreáveis) e não os endereços não rastreáveis, porque o mediador ou árbitro não seria capaz de verificar a transação com endereços não rastreáveis num explorador de blocos. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requer um processo interativo entre o remetente e o recipiente para criar a transação. Certifique-se de seguir as instruções da página web do projeto GRIN para enviar e receber de forma confiável o GRIN (o recipiente precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO Bisq suporta apenas o formato de URL da carteira Grinbox (Wallet713).\n\nO remetente GRIN é obrigado a fornecer prova de que eles enviaram GRIN com sucesso. Se a carteira não puder fornecer essa prova, uma disputa potencial será resolvida em favor do recipiente de GRIN. Por favor, certifique-se de usar o software Grinbox mais recente que suporta a prova da transação e que você entende o processo de transferência e receção do GRIN, bem como criar a prova.\n\nConsulte https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para obter mais informações sobre a ferramenta de prova Grinbox. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=O BEAM requer um processo interativo entre o remetente e o recipiente para criar a transação.\n\nCertifique-se de seguir as instruções da página Web do projeto BEAM para enviar e receber BEAM de forma confiável (o recipiente precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO remetente BEAM é obrigado a fornecer prova de que eles enviaram o BEAM com sucesso. Certifique-se de usar software de carteira que pode produzir tal prova. Se a carteira não puder fornecer a prova, uma disputa potencial será resolvida em favor do recipiente de BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=A negociação de ParsiCoin no Bisq exige que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar PARS você deve usar a versão oficial da Carteira ParsiCoin 3.0.0 ou superior.\n\nVocê pode verificar o Hash da Transação e a Chave da Transação na secção das Transações na sua carteira GUI (ParsiPay) Você precisa clicar com o lado direito na transação e, em seguida, clicar em mostrar detalhes.\n\nEm caso de arbitragem, você deve apresentar o seguinte para um mediador ou árbitro: 1) o Hash da Transação, 2) a Chave da Transação, e 3) endereço PARS do recipiente. O mediador ou árbitro irá então verificar a transferência PARS usando o Explorador de Blocos da ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\nFalha em fornecer as informações necessárias ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, o remetente da ParsiCoin carrega 100% da carga de responsabilidade em verificar as transações à um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Bisq. Primeiro procure ajuda no Discord da ParsiCoin (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=Para negociar blackcoins queimados, você precisa saber o seguinte:\n\nBlackcoins queimados não podem ser gastos. Para os negociar no Bisq, os output scripts precisam estar na forma: OP_RETURN OP_PUSHDATA, seguido pelos data bytes que, após serem codificados em hex, constituem endereços. Por exemplo, blackcoins queimados com um endereço 666f6f (“foo” em UTF-8) terá o seguinte script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPara criar blackcoins queimados, deve-se usar o comando RPC “burn” disponível em algumas carteiras.\n\nPara casos possíveis, confira https://ibo.laboratorium.ee .\n\nComo os blackcoins queimados não podem ser gastos, eles não podem voltar a ser vendidos. “Vender” blackcoins queimados significa queimar blackcoins comuns (com os dados associados iguais ao endereço de destino).\n\nEm caso de disputa, o vendedor de BLK precisa providenciar o hash da transação. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=A negociação de L-BTC no Bisq exige que você entenda o seguinte:\n\nAo receber L-BTC para um negócio no Bisq, você não pode usar a aplicação móvel Blockstream Green Wallet ou uma carteira de custódia / exchange. Você só deve receber o L-BTC na carteira Liquid Elements Core ou em outra carteira L-BTC que permita obter a chave ofuscante para o seu endereço L-BTC cego.\n\nNo caso de ser necessária mediação, ou se surgir uma disputa de negócio, você deve divulgar a chave ofuscante do seu endereço L-BTC de recebimento ao mediador ou agente de reembolso Bisq, para que eles possam verificar os detalhes da sua Transação Confidencial no seu próprio Elements Core full node.\n\nO não fornecimento das informações necessárias ao mediador ou ao agente de reembolso resultará na perda do caso de disputa. Em todos os casos de disputa, o recipiente de L-BTC suporta 100% da responsabilidade ao fornecer prova criptográfica ao mediador ou ao agente de reembolso.\n\nSe você não entender esses requerimentos, não negocie o L-BTC no Bisq. + +account.fiat.yourFiatAccounts=A sua conta de moeda nacional + +account.backup.title=Carteira de backup +account.backup.location=Localizacao do backup +account.backup.selectLocation=Selecione localização para backup +account.backup.backupNow=Fazer backup agora (o backup não é criptografado) +account.backup.appDir=Diretório de dados do programa +account.backup.openDirectory=Abrir diretório +account.backup.openLogFile=Abrir ficheiro de log +account.backup.success=Backup guardado com sucesso em:\n{0} +account.backup.directoryNotAccessible=O diretório escolhido não é acessível. {0} + +account.password.removePw.button=Remover senha +account.password.removePw.headline=Remover proteção com senha da carteira +account.password.setPw.button=Definir senha +account.password.setPw.headline=Definir proteção de senha da carteira +account.password.info=Com a proteção por senha, você precisará inserir a sua senha na inicialização do programa, ao levantar o bitcoin da sua carteira e ao restaurar a sua carteira a partir de palavras-semente. + +account.seed.backup.title=Fazer backup das palavras semente da sua carteira +account.seed.info=Por favor, anote as palavras-semente da carteira e a data! Você pode recuperar sua carteira a qualquer momento com palavras-semente e a data.\nAs mesmas palavras-semente são usadas para a carteira BTC e BSQ.\n\nVocê deve anotar as palavras-semente numa folha de papel. Não as guarde no seu computador.\n\nPor favor, note que as palavras-semente não são um substituto para um backup.\nVocê precisa criar um backup de todo o diretório do programa a partir do ecrã \"Conta/Backup\" para recuperar o estado e os dados do programa.\nA importação de palavras-semente é recomendada apenas para casos de emergência. O programa não será funcional sem um backup adequado dos arquivos da base de dados e das chaves! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Você não definiu uma senha da carteira que protegeria a exibição das palavras-semente.\n\nVocê quer exibir as palavras-semente? +account.seed.warn.noPw.yes=Sim, e não me pergunte novamente +account.seed.enterPw=Digite a senha para ver palavras-semente +account.seed.restore.info=Por favor, faça um backup antes de aplicar a restauração a partir de palavras-semente. Esteja ciente de que a restauração da carteira é apenas para casos de emergência e pode causar problemas com a base de dados interna da carteira.\nNão é uma maneira de aplicar um backup! Por favor, use um backup do diretório de dados do programa para restaurar um estado anterior do programa.\n\nDepois de restaurar o programa será desligado automaticamente. Depois de ter reiniciado o programa, ele será ressincronizado com a rede Bitcoin. Isso pode demorar um pouco e consumir muito do CPU, especialmente se a carteira for mais antiga e tiver muitas transações. Por favor, evite interromper esse processo, caso contrário, você pode precisar excluir o ficheiro da corrente do SPV novamente ou repetir o processo de restauração. +account.seed.restore.ok=Ok, restaurar e desligar Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Configuração +account.notifications.download.label=Baixar a aplicação móvel +account.notifications.waitingForWebCam=Esperando pela webcam... +account.notifications.webCamWindow.headline=Scannear código QR à partir do telemóvel +account.notifications.webcam.label=Usar webcam +account.notifications.webcam.button=Scannear código QR +account.notifications.noWebcam.button=Eu não tenho webcam +account.notifications.erase.label=Limpar notificações no telemóvel +account.notifications.erase.title=Limpar notificações +account.notifications.email.label=Token de emparelhamento +account.notifications.email.prompt=Inserir o token de emparelhamento recebido por email +account.notifications.settings.title=Definições +account.notifications.useSound.label=Reproduzir som de notificação no telemóvel +account.notifications.trade.label=Receber mensagens de negócio +account.notifications.market.label=Receber alertas de oferta +account.notifications.price.label=Receber alertas de preço +account.notifications.priceAlert.title=Alertas de preço +account.notifications.priceAlert.high.label=Notificar se o preço de BTC está acima de +account.notifications.priceAlert.low.label=Notificar se o preço de BTC está abaixo de +account.notifications.priceAlert.setButton=Definir alerta de preço +account.notifications.priceAlert.removeButton=Remover alerta de preço +account.notifications.trade.message.title=Estado do negócio mudou +account.notifications.trade.message.msg.conf=A transação do depósito para o negócio com o ID {0} está confirmada. Por favor, abra seu programa Bisq e inicie o pagamento. +account.notifications.trade.message.msg.started=O comprador do BTC iniciou o pagamento para o negócio com o ID {0}. +account.notifications.trade.message.msg.completed=O negócio com o ID {0} está completo. +account.notifications.offer.message.title=A sua oferta foi aceite +account.notifications.offer.message.msg=A sua oferta com o ID {0} foi aceite +account.notifications.dispute.message.title=Nova mensagem de disputa +account.notifications.dispute.message.msg=Recebeu uma menagem de disputa para o negócio com o ID {0} + +account.notifications.marketAlert.title=Alertas de ofertas +account.notifications.marketAlert.selectPaymentAccount=Ofertas compatíveis com a conta de pagamento +account.notifications.marketAlert.offerType.label=Tipo de oferta que me interessa +account.notifications.marketAlert.offerType.buy=Ofertas de compra (Eu quero vender BTC) +account.notifications.marketAlert.offerType.sell=Ofertas de venda (eu quero comprar BTC) +account.notifications.marketAlert.trigger=Distância do preço da oferta (%) +account.notifications.marketAlert.trigger.info=Com uma distância de preço definida, você só receberá um alerta quando uma oferta que atenda (ou exceda) os seus requerimentos for publicada. Exemplo: você quer vender BTC, mas você só venderá à um ganho de 2% sobre o atual preço de mercado. Definir esse campo como 2% garantirá que você receba apenas alertas para ofertas com preços que estão 2% (ou mais) acima do atual preço de mercado. +account.notifications.marketAlert.trigger.prompt=Distância da percentagem do preço de mercado (ex: 2.50%, -0.50%, etc) +account.notifications.marketAlert.addButton=Adicionar alerta de oferta +account.notifications.marketAlert.manageAlertsButton=Gerir alertas de oferta +account.notifications.marketAlert.manageAlerts.title=Gerir alertas de oferta +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conta de pagamento +account.notifications.marketAlert.manageAlerts.header.trigger=Preço de desencadeamento +account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta +account.notifications.marketAlert.message.title=Alerta de oferta +account.notifications.marketAlert.message.msg.below=abaixo de +account.notifications.marketAlert.message.msg.above=acima de +account.notifications.marketAlert.message.msg=Uma nova ''{0} {1}'' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento ''{5}'' foi publicada no livro de ofertas do Bisq.\nID da oferta: {6}. +account.notifications.priceAlert.message.title=Alerta de preço para {0} +account.notifications.priceAlert.message.msg=O teu alerta de preço foi desencadeado. O preço atual de {0} é de {1} {2} +account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor use a opção email para enviar o token e a chave de criptografia do seu telemóvel para o programa da Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=O preço mais alto deve ser maior que o preço mais baixo. +account.notifications.priceAlert.warning.lowerPriceTooHigh=O preço mais baixo deve ser menor que o preço mais alto. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Factos & Números +dao.tab.bsqWallet=Carteira BSQ +dao.tab.proposals=Governação +dao.tab.bonding=Vínculo +dao.tab.proofOfBurn=Taxa de listagem de ativos/Prova de destruição +dao.tab.monitor=Monitor de rede +dao.tab.news=Notícias + +dao.paidWithBsq=pago com BSQ +dao.availableBsqBalance=Disponível para gastos (verificado + outputs de trocos não-confirmados) +dao.verifiedBsqBalance=Saldo de todos UTXOs verificados +dao.unconfirmedChangeBalance=Saldo de todos outputs de trocos não confirmados +dao.unverifiedBsqBalance=Saldo de todas as transações não verificadas (aguardando confirmação do bloco) +dao.lockedForVoteBalance=Usado para votação +dao.lockedInBonds=Bloqueado em vínculos +dao.availableNonBsqBalance=Balanço não-BSQ disponível (BTC) +dao.reputationBalance=Valor de Mérito (não gastável) + +dao.tx.published.success=Sua transação foi publicada com sucesso. +dao.proposal.menuItem.make=Criar proposta +dao.proposal.menuItem.browse=Propostas abertas +dao.proposal.menuItem.vote=Votar em propostas +dao.proposal.menuItem.result=Resultado da votação +dao.cycle.headline=Ciclo de votação +dao.cycle.overview.headline=Visão geral do ciclo de votação +dao.cycle.currentPhase=Fase atual +dao.cycle.currentBlockHeight=Altura do bloco atual +dao.cycle.proposal=Fase de proposta +dao.cycle.proposal.next=Próxima fase de proposta +dao.cycle.blindVote=Fase the votação às cegas +dao.cycle.voteReveal=Fase de revelação de votos +dao.cycle.voteResult=Resultado da votação +dao.cycle.phaseDuration={0} blocos (≈{1}); Bloco {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Bloco {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=A transação da revelação da votação foi publicada +dao.voteReveal.txPublished=Sua transação de revelação do voto com o ID de transação {0} foi publicadq com sucesso.\n\nIsso acontece automaticamente pelo software se você tiver participado na votação da OAD. + +dao.results.cycles.header=Ciclos +dao.results.cycles.table.header.cycle=Ciclo +dao.results.cycles.table.header.numProposals=Propostas +dao.results.cycles.table.header.voteWeight=Peso do voto +dao.results.cycles.table.header.issuance=Emissão + +dao.results.results.table.item.cycle=Ciclo {0} começou: {1} + +dao.results.proposals.header=Propostas do ciclo selecionado +dao.results.proposals.table.header.nameLink=Nome/link +dao.results.proposals.table.header.details=Detalhes +dao.results.proposals.table.header.myVote=O meu voto +dao.results.proposals.table.header.result=Resultado da votação +dao.results.proposals.table.header.threshold=Limite +dao.results.proposals.table.header.quorum=Quórum + +dao.results.proposals.voting.detail.header=Resultado dos votos para a proposta selecionada + +dao.results.exceptions=Excepção(s) do resultado dos votos + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Indefinido + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=taxa BSQ do ofertante +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=taxa BSQ do aceitador +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Mín. taxa BSQ do ofertante +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Mín. taxa BSQ do aceitador +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=taxa BTC do ofertante +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=taxa BTC do aceitador +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Mín. taxa BTC do ofertante +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Mín. taxa BTC do aceitador +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Taxa de proposta em BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Taxa de votação em BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Mín. quantia de BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Máx. quantia de BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=mín. quantia de BSQ para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=máx. quantia de BSQ para pedido de reembolso + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Quórum necessário em BSQ para proposta genérica +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Quórum necessário em BSQ para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Quórum necessário em BSQ para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Quórum necessário em BSQ para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Quórum necessário em BSQ para remover um ativo +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Quórum necessário em BSQ para pedido de confiscação +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Quórum necessário em BSQ para pedidos de cargos vinculados + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Limite necessário em % para proposta genérica +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Limite necessário em % para pedido de compensação +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Limite necessário em % para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Limite necessário em % para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Limite necessário em % para remover um ativo +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Limite necessário em % para pedido de confiscação +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Limite necessário em % para pedidos de cargos vinculados + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Endereço BTC do recipiente + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Taxa de listagem do ativo por dia +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Mín. volume de negócio para ativos + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Tempo de bloqueio para tx de pagamento de negócio alternativa +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Taxa do árbitro em BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Máx. limite de negócio em BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Fator de unidade de cargo vinculado em BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Limite de emissão por ciclo em BSQ + +dao.param.currentValue=Valor atual: {0} +dao.param.currentAndPastValue=Valor atual: {0} (Valor quando a proposta foi feita: {1}) +dao.param.blocks={0} blocos + +dao.results.invalidVotes=Tivemos votos inválidos naquele ciclo de votação. Isso pide acontecer se o voto não foi bem distribuído pela rede do Bisq.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Pausa 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Fase the votação às cegas +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Pausa 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Fase de revelação de votos +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Pausa 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Fase de resultado + +dao.results.votes.table.header.stakeAndMerit=Peso do voto +dao.results.votes.table.header.stake=Participação +dao.results.votes.table.header.merit=Ganho +dao.results.votes.table.header.vote=Voto + +dao.bond.menuItem.bondedRoles=Cargos vinculados +dao.bond.menuItem.reputation=Reputação vinculada +dao.bond.menuItem.bonds=Vínculos + +dao.bond.dashboard.bondsHeadline=BSQ vinculado +dao.bond.dashboard.lockupAmount=Bloquear fundos +dao.bond.dashboard.unlockingAmount=Desbloqueando fundos (espere até que o tempo de bloqueio termine) + + +dao.bond.reputation.header=Bloquear um vínculo para reputação +dao.bond.reputation.table.header=Os meus vínculos de reputação +dao.bond.reputation.amount=Quantia de BSQ a bloquear +dao.bond.reputation.time=Tempo de desbloqueio em blocos +dao.bond.reputation.salt=Sal +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Bloquear +dao.bond.reputation.lockup.headline=Confirmar transação de bloqueio +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Confirmar transação de desbloqueio +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=Todos os vínculos + +dao.bond.bondedReputation=Reputação Vinculada +dao.bond.bondedRoles=Cargos vinculados + +dao.bond.details.header=Detalhes do cargo +dao.bond.details.role=Cargo +dao.bond.details.requiredBond=Vínculo de BSQ necessário +dao.bond.details.unlockTime=Tempo de desbloqueio em blocos +dao.bond.details.link=Link para descrição do cargo +dao.bond.details.isSingleton=Pode ser aceite por detentores de vários cargos +dao.bond.details.blocks={0} blocos + +dao.bond.table.column.name=Nome +dao.bond.table.column.link=Link +dao.bond.table.column.bondType=Tipo de vínculo +dao.bond.table.column.details=Detalhes +dao.bond.table.column.lockupTxId=ID da tx de bloqueio +dao.bond.table.column.bondState=Estado de vínculo +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=Data de bloqueio + +dao.bond.table.button.lockup=Bloqueio +dao.bond.table.button.unlock=Desbloquear +dao.bond.table.button.revoke=Revogar + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Ainda não vinculado +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Bloqueio pendente +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Vínculo bloqueado +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Desbloqueio pendente +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Tx de desbloqueio confirmada +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Desbloqueando vínculo +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Vínculo bloqueado +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Vínculo confiscado + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Cargo vínculado +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Reputação vinculada + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Admin de GitHub +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Admin do Fórum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Admin do Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Admin do Youtube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Mantedor de Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Mantedor do BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Mantedor Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Mantedor Website +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Operador do Fórum +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Operador do nó de semente +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Price node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Operador dos mercados +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Operador da transmissão de notificações móveis +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Detentor do nome de domínio +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Admin do DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediador +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Árbitro +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Proprietário do endereço BTC de doação + +dao.burnBsq.assetFee=Listagem de ativos +dao.burnBsq.menuItem.assetFee=Taxa de listagem de ativos +dao.burnBsq.menuItem.proofOfBurn=Prova de destruição +dao.burnBsq.header=Taxa para listagem de ativos +dao.burnBsq.selectAsset=Selecionar ativo +dao.burnBsq.fee=Taxa +dao.burnBsq.trialPeriod=Período de teste +dao.burnBsq.payFee=Pagar taxa +dao.burnBsq.allAssets=Todos os ativos +dao.burnBsq.assets.nameAndCode=Nome do ativo +dao.burnBsq.assets.state=Estado +dao.burnBsq.assets.tradeVolume=Volume de negócio +dao.burnBsq.assets.lookBackPeriod=Período de verificação +dao.burnBsq.assets.trialFee=Taxa para período de teste +dao.burnBsq.assets.totalFee=Total de taxas pagas +dao.burnBsq.assets.days={0} dias +dao.burnBsq.assets.toFewDays=A taxa do ativo é demasiado baixa. A mín. quantidade de dias para o período de teste é {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=No período de teste +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Ativamente negociado +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Deslistado devido à inatividade +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Removido por votação + +dao.proofOfBurn.header=Prova de destruição +dao.proofOfBurn.amount=Quantia +dao.proofOfBurn.preImage=Pré-imagem +dao.proofOfBurn.burn=Destruição +dao.proofOfBurn.allTxs=Todas as transações de prova de destruição +dao.proofOfBurn.myItems=A minha transação de prova de destruição +dao.proofOfBurn.date=Data +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transações +dao.proofOfBurn.pubKey=Chave Pública +dao.proofOfBurn.signature.window.title=Assinar uma mensagem com a chave da transação de prova de destruição +dao.proofOfBurn.verify.window.title=Verificar uma mensagem com a chave da transação de prova de destruição +dao.proofOfBurn.copySig=Copiar assinatura para o clipboard +dao.proofOfBurn.sign=Assinar +dao.proofOfBurn.message=Mensagem +dao.proofOfBurn.sig=Assinatura +dao.proofOfBurn.verify=Verificar +dao.proofOfBurn.verificationResult.ok=Verificação sucedida +dao.proofOfBurn.verificationResult.failed=Verificação falhada + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Pausa antes da fase de votação às cegas +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Fase de votação às cegas +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Pausa antes da fase de revelação dos votos +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Fase de revelação de votos +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Pausa antes da fase de resultado +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Fase de resultado da votação + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Fase de proposta +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Voto às cegas +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Revelação de votos +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Resultado da votação + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Pedido de compensação +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Pedido de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Proposta para um cargo vinculado +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Proposta para remover um ativo +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Proposta para mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Proposta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Proposta para confiscação de um vínculo + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Pedido de compensação +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Pedido de reembolso +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Cargo vinculado +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Remoção de uma altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Mudança de parâmetro +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Proposta genérica +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Confiscação de um vínculo + +dao.proposal.details=Detalhes da proposta +dao.proposal.selectedProposal=Proposta selecionada +dao.proposal.active.header=Propostas do ciclo atual +dao.proposal.active.remove.confirm=Tem certeza de que pretende remover essa proposta?\nA taxa da proposta paga será perdida. +dao.proposal.active.remove.doRemove=Sim, remover a minha proposta +dao.proposal.active.remove.failed=Não foi possível remover a proposta. +dao.proposal.myVote.title=Votação +dao.proposal.myVote.accept=Aceitar proposta +dao.proposal.myVote.reject=Rejeitar proposta +dao.proposal.myVote.removeMyVote=Ignorar proposta +dao.proposal.myVote.merit=Peso de voto de BSQ ganho +dao.proposal.myVote.stake=Peso de voto de participação +dao.proposal.myVote.revealTxId=ID de transação de revelação de voto +dao.proposal.myVote.stake.prompt=Máx. participação disponível para votação: {0} +dao.proposal.votes.header=Definir participação para votar e publique seus votos +dao.proposal.myVote.button=Publicar votos +dao.proposal.myVote.setStake.description=Depois de votar em todas as propostas, você deve definir a sua participação para votação bloqueando BSQ. Quanto mais BSQ você bloquear, mais peso o seu voto terá.\n\nBSQ bloqueado para votação será desbloqueado novamente durante a fase de revelação de voto. +dao.proposal.create.selectProposalType=Selecionar o tipo de proposta +dao.proposal.create.phase.inactive=Por favor espere até a próxima fase +dao.proposal.create.proposalType=Tipo de proposta +dao.proposal.create.new=Criar novo pedido de compensação +dao.proposal.create.button=Criar proposta +dao.proposal.create.publish=Publicar proposta +dao.proposal.create.publishing=Publicação de proposta em progresso ... +dao.proposal=proposta +dao.proposal.display.type=Tipo de proposta +dao.proposal.display.name=Exacto nome de usuário no Github +dao.proposal.display.link=Link para informação detalhada +dao.proposal.display.link.prompt=Link para a proposta +dao.proposal.display.requestedBsq=Quantia requerida em BSQ +dao.proposal.display.txId=ID de transação de proposta +dao.proposal.display.proposalFee=Taxa de proposta +dao.proposal.display.myVote=O meu voto +dao.proposal.display.voteResult=Resumo do resultado da votação +dao.proposal.display.bondedRoleComboBox.label=Tipo de cargo vinculado +dao.proposal.display.requiredBondForRole.label=Vínculo necessário para cargo +dao.proposal.display.option=Opção + +dao.proposal.table.header.proposalType=Tipo de proposta +dao.proposal.table.header.link=Link +dao.proposal.table.header.myVote=O meu voto +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Remover +dao.proposal.table.icon.tooltip.removeProposal=Remover a minha proposta +dao.proposal.table.icon.tooltip.changeVote=Voto atual: ''{0}''. Mudar o voto para: ''{1}'' + +dao.proposal.display.myVote.accepted=Aceite +dao.proposal.display.myVote.rejected=Rejeitado +dao.proposal.display.myVote.ignored=Ignorado +dao.proposal.display.myVote.unCounted=O voto não foi incluído no resultado +dao.proposal.myVote.summary=Votou: {0}; Peso do voto: {1} (ganho: {2} + participação: {3}) {4} +dao.proposal.myVote.invalid=O voto foi inválido + +dao.proposal.voteResult.success=Aceite +dao.proposal.voteResult.failed=Rejeitado +dao.proposal.voteResult.summary=Resultado: {0}; Limite: {1} (necessário > {2}); Quórum: {3} (necessário > {4}) + +dao.proposal.display.paramComboBox.label=Selecione o parâmetro a mudar +dao.proposal.display.paramValue=Valor do parâmetro + +dao.proposal.display.confiscateBondComboBox.label=Escolha o vínculo +dao.proposal.display.assetComboBox.label=Ativo a remover + +dao.blindVote=voto cego + +dao.blindVote.startPublishing=Publicando transação de voto cego... +dao.blindVote.success=Sua transação de voto cego foi publicada com sucesso.\n\nPor favor, note que você tem que estar online na fase de revelação de votos para que o seu programa Bisq possa publicar a transação de revelação de voto. Sem a transação da revelação do voto o seu voto seria inválido! + +dao.wallet.menuItem.send=Enviar +dao.wallet.menuItem.receive=Receber +dao.wallet.menuItem.transactions=Transações + +dao.wallet.dashboard.myBalance=O saldo da minha carteira + +dao.wallet.receive.fundYourWallet=O seu endereço recipiente de BSQ +dao.wallet.receive.bsqAddress=Endereço da carteira BSQ (endereço não utilizado) + +dao.wallet.send.sendFunds=Enviar fundos +dao.wallet.send.sendBtcFunds=Enviar fundos não-BSQ (BTC) +dao.wallet.send.amount=Quantia em BSQ +dao.wallet.send.btcAmount=Quantia em BTC (fundos não-BSQ) +dao.wallet.send.setAmount=Definir quantia a levantar (mín. quantia é {0}) +dao.wallet.send.receiverAddress=Endereço BSQ do recipiente +dao.wallet.send.receiverBtcAddress=Endereço BTC do recipiente +dao.wallet.send.setDestinationAddress=Preencha seu endereço de destino +dao.wallet.send.send=Enviar fundos BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Enviar fundos BTC +dao.wallet.send.sendFunds.headline=Confirmar pedido de levantamento. +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Último bloco verificado: {0} +dao.wallet.chainHeightSyncing=Esperando blocos... {0} dos {1} blocos verificados +dao.wallet.tx.type=Tipo + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Indefinido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Não reconhecido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Transação BSQ não verificada +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Transação BSQ inválida +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Transação gênesis +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Transferir BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ recebido +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ enviado +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Taxa de negociação +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Taxa de pedido de compensação +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Taxa para pedido de reembolso +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Taxa para proposta +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Taxa para voto cego +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Revelação de votos +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Bloquear vínculo +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Desbloquear vínculo +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Taxa de listagem de ativos +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Prova de destruição +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregular + +dao.tx.withdrawnFromWallet=BTC levantado da carteira +dao.tx.issuanceFromCompReq=Pedido de compensação/emissão +dao.tx.issuanceFromCompReq.tooltip=Pedido de compensação que levou à emissão de novo BSQ.\nData de emissão: {0} +dao.tx.issuanceFromReimbursement=Pedido de reembolso/emissão +dao.tx.issuanceFromReimbursement.tooltip=Solicitação de reembolso que levou à emissão de novo BSQ.\nData de emissão: {0} +dao.proposal.create.missingBsqFunds=Você não tem fundos suficientes para criar a proposta. Se você tem uma transação de BSQ não confirmada, você precisa esperar por uma confirmação da blockchain porque o BSQ é validado somente se estiver incluído num bloco.\nEm falta: {0} + +dao.proposal.create.missingBsqFundsForBond=Você não tem fundos suficientes para este cargo. Você ainda pode publicar essa proposta, mas precisará do valor completo de BSQ necessário para esse cargo se ela for aceite.\nEm falta: {0} + +dao.proposal.create.missingMinerFeeFunds=Você não tem suficientes fundos de BTC para criar a transação da proposta. Todas as transações de BSQ exigem uma taxa do mineiro em BTC.\nEm falta: {0} + +dao.proposal.create.missingIssuanceFunds=Você não tem suficientes fundos de BTC para criar a transação da proposta. Todas as transações de BSQ exigem uma taxa do mineiro em BTC, e as transações de emissão também exigem BTC pela quantia de BSQ solicitado ({0} satoshis/BSQ).\nEm falta: {1} + +dao.feeTx.confirm=Confirmar transação {0} +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=A OAD DO BISQ +dao.news.bisqDAO.description=Assim como o mercado de câmbio do Bisq é descentralizado e resistente à censura, o seu modelo de governação também o é - e a OAD do Bisq e o token da BSQ são as ferramentas que tornam isso possível. +dao.news.bisqDAO.readMoreLink=Saber Mais Sobre a OAD do Bisq + +dao.news.pastContribution.title=FEZ CONTRIBUIÇÕES NO PASSADO? PEÇA BSQ +dao.news.pastContribution.description=Se você contribuiu para o Bisq, por favor, use o endereço BSQ abaixo e faça um pedido para participar da distribuição genesis de BSQ. +dao.news.pastContribution.yourAddress=O seu endereço da carteira BSQ +dao.news.pastContribution.requestNow=Solicitar agora + +dao.news.DAOOnTestnet.title=EXECUTE A OAD DO BISQ NA NOSSA REDE DE TESTES +dao.news.DAOOnTestnet.description=A mainnet da OAD do Bisq ainda não foi lançada, mas você pode aprender sobre a OAD do Bisq executando-a na nossa testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Mudar para Modo Testnet da OAD +dao.news.DAOOnTestnet.firstSection.content=Mude para a Testnet da OAD no painel de Definições +dao.news.DAOOnTestnet.secondSection.title=2. Obtenha alguns BSQ +dao.news.DAOOnTestnet.secondSection.content=Solicite BSQ no Slack ou Compre BSQ no Bisq +dao.news.DAOOnTestnet.thirdSection.title=3. Participe num Ciclo de Votação +dao.news.DAOOnTestnet.thirdSection.content=Fazendo propostas e votando em propostas para mudar vários aspetos da Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Explorar um Explorador de Blocos da BSQ +dao.news.DAOOnTestnet.fourthSection.content=Desde que BSQ é apenas bitcoin, você pode ver transações de BSQ no nosso explorador de blocos de bitcoin. +dao.news.DAOOnTestnet.readMoreLink=Leia a documentação completa + +dao.monitor.daoState=Estado da OAD +dao.monitor.proposals=Estado de propostas +dao.monitor.blindVotes=Estado de votos cegos + +dao.monitor.table.peers=Pares +dao.monitor.table.conflicts=Conflitos +dao.monitor.state=Estado +dao.monitor.requestAlHashes=Pedir todos os hashes +dao.monitor.resync=Re-sincronizar estado da OAD +dao.monitor.table.header.cycleBlockHeight=Ciclo / altura do bloco +dao.monitor.table.cycleBlockHeight=Ciclo {0} / bloco {1} +dao.monitor.table.seedPeers=Nó semente: {0} + +dao.monitor.daoState.headline=Estado da OAD +dao.monitor.daoState.table.headline=Corrente de hashes do estado da OAD +dao.monitor.daoState.table.blockHeight=Altura do bloco +dao.monitor.daoState.table.hash=Hash do estado da OAD +dao.monitor.daoState.table.prev=Hash anterior +dao.monitor.daoState.conflictTable.headline=Hashes do estado da OAD de pares em conflito +dao.monitor.daoState.utxoConflicts=Conflitos de UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Altura do bloco: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Soma de todo UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Soma de todo BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=O estado da OAD não está sincronizado com a rede. Após o reinicio a OAD vai ressincronizar. + +dao.monitor.proposal.headline=Estado de propostas +dao.monitor.proposal.table.headline=Corrente hashes do estado da propostaa +dao.monitor.proposal.conflictTable.headline=Hashes de estado da proposta de pares em conflito + +dao.monitor.proposal.table.hash=Hash do estado da proposta +dao.monitor.proposal.table.prev=Hash anterior +dao.monitor.proposal.table.numProposals=Nº de propostas + +dao.monitor.isInConflictWithSeedNode=Os seus dados locais não estão em consenso com pelo menos um nó semente. Por favor, re-sincronizar o estado da OAD. +dao.monitor.isInConflictWithNonSeedNode=Um dos seus pares não está em consenso com a rede, mas o seu nó está em sincronia com os nós semente. +dao.monitor.daoStateInSync=Seu nó local está em consenso com a rede + +dao.monitor.blindVote.headline=Estado de votos cegos +dao.monitor.blindVote.table.headline=Corrente de hashes de estado de voto cego +dao.monitor.blindVote.conflictTable.headline=Hashes de estado de voto cego de pares em conflito +dao.monitor.blindVote.table.hash=Hash de estado de voto cego +dao.monitor.blindVote.table.prev=Hash anterior +dao.monitor.blindVote.table.numBlindVotes=Nº de votos cegos + +dao.factsAndFigures.menuItem.supply=Estoque de BSQ +dao.factsAndFigures.menuItem.transactions=Transações BSQ + +dao.factsAndFigures.dashboard.avgPrice90=Média de 90 dias do preço de negócio de BSQ/BTC +dao.factsAndFigures.dashboard.avgPrice30=Média de 30 dias do preço de negócio de BSQ/BTC +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=Total BSQ disponível +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ emitido v. BSQ queimado + +dao.factsAndFigures.supply.issued=BSQ emitido +dao.factsAndFigures.supply.compReq=Pedidos de compensação +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ emitido na transação genesis +dao.factsAndFigures.supply.compRequestIssueAmount=BDQ emitido para pedidos de compensação +dao.factsAndFigures.supply.reimbursementAmount=BSQ emitido para pedidos de reembolso +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ destruído + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Volume de negócio +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Estado global do BSQ bloqueado +dao.factsAndFigures.supply.totalLockedUpAmount=Bloqueado em vínculos +dao.factsAndFigures.supply.totalUnlockingAmount=Desbloqueando BSQ de vínculos +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ desbloqueado de vínculos +dao.factsAndFigures.supply.totalConfiscatedAmount=BSQ confiscado de vínculos +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Transação gênesis +dao.factsAndFigures.transactions.genesisBlockHeight=Altura do bloco genesis +dao.factsAndFigures.transactions.genesisTxId=ID da transação genesis +dao.factsAndFigures.transactions.txDetails=Estatísticas das transações de BSQ +dao.factsAndFigures.transactions.allTx=Nº de todas as transações de BSQ +dao.factsAndFigures.transactions.utxo=Nº de todos os outputs de transações não gastos +dao.factsAndFigures.transactions.compensationIssuanceTx=Nº de todas as transações de emissão de pedido de compensação +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Nº de todas as trnsações de emissão de pedido de reembolso +dao.factsAndFigures.transactions.burntTx=Nº de todas transações de pagamentos de taxa +dao.factsAndFigures.transactions.invalidTx=Nº de todas as transações inválidas +dao.factsAndFigures.transactions.irregularTx=Nº de todas as transações irregulares + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Saldo disponível + +contractWindow.title=Detalhes da disputa +contractWindow.dates=Data de oferta / Data de negócio +contractWindow.btcAddresses=Endereço bitcoin comprador BTC / vendendor BTC +contractWindow.onions=Endereço de rede comprador de BTC / vendendor de BTC +contractWindow.accountAge=Idade da conta do comprador de BTC / vendedor de BTC +contractWindow.numDisputes=Nº de disputas comprador de BTC / vendedor de BTC: +contractWindow.contractHash=Hash do contrato + +displayAlertMessageWindow.headline=Informação importante! +displayAlertMessageWindow.update.headline=Informação importante de atualização! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=Ficheiros: +displayUpdateDownloadWindow.downloadingFile=Descarregando: {0} +displayUpdateDownloadWindow.verifiedSigs=Assinaturas verificadas com chaves: +displayUpdateDownloadWindow.status.downloading=Descarregando ficheiros... +displayUpdateDownloadWindow.status.verifying=Verificando assinatura... +displayUpdateDownloadWindow.button.label=Descarregar o instalador e verificar a assinatura +displayUpdateDownloadWindow.button.downloadLater=Descarregar depois +displayUpdateDownloadWindow.button.ignoreDownload=Ignorar esta versão +displayUpdateDownloadWindow.headline=Uma nova atualização do Bisq está disponível! +displayUpdateDownloadWindow.download.failed.headline=Download falhou +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=A nova versão foi descarregada com sucesso e a assinatura foi verificada.\n\nPor favor, abra o diretório de download, desligue o programa e instale a nova versão. +displayUpdateDownloadWindow.download.openDir=Abrir diretório de download + +disputeSummaryWindow.title=Resumo +disputeSummaryWindow.openDate=Data de abertura do bilhete +disputeSummaryWindow.role=Função do negociador +disputeSummaryWindow.payout=Pagamento da quantia de negócio +disputeSummaryWindow.payout.getsTradeAmount={0} de BTC fica com o pagamento da quantia de negócio +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Pagamento personalizado +disputeSummaryWindow.payoutAmount.buyer=Quantia de pagamento do comprador +disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor +disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador +disputeSummaryWindow.reason=Razão da disputa +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Erro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sem resposta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Golpe +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Outro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Notas de resumo +disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo +disputeSummaryWindow.close.button=Fechar bilhete + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=Você também precisa fechar o bilhete dos pares de negociação! +disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=O comprador recebe {0} no endereço: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=O vendedor recebe {0} no endereço: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline={0} ferramenta de emergência da carteira +emptyWalletWindow.info=Por favor, use isso apenas em caso de emergência, se você não puder aceder o seu fundo a partir da interface do utilizador.\n\nPor favor, note que todas as ofertas abertas serão fechadas automaticamente ao usar esta ferramenta.\n\nAntes de usar essa ferramenta, faça backup do seu diretório de dados. Você pode fazer isso em \"Conta/Backup\".\n\nPor favor comunique-nos o seu problema e envie um relatório de erros no Github ou no fórum Bisq para que possamos investigar o que causou o problema. +emptyWalletWindow.balance=O saldo disponível da sua carteira: +emptyWalletWindow.bsq.btcBalance=Saldo de satoshis não-BSQ + +emptyWalletWindow.address=Seu endereço de destino +emptyWalletWindow.button=Enviar todos os fundos +emptyWalletWindow.openOffers.warn=Você tem ofertas abertas que serão removidas se você esvaziar a carteira.\nTem certeza de que deseja esvaziar a sua carteira? +emptyWalletWindow.openOffers.yes=Sim, tenho certeza +emptyWalletWindow.sent.success=O saldo da sua carteira foi transferido com sucesso. + +enterPrivKeyWindow.headline=Inserir chave privada para registro + +filterWindow.headline=Editar lista de filtragem +filterWindow.offers=Ofertas filtradas (sep. por vírgula): +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Dados da conta de negociação filtrados:\nFormato: lista de [id de método de pagamento | campo de dados | valor] sep. por vírgula +filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) +filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) +filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) +filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion sep. por virgula) +filterWindow.seedNode=Nós de semente filtrados (endereços onion sep. por vírgula) +filterWindow.priceRelayNode=Nós de transmissão de preço filtrados (endereços onion sep. por vírgula) +filterWindow.btcNode=Nós de Bitcoin filtrados (endereços + portas sep. por vírgula) +filterWindow.preventPublicBtcNetwork=Prevenir uso da rede de Bitcoin pública +filterWindow.disableDao=Desativar OAD +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Versão mín. necessária para a OAD +filterWindow.disableTradeBelowVersion=Mín. versão necessária para negociação +filterWindow.add=Adicionar filtro +filterWindow.remove=Remover filtro +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Quantia mín. de BTC +offerDetailsWindow.min=(mín. {0}) +offerDetailsWindow.distance=(distância do preço de mercado: {0}) +offerDetailsWindow.myTradingAccount=Minha conta de negociação +offerDetailsWindow.offererBankId=(ID bancário/BIC/SWIFT do ofertante) +offerDetailsWindow.offerersBankName=(nome do banco do ofertante) +offerDetailsWindow.bankId=ID do banco (ex. BIC ou SWIFT) +offerDetailsWindow.countryBank=País do banco do ofertante +offerDetailsWindow.commitment=Compromisso +offerDetailsWindow.agree=Eu concordo +offerDetailsWindow.tac=Termos e condições +offerDetailsWindow.confirm.maker=Confirmar: Criar oferta para {0} bitcoin +offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} bitcoin +offerDetailsWindow.creationDate=Data de criação +offerDetailsWindow.makersOnion=Endereço onion do ofertante + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Pedido de pagamento:\n{0} + +selectDepositTxWindow.headline=Selecionar transação de depósito para disputa +selectDepositTxWindow.msg=A transação de depósito não foi armazenada no negócio.\nPor favor, selecione uma das transações multi-assinatura existentes na sua carteira, que foi a transação de depósito usada no negócio falhado.\n\nVocê pode encontrar a transação correta abrindo a janela de detalhes de negócio (clique na ID do negócio na lista) e seguindo o output da transação de pagamento da taxa de negociação para a próxima transação onde você ver a transação de depósito multi-assinatura (o endereço começa com 3). Esse ID de transação deve estar visível na lista apresentada aqui. Depois de encontrar a transação correta, selecione a transação aqui e continue.\n\nDesculpe pela inconveniência, mas esse caso de erro deve acontecer muito raramente e, no futuro, tentaremos encontrar maneiras melhores de resolvê-lo. +selectDepositTxWindow.select=Selecionar transação de depósito + +sendAlertMessageWindow.headline=Enviar notificação global +sendAlertMessageWindow.alertMsg=Mensagem de alerta +sendAlertMessageWindow.enterMsg=Digitar mensagem +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=Nº da nova versão +sendAlertMessageWindow.send=Enviar notificação +sendAlertMessageWindow.remove=Remover notificação + +sendPrivateNotificationWindow.headline=Enviar mensagem privada +sendPrivateNotificationWindow.privateNotification=Notificação privada +sendPrivateNotificationWindow.enterNotification=Digite notificação +sendPrivateNotificationWindow.send=Enviar notificação privada + +showWalletDataWindow.walletData=Dados da carteira +showWalletDataWindow.includePrivKeys=Incluir chaves privadas + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Acordo de utilizador +tacWindow.agree=Eu concordo +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.tradeDate=Data de negócio +tradeDetailsWindow.txFee=Taxa de mineração +tradeDetailsWindow.tradingPeersOnion=Endereço onion dos parceiros de negociação +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Estado de negócio +tradeDetailsWindow.agentAddresses=Árbitro/Mediador +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Digite senha para abrir: + +torNetworkSettingWindow.header=Definições de redes Tor +torNetworkSettingWindow.noBridges=Não usar pontes +torNetworkSettingWindow.providedBridges=Conectar com as pontes providenciadas +torNetworkSettingWindow.customBridges=Inserir pontes personalizadas +torNetworkSettingWindow.transportType=Tipo de transporte +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (recomendado) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Insira uma ou mais pontes repetidoras (uma por linha) +torNetworkSettingWindow.enterBridgePrompt=digite endereço:porta +torNetworkSettingWindow.restartInfo=Precisa de reiniciar para aplicar as mudanças +torNetworkSettingWindow.openTorWebPage=Abrir à página web do projeto Tor +torNetworkSettingWindow.deleteFiles.header=Problemas de conexão? +torNetworkSettingWindow.deleteFiles.info=Se tem repetidamente problemas de conexão no início, apagar os ficheiros de Tor desatualizados pode ajudar. Para fazê-lo clique no botão em baixo e reinicia de seguida. +torNetworkSettingWindow.deleteFiles.button=Apagar ficheiros de Tor desatualizados e desligar +torNetworkSettingWindow.deleteFiles.progress=Desligar Tor em progresso +torNetworkSettingWindow.deleteFiles.success=Ficheiros de Tor desatualizados apagados com sucesso. Por favor reinicie. +torNetworkSettingWindow.bridges.header=O Tor está bloqueado? +torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu fornecedor de internet ou pelo seu país, você pode tentar usar pontes Tor.\nVisite a página web do Tor em: https://bridges.torproject.org/bridges para saber mais sobre pontes e transportes conectáveis. + +feeOptionWindow.headline=Escolha a moeda para o pagamento da taxa de negócio +feeOptionWindow.info=Pode escolher pagar a taxa de negócio em BSQ ou em BTC. Se escolher BSQ tira proveito da taxa de negócio descontada. +feeOptionWindow.optionsLabel=Escolha a moeda para o pagamento da taxa de negócio +feeOptionWindow.useBTC=Usar BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Notificação +popup.headline.instruction=Favor observar: +popup.headline.attention=Atenção +popup.headline.backgroundInfo=Informação preliminar +popup.headline.feedback=Concluído +popup.headline.confirmation=Comfirmação +popup.headline.information=Informação +popup.headline.warning=Aviso +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 bisq.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} + +error.spvFileCorrupted=Ocorreu um erro ao ler o ficheiro da corrente SPV .\nPode ser que o ficheiro da corrente SPV esteja corrompido.\n\nMensagem de erro: {0}\n\nVocê deseja apagá-lo e iniciar uma resincronização? +error.deleteAddressEntryListFailed=Não foi possível apagar o ficheiro AddressEntryList.\nErro: {0} +error.closedTradeWithUnconfirmedDepositTx=A transação de depósito do negócio fechado com o ID de negócio {0} ainda não foi confirmada.\n\nPor favor re-sinronize o ficheiro SPV em \"Definições/Informação da Rede\" para ver se a transação é inválida. +error.closedTradeWithNoDepositTx=A transação de depósito do negócio fechado com o ID de negócio {0} é null.\n\nPor favor reinicie o programa para limpar a lista de negócios fechados. + +popup.warning.walletNotInitialized=A carteira ainda não foi inicializada +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=Você provavelmente tem a versão errada do Bisq para este computador.\nA arquitetura do seu computador é: {0}.\nO binário Bisq que você instalou é: {1}.\nPor favor, desligue e reinstale a versão correta ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq já está em execução. Você não pode executar duas instâncias do Bisq. +popup.warning.tradePeriod.halfReached=Sua negociação com o ID {0} atingiu a metade do valor máx. do período de negociação permitido e ainda não está concluído.\n\nO período de negócio termina em {1}\n\nPor favor, verifique o seu estado de negócio em \"Portefólio/Ofertas abertas\" para mais informações. +popup.warning.tradePeriod.ended=O seu negócio com o ID {0} atingiu o limite do máx. período de negociação permitido e não está concluído.\n\nO período de negócio terminou em {1}\n\nPor favor, verifique o seu negócio em \"Portefólio/Negócios abertos\" para entrar em contacto com o mediador. +popup.warning.noTradingAccountSetup.headline=Você ainda não configurou uma conta de negociação +popup.warning.noTradingAccountSetup.msg=Você precisa configurar uma conta de moeda nacional ou altcoin antes de criar uma oferta.\nVocê quer configurar uma conta? +popup.warning.noArbitratorsAvailable=Não há árbitros disponíveis. +popup.warning.noMediatorsAvailable=Não há mediadores disponíveis. +popup.warning.notFullyConnected=Você precisa esperar até estar totalmente conectado à rede.\nIsso pode levar cerca de 2 minutos na inicialização. +popup.warning.notSufficientConnectionsToBtcNetwork=Você precisa esperar até que você tenha pelo menos {0} conexões com a rede Bitcoin. +popup.warning.downloadNotComplete=Você precisa esperar até que o download dos blocos de Bitcoin ausentes esteja completo. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Tem certeza de que deseja remover essa oferta?\nA taxa de ofertante de {0} será perdida se você remover essa oferta. +popup.warning.tooLargePercentageValue=Você não pode definir uma percentagem superior à 100%. +popup.warning.examplePercentageValue=Por favor digitar um número percentual como \"5.4\" para 5.4% +popup.warning.noPriceFeedAvailable=Não há feed de preço disponível para essa moeda. Você não pode usar um preço baseado em percentagem.\nPor favor, selecione o preço fixo. +popup.warning.sendMsgFailed=Enviar mensagem para seu par de negociação falhou.\nPor favor, tente novamente e se continuar a falhar relate um erro. +popup.warning.insufficientBtcFundsForBsqTx=Você não tem fundos BTC suficientes para pagar a taxa de mineração para essa transação.\nPor favor financie sua carteira BTC.\nFundos em falta: {0} +popup.warning.bsqChangeBelowDustException=Esta transação cria um output de trocos de BSQ que está abaixo do limite de poeira (5,46 BSQ) e seria rejeitada pela rede do bitcoin.\n\nVocê precisa enviar uma quantia mais elevada para evitar o output de trocos (por exemplo, adicionando a quantia de poeira ao seu montante a ser enviado) ou adicionar mais fundos de BSQ à sua carteira de modo a evitar a gerar um output de poeira.\n\nO output de poeira é {0}. +popup.warning.btcChangeBelowDustException=Esta transação cria um output de trocos que está abaixo do limite de poeira (546 satoshis) e seria rejeitada pela rede do bitcoin.\n\nVocê precisa adicionar a quantia de poeira ao seu montante à ser enviado para evitar gerar um output de poeira.\n\nO output de poeira é {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Sua carteira BSQ não possui fundos suficientes para pagar a taxa de negócio em BSQ. +popup.warning.messageTooLong=Sua mensagem excede o tamanho máx. permitido. Por favor enviá-la em várias partes ou carregá-la utilizando um serviço como https://pastebin.com. +popup.warning.lockedUpFunds=Você trancou fundos de um negócio falhado..\nSaldo trancado: {0} \nEndereço da tx de Depósito: {1}\nID de negócio: {2}.\n\nPor favor abra um bilhete de apoio selecionando o negócio no ecrã de negócios abertos e pressione \"alt + o\" ou \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +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 Bisq. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Bisq para mais informações. +popup.warning.mandatoryUpdate.dao=Por favor, atualize para a versão mais recente Bisq. Uma atualização obrigatória que desativa a OAD do Bisq e BSQ para versões antigas foi lançada. Por favor, confira o Fórum Bisq para mais informações. +popup.warning.disable.dao=A OAD do Bisq e BSQ foram temporariamente desativados. Por favor, confira o Fórum Bisq para mais informações. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 BTC 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 Bitcoin.\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 Bisq na equipa Keybase do Bisq. + +popup.warning.trade.txRejected.tradeFee=taxa de negócio +popup.warning.trade.txRejected.deposit=depósito +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=A transação de taxa de ofertante para a oferta com o ID {0} é inválida\nID da transação={1}.\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 Bisq na equipa Keybase do Bisq. + +popup.info.securityDepositInfo=Para garantir que ambos os negociadores seguem o protocolo de negócio, ambos os negociadores precisam pagar um depósito de segurança.\n\nEsse depósito é mantido na sua carteira de negócio até que o seu negócio seja concluído com sucesso, e então lhe será reembolsado.\n\nPor favor note: se você está criando uma nova oferta, o Bisq precisa estar em execução para que um outro negociador a aceite. Para manter suas ofertas online, mantenha o Bisq em execução e certifique-se de que este computador permaneça online também (ou seja, certifique-se de que ele não alterne para o modo de espera... o modo de espera do monitor não causa problema). + +popup.info.cashDepositInfo=Por favor, certifique-se de que você tem uma agência bancária na sua área para poder fazer o depósito em dinheiro.\nO ID do banco (BIC/SWIFT) do vendedor é: {0}. +popup.info.cashDepositInfo.confirm=Eu confirmo que eu posso fazer o depósito +popup.info.shutDownWithOpenOffers=Bisq está sendo fechado, mas há ofertas abertas. \n\nEstas ofertas não estarão disponíveis na rede P2P enquanto o Bisq estiver desligado, mas elas serão publicadas novamente na rede P2P na próxima vez que você iniciar o Bisq.\n\nPara manter suas ofertas on-line, mantenha o Bisq em execução e certifique-se de que este computador também permaneça online (ou seja, certifique-se de que ele não entra no modo de espera... o modo de espera do monitor não causa problema). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Notificação privada importante! + +popup.securityRecommendation.headline=Recomendação de segurança importante +popup.securityRecommendation.msg=Gostaríamos de lembrá-lo de considerar a possibilidade de usar a proteção por senha para sua carteira, caso você ainda não tenha ativado isso.\n\nTambém é altamente recomendável anotar as palavras-semente da carteira. Essas palavras-semente são como uma senha mestre para recuperar sua carteira Bitcoin.\nNa secção \"Semente da Carteira\", você encontrará mais informações.\n\nAlém disso, você deve fazer o backup da pasta completa de dados do programa na secção \"Backup\". + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Desligando +popup.shutDownInProgress.msg=Desligar o programa pode demorar alguns segundos.\nPor favor não interrompa este processo. + +popup.attention.forTradeWithId=Atenção necessária para o negócio com ID {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Múltiplas contas de pagamento disponíveis +popup.info.multiplePaymentAccounts.msg=Você tem várias contas de pagamento disponíveis para esta oferta. Por favor, verifique se você escolheu a correta. + +popup.accountSigning.selectAccounts.headline=Selecionar contas de pagamento +popup.accountSigning.selectAccounts.description=Com base no método de pagamento e no momento, todas as contas de pagamento conectadas a uma disputa em que ocorreu um pagamento ao comprador serão selecionadas para você assinar. +popup.accountSigning.selectAccounts.signAll=Assinar todos os métodos de pagamento +popup.accountSigning.selectAccounts.datePicker=Selecione o ponto no tempo até o qual as contas serão assinadas + +popup.accountSigning.confirmSelectedAccounts.headline=Confirmar contas de pagamento selecionadas +popup.accountSigning.confirmSelectedAccounts.description=Com base no seu input, {0} contas de pagamento serão selecionadas. +popup.accountSigning.confirmSelectedAccounts.button=Confirmar contas de pagamento +popup.accountSigning.signAccounts.headline=Confirmar a assinatura de contas de pagamento +popup.accountSigning.signAccounts.description=Com base na sua seleção, {0} contas de pagamento serão assinadas. +popup.accountSigning.signAccounts.button=Assinar contas de pagamento +popup.accountSigning.signAccounts.ECKey=Inserir a chave privada do árbitro +popup.accountSigning.signAccounts.ECKey.error=Má ECKey do árbitro + +popup.accountSigning.success.headline=Parabéns +popup.accountSigning.success.description=Todas as contas de pagamento de {0} foram assinadas com sucesso! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=Uma das suas contas de pagamento foi verificada e assinada por um árbitro. Fazendo negócios com esta conta assinará automaticamente a conta do seu par de negociação após um negócio bem-sucedido.\n\n{0} +popup.accountSigning.signedByPeer=Uma das suas contas de pagamento foi verificada e assinada por um par de negociação. Seu limite inicial de negociação será aumentado e você poderá assinar outras contas dentro de {0} dias a partir de agora.\n\n{1} +popup.accountSigning.peerLimitLifted=O limite inicial de uma das suas contas foi aumentado.\n\n{0} +popup.accountSigning.peerSigner=Uma das suas contas tem maturidade suficiente para assinar outras contas de pagamento e o limite inicial de uma delas foi aumentado.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Notificação para o oferta com ID {0} +notification.ticket.headline=Bilhete de apoio para o negócio com ID {0} +notification.trade.completed=O negócio completou e você já pode levantar seus fundos. +notification.trade.accepted=Sua oferta foi aceite por um {0} de BTC. +notification.trade.confirmed=Seu negócio tem pelo menos uma confirmação da blockchain.\nVocê pode começar o pagamento agora. +notification.trade.paymentStarted=O comprador de BTC iniciou o pagamento +notification.trade.selectTrade=Selecionar negócio +notification.trade.peerOpenedDispute=Seu par de negociação abriu um {0}. +notification.trade.disputeClosed=A {0} foi fechada. +notification.walletUpdate.headline=Atualização da carteira de negociação +notification.walletUpdate.msg=A sua carteira está suficientemente financiada.\nQuantia: {0} +notification.takeOffer.walletUpdate.msg=A sua carteira de negociação já estava suficientemente financiada por uma tentativa de aceitação de oferta anterior.\nQuantia: {0} +notification.tradeCompleted.headline=Negócio concluído +notification.tradeCompleted.msg=Você pode levantar seus fundos agora para sua carteira Bitcoin externa ou transferi-la para a carteira Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Mostrar janela do programa +systemTray.hide=Esconder janela do programa +systemTray.info=Informação sobre Bisq +systemTray.exit=Sair +systemTray.tooltip=Bisq: Uma rede de echange de bitcoin descentralizada + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=Contas de negociação guardadas em:\n{0} +guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação prontas para exportar. +guiUtil.accountExport.selectPath=Selecione diretório de {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Conta de negociação com id {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Nós não importamos a conta de negociação com o id {0} porque já existe.\n +guiUtil.accountExport.exportFailed=A exportação para CSV falhou devido à um erro.\nErro = {0} +guiUtil.accountExport.selectExportPath=Selecionar diretório para exportar +guiUtil.accountImport.imported=Conta de negociação importada de:\n{0}\n\nContas importadas:\n{1} +guiUtil.accountImport.noAccountsFound=Nenhuma conta de negociação exportada foi encontrada em: {0}.\nO nome do ficheiro é {1}. " +guiUtil.openWebBrowser.warning=Você vai abrir uma página web no navegador da web do seu sistema.\nVocê quer abrir a página web agora?\n\nSe você não estiver usando o \"Navegador Tor\" como seu navegador web padrão do sistema, você irá se conectar à página web em rede transparente.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Abrir a página web e não perguntar novamente +guiUtil.openWebBrowser.copyUrl=Copiar URL e cancelar +guiUtil.ofTradeAmount=da quantia de negócio +guiUtil.requiredMinimum=(mínimo requerido) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Selecione a moeda +list.currency.showAll=Ver todos +list.currency.editList=Editar lista de moedas + +table.placeholder.noItems=Atualmente não há {0} disponíveis +table.placeholder.noData=Não há dados disponíveis no momento +table.placeholder.processingData=Processando os dados... + + +peerInfoIcon.tooltip.tradePeer=Do par de negociação +peerInfoIcon.tooltip.maker=Do ofertante +peerInfoIcon.tooltip.trade.traded={0} endereço onion: {1}\nVocê já negociou {2} vez(es) com esse par\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} endereço onion: {1}\nVocê não negociou com esse par até agora.\n{2} +peerInfoIcon.tooltip.age=Conta de pagamento criada há {0}. +peerInfoIcon.tooltip.unknownAge=Idade de conta de pagamento desconhecida. + +tooltip.openPopupForDetails=Abrir popup para mais detalhes +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Abrir um explorador de blockchain externo para endereço: {0} +tooltip.openBlockchainForTx=Abrir um explorador de blockchain externo para transação: {0} + +confidence.unknown=Estado da transação desconhecido +confidence.seen=Visto por {0} par(es) / 0 confirmações +confidence.confirmed=Confirmado em {0} bloco(s) +confidence.invalid=A transação é inválida + +peerInfo.title=Informação do par +peerInfo.nrOfTrades=Número de negócios completos +peerInfo.notTradedYet=Você não negociou com esse utilizador até agora. +peerInfo.setTag=Definir o rótulo para esse par +peerInfo.age.noRisk=Idade da conta de pagamento +peerInfo.age.chargeBackRisk=Tempo desde a assinatura +peerInfo.unknownAge=Idade desconhecida + +addressTextField.openWallet=Abrir sua carteira Bitcoin padrão +addressTextField.copyToClipboard=Copiar endereço para área de transferência +addressTextField.addressCopiedToClipboard=Endereço copiado para área de transferência +addressTextField.openWallet.failed=Abrir a programa de carteira Bitcoin padrão falhou. Talvez você não tenha um instalado? + +peerInfoIcon.tooltip={0}\nRótulo: {1} + +txIdTextField.copyIcon.tooltip=Copiar ID de transação +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Conta\" +navigation.account.walletSeed=\"Conta/Semente da carteira\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"Portefólio/As minhas ofertas abertas\" +navigation.portfolio.pending=\"Portefólio/Negócios abertos\" +navigation.portfolio.closedTrades=\"Portefólio/Histórico\" +navigation.funds.depositFunds=\"Fundos/Receber fundos\" +navigation.settings.preferences=\"Definições/Preferências\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Fundos/Transações\" +navigation.support=\"Apoio\" +navigation.dao.wallet.receive=\"OAD/Carteira BSQ/Receber\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} quantia{1} +formatter.makerTaker=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.fiat=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.asMaker={0} {1} como ofertante +formatter.asTaker={0} {1} como aceitador + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Mainnet de Bitcoin +# suppress inspection "UnusedProperty" +BTC_TESTNET=Testnet de Bitcoin +# suppress inspection "UnusedProperty" +BTC_REGTEST=Regtest Bitcoin +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Testnet da OAD do Bitcoin (discontinuada) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Betanet da OAD do Bisq (Mainnet do Bitcoin) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Regtest da OAD do Bitcoin + +time.year=Ano +time.month=Mês +time.week=Semana +time.day=Dia +time.hour=Hora +time.minute10=10 Minutos +time.hours=horas +time.days=dias +time.1hour=1 hora +time.1day=1 dia +time.minute=minuto +time.second=segundo +time.minutes=minutos +time.seconds=segundos + + +password.enterPassword=Inserir senha +password.confirmPassword=Confirmar senha +password.tooLong=A senha deve ter menos de 500 caracteres. +password.deriveKey=Derivar chave a partir da senha +password.walletDecrypted=A carteira foi descriptografada com sucesso e a proteção por senha removida. +password.wrongPw=Você digitou a senha errada.\n\nPor favor, tente digitar sua senha novamente, verificando com atenção se há erros de ortografia. +password.walletEncrypted=Carteira encriptada com sucesso e proteção por senha ativada. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=As 2 senhas inseridas não são iguais. +password.forgotPassword=Esqueceu a senha? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Palavras-semente da carteira +seed.enterSeedWords=Inserir as palavras-semente da carteira +seed.date=Data da carteira +seed.restore.title=Restaurar carteira a partir de palavras-semente +seed.restore=Restaurar carteiras +seed.creationDate=Data de criação +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=Eu desejo restaurar de qualquer forma +seed.warn.walletNotEmpty.emptyWallet=Eu esvaziarei as carteiras primeiro +seed.warn.notEncryptedAnymore=Suas carteiras são encriptadas.\n\nApós a restauração, as carteiras não serão mais encriptadas e você deverá definir uma nova senha.\n\nVocê quer continuar? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Carteiras restauradas com sucesso com as novas palavras-semente.\n\nVocê precisa desligar e reiniciar o programa. +seed.restore.error=Um erro ocorreu ao restaurar as carteiras com palavras-semente.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Conta +payment.account.no=Nº da conta +payment.account.name=Nome da conta +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Nome completo do titular da conta +payment.account.fullName=Nome completo (primeiro, nome do meio, último) +payment.account.state=Estado/Província/Região +payment.account.city=Cidade +payment.bank.country=País do banco +payment.account.name.email=Nome completo do titular da conta / email +payment.account.name.emailAndHolderId=Nome completo do titular da conta / email / {0} +payment.bank.name=Nome do banco +payment.select.account=Selecione o tipo de conta +payment.select.region=Selecionar região +payment.select.country=Selecionar país +payment.select.bank.country=Selecionar país do banco +payment.foreign.currency=Tem certeza que deseja selecionar uma moeda que não seja a moeda padrão do pais? +payment.restore.default=Não, resturar para a moeda padrão +payment.email=Email +payment.country=País +payment.extras=Requerimentos adicionais +payment.email.mobile=Email ou nº de telemóvel +payment.altcoin.address=Endereço de altcoin +payment.altcoin.tradeInstantCheckbox=Negócio instantâneo (dentro de 1 hora) com esta Altcoin +payment.altcoin.tradeInstant.popup=Para negociação instantânea, é necessário que os dois pares de negociação estejam online para concluir o negócio em menos de 1 hora..\n\nSe você tem ofertas abertas e você não está disponível, por favor desative essas ofertas no ecrã 'Portefólio'. +payment.altcoin=Altcoin +payment.select.altcoin=Select or search Altcoin +payment.secret=Pergunta secreta +payment.answer=Resposta +payment.wallet=ID da carteira +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Nome de utilizador, email ou nº de telemóvel +payment.moneyBeam.accountId=Email ou nº de telemóvel +payment.venmo.venmoUserName=Nome de utilizador do Venmo +payment.popmoney.accountId=Email ou nº de telemóvel +payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telemóvel +payment.supportedCurrencies=Moedas suportadas +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Limitações +payment.salt=Sal para verificação da idade da conta +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Aceitar negócios destes países do Euro +payment.accept.nonEuro=Aceitar negócios desses países fora do Euro +payment.accepted.countries=Países aceites +payment.accepted.banks=Bancos aceites (ID) +payment.mobile=Nº de telemóvel +payment.postal.address=Morada postal +payment.national.account.id.AR=Numero CBU +shared.accountSigningState=Estado da assinatura da conta + +#new +payment.altcoin.address.dyn=Endereço de {0} +payment.altcoin.receiver.address=Endereço altcoin do recipiente +payment.accountNr=Número da conta +payment.emailOrMobile=Email ou nº de telemóvel +payment.useCustomAccountName=Usar nome de conta personalizado: +payment.maxPeriod=Período máx. de negócio +payment.maxPeriodAndLimit=Duração máx. de negócio: {0} / Compra máx.: {1} / Venda máx.: {2} / Idade da conta: {3} +payment.maxPeriodAndLimitCrypto=Período máx. de negócio {0} / Limite máx. de negócio: {1} +payment.currencyWithSymbol=Moeda: {0} +payment.nameOfAcceptedBank=Nome do banco aceite +payment.addAcceptedBank=Adicionar banco aceite +payment.clearAcceptedBanks=Limpar bancos aceites +payment.bank.nameOptional=Nome do banco (opcional) +payment.bankCode=Código do Banco +payment.bankId=ID do banco (BIC/SWIFT): +payment.bankIdOptional=ID do banco (BIC/SWIFT) (opcional) +payment.branchNr=Nº da agência: +payment.branchNrOptional=Nº da agência (opcional): +payment.accountNrLabel=Nº da conta (IBAN) +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 BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 BTC precisa enviar ao vendedor de BTC 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 BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Informação de contacto +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=Informação de contacto +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Banco +payment.japan.branch=Agência +payment.japan.account=Conta +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Transferência bancária nacional +SAME_BANK=Transferência para mesmo banco +SPECIFIC_BANKS=Transferência com banco escpecífico +US_POSTAL_MONEY_ORDER=US Postal Money Order +CASH_DEPOSIT=Depósito em dinheiro +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Face à face (em pessoa) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Bancos nacionais +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Mesmo banco +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Bancos específicos +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Depósito em dinheiro +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instantâneas + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Campo vazio não é permitido +validation.NaN=Número inválido +validation.notAnInteger=O input não é um número inteiro. +validation.zero=Número 0 não é permitido +validation.negative=Valores negativos não são permitidos. +validation.fiat.toSmall=Input menor do que a quantia mínima permitida. +validation.fiat.toLarge=Input maior do que a quantia máxima permitida. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=O input maior que {0} não é permitido. +validation.btc.toSmall=Input menor que {0} não é permitido. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=A senha inserida é muito longa. Não pode ser maior do que 50 caracteres. +validation.sortCodeNumber={0} deve consistir de {1} números. +validation.sortCodeChars={0} deve consistir de {1} caracteres. +validation.bankIdNumber={0} deve consistir de {1 números. +validation.accountNr=O número de conta deve conter {0} números. +validation.accountNrChars=O número da conta deve conter {0} caracteres. +validation.btc.invalidAddress=O endereço está incorreto. Por favor verificar o formato do endereço. +validation.integerOnly=Por favor, insira apenas números inteiros +validation.inputError=O seu input causou um erro:\n{0} +validation.bsq.insufficientBalance=O seu saldo disponível é de {0}. +validation.btc.exceedsMaxTradeLimit=O seu limite de negócio é de {0}. +validation.bsq.amountBelowMinAmount=A quantia mín. é de {0} +validation.nationalAccountId={0} tem de ser constituído por {1} números + +#new +validation.invalidInput=Input inválido: {0} +validation.accountNrFormat=O número da conta deve estar no formato: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Validação do endereço falhou pois este não é compatível com a estrutura de um endereço {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Endereço não é um endereço {0} válido! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Banco e Código de país devem ser letras +validation.bic.invalidLocationCode=BIC contém código de localização inválido +validation.bic.invalidBranchCode=BIC contém código da agência inválido +validation.bic.sepaRevolutBic=Contas Revolut SEPA não são suportadas. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Endereço inválido +validation.iban.invalidCountryCode=Código de país inválido +validation.iban.checkSumNotNumeric=Soma de verificação deve ser numérica +validation.iban.nonNumericChars=Carácter não alfanumérico detectado +validation.iban.checkSumInvalid=Soma de verificação dp IBAN é inválida +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Código de área não é canadense. +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Deve conter apenas letras, números, espaços e/ou símbolos ' _ , . ? - +validation.interacETransfer.invalidAnswer=Deve ser uma palavra e conter apenas letras, números e/ou o símbolo - +validation.inputTooLarge=O input não deve ser maior que {0} +validation.inputTooSmall=O input deve ser maior que {0} +validation.inputToBeAtLeast=O input tem de ser pelo menos {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=O comprimento deve estar entre {0} e {1} +validation.fixedLength=Length must be {0} +validation.pattern=O input deve ter o formato: {0} +validation.noHexString=O input não está no formato HEX. +validation.advancedCash.invalidFormat=Deve ser um email válido ou id de carteira de formato: X000000000000 +validation.invalidUrl=Este não é um URL válido +validation.mustBeDifferent=O seu input deve ser diferente do valor atual +validation.cannotBeChanged=O parâmetro não pode ser alterado +validation.numberFormatException=Exceção do formato do número {0} +validation.mustNotBeNegative=O input não deve ser negativo +validation.phone.missingCountryCode=É preciso o código do país de duas letras para validar o número de telefone +validation.phone.invalidCharacters=O número de telfone {0} contém carácteres inválidos +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Deve ser um lista de endereços válidos separados por vírgulas diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties new file mode 100644 index 0000000000..1b0df8706f --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Подробнее +shared.openHelp=Открыть раздел помощи +shared.warning=Предупреждение +shared.close=Закрыть +shared.cancel=Отменить +shared.ok=Ок +shared.yes=Да +shared.no=Нет +shared.iUnderstand=Я понимаю +shared.na=Н/Д +shared.shutDown=Закрыть +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=Купить биткойн +shared.sellBitcoin=Продать биткойн +shared.buyCurrency=Купить {0} +shared.sellCurrency=Продать {0} +shared.buyingBTCWith=покупка ВТС за {0} +shared.sellingBTCFor=продажа ВТС за {0} +shared.buyingCurrency=покупка {0} (продажа ВТС) +shared.sellingCurrency=продажа {0} (покупка ВТС) +shared.buy=покупки +shared.sell=продажи +shared.buying=покупка +shared.selling=продажа +shared.P2P=P2P +shared.oneOffer=предложение +shared.multipleOffers=предложения +shared.Offer=Предложение +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=текущие предложения +shared.trade=сделка +shared.trades=сделки +shared.openTrades=текущие сделки +shared.dateTime=Дата/время +shared.price=Курс +shared.priceWithCur=Цена в {0} +shared.priceInCurForCur=Цена в {0} за 1 {1} +shared.fixedPriceInCurForCur=Фиксированная цена в {0} за 1 {1} +shared.amount=Количество +shared.txFee=Transaction Fee +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Buyer Deposit +shared.sellerSecurityDeposit=Seller Deposit +shared.amountWithCur=Количество в {0} +shared.volumeWithCur=Объём в {0} +shared.currency=Валюта +shared.market=Рынок +shared.deviation=Deviation +shared.paymentMethod=Способ оплаты +shared.tradeCurrency=Торговая валюта +shared.offerType=Тип предложения +shared.details=Подробности +shared.address=Адрес +shared.balanceWithCur=Баланс в {0} +shared.utxo=Unspent transaction output +shared.txId=Идентификатор транзакции +shared.confirmations=Подтверждения +shared.revert=Отменить транзакцию +shared.select=Выбрать +shared.usage=Использование +shared.state=Статус +shared.tradeId=Идентификатор сделки +shared.offerId=Идентификатор предложения +shared.bankName=Название банка +shared.acceptedBanks=Одобренные банки +shared.amountMinMax=Количество (мин. — макс.) +shared.amountHelp=Если предложение включает диапазон сделки, вы можете обменять любую сумму в этом диапазоне. +shared.remove=Удалить +shared.goTo=Перейти к {0} +shared.BTCMinMax=ВТС (мин. — макс.) +shared.removeOffer=Удалить предложение +shared.dontRemoveOffer=Не удалять предложение +shared.editOffer=Изменить предложение +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=Торговый счёт +shared.faq=Visit FAQ page +shared.yesCancel=Да, отменить +shared.nextStep=Далее +shared.selectTradingAccount=Выбрать торговый счёт +shared.fundFromSavingsWalletButton=Перевести средства с кошелька Bisq +shared.fundFromExternalWalletButton=Открыть внешний кошелёк для пополнения +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=% ниже рыночного курса +shared.aboveInPercent=% выше рыночного курса +shared.enterPercentageValue=Ввести величину в % +shared.OR=ИЛИ +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=Ожидание средств... +shared.depositTransactionId=Идентификатор зачисления на счёт +shared.TheBTCBuyer=Покупатель ВТС +shared.You=Вы +shared.sendingConfirmation=Отправка подтверждения... +shared.sendingConfirmationAgain=Отправьте подтверждение повторно +shared.exportCSV=Export to CSV +shared.exportJSON=Экспорт в JSON +shared.summary=Show summary +shared.noDateAvailable=Дата не указана +shared.noDetailsAvailable=Подробности не указаны +shared.notUsedYet=Ещё не использовано +shared.date=Дата +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.copyToClipboard=Скопировать в буфер +shared.language=Язык +shared.country=Страна +shared.applyAndShutDown=Применить и закрыть приложение +shared.selectPaymentMethod=Выбрать способ оплаты +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Вы действительно хотите удалить выбранный счёт? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=Нет настроенных счетов +shared.manageAccounts=Управление счетами +shared.addNewAccount=Добавить новый счёт +shared.ExportAccounts=Экспортировать счета +shared.importAccounts=Импортировать счета +shared.createNewAccount=Создать новый счёт +shared.saveNewAccount=Сохранить новый счёт +shared.selectedAccount=Выбранный счёт +shared.deleteAccount=Удалить счёт +shared.errorMessageInline=\nОшибка: {0} +shared.errorMessage=Сообщение об ошибке +shared.information=Информация +shared.name=Имя +shared.id=Идентификатор +shared.dashboard=Панель управления +shared.accept=Принять +shared.balance=Баланс +shared.save=Сохранить +shared.onionAddress=Onion-адрес +shared.supportTicket=запрос в службу поддержки +shared.dispute=спор +shared.mediationCase=mediation case +shared.seller=продавец +shared.buyer=покупатель +shared.allEuroCountries=Все страны Еврозоны +shared.acceptedTakerCountries=Одобренные страны для тейкера +shared.tradePrice=Цена сделки +shared.tradeAmount=Сумма сделки +shared.tradeVolume=Объём сделки +shared.invalidKey=Введён неправильный ключ. +shared.enterPrivKey=Введите приватный ключ для разблокировки +shared.makerFeeTxId=Идентификатор транзакции комиссии мейкера +shared.takerFeeTxId=Идентификатор транзакции комиссии тейкера +shared.payoutTxId=Идентификатор транзакции выплаты +shared.contractAsJson=Контракт в формате JSON +shared.viewContractAsJson=Просмотреть контракт в формате JSON +shared.contract.title=Контракт сделки с идентификатором: {0} +shared.paymentDetails=Подробности платежа ВТС {0} +shared.securityDeposit=Залог +shared.yourSecurityDeposit=Ваш залог +shared.contract=Контракт +shared.messageArrived=Сообщение получено. +shared.messageStoredInMailbox=Сообщение сохранено в почтовом ящике. +shared.messageSendingFailed=Ошибка отправки сообщения: {0} +shared.unlock=Разблокировать +shared.toReceive=получить +shared.toSpend=потратить +shared.btcAmount=Сумма ВТС +shared.yourLanguage=Ваши языки +shared.addLanguage=Добавить язык +shared.total=Всего +shared.totalsNeeded=Требуемая сумма +shared.tradeWalletAddress=Адрес кошелька сделки +shared.tradeWalletBalance=Баланс кошелька сделки +shared.makerTxFee=Мейкер: {0} +shared.takerTxFee=Тейкер: {0} +shared.iConfirm=Подтверждаю +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Открыть {0} +shared.fiat=Нац. валюта +shared.crypto=Криптовалюта +shared.all=Все +shared.edit=Редактировать +shared.advancedOptions=Дополнительные настройки +shared.interval=Интервал +shared.actions=Действия +shared.buyerUpperCase=Покупатель +shared.sellerUpperCase=Продавец +shared.new=НОВОЕ +shared.blindVoteTxId=Идент. транзакции слепого голосования +shared.proposal=Предложение +shared.votes=Голоса +shared.learnMore=Узнать больше +shared.dismiss=Отмена +shared.selectedArbitrator=Выбранный арбитр +shared.selectedMediator=Selected mediator +shared.selectedRefundAgent=Выбранный арбитр +shared.mediator=Посредник +shared.arbitrator=Арбитр +shared.refundAgent=Арбитр +shared.refundAgentForSupportStaff=Refund agent +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Рынок +mainView.menu.buyBtc=Купить BTC +mainView.menu.sellBtc=Продать BTC +mainView.menu.portfolio=Сделки +mainView.menu.funds=Средства +mainView.menu.support=Поддержка +mainView.menu.settings=Настройки +mainView.menu.account=Счёт +mainView.menu.dao=ДАО + +mainView.marketPriceWithProvider.label=Рыночный курс {0} +mainView.marketPrice.bisqInternalPrice=Курс последней сделки в Bisq +mainView.marketPrice.tooltip.bisqInternalPrice=Нет данных от источника рыночного курса.\nПредоставлен курс последней сделки в Bisq для этой валютной пары. +mainView.marketPrice.tooltip=Рыночный курс предоставлен {0}{1}\nОбновление: {2}\nURL источника данных: {3} +mainView.balance.available=Доступный баланс +mainView.balance.reserved=Выделено на предложения +mainView.balance.locked=Используется в сделках +mainView.balance.reserved.short=Выделено +mainView.balance.locked.short=В сделках + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(локальный узел) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Подключение к сети Биткойн +mainView.footer.bsqInfo.synchronizing=/ Синхронизация ДАО +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Подключение к +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=Полный узел ДАО + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Подключение к сети Tor... +mainView.bootstrapState.torNodeCreated=(2/4) Создан узел Tor +mainView.bootstrapState.hiddenServicePublished=(3/4) Скрытый сервис опубликован +mainView.bootstrapState.initialDataReceived=(4/4) Исходные данные получены + +mainView.bootstrapWarning.noSeedNodesAvailable=Нет доступных исходных узлов +mainView.bootstrapWarning.noNodesAvailable=Нет доступных исходных узлов и пиров +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Bisq network failed + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Отсутствуют исходные узлы или постоянные пиры для запроса данных.\nПроверьте подключение к интернету или перезапустите приложение. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Bisq network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. + +mainView.walletServiceErrorMsg.timeout=Подключение к сети Биткойн не удалось из-за истечения времени ожидания. +mainView.walletServiceErrorMsg.connectionError=Не удалось подключиться к сети Биткойн из-за ошибки: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Сбой соединения со всеми {0} узлами сети.\nВозможно, вы отключились от интернета, или Ваш компьютер перешел в режим ожидания. +mainView.networkWarning.localhostBitcoinLost=Сбой соединения с локальным узлом Биткойн.\nПерезапустите приложение для подключения к другим узлам Биткойн или перезапустите свой локальный узел Биткойн. +mainView.version.update=(Имеется обновление) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Предложения +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Сделки + +# OfferBookChartView +market.offerBook.buyAltcoin=Хочу купить {0} (продать {1}) +market.offerBook.sellAltcoin=Хочу продать {0} (купить {1}) +market.offerBook.buyWithFiat=Купить {0} +market.offerBook.sellWithFiat=Продать {0} +market.offerBook.sellOffersHeaderLabel=Продать {0} +market.offerBook.buyOffersHeaderLabel=Купить {0} +market.offerBook.buy=Хочу купить биткойн +market.offerBook.sell=Хочу продать биткойн + +# SpreadView +market.spread.numberOfOffersColumn=Все предложения ({0}) +market.spread.numberOfBuyOffersColumn=Купить BTC ({0}) +market.spread.numberOfSellOffersColumn=Продать BTC ({0}) +market.spread.totalAmountColumn=Итого BTC ({0}) +market.spread.spreadColumn=Спред +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Сделки: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Открыт: +market.trades.tooltip.candle.close=Закрыт: +market.trades.tooltip.candle.high=Высший: +market.trades.tooltip.candle.low=Низший: +market.trades.tooltip.candle.average=Средний: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=Дата: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=Создать предложение +offerbook.takeOffer=Принять предложение +offerbook.takeOfferToBuy=Принять предложение купить {0} +offerbook.takeOfferToSell=Принять предложение продать {0} +offerbook.trader=Трейдер +offerbook.offerersBankId=Идент. банка (BIC/SWIFT) мейкера: {0} +offerbook.offerersBankName=Название банка мейкера: {0} +offerbook.offerersBankSeat=Местоположение банка мейкера: {0} +offerbook.offerersAcceptedBankSeatsEuro=Допустимые страны банка тейкера: все страны еврозоны +offerbook.offerersAcceptedBankSeats=Допустимые страны банка тейкера:\n {0} +offerbook.availableOffers=Доступные предложения +offerbook.filterByCurrency=Фильтровать по валюте +offerbook.filterByPaymentMethod=Фильтровать по способу оплаты +offerbook.matchingOffers=Offers matching my accounts +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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) +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.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.ageDays={0} дн. +offerbook.timeSinceSigning.notSigned.noNeed=Н/Д +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=Кол-во предложений: {0} +offerbook.volume={0} (мин. ⁠— макс.) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Создать новое предложение на покупку {0} +offerbook.createOfferToSell=Создать новое предложение на продажу {0} +offerbook.createOfferToBuy.withFiat=Создать новое предложение: купить {0} за {1} +offerbook.createOfferToSell.forFiat=Создать новое предложение: продать {0} за {1} +offerbook.createOfferToBuy.withCrypto=Создать новое предложение: продать {0} (купить {1}) +offerbook.createOfferToSell.forCrypto=Создать новое предложение: купить {0} (продать {1}) + +offerbook.takeOfferButton.tooltip=Принять предложение {0} +offerbook.yesCreateOffer=Да, создать предложение +offerbook.setupNewAccount=Создать новый торговый счёт +offerbook.removeOffer.success=Предложение удалено. +offerbook.removeOffer.failed=Не удалось удалить предложение:\n{0} +offerbook.deactivateOffer.failed=Не удалось деактивировать предложение:\n{0} +offerbook.activateOffer.failed=Не удалось опубликовать предложение:\n{0} +offerbook.withdrawFundsHint=Вы можете вывести внесённые средства в разделе «{0}». + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} + +offerbook.warning.wrongTradeProtocol=Это предложение требует другой версии протокола, чем та, что используется в вашей версии приложения.\n\nПроверьте, установлена ли у вас новейшая версия приложения. Если да, то пользователь, создавший предложение, использовал старую версию.\n\nПри использовании несовместимой версии торгового протокола торговля невозможна. +offerbook.warning.userIgnored=Onion-адрес данного пользователя добавлен в чёрный список. +offerbook.warning.offerBlocked=Это предложение заблокировано разработчиками Bisq.\nВероятно, принятие этого предложения вызывает необрабатываемую ошибку. +offerbook.warning.currencyBanned=Валюта, используемая в этом предложении, заблокирована разработчиками Bisq.\nПодробности можно узнать на форуме Bisq. +offerbook.warning.paymentMethodBanned=Метод платежа, использованный в этом предложении, заблокирован разработчиками Bisq.\nПодробности можно узнать на форуме Bisq. +offerbook.warning.nodeBlocked=Onion-адрес этого трейдера заблокирован разработчиками Bisq.\nВероятно, принятие предложения от данного трейдера вызывает необрабатываемую ошибку. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=Продажа по рыночному курсу (обновляется ежеминутно). +offerbook.info.buyAtMarketPrice=Покупка по рыночному курсу (обновляется ежеминутно). +offerbook.info.sellBelowMarketPrice=Вы получите на {0} меньше текущего рыночного курса (обновляется ежеминутно). +offerbook.info.buyAboveMarketPrice=Вы заплатите на {0} больше текущего рыночного курса (обновляется ежеминутно). +offerbook.info.sellAboveMarketPrice=Вы получите на {0} больше текущего рыночного курса (обновляется ежеминутно). +offerbook.info.buyBelowMarketPrice=Вы заплатите на {0} меньше текущего рыночного курса (обновляется ежеминутно). +offerbook.info.buyAtFixedPrice=Вы купите по этому фиксированному курсу. +offerbook.info.sellAtFixedPrice=Вы продадите по этому фиксированному курсу. +offerbook.info.noArbitrationInUserLanguage=В случае возникновения спора он будет рассматриваться арбитром на другом языке ({0}). Текущий язык: {1}. +offerbook.info.roundedFiatVolume=Сумма округлена, чтобы повысить конфиденциальность сделки. + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Введите сумму в ВТС +createOffer.price.prompt=Введите курс +createOffer.volume.prompt=Введите сумму в {0} +createOffer.amountPriceBox.amountDescription=Количество BTC для {0} +createOffer.amountPriceBox.buy.volumeDescription=Сумма затрат в {0} +createOffer.amountPriceBox.sell.volumeDescription=Сумма в {0} к получению +createOffer.amountPriceBox.minAmountDescription=Мин. количество ВТС +createOffer.securityDeposit.prompt=Залог +createOffer.fundsBox.title=Обеспечить своё предложение +createOffer.fundsBox.offerFee=Комиссия за сделку +createOffer.fundsBox.networkFee=Комиссия майнера +createOffer.fundsBox.placeOfferSpinnerInfo=Публикация предложения... +createOffer.fundsBox.paymentLabel=Сделка Bisq с идентификатором {0} +createOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) +createOffer.fundsBox.fundsStructure.BSQ=({0} — залог, {1} — комиссия майнера) + {2} — комиссия за сделку +createOffer.success.headline=Ваше предложение опубликовано +createOffer.success.info=Вы можете управлять текущими предложениями в разделе \«Сделки/Мои текущие предложения\». +createOffer.info.sellAtMarketPrice=Вы всегда будете продавать по рыночному курсу, так как курс вашего предложения будет постоянно обновляться. +createOffer.info.buyAtMarketPrice=Вы всегда будете покупать по рыночному курсу, так как курс вашего предложения будет постоянно обновляться. +createOffer.info.sellAboveMarketPrice=Вы всегда получите на {0}% больше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. +createOffer.info.buyBelowMarketPrice=Вы всегда заплатите на {0}% меньше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. +createOffer.warning.sellBelowMarketPrice=Вы всегда получите на {0}% меньше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. +createOffer.warning.buyAboveMarketPrice=Вы всегда заплатите на {0}% больше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. +createOffer.tradeFee.descriptionBTCOnly=Комиссия за сделку +createOffer.tradeFee.descriptionBSQEnabled=Выбрать валюту комиссии за сделку + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=Проверка: разместить предложение {0} биткойн +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 - использовать свой кошелёк Bisq (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. + +# 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Перезагрузите приложение и проверьте сетевое соединение. +createOffer.setAmountPrice=Указать сумму и курс +createOffer.warnCancelOffer=Вы уже обеспечили это предложение.\nВ случае его отмены ваши средства переместятся в ваш локальный кошелёк Bisq и будут доступны для вывода в разделе \«Средства/Отправить средства\».\nОтменить? +createOffer.timeoutAtPublishing=Время для публикации предложения истекло. +createOffer.errorInfo=\n\nКомиссия мейкера уже оплачена. В худшем случае вы её потеряете.\nПерезагрузите приложение и проверьте сетевое соединение, чтобы попытаться устранить проблему. +createOffer.tooLowSecDeposit.warning=Установленная сумма залога ниже рекомендуемой по умолчанию ({0}).\nИспользовать более низкую сумму залога? +createOffer.tooLowSecDeposit.makerIsSeller=Вы будете меньше защищены, если ваш контрагент нарушит торговый протокол. +createOffer.tooLowSecDeposit.makerIsBuyer=Ваш контрагент будет меньше защищён, поскольку вы рискуете меньшей суммой в случае нарушения торгового протокола. Пользователи могут предпочесть другие предложения вашему. +createOffer.resetToDefault=Сбросить до значения по умолчанию +createOffer.useLowerValue=Использовать выбранную сумму +createOffer.priceOutSideOfDeviation=Введенный курс превышает максимально допустимое отклонение от рыночного курса.\nДопустимое отклонение составляет {0}. Его размер можно установить в настройках. +createOffer.changePrice=Изменить курс +createOffer.tac=Публикуя данное предложение, я выражаю согласие торговать с любым трейдером, соответствующим условиям, указанным на экране. +createOffer.currencyForFee=Комиссия за сделку +createOffer.setDeposit=Установить сумму залога покупателя (%) +createOffer.setDepositAsBuyer=Установить мой залог как покупателя (%) +createOffer.setDepositForBothTraders=Set both traders' security deposit (%) +createOffer.securityDepositInfo=Сумма залога покупателя: {0} +createOffer.securityDepositInfoAsBuyer=Сумма вашего залога: {0} +createOffer.minSecurityDepositUsed=Min. buyer security deposit is used + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Введите сумму в ВТС +takeOffer.amountPriceBox.buy.amountDescription=Сумма BTC для продажи +takeOffer.amountPriceBox.sell.amountDescription=Сумма BTC для покупки +takeOffer.amountPriceBox.priceDescription=Цена за биткойн в {0} +takeOffer.amountPriceBox.amountRangeDescription=Возможный диапазон суммы +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=Слишком много знаков после запятой.\nКоличество знаков скорректировано до 4. +takeOffer.validation.amountSmallerThanMinAmount=Сумма не может быть меньше минимальной суммы, указанной в предложении. +takeOffer.validation.amountLargerThanOfferAmount=Введённая сумма не может превышать сумму, указанную в предложении. +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Указанная сумма придет к появлению «пыли» у продавца BTC. +takeOffer.fundsBox.title=Обеспечьте свою сделку +takeOffer.fundsBox.isOfferAvailable=Проверка доступности предложения... +takeOffer.fundsBox.tradeAmount=Сумма для продажи +takeOffer.fundsBox.offerFee=Комиссия за сделку +takeOffer.fundsBox.networkFee=Oбщая комиссия майнера +takeOffer.fundsBox.takeOfferSpinnerInfo=Принятие предложения... +takeOffer.fundsBox.paymentLabel=Сделка в Bisq с идентификатором {0} +takeOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) +takeOffer.success.headline=Вы успешно приняли предложение. +takeOffer.success.info=Статус вашей сделки отображается в разделе \«Папка/Текущие сделки\». +takeOffer.error.message=Ошибка при принятии предложения:\n\n{0} + +# new entries +takeOffer.takeOfferButton=Проверка: принять предложение {0} биткойн +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Вы можете выбрать один из двух вариантов финансирования сделки:\n - использовать свой кошелёк Bisq (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. +takeOffer.alreadyPaidInFunds=Если вы уже внесли средства, их можно вывести в разделе \«Средства/Отправить средства\». +takeOffer.paymentInfo=Информация о платеже +takeOffer.setAmountPrice=Задайте сумму +takeOffer.alreadyFunded.askCancel=Вы уже обеспечили это предложение.\nВ случае его отмены ваши средства переместятся в ваш локальный кошелёк Bisq и будут доступны для вывода в разделе \«Средства/Отправить средства\».\nОтменить? +takeOffer.failed.offerNotAvailable=Запрос принять предложение отменён, так как предложение больше недоступно. Возможно, его уже принял другой трейдер. +takeOffer.failed.offerTaken=Невозможно принять это предложение, так как его уже принял другой трейдер. +takeOffer.failed.offerRemoved=Невозможно принять это предложение, так как оно уже удалено. +takeOffer.failed.offererNotOnline=Не удалось принять предложение, так как его создатель уже не в сети. +takeOffer.failed.offererOffline=Невозможно принять это предложение, так как его создатель не в сети. +takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. + +takeOffer.error.noFundsLost=\n\nСредства ещё не сняты с вашего кошелька.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nВнесение депозита завершено.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему.\nЕсли это не поможет, свяжитесь с разработчиками. +takeOffer.error.payoutPublished=\n\nВыплата завершена.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему.\nЕсли это не поможет, свяжитесь с разработчиками. +takeOffer.tac=Принимая это предложение, я соглашаюсь с условиями сделки, указанными на экране. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Начальная цена +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=Укажите курс +editOffer.confirmEdit=Подтвердите: изменить предложение +editOffer.publishOffer=Публикация вашего предложения. +editOffer.failed=Не удалось изменить предложение:\n{0} +editOffer.success=Ваше предложение успешно отредактировано. +editOffer.invalidDeposit=Сумма залога покупателя не регулируется ДАО Bisq и больше не подлежит изменению. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Мои текущие предложения +portfolio.tab.pendingTrades=Текущие сделки +portfolio.tab.history=История +portfolio.tab.failed=Не удалось +portfolio.tab.editOpenOffer=Изменить предложение + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Ожидание подтверждения в блокчейне +portfolio.pending.step2_buyer.startPayment=Сделать платеж +portfolio.pending.step2_seller.waitPaymentStarted=Дождитесь начала платежа +portfolio.pending.step3_buyer.waitPaymentArrived=Дождитесь получения платежа +portfolio.pending.step3_seller.confirmPaymentReceived=Подтвердите получение платежа +portfolio.pending.step5.completed=Завершено + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Переведите {1} с внешнего кошелька {0}\nпродавцу ВТС.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Обратитесь в банк и заплатите {0} продавцу ВТС.\n\n +portfolio.pending.step2_buyer.cash.extra=ВАЖНОЕ ТРЕБОВАНИЕ:\nПосле оплаты напишите на бумажной квитанции «ВОЗВРАТУ НЕ ПОДЛЕЖИТ».\nЗатем разорвите квитанцию на 2 части, сфотографируйте её и отошлите на электронный адрес продавца ВТС. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Заплатите {0} продавцу BTC через MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=ВАЖНОЕ ТРЕБОВАНИЕ:\nПосле оплаты отправьте продавцу BTC по электронной почте код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, страна (штат) и сумма. Электронный адрес продавца: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Заплатите {0} продавцу BTC через Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=ВАЖНОЕ ТРЕБОВАНИЕ: \nПосле оплаты отправьте по электронной почте продавцу BTC контрольный номер MTCN и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, город, страна и сумма. Электронный адрес продавца: {0}. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым денежным переводом США\» продавцу BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Свяжитесь с продавцом BTC с помощью указанных контактных данных и договоритесь о встрече для оплаты {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Начать оплату, используя {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Сумма для перевода +portfolio.pending.step2_buyer.sellersAddress={0}-адрес продавца +portfolio.pending.step2_buyer.buyerAccount=Используемый платёжный счет +portfolio.pending.step2_buyer.paymentStarted=Платёж начат +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. +portfolio.pending.step2_buyer.paperReceipt.headline=Вы отослали бумажную квитанцию продавцу ВТС? +portfolio.pending.step2_buyer.paperReceipt.msg=Помните:\nВам необходимо написать на бумажной квитанции «ВОЗВРАТУ НЕ ПОДЛЕЖИТ».\nЗатем разорвите её пополам, сфотографируйте и отошлите по электронной почте продавцу ВТС. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Отправить код подтверждения и квитанцию +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Вам необходимо отправить по электронной почте продавцу BTC код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, страна (штат) и сумма. Электронный адрес продавца: {0}.\n\nВы отправили продавцу код подтверждения и квитанцию? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Отправить MTCN и квитанцию +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Вам необходимо отправить по электронной почте продавцу BTC контрольный номер MTCN и фотографию квитанции.\nВ квитанции должно быть четко указано полное имя продавца, город, страна и сумма. Адрес электронной почты продавца: {0}. \n\nВы отправили MTCN и контракт продавцу? +portfolio.pending.step2_buyer.halCashInfo.headline=Отправить код HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Вам необходимо отправить сообщение с кодом HalCash и идентификатором сделки ({0}) продавцу BTC.\nНомер моб. тел. продавца: {1}\n\nВы отправили код продавцу? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Bisq clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=Подтвердите начало платежа +portfolio.pending.step2_buyer.confirmStart.msg=Вы начали платеж {0} своему контрагенту? +portfolio.pending.step2_buyer.confirmStart.yes=Да +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=Ожидайте платеж +portfolio.pending.step2_seller.f2fInfo.headline=Контактная информация покупателя +portfolio.pending.step2_seller.waitPayment.msg=Депозитная транзакция подтверждена в блокчейне не менее одного раза.\nДождитесь начала платежа в {0} покупателем BTC. +portfolio.pending.step2_seller.warn=Покупатель BTC все еще не завершил платеж в {0}.\nДождитесь начала оплаты.\nЕсли сделка не завершится {1}, арбитр начнет разбирательство. +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. +tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Bisq (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Неопределено +# suppress inspection "UnusedProperty" +message.state.SENT=Сообщение отправлено +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Сообщение прибыло к контрагенту +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Контрагент подтвердил получение сообщения +# suppress inspection "UnusedProperty" +message.state.FAILED=Сбой отправки сообщения + +portfolio.pending.step3_buyer.wait.headline=Ожидание подтверждения оплаты продавцом ВТС +portfolio.pending.step3_buyer.wait.info=Ожидание подтверждения продавцом ВТС получения оплаты в {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Статус сообщения о начале платежа +portfolio.pending.step3_buyer.warn.part1a=в блокчейне {0} +portfolio.pending.step3_buyer.warn.part1b=у вашего поставщика платёжных услуг (напр., банка) +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Ваш контрагент подтвердил начало оплаты в {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=в вашем любимом обозревателе блоков {0} +portfolio.pending.step3_seller.altcoin.wallet=в вашем кошельке {0} +portfolio.pending.step3_seller.altcoin={0}Проверьте {1}, была ли транзакция в ваш адрес\n{2}\nподтверждена достаточное количество раз.\nСумма платежа должна составлять {3}.\n\n Вы можете скопировать и вставить свой адрес {4} из главного окна после закрытия этого окна. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Так как оплата осуществляется наличными на счёт, покупатель BTC должен написать \«НЕ ПОДЛЕЖИТ ВОЗВРАТУ\» на квитанции, разорвать её на 2 части и отправить вам её фото по электронной почте.\n\nЧтобы избежать возврата платёжа, подтверждайте его получение только после получения этого фото, если вы не сомневаетесь в подлинности квитанции.\nЕсли вы не уверены, {0} +portfolio.pending.step3_seller.moneyGram=Покупатель обязан отправить вам по электронной почте код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано ваше полное имя, страна (штат) и сумма. Убедитесь, что вы получили код подтверждения по электронной почте.\n\nПосле закрытия этого окна вы увидите имя и адрес покупателя BTC, которые необходимо указать для получения денег от MoneyGram.\n\nПодтвердите получение только после того, как вы успешно заберете деньги! +portfolio.pending.step3_seller.westernUnion=Покупатель обязан отправить вам по электронной почте контрольный номер MTCN и фото квитанции.\nВ квитанции должно быть четко указано ваше полное имя, город, страна и сумма. Убедитесь, что вы получили номер MTCN по электронной почте.\n\nПосле закрытия этого окна вы увидите имя и адрес покупателя BTC, которые необходимо указать для получения денег от Western Union. \n\nПодтвердите получение только после того, как вы успешно заберете деньги! +portfolio.pending.step3_seller.halCash=Покупатель должен отправить вам код HalCash в текстовом сообщении. Кроме того, вы получите сообщение от HalCash с информацией, необходимой для снятия EUR в банкомате, поддерживающем HalCash.\n\nПосле того, как вы заберете деньги из банкомата, подтвердите получение платежа в приложении! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Подтвердите получение платежа +portfolio.pending.step3_seller.amountToReceive=Сумма поступления +portfolio.pending.step3_seller.yourAddress=Ваш адрес {0} +portfolio.pending.step3_seller.buyersAddress=Адрес {0} покупателя +portfolio.pending.step3_seller.yourAccount=Ваш торговый счёт +portfolio.pending.step3_seller.xmrTxHash=Идентификатор транзакции +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Подтвердить получение платежа +portfolio.pending.step3_seller.buyerStartedPayment=Покупатель ВТС начал оплату в {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Проверьте количество подтверждений в блокчейне в своём алтькойн-кошельке или обозревателе блоков и подтвердите платеж, если подтверждений достаточно. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Проверьте получение на свой торговый счёт (напр. банковский счёт) и подтвердите после получения платежа. +portfolio.pending.step3_seller.warn.part1a=в блокчейне {0} +portfolio.pending.step3_seller.warn.part1b=у вашего поставщика платёжных услуг (напр., банка) +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Вы получили платеж в {0} от своего контрагента?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Подтвердите получение платежа +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Да, я получил (-а) платёж +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. + +portfolio.pending.step5_buyer.groupTitle=Детали завершённой сделки +portfolio.pending.step5_buyer.tradeFee=Комиссия за сделку +portfolio.pending.step5_buyer.makersMiningFee=Комиссия майнера +portfolio.pending.step5_buyer.takersMiningFee=Oбщая комиссия майнера +portfolio.pending.step5_buyer.refunded=Сумма возмещённого залога +portfolio.pending.step5_buyer.withdrawBTC=Вывести биткойны +portfolio.pending.step5_buyer.amount=Сумма для вывода +portfolio.pending.step5_buyer.withdrawToAddress=Вывести на адрес +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=Вывести на внешний кошелёк +portfolio.pending.step5_buyer.alreadyWithdrawn=Ваши средства уже сняты.\nПросмотрите журнал транзакций. +portfolio.pending.step5_buyer.confirmWithdrawal=Подтвердите запрос на вывод +portfolio.pending.step5_buyer.amountTooLow=Сумма перевода ниже комиссии за транзакцию и минимально возможного значения («пыли»). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Вывод выполнен +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Ваши завершенные сделки хранятся в разделе \«Сделки/История\».\nВсе ваши биткойн-транзакции указаны в разделе \«Средства/Транзакции\». +portfolio.pending.step5_buyer.bought=Вы купили +portfolio.pending.step5_buyer.paid=Вы заплатили + +portfolio.pending.step5_seller.sold=Вы продали +portfolio.pending.step5_seller.received=Вы получили + +tradeFeedbackWindow.title=Поздравляем с завершением сделки! +tradeFeedbackWindow.msg.part1=Мы были бы рады услышать ваши отзывы. Они помогут нам улучшить приложение и исправить любые ошибки. Если вы хотите оставить отзыв, заполните эту небольшую форму (регистрация не требуется): +tradeFeedbackWindow.msg.part2=Если у вас возникли вопросы или сложности, свяжитесь с другими пользователями и разработчиками приложения на форуме Bisq: +tradeFeedbackWindow.msg.part3=Спасибо, что пользуетесь Bisq! + +portfolio.pending.role=Моя роль +portfolio.pending.tradeInformation=Информация о сделке +portfolio.pending.remainingTime=Оставшееся время +portfolio.pending.remainingTimeDetail={0} (до {1}) +portfolio.pending.tradePeriodInfo=Начало отсчета срока сделки начинается после первого подтверждения в блокчейне. Срок сделки зависит от выбранного метода платежа. +portfolio.pending.tradePeriodWarning=При превышении срока оба трейдера могут начать спор. +portfolio.pending.tradeNotCompleted=Сделка не завершена вовремя (до {0}) +portfolio.pending.tradeProcess=Процесс сделки +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Начать спор заново +portfolio.pending.openSupportTicket.headline=Обратиться за поддержкой +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. + +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. +portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Bisq community at https://bisq.community. If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Время сделки истекло + +portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested +portfolio.pending.openSupport=Обратиться за поддержкой +portfolio.pending.supportTicketOpened=Запрос на поддержку отправлен +portfolio.pending.communicateWithArbitrator=Свяжитесь с арбитром в разделе \«Поддержка\». +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. +portfolio.pending.disputeOpenedMyUser=Вы уже начали спор.\n{0} +portfolio.pending.disputeOpenedByPeer=Ваш контрагент начал спор\n{0} +portfolio.pending.noReceiverAddressDefined=Адрес получателя не установлен + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=View proposed resolution +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Завершена +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated +portfolio.closed.canceled=Отменена +portfolio.failed.Failed=Не удалась +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Получить средства +funds.tab.withdrawal=Отправить средства +funds.tab.reserved=Выделенные средства +funds.tab.locked=В сделках +funds.tab.transactions=Транзакции + +funds.deposit.unused=Не использован +funds.deposit.usedInTx=Использован в {0} транзакциях +funds.deposit.fundBisqWallet=Пополнить кошелёк Bisq +funds.deposit.noAddresses=Адреса для перевода средств ещё не созданы +funds.deposit.fundWallet=Пополнить кошелёк +funds.deposit.withdrawFromWallet=Отправить средства из кошелька +funds.deposit.amount=Сумма в ВТС (необязательно) +funds.deposit.generateAddress=Создать новый адрес +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Выберите неиспользованный адрес из таблицы выше вместо создания нового. + +funds.withdrawal.arbitrationFee=Комиссия арбитра +funds.withdrawal.inputs=Выбор адресов +funds.withdrawal.useAllInputs=Использовать все доступные адреса +funds.withdrawal.useCustomInputs=Использовать выбранные адреса +funds.withdrawal.receiverAmount=Сумма для получения +funds.withdrawal.senderAmount=Сумма для отправления +funds.withdrawal.feeExcluded=Сумма не включает комиссию майнера +funds.withdrawal.feeIncluded=Сумма включает комиссию майнера +funds.withdrawal.fromLabel=Вывести с адреса +funds.withdrawal.toLabel=Вывести на адрес +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Снять указанную сумму +funds.withdrawal.noFundsAvailable=Нет доступных для вывода средств +funds.withdrawal.confirmWithdrawalRequest=Подтвердите запрос на вывод +funds.withdrawal.withdrawMultipleAddresses=Снять с нескольких адресов ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Снять с нескольких адресов:\n{0} +funds.withdrawal.notEnoughFunds=В вашем кошельке недостаточно средств. +funds.withdrawal.selectAddress=Выберите адрес-источник из таблицы +funds.withdrawal.setAmount=Укажите сумму для вывода +funds.withdrawal.fillDestAddress=Введите адрес получателя +funds.withdrawal.warn.noSourceAddressSelected=Необходимо выбрать адрес-источник в таблице выше. +funds.withdrawal.warn.amountExceeds=Сумма превышает доступную на данном адресе.\nВыберите несколько адресов в таблице выше или включите в сумму комиссию майнера. + +funds.reserved.noFunds=Не выделено средств для текущих предложений +funds.reserved.reserved=Выделено в локальном кошельке на предложение с идент.: {0} + +funds.locked.noFunds=Средства в сделках не используются +funds.locked.locked=Заблокировано в multisig-адресе для сделки с идентификатором: {0} + +funds.tx.direction.sentTo=Отправлено: +funds.tx.direction.receivedWith=Получено: +funds.tx.direction.genesisTx=Из первичной транзакции: +funds.tx.txFeePaymentForBsqTx=Комиссия майнера за транзакцию в BSQ +funds.tx.createOfferFee=Комиссия мейкера и плата за сделку: {0} +funds.tx.takeOfferFee=Комиссия тейкера и плата за сделку: {0} +funds.tx.multiSigDeposit=Депозит на multisig-адрес: {0} +funds.tx.multiSigPayout=Выплата с multisig-адреса: {0} +funds.tx.disputePayout=Выплата по спору: {0} +funds.tx.disputeLost=Проигранный спор: {0} +funds.tx.collateralForRefund=Refund collateral: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} +funds.tx.unknown=Неизвестная причина: {0} +funds.tx.noFundsFromDispute=Без возмещения по спору +funds.tx.receivedFunds=Полученные средства +funds.tx.withdrawnFromWallet=Выведено из кошелька +funds.tx.withdrawnFromBSQWallet=BTC выведен из кошелька BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=Транзакции отсутствуют +funds.tx.revert=Отменить +funds.tx.txSent=Транзакция успешно отправлена на новый адрес локального кошелька Bisq. +funds.tx.direction.self=Транзакция внутри кошелька +funds.tx.daoTxFee=Комиссия майнера за транзакцию в BSQ +funds.tx.reimbursementRequestTxFee=Запрос возмещения +funds.tx.compensationRequestTxFee=Запрос компенсации +funds.tx.dustAttackTx=Полученная «пыль» +funds.tx.dustAttackTx.popup=Вы получили очень маленькую сумму BTC, что может являться попыткой компаний, занимающихся анализом блокчейна, проследить за вашим кошельком.\n\nЕсли вы воспользуетесь этими средствами для совершения исходящей транзакции, они смогут узнать, что вы также являетесь вероятным владельцем другого адреса (т. н. «объединение монет»).\n\nДля защиты вашей конфиденциальности кошелёк Bisq игнорирует такую «пыль» при совершении исходящих транзакций и отображении баланса. Вы можете самостоятельно установить сумму, которая будет рассматриваться в качестве «пыли» в настройках. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.legacyArbitration.support=Legacy Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets +support.filter=Search disputes +support.filter.prompt=Введите идентификатор сделки, дату, onion-адрес или данные учётной записи + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Личное уведомление +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=Нет текущих обращений +support.sendingMessage=Отправка сообщения... +support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. +support.sendMessageError=Сбой отправки сообщения. Ошибка: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=Предложение, по которому открыт этот спор, было создано в устаревшей версии Bisq.\nНевозможно закрыть спор с помощью текущей версии приложения.\n\nВоспользуйтесь старой версией протокола: {0} +support.openFile=Открыть файл для отправки (макс. размер файла: {0} Кб) +support.attachmentTooLarge=Общий объём вложенных файлов составляет {0} Кб, что превышает максимально допустимый размер сообщения ({1} Кб). +support.maxSize=Максимально допустимый объём файла: {0} Кб. +support.attachment=Вложенный файл +support.tooManyAttachments=Нельзя отправлять более 3 файлов в одном сообщении. +support.save=Сохранить файл на диск +support.messages=Сообщения +support.input.prompt=Enter message... +support.send=Отправить +support.addAttachments=Прикрепить файлы +support.closeTicket=Закрыть запрос +support.attachments=Вложенные файлы: +support.savedInMailbox=Сообщение сохранено в почтовом ящике получателя +support.arrived=Сообщение доставлено адресату +support.acknowledged=Прибытие сообщения подтверждено получателем +support.error=Получателю не удалось обработать сообщение. Ошибка: {0} +support.buyerAddress=Адрес покупателя ВТС +support.sellerAddress=Адрес продавца ВТС +support.role=Роль +support.agent=Support agent +support.state=Состояние +support.chat=Chat +support.closed=Закрыто +support.open=Открыто +support.process=Process +support.buyerOfferer=Покупатель ВТС/мейкер +support.sellerOfferer=Продавец ВТС/мейкер +support.buyerTaker=Покупатель ВТС/тейкер +support.sellerTaker=Продавец BTC/тейкер + +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\nTraders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and suggest a payout of trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. If one or both traders do not agree to the mediator's suggested payout, they can request arbitration.The arbitrator will re-evaluate the situation and, if warranted, personally pay the trader back and request reimbursement for this payment from the Bisq DAO. +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Bisq are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.systemMsg=Системное сообщение: {0} +support.youOpenedTicket=Вы запросили поддержку.\n\n{0}\n\nВерсия Bisq: {1} +support.youOpenedDispute=Вы начали спор.\n\n{0}\n\nВерсия Bisq: {1} +support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq version: {1} +support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Параметры +settings.tab.network=Информация о сети +settings.tab.about=О проекте + +setting.preferences.general=Основные настройки +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Макс. отклонение от рыночного курса +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Избегать режима ожидания +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Значения выше {0}% запрещены. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Задать своё значение +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Игнорируемые пиры [onion-адрес:порт] +setting.preferences.ignoreDustThreshold=Мин. значение, не являющееся «пылью» +setting.preferences.currenciesInList=Валюты в перечне источника рыночного курса +setting.preferences.prefCurrency=Предпочитаемая валюта +setting.preferences.displayFiat=Показать нац. валюты +setting.preferences.noFiat=Национальные валюты не выбраны +setting.preferences.cannotRemovePrefCurrency=Нельзя удалить выбранную предпочитаемую валюту +setting.preferences.displayAltcoins=Показать альткойны +setting.preferences.noAltcoins=Альткойны не выбраны +setting.preferences.addFiat=Добавить национальную валюту +setting.preferences.addAltcoin=Добавить альткойн +setting.preferences.displayOptions=Параметры отображения +setting.preferences.showOwnOffers=Показать мои предложения в списке предложений +setting.preferences.useAnimations=Использовать анимацию +setting.preferences.useDarkMode=Use dark mode +setting.preferences.sortWithNumOffers=Сортировать списки по кол-ву предложений/сделок +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Сбросить все флажки \«Не показывать снова\» +settings.preferences.languageChange=Изменение языка во всех разделах вступит в силу после перезагрузки приложения. +settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. +setting.preferences.daoOptions=Настройки ДАО +setting.preferences.dao.resyncFromGenesis.label=Перестроить состояние ДАО от первичной транзакции +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Запустить Bisq в режиме полного узла ДАО +setting.preferences.dao.rpcUser=Логин RPC +setting.preferences.dao.rpcPw=Пароль RPC +setting.preferences.dao.blockNotifyPort=Блокировать сообщающий порт +setting.preferences.dao.fullNodeInfo=Для запуска Bisq в качестве полного узла ДАО вам необходим локальный узел Bitcoin Core с включенным удаленным вызовом процедур (RPC). Другие требования описаны в «{0}».\n\nПосле смены режима необходимо перезапустить приложение. +setting.preferences.dao.fullNodeInfo.ok=Открыть документацию +setting.preferences.dao.fullNodeInfo.cancel=Нет, остаюсь в режиме облегчённого узла +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Имя +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Сеть Биткойн +settings.net.p2pHeader=Bisq network +settings.net.onionAddressLabel=Мой onion-адрес +settings.net.btcNodesLabel=Использовать особые узлы Bitcoin Core +settings.net.bitcoinPeersLabel=Подключенные пиры +settings.net.useTorForBtcJLabel=Использовать Tor для сети Биткойн +settings.net.bitcoinNodesLabel=Узлы Bitcoin Core для подключения +settings.net.useProvidedNodesRadio=Использовать предоставленные узлы Bitcoin Core +settings.net.usePublicNodesRadio=Использовать общедоступную сеть Bitcoin +settings.net.useCustomNodesRadio=Использовать особые узлы Bitcoin Core +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=Нет, использовать предоставленные узлы +settings.net.warn.usePublicNodes.usePublic=Да, использовать общедоступную сеть +settings.net.warn.useCustomNodes.B2XWarning=Убедитесь, что ваш узел Биткойн является доверенным узлом Bitcoin Core! \n\nПодключение к узлам, не следующим правилам консенсуса Bitcoin Core, может повредить ваш кошелек и вызвать проблемы в процессе торговли.\n\nПользователи, подключающиеся к узлам, нарушающим правила консенсуса, несут ответственность за любой причиненный ущерб. Любые споры в таком случае будут решаться в пользу вашего контрагента. Пользователям, игнорирующим это предупреждение и механизмы защиты, техническая поддержка предоставляться не будет! +settings.net.warn.invalidBtcConfig=Connection to the Bitcoin network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Bitcoin nodes instead. You will need to restart the application. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=Подключенные пиры +settings.net.onionAddressColumn=Onion-адрес +settings.net.creationDateColumn=Создано +settings.net.connectionTypeColumn=Вх./Вых. +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Задержка +settings.net.sentBytesColumn=Отправлено +settings.net.receivedBytesColumn=Получено +settings.net.peerTypeColumn=Тип узла +settings.net.openTorSettingsButton=Открыть настройки Tor + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Height + +settings.net.needRestart=Необходимо перезагрузить приложение, чтобы применить это изменение.\nСделать это сейчас? +settings.net.notKnownYet=Пока неизвестно... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[IP-адрес:порт | хост:порт | onion-адрес:порт] (через запятые). Порт можно не указывать, если используется порт по умолчанию (8333). +settings.net.seedNode=Исходный узел +settings.net.directPeer=Пир (прямой) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Пир +settings.net.inbound=входящий +settings.net.outbound=выходящий +settings.net.reSyncSPVChainLabel=Синхронизировать цепь SPV заново +settings.net.reSyncSPVChainButton=Удалить файл SPV и синхронизировать повторно +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=Файл цепи SPV удален. Подождите. Повторная синхронизации с сетью может занять некоторое время. +settings.net.reSyncSPVAfterRestartCompleted=Повторная синхронизация завершена. Перезагрузите приложение. +settings.net.reSyncSPVFailed=Не удалось удалить файл цепи SPV.\nОшибка: {0} +setting.about.aboutBisq=О Bisq +setting.about.about=Bisq — это программа с открытым исходным кодом, предназначенная для обмена биткойна на национальные валюты (и другие криптовалюты) через децентрализованную Р2Р-сеть, обеспечивающая надежную защиту конфиденциальности. Узнайте больше о Bisq на веб-странице нашего проекта. +setting.about.web=Веб-страница Bisq +setting.about.code=Исходный код +setting.about.agpl=Лицензия AGPL +setting.about.support=Поддержать Bisq +setting.about.def=Bisq не является компанией, а представляет собой общественный проект, открытый для участия. Если вы хотите принять участие или поддержать Bisq, перейдите по ссылкам ниже. +setting.about.contribute=Помочь +setting.about.providers=Источники данных +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Рыночный курс предоставлен +setting.about.feeEstimation.label=Расчёт комиссии майнера предоставлен +setting.about.versionDetails=Подробности версии +setting.about.version=Версия приложения +setting.about.subsystems.label=Версии подсистем +setting.about.subsystems.val=Версия сети: {0}; версия P2P-сообщений: {1}; версия локальной базы данных: {2}; версия торгового протокола: {3} + +setting.about.shortcuts=Short cuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigate main menu +setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' + +setting.about.shortcuts.close=Close Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Close popup or dialog window +setting.about.shortcuts.closePopup.value='ESCAPE' key + +setting.about.shortcuts.chatSendMsg=Send trader chat message +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Open dispute +setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} + +setting.about.shortcuts.walletDetails=Open wallet details window + +setting.about.shortcuts.openEmergencyBtcWalletTool=Open emergency wallet tool for BTC wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool for BSQ wallet + +setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN + +setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) +setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} + +setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) +setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} + +setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) + +setting.about.shortcuts.sendFilter=Set Filter (privileged activity) + +setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing +account.info.headline=Добро пожаловать в ваш счёт Bisq +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). + +account.menu.paymentAccount=Счета в нац. валюте +account.menu.altCoinsAccountView=Альткойн-счета +account.menu.password=Пароль кошелька +account.menu.seedWords=Мнемоническая фраза +account.menu.walletInfo=Wallet info +account.menu.backup=Резервное копирование +account.menu.notifications=Уведомления + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Публичный ключ + +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=Аннулировать +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.warn.min1Language=Необходимо указать хотя бы 1 язык.\nМы добавили язык по умолчанию. +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} + +account.altcoin.yourAltcoinAccounts=Ваши альткойн-счета +account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.altcoin.popup.wallet.confirm=Я понимаю и подтверждаю, что знаю, какой кошелёк нужно использовать. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=При создании транзакции в GRIN требуется взаимодействие в реальном времени между отправителем и получателем. Следуйте инструкциям на веб-сайте проекта GRIN, чтобы узнать, как отправлять и получать GRIN (получатель должен находиться в сети в момент отправки перевода или в течение определенного периода). \n\nBisq поддерживает кошельки только в формате Grinbox (Wallet713). \n\nОтправитель GRIN должен предоставить доказательство успешной отправки перевода. Если отправитель не сможет предоставить это доказательство, потенциальный спор будет решен в пользу получателя GRIN. Убедитесь, что используете последнюю версию Grinbox с поддержкой доказательства транзакций и что понимаете, как нужно отправлять и получать GRIN, а также как создавать доказательство перевода. \n\nЧтобы узнать подробности работы с инструментом доказательства транзакции в Grinbox, см. https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=При создании транзакции в BEAM требуется взаимодействие в реальном времени между отправителем и получателем. \n\nСледуйте инструкциям на веб-сайте проекта BEAM, чтобы узнать, как отправлять и получать BEAM (получатель должен находиться в сети в момент отправки перевода или в течение определенного периода). \n\nОтправитель BEAM должен предоставить доказательство успешной отправки перевода. Используйте кошелёк, позволяющий получить доказательство перевода. Если отправитель не сможет предоставить это доказательство, потенциальный спор будет решен в пользу получателя BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Trading L-BTC on Bisq requires that you understand the following:\n\nWhen receiving L-BTC for a trade on Bisq, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-BTC into the Liquid Elements Core wallet, or another L-BTC wallet which allows you to obtain the blinding key for your blinded L-BTC address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-BTC address to the Bisq mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-BTC receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-BTC on Bisq. + +account.fiat.yourFiatAccounts=Ваши счета в нац. валюте + +account.backup.title=Резервный кошелёк +account.backup.location=Место хранения резервной копии +account.backup.selectLocation=Выбрать место сохранения резервной копии +account.backup.backupNow=Создать резервную копию (резервная копия не зашифрована!) +account.backup.appDir=Каталог данных приложения +account.backup.openDirectory=Открыть каталог +account.backup.openLogFile=Открыть файл журнала +account.backup.success=Резервная копия успешно сохранена в:\n{0} +account.backup.directoryNotAccessible=Выбранный вами каталог недоступен. {0} + +account.password.removePw.button=Удалить пароль +account.password.removePw.headline=Удалить защиту паролем для кошелька +account.password.setPw.button=Установить пароль +account.password.setPw.headline=Установить пароль для защиты кошелька +account.password.info=При использовании пароля его необходимо вводить при запуске приложения, при выводе биткойнов из вашего кошелька и восстановлении кошелька с помощью мнемонической фразы. + +account.seed.backup.title=Сохраните мнемоническую фразу для вашего кошелька +account.seed.info=Запишите мнемоническую фразу для кошелька и дату создания его! Используя эти данные, вы сможете восстановить ваш кошелёк когда угодно.\nДля обоих кошельков, BTC и BSQ, используется одна и та же мнемоническая фраза.\n\nВам следует записать её на бумаге и не хранить на компьютере.\n\nМнемоническая фраза НЕ заменяет резервную копию.\nВам следует сделать резервную копию всего каталога приложения в разделе \«Счёт/Резервное копирование\» для восстановления состояния приложения и данных.\nИмпорт мнемонической фразы рекомендуется только в экстренных случаях. Приложение не будет функционировать должным образом без наличия резервной копии файлов базы данных и ключей! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Вы не установили пароль от кошелька для защиты мнемонической фразы.\n\nОтобразить мнемоническую фразу на экране? +account.seed.warn.noPw.yes=Да и не спрашивать снова +account.seed.enterPw=Введите пароль, чтобы увидеть мнемоническую фразу +account.seed.restore.info=Создайте резервную копию перед восстановлением с помощью мнемонической фразы. Помните, что восстановление кошелька используется в экстренных случаях и может вызвать сбой внутренней базы данных кошелька.\nЭто не способ резервного копирования! Используйте резервную копию из каталога данных приложения для восстановления его предыдущего состояния.\n\nПосле восстановления приложение автоматически закроется. Когда вы повторно запустите приложение, оно снова синхронизируется с сетью Биткойн. Это может занять долгое время и привести к высокой нагрузке на центральный процессор, особенно если кошелёк был создан давно и хранил много транзакций. Не прерывайте данный процесс. Иначе вам придется удалить файл цепи SPV или повторить процесс восстановления сначала. +account.seed.restore.ok=Восстановить и закрыть Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Установка +account.notifications.download.label=Скачать мобильное приложение +account.notifications.waitingForWebCam=Ожидание веб-камеры... +account.notifications.webCamWindow.headline=Сканировать код QR с телефона +account.notifications.webcam.label=Использовать веб-камеру +account.notifications.webcam.button=Сканировать код QR +account.notifications.noWebcam.button=Нет веб-камеры +account.notifications.erase.label=Очистить уведомления на телефоне +account.notifications.erase.title=Очистить уведомления +account.notifications.email.label=Токен сопряжения +account.notifications.email.prompt=Введите токен сопряжения, полученный по электронной почте +account.notifications.settings.title=Настройки +account.notifications.useSound.label=Проигрывать уведомление на телефоне +account.notifications.trade.label=Получать сообщения по сделке +account.notifications.market.label=Получать оповещения о предложении +account.notifications.price.label=Получать оповещения о курсе +account.notifications.priceAlert.title=Оповещения о курсе +account.notifications.priceAlert.high.label=Уведомить, если курс BTC выше +account.notifications.priceAlert.low.label=Уведомить, если курс BTC ниже +account.notifications.priceAlert.setButton=Установить оповещение о курсе +account.notifications.priceAlert.removeButton=Удалить оповещение о курсе +account.notifications.trade.message.title=Состояние сделки изменилось +account.notifications.trade.message.msg.conf=Депозит по сделке с идентификатором {0} внесен. Откройте приложение Bisq и начните платеж. +account.notifications.trade.message.msg.started=Покупатель BTC начал платеж по сделке с идентификатором {0}. +account.notifications.trade.message.msg.completed=Сделка с идентификатором {0} завершена. +account.notifications.offer.message.title=Ваше предложение было принято +account.notifications.offer.message.msg=Ваше предложение с идентификатором {0} было принято +account.notifications.dispute.message.title=Новое сообщение по спору +account.notifications.dispute.message.msg=Получено сообщение по спору в сделке с идентификатором {0} + +account.notifications.marketAlert.title=Оповещения о предложении +account.notifications.marketAlert.selectPaymentAccount=Предложения, соответствующие платежному счету +account.notifications.marketAlert.offerType.label=Интересующий тип предложения +account.notifications.marketAlert.offerType.buy=Предложения купить (хочу продать BTC) +account.notifications.marketAlert.offerType.sell=Предложения продать (хочу купить BTC) +account.notifications.marketAlert.trigger=Отклонение предложения от курса (%) +account.notifications.marketAlert.trigger.info=Если задано отклонение от курса, вы получите оповещение только при публикации предложения, соответствующего вашим требованиям (или превышающего их). Например: вы хотите продать BTC, но только с надбавкой 2% к текущему рыночному курсу. Указав 2% в этом поле, вы получите оповещение только о предложениях с курсом, превышающим текущий рыночный курс на 2% (или более). +account.notifications.marketAlert.trigger.prompt=Отклонение в процентах от рыночного курса (напр., 2,50%, -0,50% и т. д.) +account.notifications.marketAlert.addButton=Добавить оповещение о предложении +account.notifications.marketAlert.manageAlertsButton=Управление оповещениями о предложениях +account.notifications.marketAlert.manageAlerts.title=Управление оповещениями о предложениях +account.notifications.marketAlert.manageAlerts.header.paymentAccount=Платёжный счёт +account.notifications.marketAlert.manageAlerts.header.trigger=Начальная цена +account.notifications.marketAlert.manageAlerts.header.offerType=Тип предложения +account.notifications.marketAlert.message.title=Оповещение о предложении +account.notifications.marketAlert.message.msg.below=ниже +account.notifications.marketAlert.message.msg.above=выше +account.notifications.marketAlert.message.msg=Новое предложение «{0} {1}» с ценой {2} (рыночная цена — {3} {4}) и методом платежа «{5}» было опубликовано в Bisq.\nИдент. предложения: {6}. +account.notifications.priceAlert.message.title=Оповещение о цене для {0} +account.notifications.priceAlert.message.msg=Ваше оповещение о цене сработало. Текущая цена {0} — {1} {2} +account.notifications.noWebCamFound.warning=Веб-камера не найдена.\n\nВоспользуйтесь электронной почтой для отправки токена и ключа шифрования с вашего мобильного телефона в приложение Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=Более высокая цена должна быть выше более низкой цены. +account.notifications.priceAlert.warning.lowerPriceTooHigh=Более низкая цена должна быть ниже более высокой цены. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Факты и цифры +dao.tab.bsqWallet=BSQ-кошелёк +dao.tab.proposals=Управление +dao.tab.bonding=Гарантийный депозит +dao.tab.proofOfBurn=Сбор за листинг активов/Proof of burn +dao.tab.monitor=Монитор сети +dao.tab.news=Новости + +dao.paidWithBsq=выплачено в BSQ +dao.availableBsqBalance=Доступно для трат (проверенные + неподтвержденные суммы) +dao.verifiedBsqBalance=Баланс всех проверенных UTXO +dao.unconfirmedChangeBalance=Остаток всех неподтвержденных сумм +dao.unverifiedBsqBalance=Баланс всех неподтвержденных транзакций (ожидающих подтверждения) +dao.lockedForVoteBalance=Использовано для голосования +dao.lockedInBonds=Заблокировано в гарантийных депозитах +dao.availableNonBsqBalance=Доступный баланс в ВТС (без учёта BSQ) +dao.reputationBalance=Merit Value (not spendable) + +dao.tx.published.success=Ваша транзакция опубликована. +dao.proposal.menuItem.make=Выступить с предложением +dao.proposal.menuItem.browse=Просмотр открытых предложений +dao.proposal.menuItem.vote=Голосование по предложениям +dao.proposal.menuItem.result=Результаты голосования +dao.cycle.headline=Цикл голосования +dao.cycle.overview.headline=Обзор цикла голосования +dao.cycle.currentPhase=Текущий этап +dao.cycle.currentBlockHeight=Номер текущего блока +dao.cycle.proposal=Этап выдвижения предложений +dao.cycle.proposal.next=Следующий этап выдвижения предложений +dao.cycle.blindVote=Этап слепого голосования +dao.cycle.voteReveal=Этап выявления голосов +dao.cycle.voteResult=Результат голосования +dao.cycle.phaseDuration={0} блоков (≈{1}); блок {2} — {3} (≈{4} — ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Блок {0} — {1} (≈{2} — ≈{3}) + +dao.voteReveal.txPublished.headLine=Транзакция выявления голоса опубликована +dao.voteReveal.txPublished=Ваша транзакция выявления голоса с идентификатором {0} была успешно опубликована. \n\nЭто происходит автоматически, если вы участвовали в голосовании ДАО. + +dao.results.cycles.header=Циклы +dao.results.cycles.table.header.cycle=Цикл +dao.results.cycles.table.header.numProposals=Предложения +dao.results.cycles.table.header.voteWeight=Вес голоса +dao.results.cycles.table.header.issuance=Эмиссия + +dao.results.results.table.item.cycle=Цикл {0} начат: {1} + +dao.results.proposals.header=Предложения выбранного цикла +dao.results.proposals.table.header.nameLink=Name/link +dao.results.proposals.table.header.details=Подробности +dao.results.proposals.table.header.myVote=Мой голос +dao.results.proposals.table.header.result=Результат голосования +dao.results.proposals.table.header.threshold=Threshold +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Результаты голосования по выбранному предложению + +dao.results.exceptions=Исключение (-я) результата голосования + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Неопределено + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=Комиссия мейкера в BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=Комиссия тейкера в BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Мин. комиссия мейкера в BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Мин. комиссия тейкера в BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Комиссия мейкера в BТС +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Комиссия тейкера в BТС +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Мин. комиссия мейкера в BТС +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Мин. комиссия тейкера в BТС +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Комиссия за предложение (в BSQ) +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Комиссия за голосование (в BSQ) + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Мин. сумма запроса компенсации в BSQ +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Макс. сумма запроса компенсации в BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Мин. сумма запроса на возмещение средств в BSQ +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Макс. сумма запроса на возмещение средств в BSQ + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Требуемый кворум в BSQ для обычного предложения +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Требуемый кворум в BSQ для запроса компенсации +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Требуемый кворум в BSQ для запроса на возмещение средств +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Требуемый кворум в BSQ для изменения параметра +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Необходимый кворум в BSQ для удаления актива +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Необходимый кворум в BSQ для запроса конфискации +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Требуемый кворум в BSQ для запроса обеспеченной роли + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Требуемый порог в % для обычного предложения +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Требуемый порог в % для запроса компенсации +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Требуемый порог в % для запроса возмещения средств +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Требуемый порог в % для изменения параметра +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Требуемый порог в % для удаления актива +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Требуемый порог в % для запроса конфискации +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Требуемый порог в % для запросов обеспеченной роли + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=BTC-адрес получателя + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Сбор за листинг актива в день +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Мин. объём торговли активами + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Срок блокировки для альтернативных торговых выплат +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Комиссия арбитра в ВТС + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Макс. торговый лимит в BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Обеспеченная роль до принятия в расчёт BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Лимит эмиссии на цикл в BSQ + +dao.param.currentValue=Текущее значение {0} +dao.param.currentAndPastValue=Current value: {0} (Value when proposal was made: {1}) +dao.param.blocks={0} блоков + +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was not distributed well in the Bisq network.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Неопределено +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Этап предложения +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Пауза 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Этап слепого голосования +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Пауза 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Этап выявления голосов +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Пауза 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Этап результатов + +dao.results.votes.table.header.stakeAndMerit=Вес голоса +dao.results.votes.table.header.stake=Доля +dao.results.votes.table.header.merit=Заработано +dao.results.votes.table.header.vote=Голосование + +dao.bond.menuItem.bondedRoles=Обеспеченные роли +dao.bond.menuItem.reputation=Обеспеченная репутация +dao.bond.menuItem.bonds=Гарантийные депозиты + +dao.bond.dashboard.bondsHeadline=BSQ в гарантийных депозитах +dao.bond.dashboard.lockupAmount=Заблокировать средства +dao.bond.dashboard.unlockingAmount=Разблокировка средств (дождитесь окончания блокировки) + + +dao.bond.reputation.header=Заблокировать депозит для репутации +dao.bond.reputation.table.header=Мои залоговые депозиты для репутации +dao.bond.reputation.amount=Заблокировать BSQ на сумму +dao.bond.reputation.time=Срок разблокировки в блоках +dao.bond.reputation.salt=Соль +dao.bond.reputation.hash=Хеш +dao.bond.reputation.lockupButton=Заблокировать +dao.bond.reputation.lockup.headline=Подтвердить транзакцию блокировки +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Подтвердить транзакцию разблокировки +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=Все гарантийные депозиты + +dao.bond.bondedReputation=Обеспеченная репутация +dao.bond.bondedRoles=Обеспеченные роли + +dao.bond.details.header=Описание роли +dao.bond.details.role=Роль +dao.bond.details.requiredBond=Необходимый гарантийный депозит в BSQ +dao.bond.details.unlockTime=Срок разблокировки в блоках +dao.bond.details.link=Ссылка на описание роли +dao.bond.details.isSingleton=Может быть принято несколькими обладателями роли +dao.bond.details.blocks={0} блока (-ов) + +dao.bond.table.column.name=Имя +dao.bond.table.column.link=Ссылка +dao.bond.table.column.bondType=Тип гарант. депозита +dao.bond.table.column.details=Подробности +dao.bond.table.column.lockupTxId=Идент. транз. блокировки +dao.bond.table.column.bondState=Состояние гарант. депозита +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=Дата блокировки + +dao.bond.table.button.lockup=Заблокировать +dao.bond.table.button.unlock=Разблокировать +dao.bond.table.button.revoke=Аннулировать + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Ещё не обеспечено +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=В стадии блокировки +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Гарантийный депозит заблокирован +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=В стадии разблокировки +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Транз. разблок. подтверждена +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Гарант. депозит в стадии разблокировки +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Гарантийный депозит разблокирован +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Гарантийный депозит конфискован + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Обеспеченная роль +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Обеспеченная репутация + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Администратор Github +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Администратор форума +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Администратор Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Администратор YouTube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Мейнтейнер Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Мейнтейнер форка BitcoinJ +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Мейнтейнер сетевого уровня +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Оператор веб-сайта +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Оператор форума +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Оператор исходного узла +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Оператор узла данных курса +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Оператор узла данных рынка +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Оператор ретранс. моб. уведомлений +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Владелец имени домена +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Администратор DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Посредник +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Арбитр +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Владелец адреса BTC для пожертвований + +dao.burnBsq.assetFee=Листинг актива +dao.burnBsq.menuItem.assetFee=Сбор за листинг актива +dao.burnBsq.menuItem.proofOfBurn=Proof of burn +dao.burnBsq.header=Сбор за листинг актива +dao.burnBsq.selectAsset=Выбрать актив +dao.burnBsq.fee=Сбор +dao.burnBsq.trialPeriod=Пробный период +dao.burnBsq.payFee=Оплатить сбор +dao.burnBsq.allAssets=Все активы +dao.burnBsq.assets.nameAndCode=Название актива +dao.burnBsq.assets.state=Состояние +dao.burnBsq.assets.tradeVolume=Объём сделки +dao.burnBsq.assets.lookBackPeriod=Период проверки +dao.burnBsq.assets.trialFee=Сбор за испытательный срок +dao.burnBsq.assets.totalFee=Сумма уплаченного сбора +dao.burnBsq.assets.days={0} дн. +dao.burnBsq.assets.toFewDays=Взнос за листинг актива слишком низок. Мин. срок пробного периода: {0} дн. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Неопределено +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Пробный период +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Активно торгуется +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Удалён из-за неактивности +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Удалён голосованием + +dao.proofOfBurn.header=Proof of burn +dao.proofOfBurn.amount=Количество +dao.proofOfBurn.preImage=Прообраз +dao.proofOfBurn.burn=Сжечь +dao.proofOfBurn.allTxs=Все транзакции proof of burn +dao.proofOfBurn.myItems=Мои транзакции proof of burn +dao.proofOfBurn.date=Дата +dao.proofOfBurn.hash=Хеш +dao.proofOfBurn.txs=Транзакции +dao.proofOfBurn.pubKey=Публичный ключ +dao.proofOfBurn.signature.window.title=Подписать сообщение ключом из транзакции proof or burn +dao.proofOfBurn.verify.window.title=Подтвердить сообщение ключом из транзакции proof or burn +dao.proofOfBurn.copySig=Скопировать подпись в буфер +dao.proofOfBurn.sign=Подписать +dao.proofOfBurn.message=Сообщение +dao.proofOfBurn.sig=Подпись +dao.proofOfBurn.verify=Подтвердить +dao.proofOfBurn.verificationResult.ok=Подтверждено +dao.proofOfBurn.verificationResult.failed=Проверка не удалась + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Этап предложения +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Пауза перед началом этапа слепого голосования +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Этап слепого голосования +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Пауза перед началом этапа выявления голосов +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Этап выявления голосов +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Пауза перед публикацией результатов +# suppress inspection "UnusedProperty" +dao.phase.RESULT=Этап результатов голосования + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Этап предложения +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Слепое голосование +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Выявление голосов +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Результаты голосования + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Запрос компенсации +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Запрос на возмещение затрат +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Предложение учредить обеспеченную роль +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Предложение удалить актив +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Предложение изменить параметр +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Общее предложение +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Предложение о конфискации гарантийного депозита + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Запрос компенсации +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Запрос на возмещение затрат +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Обеспеченная роль +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Удаление альткойна +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Изменение параметра +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Общее предложение +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Конфискация гарантийного депозита + +dao.proposal.details=Подробности предложения +dao.proposal.selectedProposal=Избранное предложение +dao.proposal.active.header=Предложения текущего цикла +dao.proposal.active.remove.confirm=Действительно хотите удалить это предложение?\nВзнос за его создание компенсации не подлежит. +dao.proposal.active.remove.doRemove=Да, удалить мое предложение +dao.proposal.active.remove.failed=Не удалось удалить предложение. +dao.proposal.myVote.title=Голосование +dao.proposal.myVote.accept=Принять предложение +dao.proposal.myVote.reject=Отклонить предложение +dao.proposal.myVote.removeMyVote=Игнорировать предложение +dao.proposal.myVote.merit=Вес голоса в зависимости от заработанных BSQ +dao.proposal.myVote.stake=Вес голоса в зависимости от доли +dao.proposal.myVote.revealTxId=Идент. транзакции выявления голоса +dao.proposal.myVote.stake.prompt=Макс. доступная доля для голосования: {0} +dao.proposal.votes.header=Установите долю для голосования и опубликуйте свои голоса +dao.proposal.myVote.button=Опубликовать голоса +dao.proposal.myVote.setStake.description=После голосования по всем предложениям необходимо установить долю для голосования, заблокировав какое-то количество BSQ. Чем больше BSQ вы заблокируете, тем больший вес будет у вашего голоса. \n\nЗаблокированные для голосования BSQ будут разблокированы на этапе выявления голосов. +dao.proposal.create.selectProposalType=Выберите тип предложения +dao.proposal.create.phase.inactive=Просьба дождаться следующего этапа выдвижения предложений +dao.proposal.create.proposalType=Тип предложения +dao.proposal.create.new=Создать новое предложение +dao.proposal.create.button=Создать предложение +dao.proposal.create.publish=Опубликовать предложение +dao.proposal.create.publishing=Публикация предложения... +dao.proposal=предложение +dao.proposal.display.type=Тип предложения +dao.proposal.display.name=Exact GitHub username +dao.proposal.display.link=Ссылка на подробную информацию +dao.proposal.display.link.prompt=Ссылка на предложение +dao.proposal.display.requestedBsq=Запрашиваемая сумма в BSQ +dao.proposal.display.txId=Идент. транзакции предложения +dao.proposal.display.proposalFee=Сбор за предложение +dao.proposal.display.myVote=Мой голос +dao.proposal.display.voteResult=Сводка итогов голосования +dao.proposal.display.bondedRoleComboBox.label=Тип обеспеченной роли +dao.proposal.display.requiredBondForRole.label=Требуемый гарантийный депозит для роли +dao.proposal.display.option=Вариант + +dao.proposal.table.header.proposalType=Тип предложения +dao.proposal.table.header.link=Ссылка +dao.proposal.table.header.myVote=Мой голос +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Удалить +dao.proposal.table.icon.tooltip.removeProposal=Удалить моё предложение +dao.proposal.table.icon.tooltip.changeVote=Текущий голос: «{0}». Изменить на: «{1}» + +dao.proposal.display.myVote.accepted=Принято +dao.proposal.display.myVote.rejected=Отклонено +dao.proposal.display.myVote.ignored=Проигнорировано +dao.proposal.display.myVote.unCounted=Vote was not included in result +dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Голосование было недействительным + +dao.proposal.voteResult.success=Принято +dao.proposal.voteResult.failed=Отклонено +dao.proposal.voteResult.summary=Результат: {0}; порог: {1} (требуется > {2}); кворум: {3} (требуется > {4}) + +dao.proposal.display.paramComboBox.label=Выбрать параметр для изменения +dao.proposal.display.paramValue=Значение параметра + +dao.proposal.display.confiscateBondComboBox.label=Выбрать гарантийный депозит +dao.proposal.display.assetComboBox.label=Актив для удаления + +dao.blindVote=слепое голосование + +dao.blindVote.startPublishing=Публикация транзакции слепого голосования... +dao.blindVote.success=Ваша транзакция слепого голосованию успешно опубликована.\n\nОбратите внимание, что вы должны находиться в сети на стадии выявления голосов, для того чтобы приложение Bisq смогло опубликовать транзакцию выявления вашего голоса. Без этого ваш голос не будет засчитан! + +dao.wallet.menuItem.send=Отправить +dao.wallet.menuItem.receive=Получить +dao.wallet.menuItem.transactions=Транзакции + +dao.wallet.dashboard.myBalance=Баланс моего кошелька + +dao.wallet.receive.fundYourWallet=Ваш адрес BSQ +dao.wallet.receive.bsqAddress=Адрес кошелька BSQ (новый неиспользованный адрес) + +dao.wallet.send.sendFunds=Отправить средства +dao.wallet.send.sendBtcFunds=Отправить средства (BTC), исключая BSQ +dao.wallet.send.amount=Сумма в BSQ +dao.wallet.send.btcAmount=Сумма в ВТС (исключая BSQ) +dao.wallet.send.setAmount=Установленная сумма для вывода (мин. сумма равна {0}) +dao.wallet.send.receiverAddress=BSQ-адрес получателя +dao.wallet.send.receiverBtcAddress=BTC-адрес получателя +dao.wallet.send.setDestinationAddress=Укажите адрес получателя +dao.wallet.send.send=Отправить BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Отправить BTC +dao.wallet.send.sendFunds.headline=Подтвердите запрос на вывод средств +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Последний проверенный блок: {0} +dao.wallet.chainHeightSyncing=Ожидание блоков... Проверено: {0} бл. из {1} +dao.wallet.tx.type=Тип + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Не определено +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Не признано +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Непроверенная транзакция в BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Недействительная транзакция в BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Первичная транзакция +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Перевести BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=Полученные BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=Отправленные BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Торговый сбор +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Сбор за запрос компенсации +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Сбор за запрос на возмещение затрат +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Сбор за предложение +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Сбор за голосование вслепую +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Выявление голосов +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Заблокировать гарантийный депозит +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Разблокировать гарантийный депозит +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Сбор за размещение актива +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Proof of burn +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Неправильные + +dao.tx.withdrawnFromWallet=BTC выведен из кошелька +dao.tx.issuanceFromCompReq=Запрос/выдача компенсации +dao.tx.issuanceFromCompReq.tooltip=Запрос компенсации, который привел к эмиссии новых BSQ.\nДата эмиссии: {0} +dao.tx.issuanceFromReimbursement=Запрос/выдача возмещения +dao.tx.issuanceFromReimbursement.tooltip=Запрос на возмещение затрат, который привел к эмиссии новых BSQ.\nДата эмиссии: {0} +dao.proposal.create.missingBsqFunds=У вас недостаточно BSQ для создания предложения. Если у вас есть неподтвержденная транзакция в BSQ, необходимо дождаться подтверждения в блокчейне, так как подтверждение операций в BSQ происходит только после включения транзакции в блок.\nНе хватает: {0} + +dao.proposal.create.missingBsqFundsForBond=У вас недостаточно BSQ для этой роли. Вы все же можете опубликовать это предложение, но вам понадобится полная сумма в BSQ, необходимая для этой роли, если оно будет принято.\nНе хватает: {0} + +dao.proposal.create.missingMinerFeeFunds=У вас недостаточно BTC для создания предложения. Любая транзакция в BSQ требует оплаты комиссии майнера в ВТС.\nНе хватает {0} + +dao.proposal.create.missingIssuanceFunds=У вас недостаточно BTC для создания предложения. Любая транзакция в BSQ требует оплаты комиссии в ВТС. Все транзакции, в том числе связанные с эмиссией BSQ, для выплаты запрошенной суммы BSQ требуют оплаты комиссии в ВТС ({0} сатоши/BSQ).\nНе хватает: {1} + +dao.feeTx.confirm=Подтвердить транзакцию {0} +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=ДАО BISQ +dao.news.bisqDAO.description=Модель управления Bisq так же децентрализована и защищена от цензуры, как и сама биржа Bisq. Это возможно благодаря ДАО Bisq и токену BSQ. +dao.news.bisqDAO.readMoreLink=Подробнее о ДАО Bisq + +dao.news.pastContribution.title=ПОМОГАЛИ НАМ В ПРОШЛОМ? ЗАПРОСИТЕ BSQ +dao.news.pastContribution.description=Если вы помогли Bisq в прошлом, используйте свой адрес BSQ ниже и запросите участие в первоначальном распределении BSQ. +dao.news.pastContribution.yourAddress=Адрес вашего кошелька BSQ +dao.news.pastContribution.requestNow=Запросить + +dao.news.DAOOnTestnet.title=ЗАПУСТИТЬ ДАО BISQ В НАШЕЙ ТЕСТОВОЙ СЕТИ +dao.news.DAOOnTestnet.description=Основная сеть ДАО Bisq еще не запущена, но вы можете узнать о ней подробнее, запустив ДАО в тестовой сети. +dao.news.DAOOnTestnet.firstSection.title=1. Переключиться в режим тестовой сети ДАО +dao.news.DAOOnTestnet.firstSection.content=Переключитесь на тестовую сеть ДАО в настройках. +dao.news.DAOOnTestnet.secondSection.title=2. Приобрести BSQ +dao.news.DAOOnTestnet.secondSection.content=Запросите BSQ в Slack или купите BSQ в Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Принять участие в цикле голосования +dao.news.DAOOnTestnet.thirdSection.content=Вносите предложения и голосуйте по предложениям об изменении различных аспектов Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Ознакомиться с обозревателем блоков BSQ +dao.news.DAOOnTestnet.fourthSection.content=Поскольку BSQ — это тоже биткойн, транзакции BSQ видны в нашем обозревателе блоков Биткойн. +dao.news.DAOOnTestnet.readMoreLink=Ознакомиться с полной документацией + +dao.monitor.daoState=Состояние ДАО +dao.monitor.proposals=Состояние выдвижения предложений +dao.monitor.blindVotes=Состояние слепого голосования + +dao.monitor.table.peers=Пиры +dao.monitor.table.conflicts=Конфликты +dao.monitor.state=Статус +dao.monitor.requestAlHashes=Запросить все хеши +dao.monitor.resync=Повт. синхр. сост. ДАО +dao.monitor.table.header.cycleBlockHeight=Номер блока/цикла +dao.monitor.table.cycleBlockHeight=Цикл {0} / блок {1} +dao.monitor.table.seedPeers=Исходный узел: {0} + +dao.monitor.daoState.headline=Состояние ДАО +dao.monitor.daoState.table.headline=Цепочка хешей состояния ДАО +dao.monitor.daoState.table.blockHeight=Номер блока +dao.monitor.daoState.table.hash=Хеш состояния ДАО +dao.monitor.daoState.table.prev=Предыдущий хеш +dao.monitor.daoState.conflictTable.headline=Хеши состояния ДАО от конфликтующих пиров +dao.monitor.daoState.utxoConflicts=Конфликты UTXO +dao.monitor.daoState.utxoConflicts.blockHeight=Номер блока: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Сумма всех UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Сумма всех BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. After restart the DAO state will resync. + +dao.monitor.proposal.headline=Состояние выдвижения предложений +dao.monitor.proposal.table.headline=Цепочка хешей состояния выдвижения предложений +dao.monitor.proposal.conflictTable.headline=Хеши состояния выдвижения предложений от конфликтующих пиров + +dao.monitor.proposal.table.hash=Хеш состояния выдвижения предложений +dao.monitor.proposal.table.prev=Предыдущий хеш +dao.monitor.proposal.table.numProposals=Кол-во предложений + +dao.monitor.isInConflictWithSeedNode=Данные на вашем компьютере не достигли состояния консенсуса по крайней мере с одним исходным узлом. Необходима повторная синхронизация состояния ДАО. +dao.monitor.isInConflictWithNonSeedNode=Один из ваших пиров не достиг состояния консенсуса с сетью, однако ваш узел синхронизирован с исходными узлами. +dao.monitor.daoStateInSync=Узел на вашем компьютере находится в состоянии консенсуса с сетью + +dao.monitor.blindVote.headline=Состояние слепого голосования +dao.monitor.blindVote.table.headline=Цепочка хешей состояния слепого голосования +dao.monitor.blindVote.conflictTable.headline=Хеши состояния слепого голосования от конфликтующих пиров +dao.monitor.blindVote.table.hash=Хеш состояния слепого голосования +dao.monitor.blindVote.table.prev=Предыдущий хеш +dao.monitor.blindVote.table.numBlindVotes=Кол-во слепых голосов + +dao.factsAndFigures.menuItem.supply=Предложение BSQ +dao.factsAndFigures.menuItem.transactions=Транзакции в BSQ + +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=Доступное количество BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt + +dao.factsAndFigures.supply.issued=Эмиссия BSQ +dao.factsAndFigures.supply.compReq=Запросы компенсации +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=Эмиссия BSQ в первичной транзакции +dao.factsAndFigures.supply.compRequestIssueAmount=Эмиссия BSQ в качестве компенсации +dao.factsAndFigures.supply.reimbursementAmount=Эмиссия BSQ в качестве возмещения затрат +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=Выведено из обращения BSQ + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Объём сделки +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Глобальное состояние заблокированных BSQ +dao.factsAndFigures.supply.totalLockedUpAmount=Заблокировано в гарантийных депозитах +dao.factsAndFigures.supply.totalUnlockingAmount=Разблокировка BSQ из гарантийных депозитов +dao.factsAndFigures.supply.totalUnlockedAmount=Разблокировано BSQ из гарантийных депозитов +dao.factsAndFigures.supply.totalConfiscatedAmount=Конфисковано BSQ из гарантийных депозитов +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Первичная транзакция +dao.factsAndFigures.transactions.genesisBlockHeight=Номер первичного блока +dao.factsAndFigures.transactions.genesisTxId=Идент. первичной транзакции +dao.factsAndFigures.transactions.txDetails=Статистика транзакций BSQ +dao.factsAndFigures.transactions.allTx=Общее кол-во транзакций BSQ +dao.factsAndFigures.transactions.utxo=Общее кол-во неизрасходованных выводов (UTXO) +dao.factsAndFigures.transactions.compensationIssuanceTx=Общее кол-во транзакций, связанных с эмиссией BSQ для выплаты компенсаций +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Общее кол-во транзакций, связанных с эмиссией BSQ для возмещения затрат +dao.factsAndFigures.transactions.burntTx=Общее кол-во транзакций, связанных с оплатой комиссий +dao.factsAndFigures.transactions.invalidTx=Кол-во недействительных транзакций +dao.factsAndFigures.transactions.irregularTx=Кол-во неправильных транзакций + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Доступный баланс + +contractWindow.title=Подробности спора +contractWindow.dates=Дата предложения / Дата сделки +contractWindow.btcAddresses=Биткойн-адрес покупателя BTC / продавца BTC +contractWindow.onions=Сетевой адрес покупателя BTC / продавца BTC +contractWindow.accountAge=Возраст счёта (покупатель/продавец BTC) +contractWindow.numDisputes=Кол-во споров покупателя BTC / продавца BTC +contractWindow.contractHash=Хеш контракта + +displayAlertMessageWindow.headline=Важная информация! +displayAlertMessageWindow.update.headline=Информация о важном обновлении! +displayAlertMessageWindow.update.download=Загрузить: +displayUpdateDownloadWindow.downloadedFiles=Файлы: +displayUpdateDownloadWindow.downloadingFile=Загрузka: {0} +displayUpdateDownloadWindow.verifiedSigs=Подпись, подтвержденная ключами: +displayUpdateDownloadWindow.status.downloading=Файлы загружаются... +displayUpdateDownloadWindow.status.verifying=Проверка подписи... +displayUpdateDownloadWindow.button.label=Загрузите установщик и проверьте подпись +displayUpdateDownloadWindow.button.downloadLater=Скачать позже +displayUpdateDownloadWindow.button.ignoreDownload=Игнорировать эту версию +displayUpdateDownloadWindow.headline=Доступно новое обновление Bisq! +displayUpdateDownloadWindow.download.failed.headline=Загрузка не удалась +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=Новая версия загружена, а её подпись проверена.\n\nОткройте директорию загрузки, закройте приложение и установите новую версию. +displayUpdateDownloadWindow.download.openDir=Открыть директорию загрузки + +disputeSummaryWindow.title=Сводка +disputeSummaryWindow.openDate=Дата обращения за поддержкой +disputeSummaryWindow.role=Роль трейдера +disputeSummaryWindow.payout=Выплата суммы сделки +disputeSummaryWindow.payout.getsTradeAmount={0} BTC получит выплату суммы сделки +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Пользовательская выплата +disputeSummaryWindow.payoutAmount.buyer=Сумма выплаты покупателя +disputeSummaryWindow.payoutAmount.seller=Сумма выплаты продавца +disputeSummaryWindow.payoutAmount.invert=Проигравший публикует +disputeSummaryWindow.reason=Причина спора +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Ошибка +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Удобство использования +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Нарушение протокола +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Отсутствие ответа +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Мошенничество +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Другое +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Банк +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Примечания +disputeSummaryWindow.addSummaryNotes=Добавить примечания +disputeSummaryWindow.close.button=Закрыть обращение + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=Вам также необходимо закрыть обращение контрагента! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline=Аварийный кошелёк {0} +emptyWalletWindow.info=Используйте этот инструмент только в экстренном случае, если вам недоступны средства из пользовательского интерфейса.\n\nУчтите, что все открытые предложения будут автоматически закрыты при использовании этого инструмента.\n\nПрежде чем воспользоваться этим инструментом, создайте резервную копию своего каталога данных. Это можно сделать в разделе \«Счёт/Резервное копирование\».\n\nСообщите нам о неисправности и создайте отчёт о ней в Github или на форуме Bisq, чтобы мы могли выявить её причину. +emptyWalletWindow.balance=Доступный баланс кошелька +emptyWalletWindow.bsq.btcBalance=Баланс в сатоши (без учёта BSQ) + +emptyWalletWindow.address=Адрес получателя +emptyWalletWindow.button=Отправить все средства +emptyWalletWindow.openOffers.warn=У вас есть открытые предложения, которые будут удалены, если вы выведите все средства с кошелька.\nВывести все средства? +emptyWalletWindow.openOffers.yes=Да, я уверен (-а) +emptyWalletWindow.sent.success=Все средства с вашего кошелька были успешно отправлены. + +enterPrivKeyWindow.headline=Enter private key for registration + +filterWindow.headline=Изменить список фильтров +filterWindow.offers=Отфильтрованные предложения (через запят.) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Отфильтрованные данные торгового счёта:\nФормат: список, через запятые [идентификатор метода платежа | поле данных | значение] +filterWindow.bannedCurrencies=Отфильтрованные коды валют (через запят.) +filterWindow.bannedPaymentMethods=Отфильтрованные идент. методов платежа (через запят.) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Отфильтрованные арбитры (onion-адреса через запят.) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) +filterWindow.seedNode=Отфильтрованные исходные узлы (onion-адреса через запят.) +filterWindow.priceRelayNode=Отфильтрованные ретрансляторы курса (onion-адреса через запят.) +filterWindow.btcNode=Отфильтрованные узлы Биткойн (адреса + порты через запят.) +filterWindow.preventPublicBtcNetwork=Не использовать общедоступную сеть Биткойн +filterWindow.disableDao=Отключить ДАО +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Мин. версия, необходимая для работы с ДАО +filterWindow.disableTradeBelowVersion=Мин. версия, необходимая для торговли +filterWindow.add=Добавить фильтр +filterWindow.remove=Удалить фильтр +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Мин. количество BTC +offerDetailsWindow.min=(мин. {0}) +offerDetailsWindow.distance=(отклонение от рыночного курса: {0}) +offerDetailsWindow.myTradingAccount=Мой торговый счёт +offerDetailsWindow.offererBankId=(Идент./BIC/SWIFT банка мейкера) +offerDetailsWindow.offerersBankName=(Название банка мейкера) +offerDetailsWindow.bankId=Идентификатор банка (напр., BIC или SWIFT) +offerDetailsWindow.countryBank=Страна банка мейкера +offerDetailsWindow.commitment=Обязательство +offerDetailsWindow.agree=Подтверждаю +offerDetailsWindow.tac=Пользовательское соглашение +offerDetailsWindow.confirm.maker=Подтвердите: разместить предложение {0} биткойн +offerDetailsWindow.confirm.taker=Подтвердите: принять предложение {0} биткойн +offerDetailsWindow.creationDate=Дата создания +offerDetailsWindow.makersOnion=Onion-адрес мейкера + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Запрос платежа:\n{0} + +selectDepositTxWindow.headline=Выберите транзакцию ввода средств для включения в спор +selectDepositTxWindow.msg=Транзакция ввода средств не сохранилась в сделке.\nВыберите в своём кошельке одну из существующих multisig-транзакций, которая использовалась для ввода средств в неудавшейся сделке.\n\nНужную транзакцию можно найти, открыв окно сведений о сделке (щелкните на идентификаторе сделки в списке). Multisig-транзакция ввода средств (её адрес начинается с 3) следует сразу за транзакцией оплаты торгового сбора. Этот идентификатор транзакции должен быть виден в представленном здесь списке. После того, как вы найдёте нужную транзакцию, выберите её здесь и продолжите.\n\nПриносим извинения за неудобство. Такие ошибки случаются редко, и мы работаем над тем, чтобы найти наилучший способ их решения. +selectDepositTxWindow.select=Выберите транзакцию ввода средств + +sendAlertMessageWindow.headline=Отправить глобальное уведомление +sendAlertMessageWindow.alertMsg=Предупреждение +sendAlertMessageWindow.enterMsg=Введите сообщение +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=Номер новой версии +sendAlertMessageWindow.send=Отправить уведомление +sendAlertMessageWindow.remove=Удалить уведомление + +sendPrivateNotificationWindow.headline=Отправить личное сообщение +sendPrivateNotificationWindow.privateNotification=Личное уведомление +sendPrivateNotificationWindow.enterNotification=Введите уведомление +sendPrivateNotificationWindow.send=Отправить личное уведомление + +showWalletDataWindow.walletData=Данные кошелька +showWalletDataWindow.includePrivKeys=Добавить приватные ключи + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Пользовательское соглашение +tacWindow.agree=Подтверждаю +tacWindow.disagree=Не согласен (-сна) и выхожу +tacWindow.arbitrationSystem=Dispute resolution + +tradeDetailsWindow.headline=Сделка +tradeDetailsWindow.disputedPayoutTxId=Идент. оспоренной транзакции выплаты: +tradeDetailsWindow.tradeDate=Дата сделки +tradeDetailsWindow.txFee=Комиссия майнера +tradeDetailsWindow.tradingPeersOnion=Оnion-адрес контрагента +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Статус сделки +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Введите пароль для разблокировки + +torNetworkSettingWindow.header=Настройки сети Тоr +torNetworkSettingWindow.noBridges=Не использовать мосты +torNetworkSettingWindow.providedBridges=Подключиться через предоставленные мосты +torNetworkSettingWindow.customBridges=Ввести свои мосты +torNetworkSettingWindow.transportType=Тип транспортировки +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (рекомендуется) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Введите один или более мостовой узел (по одному на строку) +torNetworkSettingWindow.enterBridgePrompt=печатать адрес:порт +torNetworkSettingWindow.restartInfo=Чтобы изменения вступили в силу, необходимо перезапустить приложение +torNetworkSettingWindow.openTorWebPage=Открыть веб-страницу проекта Tor +torNetworkSettingWindow.deleteFiles.header=Проблемы с подключением? +torNetworkSettingWindow.deleteFiles.info=Если при запуске повторно возникают проблемы с подключением, попробуйте удалить устаревшие файлы Tor. Для этого нажмите на кнопку ниже и перезагрузите приложение. +torNetworkSettingWindow.deleteFiles.button=Удалить устаревшие файлы Tor и завершить работу +torNetworkSettingWindow.deleteFiles.progress=Tor завершает работу +torNetworkSettingWindow.deleteFiles.success=Устаревшие файлы Tor успешно удалены. Пожалуйста, перезапустите приложение. +torNetworkSettingWindow.bridges.header=Tor сеть заблокирована? +torNetworkSettingWindow.bridges.info=Если Tor заблокирован вашим интернет-провайдером или правительством, попробуйте использовать мосты Tor.\nПосетите веб-страницу Tor по адресу: https://bridges.torproject.org/bridges, чтобы узнать больше о мостах и подключаемых транспортных протоколах. + +feeOptionWindow.headline=Выберите валюту для оплаты торгового сбора +feeOptionWindow.info=Вы можете оплатить комиссию за сделку в BSQ или BTC. Если вы выберите BSQ, то сумма комиссии будет ниже. +feeOptionWindow.optionsLabel=Выберите валюту для оплаты комиссии за сделку +feeOptionWindow.useBTC=Использовать ВТС +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Уведомление +popup.headline.instruction=Обратите внимание: +popup.headline.attention=Внимание +popup.headline.backgroundInfo=Справочная информация +popup.headline.feedback=Завершено +popup.headline.confirmation=Подтверждение +popup.headline.information=Информация +popup.headline.warning=Предупреждение +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Если вы прикрепите к отчету о неисправности файл журнала bisq.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. + +popup.error.tryRestart=Попробуйте перезагрузить приложение и проверьте подключение к сети, чтобы попробовать решить проблему. +popup.error.takeOfferRequestFailed=Произошла ошибка, когда контрагент попытался принять одно из ваших предложений:\n{0} + +error.spvFileCorrupted=Произошла ошибка при чтении файла цепи SPV.\nВозможно, файл цепи SPV повреждён.\n\nСообщение об ошибке: {0}\n\nУдалить файл SPV и начать повторную синхронизацию? +error.deleteAddressEntryListFailed=Не удалось удалить файл AddressEntryList. \nОшибка: {0} +error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. +error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. + +popup.warning.walletNotInitialized=Кошелёк ещё не инициализирован +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=Вероятно, у вас установлена не та версия Bisq.\nАрхитектура Вашего компьютера: {0}.\nУстановленная версия Bisq: {1}.\nЗакройте приложение и установите нужную версию ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq уже запущен. Нельзя запустить два экземпляра Bisq. +popup.warning.tradePeriod.halfReached=Половина макс. допустимого срока сделки с идентификатором {0} истекла, однако она до сих пор не завершена.\n\nСрок сделки заканчивается {1}\n\nДополнительную информацию о состоянии сделки можно узнать в разделе \«Сделки/Текущие сделки\». +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the mediator. +popup.warning.noTradingAccountSetup.headline=Вы не создали торговый счёт +popup.warning.noTradingAccountSetup.msg=Перед созданием предложения необходимо создать счета в национальной валюте или альткойнах. \nСоздать счёт? +popup.warning.noArbitratorsAvailable=Нет доступных арбитров. +popup.warning.noMediatorsAvailable=There are no mediators available. +popup.warning.notFullyConnected=Необходимо дождаться полного подключения к сети.\nОно может занять до 2 минут. +popup.warning.notSufficientConnectionsToBtcNetwork=Необходимо дождаться не менее {0} соединений с сетью Биткойн. +popup.warning.downloadNotComplete=Необходимо дождаться завершения загрузки недостающих блоков сети Биткойн. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Действительно хотите удалить это предложение?\nКомиссия мейкера в размере {0} компенсации не подлежит. +popup.warning.tooLargePercentageValue=Нельзя установить процент в размере 100% или выше. +popup.warning.examplePercentageValue=Введите процент, например \«5,4\» для 5,4% +popup.warning.noPriceFeedAvailable=Источник рыночного курса для этой валюты отсутствует. Невозможно использовать процентный курс.\nПросьба указать фиксированный курс. +popup.warning.sendMsgFailed=Не удалось отправить сообщение вашему контрагенту .\nПопробуйте еще раз, и если неисправность повторится, сообщите о ней. +popup.warning.insufficientBtcFundsForBsqTx=У вас недостаточно BTC для оплаты комиссии майнера за эту транзакцию.\nПополните свой кошелек BTC.\nНе хватает: {0} +popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. +popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=В вашем кошельке BSQ недостаточно средств для оплаты комиссии за сделку в BSQ. +popup.warning.messageTooLong=Ваше сообщение превышает макс. разрешённый размер. Разбейте его на несколько частей или загрузите в веб-приложение для работы с отрывками текста, например https://pastebin.com. +popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=ретранслятор курса +popup.warning.seed=мнемоническая фраза +popup.warning.mandatoryUpdate.trading=Обновите Bisq до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Bisq, чтобы узнать подробности. +popup.warning.mandatoryUpdate.dao=Обновите Bisq до последней версии. Вышло обязательное обновление, которое делает невозможным участие в ДАО Bisq и торговлю BSQ в старых версиях приложения. Посетите форум Bisq, чтобы узнать подробности. +popup.warning.disable.dao=ДАО Bisq и торговля BSQ временно недоступны. Посетите форум Bisq, чтобы узнать подробности. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше BTC для завершения перевода. + +popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Bitcoin 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 Bisq support channel at the Bisq Keybase team. + +popup.warning.trade.txRejected.tradeFee=trade fee +popup.warning.trade.txRejected.deposit=deposit +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.info.securityDepositInfo=Чтобы гарантировать соблюдение торгового протокола трейдерами, им обоим необходимо внести залог.\n\nЗалог останется в вашем кошельке до успешного завершения сделки, а затем будет возвращен вам.\n\nОбратите внимание, что если вы создаёте новое предложение, приложение Bisq должно быть подключено к сети, чтобы его могли принять другие трейдеры. Чтобы ваши предложения были доступны в сети, компьютер и приложение должны быть включены и подключены к сети (убедитесь, что компьютер не перешёл в режим ожидания; переход монитора в спящий режим не влияет на работу приложения). + +popup.info.cashDepositInfo=Убедитесь, что в вашем районе есть отделение банка, где можно произвести перевод наличных.\nИдентификатор (BIC/SWIFT) банка продавца: {0}. +popup.info.cashDepositInfo.confirm=Я подтверждаю, что могу внести оплату +popup.info.shutDownWithOpenOffers=Bisq закрывается, но у вас есть открытые предложения.\n\nЭти предложения будут недоступны в сети P2P, пока приложение Bisq закрыто, но будут повторно опубликованы в сети P2P при следующем запуске Bisq.\n\nЧтобы ваши предложения были доступны в сети, компьютер и приложение должны быть включены и подключены к сети (убедитесь, что компьютер не перешёл в режим ожидания; переход монитора в спящий режим не влияет на работу приложения). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Важное личное уведомление! + +popup.securityRecommendation.headline=Важная рекомендация по безопасности +popup.securityRecommendation.msg=Рекомендуем вам защитить свой кошелёк паролем, если вы ещё этого не сделали.\n\nТакже убедительно рекомендуем записать вашу мнемоническую фразу. Она поможет восстановить ваш кошелёк Биткойн.\nДополнительная информация указана в разделе \«Мнемоническая фраза\».\n\nВам также следует сохранить копию папки со всеми данными программы в разделе \«Резервное копирование\». + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Завершение работы +popup.shutDownInProgress.msg=Завершение работы приложения может занять несколько секунд.\nПросьба не прерывать этот процесс. + +popup.attention.forTradeWithId=Обратите внимание на сделку с идентификатором {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Доступно несколько платёжных счетов +popup.info.multiplePaymentAccounts.msg=У вас есть несколько платёжных счетов, доступных для этого предложения. Просьба убедиться, что вы выбрали правильный счёт. + +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.signAll=Sign all payment methods +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Уведомление о сделке с идентификатором {0} +notification.ticket.headline=Запрос в службу поддержки для сделки с идентификатором {0} +notification.trade.completed=Сделка завершена, и вы можете вывести свои средства. +notification.trade.accepted=Ваше предложение принял {0} ВТС. +notification.trade.confirmed=Ваша сделка была подтверждена в блокчейне не менее одного раза.\nМожете начать оплату. +notification.trade.paymentStarted=Покупатель ВТС начал оплату. +notification.trade.selectTrade=Выбрать сделку +notification.trade.peerOpenedDispute=Ваш контрагент открыл {0}. +notification.trade.disputeClosed={0} закрыт. +notification.walletUpdate.headline=Обновление торгового кошелька +notification.walletUpdate.msg=Ваш торговый кошелёк содержит достаточно средств.\nСумма: {0} +notification.takeOffer.walletUpdate.msg=Ваш торговый кошелёк уже содержит достаточно средств с прошлой попытки принять предложение.\nСумма: {0} +notification.tradeCompleted.headline=Сделка завершена +notification.tradeCompleted.msg=Теперь вы можете вывести свои средства на свой внешний кошелёк Биткойн или перевести их в кошелёк Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Показать окно приложения +systemTray.hide=Скрыть окно приложения +systemTray.info=Информация о Bisq +systemTray.exit=Выход +systemTray.tooltip=Bisq: A decentralized bitcoin exchange network + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=Торговые счета в:\n{0} +guiUtil.accountExport.noAccountSetup=У вас нет торговых счетов для экспортирования. +guiUtil.accountExport.selectPath=Выбрать путь к {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=Торговый счёт с идентификатором {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Торговый счёт с идентификатором {0} не был импортирован, так как он уже существует.\n +guiUtil.accountExport.exportFailed=Экспорт в CSV не удался из-за ошибки.\nОшибка = {0} +guiUtil.accountExport.selectExportPath=Выбрать директорию для экспорта +guiUtil.accountImport.imported=Торговый счёт импортирован из:\n{0}\n\nИмпортированные счета:\n{1} +guiUtil.accountImport.noAccountsFound=Экспортированные торговые счета не найдены в: {0}.\nИмя файла {1}. +guiUtil.openWebBrowser.warning=Вы собираетесь открыть веб-страницу в веб-браузере.\nСделать это сейчас? \n\nЕсли вы не используете \«Tor\» в качестве веб-браузера по умолчанию, вы откроете веб-страницу в клирнете.\n\nURL: \«{0}\» +guiUtil.openWebBrowser.doOpen=Открыть веб-страницу и не спрашивать снова +guiUtil.openWebBrowser.copyUrl=Скопировать URL и отменить +guiUtil.ofTradeAmount=от суммы сделки +guiUtil.requiredMinimum=(required minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Выбрать валюту +list.currency.showAll=Показать все +list.currency.editList=Редактировать перечень валют + +table.placeholder.noItems=Нет доступных {0} +table.placeholder.noData=Данные недоступны +table.placeholder.processingData=Processing data... + + +peerInfoIcon.tooltip.tradePeer=контрагента +peerInfoIcon.tooltip.maker=мейкера +peerInfoIcon.tooltip.trade.traded=Onion-адрес {0}: {1}\nВы уже торговали с данным контрагентом {2} раз (-a).\n{3} +peerInfoIcon.tooltip.trade.notTraded=Onion-адрес {0}: {1}\nВы ещё не торговали с этим контрагентом.\n {2} +peerInfoIcon.tooltip.age=Платёжный счёт создан {0} назад. +peerInfoIcon.tooltip.unknownAge=Возраст платёжного счёта неизвестен. + +tooltip.openPopupForDetails=Открыть окно с подробностями +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Открыть адрес {0} во внешнем обозревателе блоков +tooltip.openBlockchainForTx=Открыть транзакцию {0} во внешнем обозревателе блоков + +confidence.unknown=Статус транзакции неизвестен +confidence.seen=Замечена {0} пиром (-ами) / 0 подтверждений +confidence.confirmed=Подтверждена в {0} блоке (-ах) +confidence.invalid=Недействительная транзакция + +peerInfo.title=Данные трейдера +peerInfo.nrOfTrades=Количество завершенных сделок +peerInfo.notTradedYet=Вы ещё не торговали с этим пользователем. +peerInfo.setTag=Установить метку для данного участника +peerInfo.age.noRisk=Возраст платёжного счёта +peerInfo.age.chargeBackRisk=Time since signing +peerInfo.unknownAge=Возраст неизвестен + +addressTextField.openWallet=Открыть Биткойн-кошелёк по умолчанию +addressTextField.copyToClipboard=Скопировать адрес в буфер +addressTextField.addressCopiedToClipboard=Адрес скопирован в буфер обмена +addressTextField.openWallet.failed=Не удалось открыть Биткойн-кошелёк, использующийся по умолчанию. Возможно, у вас не установлено его приложение? + +peerInfoIcon.tooltip={0}\nМетка: {1} + +txIdTextField.copyIcon.tooltip=Скопировать идентификатор транзакции в буфер +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\«Счёт\» +navigation.account.walletSeed=\«Счёт/Мнемоническая фраза\» +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\«Сделки/Мои текущие предложения\» +navigation.portfolio.pending=\«Сделки/Текущие сделки\» +navigation.portfolio.closedTrades=\«Сделки/История\» +navigation.funds.depositFunds=\«Средства/Получить средства\» +navigation.settings.preferences=\«Настройки/Параметры\» +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\«Средства/Транзакции\» +navigation.support=\«Поддержка\» +navigation.dao.wallet.receive=\«ДАО/BSQ-кошелёк/Получить\» + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} сумма {1} +formatter.makerTaker=Мейкер как {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.fiat=Вы создаете предложение {0} {1} +formatter.youAreCreatingAnOffer.altcoin=Вы создаете предложение {0} {1} ({2} {3}) +formatter.asMaker={0} {1} как мейкер +formatter.asTaker={0} {1} как тейкер + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Осн. сеть Биткойн +# suppress inspection "UnusedProperty" +BTC_TESTNET=Тестовая сеть Биткойн +# suppress inspection "UnusedProperty" +BTC_REGTEST=Режим регрессионного тестирования в сети Биткойн +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Тестовая сеть ДАО Биткойн (устаревшая) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Режим регрессионного тестирования ДАО Биткойн + +time.year=Год +time.month=Месяц +time.week=Неделя +time.day=День +time.hour=Час +time.minute10=10 минут +time.hours=ч. +time.days=дн. +time.1hour=1 час +time.1day=1 день +time.minute=минута +time.second=секунда +time.minutes=мин. +time.seconds=сек. + + +password.enterPassword=Введите пароль +password.confirmPassword=Подтвердите пароль +password.tooLong=Пароль не должен превышать 500 символов. +password.deriveKey=Извлечь ключ из пароля +password.walletDecrypted=Кошелёк успешно расшифрован, защита паролем удалена. +password.wrongPw=Вы ввели неверный пароль.\n\nПопробуйте снова, обратив внимание на возможные ошибки ввода. +password.walletEncrypted=Кошелёк успешно зашифрован, защита паролем включена. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=Введённые вами 2 пароля не совпадают. +password.forgotPassword=Забыли пароль? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Мнемоническая фраза кошелька +seed.enterSeedWords=Введите мнемоническую фразу кошелька +seed.date=Дата создания кошелька +seed.restore.title=Восстановить кошельки с помощью мнемонической фразы +seed.restore=Восстановить кошельки +seed.creationDate=Дата создания +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=Всё равно хочу восстановить +seed.warn.walletNotEmpty.emptyWallet=Вывести все средства с моих кошельков +seed.warn.notEncryptedAnymore=Ваши кошельки зашифрованы.\n\nПосле восстановления кошельки больше не будут зашифрованы, и вам потребуется установить новый пароль.\n\nПродолжить? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Кошельки успешно восстановлены с новой мнемонической фразой.\n\nЗакройте и перезапустите приложение. +seed.restore.error=Произошла ошибка при восстановлении кошельков с помощью мнемонической фразы.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Счёт +payment.account.no=Номер счёта +payment.account.name=Название счёта +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Полное имя владельца счёта +payment.account.fullName=Полное имя (имя, отчество, фамилия) +payment.account.state=Штат/Провинция/Область +payment.account.city=Город +payment.bank.country=Страна банка +payment.account.name.email=Полное имя / электронный адрес владельца счета +payment.account.name.emailAndHolderId=Полное имя / электронный адрес / {0} владельца счета +payment.bank.name=Название банка +payment.select.account=Выбрать тип счёта +payment.select.region=Выбрать регион +payment.select.country=Выбрать страну +payment.select.bank.country=Выбрать страну банка +payment.foreign.currency=Вы уверены, что хотите выбрать валюту, отличную от национальной валюты по умолчанию? +payment.restore.default=Нет, восстановить валюту по умолчанию +payment.email=Электронный адрес +payment.country=Страна +payment.extras=Дополнительные требования +payment.email.mobile=Эл. адрес или номер моб. тел. +payment.altcoin.address=Альткойн-адрес +payment.altcoin.tradeInstantCheckbox=Совершайте мгновенные сделки (в течение 1 часа) с этим альткойном +payment.altcoin.tradeInstant.popup=Для ускоренной торговли требуется, чтобы оба контрагента были в сети и могли завершить сделку менее чем за 1 час.\n\nЕсли у вас есть текущие предложения, но вы не можете находиться в сети, отключите их в разделе «Папка». +payment.altcoin=Альткойны +payment.select.altcoin=Select or search Altcoin +payment.secret=Секретный вопрос +payment.answer=Ответ +payment.wallet=Идентификатор кошелька +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Имя пользователя, эл. адрес или тел. номер +payment.moneyBeam.accountId=Эл. адрес или тел. номер +payment.venmo.venmoUserName=Имя пользователя Venmo +payment.popmoney.accountId=Эл. адрес или тел. номер +payment.promptPay.promptPayId=Удостовер. личности / налог. номер или номер телефона +payment.supportedCurrencies=Поддерживаемые валюты +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Ограничения +payment.salt=Модификатор («соль») для подтверждения возраста счёта +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Принять сделки из этих стран, использующих EUR +payment.accept.nonEuro=Принять сделки из этих стран, не использующих EUR +payment.accepted.countries=Одобренные страны +payment.accepted.banks=Одобренные банки (идент.) +payment.mobile=Номер моб. тел. +payment.postal.address=Почтовый адрес +payment.national.account.id.AR=Номер CBU +shared.accountSigningState=Account signing status + +#new +payment.altcoin.address.dyn={0}-адрес +payment.altcoin.receiver.address=Альткойн-адрес получателя +payment.accountNr=Номер счёта +payment.emailOrMobile=Эл. адрес или номер моб. тел. +payment.useCustomAccountName=Использовать своё название счёта +payment.maxPeriod=Макс. допустимый срок сделки +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} +payment.maxPeriodAndLimitCrypto=Макс. срок сделки: {0} / Макс. торговый лимит: {1} +payment.currencyWithSymbol=Валюта: {0} +payment.nameOfAcceptedBank=Название одобренного банка +payment.addAcceptedBank=Добавить одобренный банк +payment.clearAcceptedBanks=Очистить одобренные банки +payment.bank.nameOptional=Название банка (необязательно) +payment.bankCode=Код банка +payment.bankId=Идентификатор банка (BIC/SWIFT): +payment.bankIdOptional=Идентификатор банка (BIC/SWIFT) (необяз.) +payment.branchNr=Номер отделения +payment.branchNrOptional=Номер отделения (необяз.) +payment.accountNrLabel=Номер счёта (IBAN) +payment.accountType=Тип счёта +payment.checking=Текущий +payment.savings=Сберегательный +payment.personalId=Личный идентификатор +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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, покупатель BTC обязуется отправить продавцу BTC код HalCash через СМС с мобильного телефона.\n\nУбедитесь, что не вы не превысили максимальную сумму, которую ваш банк позволяет отправить с HalCash. Минимальная сумма на вывод средств составляет 10 EUR, а и максимальная — 600 EUR. При повторном выводе средств лимит составляет 3000 EUR на получателя в день и 6000 EUR на получателя в месяц. Просьба сверить эти лимиты с вашим банком и убедиться, что лимиты банка соответствуют лимитам, указанным здесь.\n\nВыводимая сумма должна быть кратна 10 EUR, так как другие суммы снять из банкомата невозможно. Приложение само отрегулирует сумму BTC, чтобы она соответствовала сумме в EUR, во время создания или принятия предложения. Вы не сможете использовать текущий рыночный курс, так как сумма в EUR будет меняться с изменением курса.\n\nВ случае спора покупателю BTC необходимо предоставить доказательство отправки 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/Account_limits]. + +payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=Контактная информация +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=Контактная информация +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/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.japan.bank=Банк +payment.japan.branch=Branch +payment.japan.account=Счёт +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Перевод национальным банком +SAME_BANK=Перевод в тот же банк +SPECIFIC_BANKS=Перевод через определённый банк +US_POSTAL_MONEY_ORDER=Почтовый денежный перевод США +CASH_DEPOSIT=Внесение наличных +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Личная встреча +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Национальные банки +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Тот же банк +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Определенные банки +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=Денежный перевод США +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Внесение наличных +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=Личная встреча +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Альткойны +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Альткойны (мгновенно) + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Альткойны +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Альткойны (мгновенно) + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Нужно ввести данные. +validation.NaN=Введённое число недопустимо. +validation.notAnInteger=Введённое число не является целым. +validation.zero=Введённое значение не может быть равно 0. +validation.negative=Отрицательное значение недопустимо. +validation.fiat.toSmall=Ввод значения меньше минимально возможного не допускается. +validation.fiat.toLarge=Ввод значения больше максимально возможного не допускается. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=Значение не может превышать {0}. +validation.btc.toSmall=Значение не может быть меньше {0}. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=Введенный пароль слишком длинный. Его длина не должна превышать 50 символов. +validation.sortCodeNumber={0} должен состоять из {1} цифр. +validation.sortCodeChars={0} должен состоять из {1} символов. +validation.bankIdNumber={0} должен состоять из {1} цифр. +validation.accountNr=Номер счёта должен состоять из {0} цифр. +validation.accountNrChars=Номер счёта должен состоять из {0} символов. +validation.btc.invalidAddress=Неправильный адрес. Проверьте формат адреса. +validation.integerOnly=Введите только целые числа. +validation.inputError=Введённое значение вызвало ошибку:\n{0} +validation.bsq.insufficientBalance=Ваш доступный баланс составляет {0}. +validation.btc.exceedsMaxTradeLimit=Ваш торговый лимит составляет {0}. +validation.bsq.amountBelowMinAmount=Мин. сумма {0} +validation.nationalAccountId={0} должен состоять из {1} цифр. + +#new +validation.invalidInput=Недействительное значение: {0} +validation.accountNrFormat=Допустимый формат номера счёта: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Сбой проверки адреса. Адрес не соответствует структуре адреса {0}. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Адрес не является действительным адресом {0}! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Код банка и страны должен быть буквенным +validation.bic.invalidLocationCode=BIC содержит недействительный код местности +validation.bic.invalidBranchCode=BIC содержит недействительный код отделения +validation.bic.sepaRevolutBic=Счета SEPA в Revolut не поддерживаются. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Недействительный адрес +validation.iban.invalidCountryCode=Код страны недействителен +validation.iban.checkSumNotNumeric=Контрольная сумма должна иметь числовой формат +validation.iban.nonNumericChars=Введен не буквенно-цифровой знак +validation.iban.checkSumInvalid=Контрольная сумма IBAN недействительна +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Код не канадского региона +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Должен содержать только буквы, цифры, пробелы и/или символы ' _ , . ? - +validation.interacETransfer.invalidAnswer=Должен состоять из одного слова, содержащего только буквы, цифры и/или символ - +validation.inputTooLarge=Значение не должно превышать {0} +validation.inputTooSmall=Значение должно быть более {0} +validation.inputToBeAtLeast=Значение должно быть не менее {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=Длина должна составлять от {0} до {1} +validation.fixedLength=Length must be {0} +validation.pattern=Формат значения: {0} +validation.noHexString=Значение не соответствует шестнадцатеричному формату. +validation.advancedCash.invalidFormat=Требуется действительный электронный адрес или идентификатор кошелька в формате: x000000000000 +validation.invalidUrl=Недопустимый URL-адрес +validation.mustBeDifferent=Your input must be different from the current value +validation.cannotBeChanged=Неизменяемый параметр +validation.numberFormatException=Исключение числового формата {0} +validation.mustNotBeNegative=Значение не может быть отрицательным +validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.invalidCharacters=Phone number {0} contains invalid characters +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Must be comma separated list of valid addresses diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties new file mode 100644 index 0000000000..3742a5261b --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=อ่านเพิ่มเติม +shared.openHelp=การช่วยเหลือ +shared.warning=คำเตือน +shared.close=ปิด +shared.cancel=ยกเลิก +shared.ok=ตกลง +shared.yes=ใช่ +shared.no=ไม่ใช่ +shared.iUnderstand=ฉันเข้าใจ +shared.na=ไม่พร้อมใช้งาน +shared.shutDown=ปิดใช้งาน +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=ซื้อ bitcoin (บิตคอยน์) +shared.sellBitcoin=ขาย bitcoin (บิตคอยน์) +shared.buyCurrency=ซื้อ {0} +shared.sellCurrency=ขาย {0} +shared.buyingBTCWith=การซื้อ BTC กับ {0} +shared.sellingBTCFor=การขาย BTC แก่ {0} +shared.buyingCurrency=การซื้อ {0} (การขาย BTC) +shared.sellingCurrency=การขาย {0} (การซื้อ BTC) +shared.buy=ซื้อ +shared.sell=ขาย +shared.buying=การซื้อ +shared.selling=การขาย +shared.P2P=P2P +shared.oneOffer=เสนอ +shared.multipleOffers=ข้อเสนอ +shared.Offer=การเสนอ +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=เปิดข้อเสนอ +shared.trade=ซื้อขาย +shared.trades=การซื้อขายแลกเปลี่ยน +shared.openTrades=เปิดตลาดการซื้อขาย +shared.dateTime=วันที่/เวลา +shared.price=ราคา +shared.priceWithCur=ราคาใน {0} +shared.priceInCurForCur=ราคาใน {0} แก่ 1 {1} +shared.fixedPriceInCurForCur=ราคาคงที่ ใน {0} แก่ 1 {1} +shared.amount=จำนวน +shared.txFee=Transaction Fee +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Buyer Deposit +shared.sellerSecurityDeposit=Seller Deposit +shared.amountWithCur=จำนวนใน {0} +shared.volumeWithCur=ปริมาณการซื้อขายใน {0} +shared.currency=เงินตรา +shared.market=ตลาด +shared.deviation=Deviation +shared.paymentMethod=วิธีการชำระเงิน +shared.tradeCurrency=สกุลเงินตราการค้า +shared.offerType=ประเภทข้อเสนอ +shared.details=รายละเอียด +shared.address=ที่อยู่ +shared.balanceWithCur=ยอดคงเหลือใน {0} +shared.utxo=Unspent transaction output +shared.txId=เลขอ้างอิงการทำธุรกรรม +shared.confirmations=การยืนยัน +shared.revert=ย้อนกลับสู่การทำธุรกรรม +shared.select=เลือก +shared.usage=การใช้งาน +shared.state=สถานะ +shared.tradeId=ID ทางการค้า +shared.offerId=ID ข้อเสนอ +shared.bankName=ชื่อธนาคาร +shared.acceptedBanks=ธนาคารที่ได้รับการยอมรับ +shared.amountMinMax=ยอดจำนวน (ต่ำสุด-สูงสุด) +shared.amountHelp=หากข้อเสนอนั้นถูกจัดอยู่ในระดับเซ็ทขั้นต่ำและสูงสุด คุณสามารถซื้อขายได้ทุกช่วงระดับของจำนวนที่มีอยู่ +shared.remove=ลบออก +shared.goTo=ไปที่ {0} +shared.BTCMinMax=BTC (ต่ำสุด-สูงสุด) +shared.removeOffer=ลบข้อเสนอ +shared.dontRemoveOffer=ห้ามลบข้อเสนอ +shared.editOffer=แก้ไขข้อเสนอ +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=บัญชีการซื้อขาย +shared.faq=Visit FAQ page +shared.yesCancel=ใช่ ยกเลิก +shared.nextStep=ขั้นถัดไป +shared.selectTradingAccount=เลือกบัญชีการซื้อขาย +shared.fundFromSavingsWalletButton=โอนเงินจาก Bisq wallet +shared.fundFromExternalWalletButton=เริ่มทำการระดมเงินทุนหาแหล่งเงินจากกระเป๋าสตางค์ภายนอกของคุณ +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=ต่ำกว่า % จากราคาตลาด +shared.aboveInPercent=สูงกว่า % จากราคาตาด +shared.enterPercentageValue=เข้าสู่ % ตามมูลค่า +shared.OR=หรือ +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=กำลังรอเงิน ... +shared.depositTransactionId=รหัสธุรกรรมการฝากเงิน (transaction ID) +shared.TheBTCBuyer=ผู้ซื้อ BTC +shared.You=คุณ +shared.sendingConfirmation=กำลังส่งการยืนยัน ... +shared.sendingConfirmationAgain=โปรดยืนยันการส่งอีกครั้ง +shared.exportCSV=Export to CSV +shared.exportJSON=Export to JSON +shared.summary=Show summary +shared.noDateAvailable=ไม่มีวันที่ให้แสดง +shared.noDetailsAvailable=ไม่มีรายละเอียด +shared.notUsedYet=ยังไม่ได้ใช้งาน +shared.date=วันที่ +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.copyToClipboard=คัดลอกไปที่คลิปบอร์ด +shared.language=ภาษา +shared.country=ประเทศ +shared.applyAndShutDown=ใช้และปิดใช้งาน +shared.selectPaymentMethod=เลือกวิธีการชำระเงิน +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=คุณต้องการลบบัญชีที่เลือกหรือไม่ +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=บัญชียังไม่ได้มีการตั้งค่าใดๆ +shared.manageAccounts=จัดการบัญชี +shared.addNewAccount=เพิ่มบัญชีใหม่ +shared.ExportAccounts=บัญชีส่งออก +shared.importAccounts=บัญชีนำเข้า +shared.createNewAccount=สร้างบัญชีใหม่ +shared.saveNewAccount=บันทึกบัญชีใหม่ +shared.selectedAccount=บัญชีที่เลือก +shared.deleteAccount=ลบบัญชี +shared.errorMessageInline=\nเกิดข้อผิดพลาด: {0} +shared.errorMessage=เกิดข้อผิดพลาด +shared.information=ข้อมูล +shared.name=ชื่อ +shared.id=ID +shared.dashboard=Dashboard (หน้าแสดงผลรวม) +shared.accept=ยอมรับ +shared.balance=คงเหลือ +shared.save=บันทึก +shared.onionAddress=ที่อยู่ onion +shared.supportTicket=ศูนย์ช่วยเหลือ +shared.dispute=ข้อพิพาท +shared.mediationCase=mediation case +shared.seller=ผู้ขาย +shared.buyer=ผู้ซื้อ +shared.allEuroCountries=ทุกประเทศในทวีปยูโร +shared.acceptedTakerCountries=ประเทศที่รับการยอมรับ +shared.tradePrice=ราคาการซื้อขาย +shared.tradeAmount=ยอดจำนวนการซื้อขาย +shared.tradeVolume=ปริมาณการซื้อขาย +shared.invalidKey=คีย์ที่คุณป้อนไม่ถูกต้อง +shared.enterPrivKey=ป้อนคีย์ส่วนตัวเพื่อปลดล็อก +shared.makerFeeTxId=ID ธุรกรรมของผู้ทำ +shared.takerFeeTxId=ID การทำธุรกรรมของผู้รับ +shared.payoutTxId=ID ธุรกรรมการชำระเงิน +shared.contractAsJson=สัญญาในรูปแบบ JSON +shared.viewContractAsJson=ดูสัญญาในรูปแบบ JSON: +shared.contract.title=สัญญาการซื้อขายด้วยรหัส ID: {0} +shared.paymentDetails=BTC {0} รายละเอียดการชำระเงิน +shared.securityDeposit=เงินประกัน +shared.yourSecurityDeposit=เงินประกันของคุณ +shared.contract=สัญญา +shared.messageArrived=มีข้อความเข้าแล้ว +shared.messageStoredInMailbox=ข้อความถูกเก็บไว้ในกล่องจดหมาย +shared.messageSendingFailed=การส่งข้อความล้มเหลว เกิดข้อผิดพลาด: {0} +shared.unlock=ปลดล็อค +shared.toReceive=รับ +shared.toSpend=จ่าย +shared.btcAmount=BTC ยอดจำนวน +shared.yourLanguage=ภาษาของคุณ +shared.addLanguage=เพิ่มภาษา +shared.total=ยอดทั้งหมด +shared.totalsNeeded=เงินที่จำเป็น +shared.tradeWalletAddress=ที่อยู่ Trade wallet +shared.tradeWalletBalance=ยอดคงเหลือของ Trade wallet +shared.makerTxFee=ผู้ทำ: {0} +shared.takerTxFee=ผู้รับ: {0} +shared.iConfirm=ฉันยืนยัน +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=เปิด {0} +shared.fiat=คำสั่ง +shared.crypto=คริปโต +shared.all=ทั้งหมด +shared.edit=แก้ไข +shared.advancedOptions=ทางเลือกขั้นสูง +shared.interval=ระยะห่าง +shared.actions=การปฏิบัติการ +shared.buyerUpperCase=ผู้ซื้อ +shared.sellerUpperCase=ผู้ขาย +shared.new=NEW +shared.blindVoteTxId=ID การทำธุรกรรมการลงคะแนนเสียงแบบไม่ระบุตัวตน +shared.proposal=คำขอ +shared.votes=โหวต +shared.learnMore=Learn more +shared.dismiss=Dismiss +shared.selectedArbitrator=ผู้ไกล่เกลี่ยที่ได้รับการแต่งตั้ง +shared.selectedMediator=Selected mediator +shared.selectedRefundAgent=ผู้ไกล่เกลี่ยที่ได้รับการแต่งตั้ง +shared.mediator=ผู้ไกล่เกลี่ย +shared.arbitrator=ผู้ไกล่เกลี่ย +shared.refundAgent=ผู้ไกล่เกลี่ย +shared.refundAgentForSupportStaff=Refund agent +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=ตลาด +mainView.menu.buyBtc=ซื้อ BTC +mainView.menu.sellBtc=ขาย BTC +mainView.menu.portfolio=แฟ้มผลงาน +mainView.menu.funds=เงิน +mainView.menu.support=สนับสนุน +mainView.menu.settings=ตั้งค่า +mainView.menu.account=บัญชี +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=ราคาตลาดโดย {0} +mainView.marketPrice.bisqInternalPrice=ราคาของการซื้อขาย Bisq ล่าสุด +mainView.marketPrice.tooltip.bisqInternalPrice=ไม่มีราคาตลาดจากผู้ให้บริการด้านราคาภายนอก\nราคาที่แสดงเป็นราคาล่าสุดของ Bisq สำหรับสกุลเงินนั้น +mainView.marketPrice.tooltip=ราคาตลาดจัดทำโดย {0} {1} \nอัปเดตล่าสุด: {2} \nnode URL ของผู้ให้บริการ: {3} +mainView.balance.available=ยอดคงเหลือที่พร้อมใช้งาน +mainView.balance.reserved=ข้อเสนอได้รับการจองแล้ว +mainView.balance.locked=ล็อคในการซื้อขาย +mainView.balance.reserved.short=จองแล้ว +mainView.balance.locked.short=ถูกล็อคไว้ + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(แม่ข่ายเฉพาะที่) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Connecting to Bitcoin network +mainView.footer.bsqInfo.synchronizing=/ Synchronizing DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Connecting to +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=DAO full node + +mainView.bootstrapState.connectionToTorNetwork=(1/4) เชื่อมต่อไปยัง Tor network... +mainView.bootstrapState.torNodeCreated=(2/4) Tor node ถูกสร้างแล้ว +mainView.bootstrapState.hiddenServicePublished=(3/4) บริการที่ซ่อนอยู่ถูกเผยแพร่ +mainView.bootstrapState.initialDataReceived=(4/4) ได้รับข้อมูลเบื้องต้นเรียบร้อย + +mainView.bootstrapWarning.noSeedNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายให้ใช้งาน +mainView.bootstrapWarning.noNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายและpeers(ระบบเพียร์)ให้ใช้งาน +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Bisq network failed + +mainView.p2pNetworkWarnMsg.noNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายและ peers (ระบบเพียร์) พร้อมให้บริการสำหรับการขอข้อมูล\nโปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือลองรีสตาร์ทแอพพลิเคชัน +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Bisq network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. + +mainView.walletServiceErrorMsg.timeout=การเชื่อมต่อกับเครือข่าย Bitcoin ล้มเหลวเนื่องจากหมดเวลา +mainView.walletServiceErrorMsg.connectionError=การเชื่อมต่อกับเครือข่าย Bitcoin ล้มเหลวเนื่องจากข้อผิดพลาด: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} + +mainView.networkWarning.allConnectionsLost=คุณสูญเสียการเชื่อมต่อกับ {0} เครือข่าย peers\nบางทีคุณอาจขาดการเชื่อมต่ออินเทอร์เน็ตหรืออาจเป็นเพราะคอมพิวเตอร์ของคุณอยู่ในโหมดสแตนด์บาย +mainView.networkWarning.localhostBitcoinLost=คุณสูญเสียการเชื่อมต่อไปยังโหนดเครือข่าย Bitcoin localhost (แม่ข่ายเฉพาะที่)\nโปรดรีสตาร์ทแอ็พพลิเคชัน Bisq เพื่อเชื่อมต่อโหนด Bitcoin อื่นหรือรีสตาร์ทโหนด Bitcoin localhost +mainView.version.update=(การอัพเดตพร้อมใช้งาน) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=การจองข้อเสนอ +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=การซื้อขาย + +# OfferBookChartView +market.offerBook.buyAltcoin=ซื้อ {0} (ขาย {1}) +market.offerBook.sellAltcoin=ขาย {0} (ซื้อ {1}) +market.offerBook.buyWithFiat=ซื้อ {0} +market.offerBook.sellWithFiat=ขาย {0} +market.offerBook.sellOffersHeaderLabel=ขาย {0} ไปยัง +market.offerBook.buyOffersHeaderLabel=ซื้อ {0} จาก +market.offerBook.buy=ฉันต้องการจะซื้อ bitcoin +market.offerBook.sell=ฉันต้องการจะขาย bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=ข้อเสนอทั้งหมด ({0}) +market.spread.numberOfBuyOffersColumn=ซื้อ BTC ({0}) +market.spread.numberOfSellOffersColumn=ขาย BTC ({0}) +market.spread.totalAmountColumn=ยอด BTC ทั้งหมด ({0}) +market.spread.spreadColumn=กระจาย +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=การซื้อขาย: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=เปิด: +market.trades.tooltip.candle.close=ปิด: +market.trades.tooltip.candle.high=สูง: +market.trades.tooltip.candle.low=ต่ำ: +market.trades.tooltip.candle.average=เฉลี่ย: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=วันที่: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=สร้างข้อเสนอ +offerbook.takeOffer=รับข้อเสนอ +offerbook.takeOfferToBuy=Take offer to buy {0} +offerbook.takeOfferToSell=Take offer to sell {0} +offerbook.trader=Trader (เทรดเดอร์) +offerbook.offerersBankId=รหัสธนาคารของผู้สร้าง (BIC / SWIFT): {0} +offerbook.offerersBankName=ชื่อธนาคารของผู้สร้าง: {0} +offerbook.offerersBankSeat=ตำแหน่งประเทศของธนาคารของผู้สร้าง: {0} +offerbook.offerersAcceptedBankSeatsEuro=ยอมรับตำแหน่งประเทศของธนาคาร (ผู้รับ): ทุกประเทศในทวีปยูโร +offerbook.offerersAcceptedBankSeats=ยอมรับตำแหน่งประเทศของธนาคาร (ผู้รับ):\n {0} +offerbook.availableOffers=ข้อเสนอที่พร้อมใช้งาน +offerbook.filterByCurrency=กรองตามสกุลเงิน +offerbook.filterByPaymentMethod=ตัวกรองตามวิธีการชำระเงิน +offerbook.matchingOffers=Offers matching my accounts +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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) +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.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.ageDays={0} วัน +offerbook.timeSinceSigning.notSigned.noNeed=ไม่พร้อมใช้งาน +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=No. ของข้อเสนอ: {0} +offerbook.volume={0} (ต่ำสุด - สูงสุด) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Create new offer to buy {0} +offerbook.createOfferToSell=Create new offer to sell {0} +offerbook.createOfferToBuy.withFiat=Create new offer to buy {0} with {1} +offerbook.createOfferToSell.forFiat=Create new offer to sell {0} for {1} +offerbook.createOfferToBuy.withCrypto=Create new offer to sell {0} (buy {1}) +offerbook.createOfferToSell.forCrypto=Create new offer to buy {0} (sell {1}) + +offerbook.takeOfferButton.tooltip=รับข้อเสนอเพื่อ {0} +offerbook.yesCreateOffer=ใช่ สร้างข้อเสนอ +offerbook.setupNewAccount=ตั้งค่าบัญชีการซื้อขายใหม่ +offerbook.removeOffer.success=นำข้อเสนอออกเรียบร้อยแล้ว +offerbook.removeOffer.failed=เกิดข้อผิดพลาดในการลบข้อเสนอ:\n{0} +offerbook.deactivateOffer.failed=เกิดข้อผิดพลาดในการยกเลิกข้อเสนอ: \n{0} +offerbook.activateOffer.failed=การเผยแพร่ข้อเสนอล้มเหลว: \n{0} +offerbook.withdrawFundsHint=คุณสามารถถอนเงินที่คุณชำระมาได้จาก {0} หน้าจอ + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} + +offerbook.warning.wrongTradeProtocol=ข้อเสนอดังกล่าวต้องใช้โปรโตคอลเวอร์ชันอื่นเหมือนกับเวอร์ชันที่ใช้ในซอฟต์แวร์เวอร์ชันของคุณ\n\nโปรดตรวจสอบว่าคุณได้ติดตั้งเวอร์ชั่นล่าสุด อีกนัยหนึ่งผู้ใช้ที่สร้างข้อเสนอได้ใช้รุ่นที่เก่ากว่า\n\nผู้ใช้ไม่สามารถซื้อขายกับโปรโตคอลการค้าเวอร์ชั่นซอฟต์แวร์ที่แตกต่างกันได้ +offerbook.warning.userIgnored=คุณได้เพิ่มที่อยู่ onion ของผู้ใช้ลงในรายการที่ไม่สนใจแล้ว +offerbook.warning.offerBlocked=ข้อเสนอดังกล่าวถูกบล็อกโดยนักพัฒนาซอฟต์แวร์ Bisq\nอาจมีข้อบกพร่องที่ไม่ได้รับการจัดการซึ่งก่อให้เกิดปัญหาเมื่อมีข้อเสนอนั้น +offerbook.warning.currencyBanned=สกุลเงินที่ใช้ในข้อเสนอนั้นถูกบล็อกโดยนักพัฒนา Bisq\nสามารถอ่านข้อมูลเพิ่มเติมได้ที่ฟอรั่มของ Bisq +offerbook.warning.paymentMethodBanned=วิธีการชำระเงินที่ใช้ในข้อเสนอนั้นถูกบล็อกโดยนักพัฒนา Bisq\nกรุณาเข้าไปอ่านที่ Forum ของ Bisq สำหรับข้อมูลเพิ่มเติม +offerbook.warning.nodeBlocked=ที่อยู่ onion ของผู้ซื้อขายรายนั้นถูกบล็อกโดยนักพัฒนา Bisq\nอาจมีข้อบกพร่องที่ไม่ได้รับการจัดการ ซึ่งก่อให้เกิดปัญหาเมื่อรับข้อเสนอจากผู้ซื้อขายรายนั้น +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=คุณจะขายในราคาตลาด (อัปเดตทุกนาที) +offerbook.info.buyAtMarketPrice=คุณจะซื้อในราคาตลาด (อัปเดตทุกนาที) +offerbook.info.sellBelowMarketPrice=คุณจะได้รับ {0} น้อยกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) +offerbook.info.buyAboveMarketPrice=คุณจะจ่าย {0} มากกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) +offerbook.info.sellAboveMarketPrice=คุณจะได้รับ {0} มากกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) +offerbook.info.buyBelowMarketPrice=คุณจะจ่าย {0} น้อยกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) +offerbook.info.buyAtFixedPrice=คุณจะซื้อในราคาที่ถูกกำหนดไว้ +offerbook.info.sellAtFixedPrice=คุณจะขายในราคาที่ถูกกำหนดไว้ +offerbook.info.noArbitrationInUserLanguage=ในกรณีที่มีข้อพิพาท โปรดทราบว่ากระบวนการไกล่เกลี่ยสำหรับข้อเสนอนี้จะได้รับการจัดการ {0} ภาษาที่มีการตั้งค่าในปัจจุบัน {1} +offerbook.info.roundedFiatVolume=จำนวนเงินจะปัดเศษเพื่อเพิ่มความเป็นส่วนตัวในการค้าของคุณ + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=ป้อนจำนวนเงินใน BTC +createOffer.price.prompt=ป้อนราคา +createOffer.volume.prompt=ป้อนจำนวนเงินใน {0} +createOffer.amountPriceBox.amountDescription=ยอดจำนวน BTC ถึง {0} +createOffer.amountPriceBox.buy.volumeDescription=ยอดจำนวน {0} ที่ต้องจ่าย +createOffer.amountPriceBox.sell.volumeDescription=จำนวนเงิน {0} ที่ได้รับ +createOffer.amountPriceBox.minAmountDescription=จำนวนเงินขั้นต่ำของ BTC +createOffer.securityDeposit.prompt=เงินประกัน +createOffer.fundsBox.title=เงินทุนสำหรับข้อเสนอของคุณ +createOffer.fundsBox.offerFee=ค่าธรรมเนียมการซื้อขาย +createOffer.fundsBox.networkFee=ค่าธรรมเนียมการขุด +createOffer.fundsBox.placeOfferSpinnerInfo=การประกาศข้อเสนออยู่ระหว่างดำเนินการ ... +createOffer.fundsBox.paymentLabel=การซื้อขาย Bisq ด้วย ID {0} +createOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) +createOffer.fundsBox.fundsStructure.BSQ=({0} security deposit, {1} mining fee) + {2} trade fee +createOffer.success.headline=ข้อเสนอของคุณได้รับการเผยแพร่แล้ว +createOffer.success.info=คุณสามารถจัดการข้อเสนอแบบเปิดของคุณได้ที่ \"Portfolio (แฟ้มผลงาน) / My open offers (ข้อเสนอแบบเปิดของฉัน) \" +createOffer.info.sellAtMarketPrice=คุณจะขายในราคาตลาดเสมอ เนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.info.buyAtMarketPrice=คุณจะซื้อในราคาตลาดเสมอ เนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.info.sellAboveMarketPrice=คุณจะได้รับ {0}% มากกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.info.buyBelowMarketPrice=คุณจะจ่าย {0}% น้อยกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.warning.sellBelowMarketPrice=คุณจะได้รับ {0}% น้อยกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.warning.buyAboveMarketPrice=คุณจะต้องจ่ายเงิน {0}% มากกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง +createOffer.tradeFee.descriptionBTCOnly=ค่าธรรมเนียมการซื้อขาย +createOffer.tradeFee.descriptionBSQEnabled=เลือกสกุลเงินค่าธรรมเนียมในการเทรด + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=รีวิว: ใส่ข้อเสนอไปยัง {0} บิตคอย +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- ใช้กระเป๋าสตางค์ Bisq ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากเงินภายนอกเข้ามา (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการระดมทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ + +# 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โปรดเริ่มแอปพลิเคชันใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณ +createOffer.setAmountPrice=กำหนดจำนวนและราคา +createOffer.warnCancelOffer=คุณได้รับเงินจากข้อเสนอนั้นแล้ว\nหากคุณยกเลิกตอนนี้เงินของคุณจะถูกย้ายไปที่กระเป๋าสตางค์ Bisq ในประเทศของคุณและพร้อมสำหรับการถอนเงินโดยไปที่หน้า \"เงิน / ส่งเงิน \" \nคุณแน่ใจหรือไม่ว่าต้องการยกเลิก +createOffer.timeoutAtPublishing=มีกำหนดเวลาในการเผยแพร่ข้อเสนอ +createOffer.errorInfo=\n\nมีการชำระค่าธรรมเนียมผู้สร้างแล้ว ในกรณีที่คุณต้องสูญเสียค่าธรรมเนียมนั้นไป\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ +createOffer.tooLowSecDeposit.warning=คุณได้ตั้งค่าเงินประกันเป็นค่าต่ำกว่าค่าเริ่มต้นที่แนะนำไว้ที่ {0} \nคุณแน่ใจหรือไม่ว่าต้องการใช้เงินประกันที่ต่ำกว่า +createOffer.tooLowSecDeposit.makerIsSeller=มันทำให้คุณได้รับความคุ้มครองน้อยลงในกรณีที่ผู้ค้าไม่ปฏิบัติตามโปรโตคอลทางการซื้อขาย +createOffer.tooLowSecDeposit.makerIsBuyer=จะให้การคุ้มครองที่น้อยกว่าสำหรับผู้ซื้อขายที่ทำตามโปรโตคอลการค้าเนื่องจากคุณมีเงินฝากน้อยลง ผู้ใช้รายอื่นอาจต้องการรับข้อเสนอจากที่อื่นมากกว่าของคุณ +createOffer.resetToDefault=ไม่ รีเซ็ตเป็นค่าเริ่มต้น +createOffer.useLowerValue=ใช่ ใช้ค่าต่ำกว่าของฉัน +createOffer.priceOutSideOfDeviation=ราคาที่คุณป้อนอยู่เกินออกจากส่วนเบี่ยงเบนที่ได้รับอนุญาตจากราคาตลาด\nค่าเบี่ยงเบนสูงสุดที่อนุญาตคือ {0} และสามารถปรับได้ตามความต้องการ +createOffer.changePrice=เปลี่ยนราคา +createOffer.tac=ด้วยการเผยแพร่ข้อเสนอพิเศษนี้ ฉันยอมรับการซื้อขายกับผู้ค้ารายย่อยที่ปฏิบัติตามเงื่อนไขที่กำหนดไว้บนหน้าจอนี้ +createOffer.currencyForFee=ค่าธรรมเนียมการซื้อขาย +createOffer.setDeposit=Set buyer's security deposit (%) +createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) +createOffer.setDepositForBothTraders=Set both traders' security deposit (%) +createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} +createOffer.minSecurityDepositUsed=Min. buyer security deposit is used + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=ป้อนจำนวนเงินใน BTC +takeOffer.amountPriceBox.buy.amountDescription=จำนวน BTC ที่จะขาย +takeOffer.amountPriceBox.sell.amountDescription=จำนวน BTC ที่จะซื้อ +takeOffer.amountPriceBox.priceDescription=ราคาต่อ bitcoin ใน {0} +takeOffer.amountPriceBox.amountRangeDescription=ช่วงจำนวนที่เป็นไปได้ +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=จำนวนเงินที่คุณป้อนเกินจำนวนตำแหน่งทศนิยมที่อนุญาต\nจำนวนเงินได้รับการปรับเป็นตำแหน่งทศนิยม 4 ตำแหน่ง +takeOffer.validation.amountSmallerThanMinAmount=จำนวนเงินต้องไม่น้อยกว่าจำนวนเงินขั้นต่ำที่ระบุไว้ในข้อเสนอ +takeOffer.validation.amountLargerThanOfferAmount=จำนวนเงินที่ป้อนต้องไม่สูงกว่าจำนวนที่กำหนดไว้ในข้อเสนอ +takeOffer.validation.amountLargerThanOfferAmountMinusFee=จำนวนเงินที่ป้อนจะสร้างการเปลี่ยนแปลง dust (Bitcoin ที่มีขนาดเล็กมาก) สำหรับผู้ขาย BTC +takeOffer.fundsBox.title=ทุนการซื้อขายของคุณ +takeOffer.fundsBox.isOfferAvailable=ตรวจสอบว่ามีข้อเสนออื่นๆหรือไม่ ... +takeOffer.fundsBox.tradeAmount=จำนวนที่จะขาย +takeOffer.fundsBox.offerFee=ค่าธรรมเนียมการซื้อขาย +takeOffer.fundsBox.networkFee=ยอดรวมค่าธรรมเนียมการขุด +takeOffer.fundsBox.takeOfferSpinnerInfo=การรับข้อเสนออยู่ระหว่างการดำเนินการ... +takeOffer.fundsBox.paymentLabel=การซื้อขาย Bisq ด้วย ID {0} +takeOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) +takeOffer.success.headline=คุณได้รับข้อเสนอเป็นที่เรีบยร้อยแล้ว +takeOffer.success.info=คุณสามารถดูสถานะการค้าของคุณได้ที่ \ "Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" +takeOffer.error.message=เกิดข้อผิดพลาดขณะรับข้อเสนอ\n\n{0} + +# new entries +takeOffer.takeOfferButton=รีวิว: รับข้อเสนอจาก {0} bitcoin +takeOffer.noPriceFeedAvailable=คุณไม่สามารถรับข้อเสนอดังกล่าวเนื่องจากใช้ราคาร้อยละตามราคาตลาด แต่ไม่มีฟีดราคาที่พร้อมใช้งาน +takeOffer.takeOfferFundWalletInfo.headline=ทุนการซื้อขายของคุณ +# suppress inspection "TrailingSpacesInProperty" +takeOffer.takeOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} +takeOffer.takeOfferFundWalletInfo.msg=คุณต้องวางเงินประกัน {0} เพื่อรับข้อเสนอนี้\n\nจำนวนเงินคือผลรวมของ: \n{1} - เงินประกันของคุณ: {2} \n- ค่าธรรมเนียมการซื้อขาย: {3} \n- ค่าธรรมเนียมการขุดทั้งหมด: {4} \n\nคุณสามารถเลือกระหว่างสองตัวเลือกเมื่อลงทุนการซื้อขายของคุณ: \n- ใช้กระเป๋าสตางค์ Bisq ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากแหล่งเงินภายนอก (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการลงทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ +takeOffer.alreadyPaidInFunds=หากคุณได้ชำระเงินแล้วคุณสามารถถอนเงินออกได้ในหน้าจอ \"เงิน / ส่งเงิน \" +takeOffer.paymentInfo=ข้อมูลการชำระเงิน +takeOffer.setAmountPrice=ตั้งยอดจำนวน +takeOffer.alreadyFunded.askCancel=คุณได้รับเงินจากข้อเสนอนั้นแล้ว\nหากคุณยกเลิกตอนนี้เงินของคุณจะถูกย้ายไปที่กระเป๋าสตางค์ Bisq ในประเทศของคุณและพร้อมสำหรับการถอนเงินโดยไปที่หน้า \"เงิน / ส่งเงิน \"\nคุณแน่ใจหรือไม่ว่าต้องการยกเลิก +takeOffer.failed.offerNotAvailable=การขอข้อเสนอล้มเหลวเนื่องจากข้อเสนอไม่พร้อมใช้งานอีกต่อไป บางทีผู้ค้ารายอื่นอาจรับข้อเสนอนี้ไปแล้ว +takeOffer.failed.offerTaken=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากข้อเสนอนี้ได้ถูกดำเนินการโดยผู้ค้ารายอื่นแล้ว +takeOffer.failed.offerRemoved=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากข้อเสนอถูกลบออกไปแล้ว +takeOffer.failed.offererNotOnline=คำขอข้อเสนอล้มเหลว เนื่องจากผู้สร้างไม่ได้ออนไลน์อยู่ในระบบ +takeOffer.failed.offererOffline=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากผู้สร้างออฟไลน์ +takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. + +takeOffer.error.noFundsLost=\n\nยังไม่มีเงินเหลือ wallet อยู่เลย\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nธุรกรรมเงินฝากได้รับการเผยแพร่เป็นที่เรียบร้อยแล้ว\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่?\nหากปัญหายังคงอยู่โปรดติดต่อนักพัฒนาซอฟต์แวร์เพื่อขอความช่วยเหลือ +takeOffer.error.payoutPublished=\n\nมีการเผยแพร่รายการการชำระแล้ว\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่?\nหากปัญหายังคงอยู่โปรดติดต่อนักพัฒนาซอฟต์แวร์เพื่อขอความช่วยเหลือ +takeOffer.tac=ด้วยข้อเสนอนี้ฉันยอมรับเงื่อนไขทางการค้าตามที่กำหนดไว้ในหน้าจอนี้ + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=ราคาเงื่อนไขที่ตั้งไว้ +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=ตั้งราคา +editOffer.confirmEdit=ยืนยัน: แก้ไขข้อเสนอ +editOffer.publishOffer=กำลังเผยแพร่ข้อเสนอของคุณ +editOffer.failed=การแก้ไขข้อเสนอล้มเหลว: \n{0} +editOffer.success=ข้อเสนอของคุณได้รับการแก้ไขเรียบร้อยแล้ว +editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by the Bisq DAO and can no longer be edited. + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=ข้อเสนอแบบเปิดของฉัน +portfolio.tab.pendingTrades=เปิดการซื้อขาย +portfolio.tab.history=ประวัติ +portfolio.tab.failed=ผิดพลาด +portfolio.tab.editOpenOffer=แก้ไขข้อเสนอ + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=รอการยืนยันของบล็อกเชน +portfolio.pending.step2_buyer.startPayment=เริ่มการชำระเงิน +portfolio.pending.step2_seller.waitPaymentStarted=รอจนกว่าการชำระเงินจะเริ่มขึ้น +portfolio.pending.step3_buyer.waitPaymentArrived=รอจนกว่าจะถึงการชำระเงิน +portfolio.pending.step3_seller.confirmPaymentReceived=การยืนยันการชำระเงินที่ได้รับ +portfolio.pending.step5.completed=เสร็จสิ้น + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=โปรดโอนจาก wallet {0} ภายนอก\n{1} ให้กับผู้ขาย BTC\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=โปรดไปที่ธนาคารและจ่ายเงิน {0} ให้กับผู้ขาย BTC\n +portfolio.pending.step2_buyer.cash.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้เขียนลงในใบเสร็จรับเงิน: NO REFUNDS (ไม่มีการคืนเงิน)\nจากนั้นแบ่งออกเป็น 2 ส่วนถ่ายรูปและส่งไปที่ที่อยู่อีเมลของผู้ขาย BTC +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=โปรดชำระเงิน {0} ให้กับผู้ขาย BTC โดยใช้ MoneyGram\n +portfolio.pending.step2_buyer.moneyGram.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้ส่งหมายเลข Authorization (การอนุมัติ) และรูปใบเสร็จรับเงินไปยังผู้ขาย BTC ทางอีเมล\nใบเสร็จจะต้องแสดงชื่อเต็มของผู้ขาย ประเทศ รัฐ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=โปรดชำระเงิน {0} ให้กับผู้ขาย BTC โดยใช้ Western Union +portfolio.pending.step2_buyer.westernUnion.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้ส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินไปยังผู้ขาย BTC ทางอีเมล\nใบเสร็จจะต้องแสดงชื่อเต็ม เมือง ประเทศ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธนาณัติ \"US Postal Money Order \" ไปยังผู้ขาย BTC\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=กรุณาติดต่อผู้ขายของ BTC ตามรายชื่อที่ได้รับและนัดประชุมเพื่อจ่ายเงิน {0}\n\n +portfolio.pending.step2_buyer.startPaymentUsing=เริ่มต้นการชำระเงินโดยใช้ {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=จำนวนเงินที่จะโอน +portfolio.pending.step2_buyer.sellersAddress=ที่อยู่ของผู้ขาย {0} +portfolio.pending.step2_buyer.buyerAccount=บัญชีการชำระเงินที่ต้องการใข้งาน +portfolio.pending.step2_buyer.paymentStarted=การชำระเงินเริ่มต้นแล้ว +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. +portfolio.pending.step2_buyer.paperReceipt.headline=คุณได้ส่งใบเสร็จรับเงินให้กับผู้ขาย BTC หรือไม่? +portfolio.pending.step2_buyer.paperReceipt.msg=ข้อควรจำ: \nคุณต้องเขียนลงในใบเสร็จรับเงิน: NO REFUNDS (ไม่มีการคืนเงิน)\nจากนั้นแบ่งออกเป็น 2 ส่วนถ่ายรูปและส่งไปที่ที่อยู่อีเมลของผู้ขาย BTC +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=ส่งหมายเลขการอนุมัติและใบเสร็จรับเงิน +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=คุณต้องส่งหมายเลขการอนุมัติและรูปใบเสร็จรับเงินทางอีเมลไปยังผู้ขาย BTC \nใบเสร็จจะต้องแสดงชื่อเต็มของประเทศ รัฐ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} .\n\nคุณได้ส่งหมายเลขการอนุมัติและทำสัญญากับผู้ขายหรือไม่?\n +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=ส่ง MTCN (หมายเลขติดตาม) และใบเสร็จรับเงิน +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=คุณต้องส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินทางอีเมลไปยังผู้ขาย BTC \nใบเสร็จจะต้องแสดงชื่อเต็ม เมือง ประเทศ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} .\n\nคุณได้ส่ง MTCN และทำสัญญากับผู้ขายหรือไม่ +portfolio.pending.step2_buyer.halCashInfo.headline=ส่งรหัส HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=คุณต้องส่งข้อความที่มีรหัส HalCash พร้อมกับ IDการค้า ({0}) ไปยังผู้ขาย BTC \nเบอร์โทรศัพท์มือถือของผู้ขาย คือ {1}\n\nคุณได้ส่งรหัสให้กับผู้ขายหรือยัง? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Bisq clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=ยืนยันว่าคุณได้เริ่มต้นการชำระเงินแล้ว +portfolio.pending.step2_buyer.confirmStart.msg=คุณได้เริ่มต้นการ {0} การชำระเงินให้กับคู่ค้าของคุณแล้วหรือยัง +portfolio.pending.step2_buyer.confirmStart.yes=ใช่ฉันได้เริ่มต้นการชำระเงินแล้ว +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=รอการชำระเงิน +portfolio.pending.step2_seller.f2fInfo.headline=ข้อมูลการติดต่อของผู้ซื้อ +portfolio.pending.step2_seller.waitPayment.msg=ธุรกรรมการฝากเงินมีการยืนยันบล็อกเชนอย่างน้อยหนึ่งรายการ\nคุณต้องรอจนกว่าผู้ซื้อ BTC จะเริ่มการชำระเงิน {0} +portfolio.pending.step2_seller.warn=ผู้ซื้อ BTC ยังไม่ได้ทำ {0} การชำระเงิน\nคุณต้องรอจนกว่าผู้ซื้อจะเริ่มชำระเงิน\nหากการซื้อขายยังไม่เสร็จสิ้นในวันที่ {1} ผู้ไกล่เกลี่ยจะดำเนินการตรวจสอบ +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. +tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Bisq (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +message.state.SENT=ข้อความที่ถูกส่ง +# suppress inspection "UnusedProperty" +message.state.ARRIVED=ข้อความถึง เน็ตเวิร์ก peer แล้ว +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=เน็ตเวิร์ก peer ยืนยันการรับข้อความแล้ว +# suppress inspection "UnusedProperty" +message.state.FAILED=การส่งข้อความล้มเหลว + +portfolio.pending.step3_buyer.wait.headline=รอการยืนยันการชำระเงินของผู้ขาย BTC +portfolio.pending.step3_buyer.wait.info=กำลังรอการยืนยันจากผู้ขาย BTC สำหรับการรับ {0} การชำระเงิน +portfolio.pending.step3_buyer.wait.msgStateInfo.label=เริ่มต้นสถานะการชำระเงิน +portfolio.pending.step3_buyer.warn.part1a=ใน {0} บล็อกเชน +portfolio.pending.step3_buyer.warn.part1b=ที่ผู้ให้บริการการชำระเงิน (เช่น ธนาคาร) +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=พันธมิตรทางการค้าของคุณได้ยืนยันว่าพวกเขาได้เริ่มต้น {0} การชำระเงิน\n\n +portfolio.pending.step3_seller.altcoin.explorer=ผู้สำรวจบล็อกเชน {0} ที่ถูกใจของคุณ +portfolio.pending.step3_seller.altcoin.wallet=ณ กระเป๋าสตางค์ {0} ของคุณ +portfolio.pending.step3_seller.altcoin={0}โปรดตรวจสอบ {1} หากการทำธุรกรรมส่วนที่อยู่รับของคุณ\n{2}\nมีการยืนยันบล็อกเชนแล้วเรียบร้อย\nยอดการชำระเงินต้องเป็น {3}\n\nคุณสามารถคัดลอกและวาง {4} ข้อมูลที่อยู่ของคุณได้จากหน้าจอหลักหลังจากปิดหน้าต่างป๊อปอัพ +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=เนื่องจากการชำระเงินผ่าน Cash Deposit (ฝากเงินสด) ผู้ซื้อ BTC จะต้องเขียน \"NO REFUND \" ในใบเสร็จรับเงินและให้แบ่งออกเป็น 2 ส่วนและส่งรูปถ่ายทางอีเมล\n\nเพื่อหลีกเลี่ยงความเสี่ยงจากการปฏิเสธการชำระเงิน ให้ยืนยันเฉพาะถ้าคุณได้รับอีเมลและหากคุณแน่ใจว่าใบเสร็จถูกต้องแล้ว\nถ้าคุณไม่แน่ใจ {0} +portfolio.pending.step3_seller.moneyGram=ผู้ซื้อต้องส่งหมายเลขอนุมัติและรูปใบเสร็จรับเงินทางอีเมล\nใบเสร็จรับเงินต้องแสดงชื่อเต็มของคุณ ประเทศ รัฐ และจำนวนเงิน โปรดตรวจสอบอีเมลของคุณหากคุณได้รับหมายเลขการให้สิทธิ์\n\nหลังจากปิดป๊อปอัปคุณจะเห็นชื่อและที่อยู่ของผู้ซื้อ BTC เพื่อรับเงินจาก MoneyGram\n\nยืนยันเฉพาะใบเสร็จหลังจากที่คุณได้รับเงินเรียบร้อยแล้ว! +portfolio.pending.step3_seller.westernUnion=ผู้ซื้อต้องส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินทางอีเมล\nใบเสร็จรับเงินต้องแสดงชื่อ เมือง ประเทศ และจำนวนเงินทั้งหมดไว้อย่างชัดเจน โปรดตรวจสอบอีเมลของคุณหากคุณได้รับ MTCN\n\nหลังจากปิดป๊อปอัปคุณจะเห็นชื่อและที่อยู่ของผู้ซื้อ BTC สำหรับการขอรับเงินจาก Western Union \n\nยืนยันเฉพาะใบเสร็จหลังจากที่คุณได้รับเงินเรียบร้อยแล้ว! +portfolio.pending.step3_seller.halCash=ผู้ซื้อต้องส่งข้อความรหัส HalCash ให้คุณ ในขณะเดียวกันคุณจะได้รับข้อความจาก HalCash พร้อมกับคำขอข้อมูลจำเป็นในการถอนเงินยูโรุจากตู้เอทีเอ็มที่รองรับ HalCash \n\n หลังจากที่คุณได้รับเงินจากตู้เอทีเอ็มโปรดยืนยันใบเสร็จรับเงินจากการชำระเงินที่นี่ ! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=ใบเสร็จยืนยันการชำระเงิน +portfolio.pending.step3_seller.amountToReceive=จำนวนเงินที่ได้รับ +portfolio.pending.step3_seller.yourAddress=ที่อยู่ {0} ของคุณ +portfolio.pending.step3_seller.buyersAddress=ที่อยู่ {0} ผู้ซื้อ +portfolio.pending.step3_seller.yourAccount=บัญชีการซื้อขายของคุณ +portfolio.pending.step3_seller.xmrTxHash=เลขอ้างอิงการทำธุรกรรม +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=ใบเสร็จยืนยันการชำระเงิน +portfolio.pending.step3_seller.buyerStartedPayment=ผู้ซื้อ BTC ได้เริ่มการชำระเงิน {0}\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=ตรวจสอบการยืนยันบล็อกเชนที่ altcoin wallet ของคุณหรือบล็อก explorer และยืนยันการชำระเงินเมื่อคุณมีการยืนยันบล็อกเชนที่เพียงพอ +portfolio.pending.step3_seller.buyerStartedPayment.fiat=ตรวจสอบบัญชีการซื้อขายของคุณ (เช่น บัญชีธนาคาร) และยืนยันเมื่อคุณได้รับการชำระเงิน +portfolio.pending.step3_seller.warn.part1a=ใน {0} บล็อกเชน +portfolio.pending.step3_seller.warn.part1b=ที่ผู้ให้บริการการชำระเงิน (เช่น ธนาคาร) +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=คุณได้รับ {0} การชำระเงินจากคู่ค้าของคุณหรือไม่\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=ยืนยันว่าคุณได้รับการชำระเงินแล้ว +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=ใช่ ฉันได้รับการชำระเงินแล้ว +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. + +portfolio.pending.step5_buyer.groupTitle=ผลสรุปการซื้อขายที่เสร็จสิ้น +portfolio.pending.step5_buyer.tradeFee=ค่าธรรมเนียมการซื้อขาย +portfolio.pending.step5_buyer.makersMiningFee=ค่าธรรมเนียมการขุด +portfolio.pending.step5_buyer.takersMiningFee=ยอดรวมค่าธรรมเนียมการขุด +portfolio.pending.step5_buyer.refunded=เงินประกันความปลอดภัยที่ถูกคืน +portfolio.pending.step5_buyer.withdrawBTC=ถอนเงิน bitcoin ของคุณ +portfolio.pending.step5_buyer.amount=จำนวนเงินที่จะถอน +portfolio.pending.step5_buyer.withdrawToAddress=ถอนไปยังที่อยู่ +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=ถอนไปยัง wallet ภายนอก +portfolio.pending.step5_buyer.alreadyWithdrawn=เงินทุนของคุณถูกถอนออกไปแล้ว\nโปรดตรวจสอบประวัติการทำธุรกรรม +portfolio.pending.step5_buyer.confirmWithdrawal=ยืนยันคำขอถอนเงิน +portfolio.pending.step5_buyer.amountTooLow=จำนวนเงินที่โอนจะต่ำกว่าค่าธรรมเนียมการทำธุรกรรมและมูลค่าต่ำกว่าที่น่าจะเป็น (dust หน่วยเล็กสุดของ bitcoin) +portfolio.pending.step5_buyer.withdrawalCompleted.headline=การถอนเสร็จสิ้น +portfolio.pending.step5_buyer.withdrawalCompleted.msg=การซื้อขายที่เสร็จสิ้นของคุณจะถูกเก็บไว้ภายใต้ \"Portfolio (แฟ้มผลงาน) / ประวัติ\" \nคุณสามารถตรวจสอบการทำธุรกรรม Bitcoin ทั้งหมดภายใต้ \"เงิน / ธุรกรรม \" +portfolio.pending.step5_buyer.bought=คุณได้ซื้อ +portfolio.pending.step5_buyer.paid=คุณได้จ่าย + +portfolio.pending.step5_seller.sold=คุณได้ขาย +portfolio.pending.step5_seller.received=คุณได้รับ + +tradeFeedbackWindow.title=ขอแสดงความยินดีกับการซื้อขายที่เสร็จสมบูรณ์ของคุณ +tradeFeedbackWindow.msg.part1=เรายินดีที่จะฟังความเห็นเกี่ยวกับประสบการณ์ของคุณ มันจะช่วยให้เราปรับปรุงซอฟต์แวร์และระบบดียิ่งขึ้น หากคุณต้องการแสดงความคิดเห็นโปรดกรอกแบบสำรวจสั้น ๆ (ไม่ต้องลงทะเบียน) ที่: +tradeFeedbackWindow.msg.part2=หากคุณมีข้อสงสัยหรือประสบปัญหาใด ๆ โปรดติดต่อกับผู้ใช้และผู้สนับสนุนคนอื่น ๆ ผ่านทางฟอรัม Bisq ที่: +tradeFeedbackWindow.msg.part3=ขอบคุณที่ใช้ Bisq! + +portfolio.pending.role=บทบาทของฉัน +portfolio.pending.tradeInformation=ข้อมูลทางการซื้อขาย +portfolio.pending.remainingTime=เวลาที่เหลือ +portfolio.pending.remainingTimeDetail={0} (จนถึง {1}) +portfolio.pending.tradePeriodInfo=หลังจากการยืนยันบล็อกเชนครั้งแรก ระยะเวลาการซื้อขายจะเริ่มต้นขึ้น โดยจะขึ้นอยู่กับวิธีการชำระเงินที่ใช้ ระยะเวลาการซื้อขายที่ได้รับอนุญาตสูงสุดนั้นแตกต่างกัน +portfolio.pending.tradePeriodWarning=หากเกินระยะเวลานักซื้อขายทั้งสองฝ่ายสามารถเปิดข้อพิพาทได้ +portfolio.pending.tradeNotCompleted=การซื้อขายไม่เสร็จสิ้นภายในเวลา (จนถึง {0}) +portfolio.pending.tradeProcess=กระบวนการทางการซื้อขาย +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=เปิดข้อพิพาทอีกครั้ง +portfolio.pending.openSupportTicket.headline=เปิดปุ่มช่วยเหลือ +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. + +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. +portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Bisq community at https://bisq.community. If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Trade period is over + +portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested +portfolio.pending.openSupport=เปิดปุ่มช่วยเหลือ +portfolio.pending.supportTicketOpened=ปุ่มช่วยเหลือถูกเปิดแล้ว +portfolio.pending.communicateWithArbitrator=กรุณาติดต่อโดยไปที่ \"ช่วยเหลือและสนับสนุน \" กับผู้ไกล่เกลี่ย +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. +portfolio.pending.disputeOpenedMyUser=คุณได้เปิดข้อพิพาทแล้ว\n{0} +portfolio.pending.disputeOpenedByPeer=ผู้ร่วมการค้าของคุณได้เปิดประเด็นการอภิปรายขึ้น\n{0} +portfolio.pending.noReceiverAddressDefined=ไม่ได้ระบุที่อยู่ผู้รับ + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=View proposed resolution +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=เสร็จสิ้น +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated +portfolio.closed.canceled=ยกเลิกแล้ว +portfolio.failed.Failed=ผิดพลาด +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=รับเงิน +funds.tab.withdrawal=ส่งเงิน +funds.tab.reserved=เงินที่ถูกจองไว้ +funds.tab.locked=เงินที่ถูกล็อคไว้ +funds.tab.transactions=การทำธุรกรรม + +funds.deposit.unused=ไม่ได้ใช้ +funds.deposit.usedInTx=ใช้ใน {0} ธุรกรรม(ต่าง ๆ ) +funds.deposit.fundBisqWallet=เติมเงิน Bisq wallet +funds.deposit.noAddresses=ยังไม่มีการสร้างที่อยู่ของเงินฝาก +funds.deposit.fundWallet=เติมเงินใน wallet ของคุณ +funds.deposit.withdrawFromWallet=ส่งเงินทุนจากกระเป๋าสตางค์ของคุณ +funds.deposit.amount=จำนวนเงินใน BTC (ตัวเลือก) +funds.deposit.generateAddress=สร้างที่อยู่ใหม่ +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=โปรดเลือกที่อยู่ที่ไม่ได้ใช้จากตารางด้านบนแทนที่จะสร้างที่อยู่ใหม่ + +funds.withdrawal.arbitrationFee=ค่าธรรมเนียมอนุญาโตตุลาการ +funds.withdrawal.inputs=การคัดเลือกปัจจัยการนำเข้า +funds.withdrawal.useAllInputs=ใช้ปัจจัยการนำเข้าที่มีอยู่ทั้งหมด +funds.withdrawal.useCustomInputs=ใช้ปัจจัยการนำเข้าที่กำหนดเอง +funds.withdrawal.receiverAmount=จำนวนของผู้รับ +funds.withdrawal.senderAmount=จำนวนของผู้ส่ง +funds.withdrawal.feeExcluded=จำนวนเงินไม่รวมค่าธรรมเนียมการขุด +funds.withdrawal.feeIncluded=จำนวนเงินรวมค่าธรรมเนียมการขุด +funds.withdrawal.fromLabel=ถอนจากที่อยู่ +funds.withdrawal.toLabel=ถอนไปยังที่อยู่ +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=การถอนที่ถูกเลือก +funds.withdrawal.noFundsAvailable=ไม่มีเงินที่ใช้ถอนได้ +funds.withdrawal.confirmWithdrawalRequest=ยืนยันคำขอถอนเงิน +funds.withdrawal.withdrawMultipleAddresses=ถอนจากที่อยู่หลายแห่ง ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=ถอนจากที่อยู่หลายแห่ง \n{0} +funds.withdrawal.notEnoughFunds=คุณมีเงินไม่เพียงพอใน wallet ของคุณ +funds.withdrawal.selectAddress=เลือกแหล่งที่อยู่จากตาราง +funds.withdrawal.setAmount=กำหนดจำนวนที่จะถอน +funds.withdrawal.fillDestAddress=กรอกที่อยู่ปลายทางของคุณ +funds.withdrawal.warn.noSourceAddressSelected=คุณต้องเลือกแหล่งที่อยู่ในตารางด้านบน +funds.withdrawal.warn.amountExceeds=คุณมีเงินไม่เพียงพอจากที่อยู่ที่คุณเลือก\nพิจารณาเลือกที่อยู่หลายแห่งในตารางด้านบนหรือเปลี่ยนปรับค่าธรรมเนียมที่รวมค่าธรรมเนียมของผู้ขุด + +funds.reserved.noFunds=ไม่มีเงินสำรองในข้อเสนอแบบเปิด +funds.reserved.reserved=สำรองใน wallet ท้องถิ่นเพื่อข้อเสนอด้วย ID: {0} + +funds.locked.noFunds=ไม่มีเงินถูกล็อคในการซื้อขาย +funds.locked.locked=ถูกล็อคใน multisig สำหรับการซื้อขายด้วย ID: {0} + +funds.tx.direction.sentTo=ส่งไปยัง: +funds.tx.direction.receivedWith=ได้รับโดย: +funds.tx.direction.genesisTx=จากการทำธุรกรรมทั่วไป : +funds.tx.txFeePaymentForBsqTx=ค่าธรรมเนียมของนักขุดบิทคอยน์สำหรับการทำธุรกรรม BSQ +funds.tx.createOfferFee=ผู้สร้างและค่าธรรมเนียมการทำธุรกรรม: {0} +funds.tx.takeOfferFee=ค่าธรรมเนียมของผู้รับและการทำธุรกรรม: {0} +funds.tx.multiSigDeposit=เงินฝาก Multisig (การรองรับหลายลายเซ็น): {0} +funds.tx.multiSigPayout=การจ่ายเงิน Multisig (การรองรับหลายลายเซ็น): {0} +funds.tx.disputePayout=การจ่ายเงินข้อพิพาท: {0} +funds.tx.disputeLost=กรณีการสูญเสียข้อพิพาท: {0} +funds.tx.collateralForRefund=Refund collateral: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} +funds.tx.unknown=เหตุผลที่ไม่ระบุ: {0} +funds.tx.noFundsFromDispute=ไม่มีการคืนเงินจากการพิพาท +funds.tx.receivedFunds=เงินที่ได้รับ +funds.tx.withdrawnFromWallet=ถอนออกจาก wallet +funds.tx.withdrawnFromBSQWallet=BTC withdrawn from BSQ wallet +funds.tx.memo=Memo +funds.tx.noTxAvailable=ไม่มีธุรกรรมใด ๆ +funds.tx.revert=กลับสู่สภาพเดิม +funds.tx.txSent=ธุรกรรมถูกส่งสำเร็จไปยังที่อยู่ใหม่ใน Bisq wallet ท้องถิ่นแล้ว +funds.tx.direction.self=ส่งถึงตัวคุณเอง +funds.tx.daoTxFee=ค่าธรรมเนียมของนักขุดบิทคอยน์สำหรับการทำธุรกรรม BSQ +funds.tx.reimbursementRequestTxFee=ยื่นคำขอการชำระเงินคืน +funds.tx.compensationRequestTxFee=คำขอค่าสินไหมทดแทน +funds.tx.dustAttackTx=Received dust +funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount to your wallet and might be an attempt from chain analysis companies to spy on your wallet.\n\nIf you use that transaction output in a spending transaction they will learn that you are likely the owner of the other address as well (coin merge).\n\nTo protect your privacy the Bisq wallet ignores such dust outputs for spending purposes and in the balance display. You can set the threshold amount when an output is considered dust in the settings. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.legacyArbitration.support=Legacy Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets +support.filter=Search disputes +support.filter.prompt=Enter trade ID, date, onion address or account data + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=การแจ้งเตือนส่วนตัว +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=ไม่มีการเปิดรับคำขอร้องหรือความช่วยเหลือ +support.sendingMessage=กำลังส่งข้อความ... +support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. +support.sendMessageError=การส่งข้อความล้มเหลว ข้อผิดพลาด: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=ข้อเสนอในข้อพิพาทดังกล่าวได้รับการสร้างขึ้นโดยใช้ Bisq เวอร์ชั่นเก่ากว่า\nคุณไม่สามารถปิดข้อพิพาทดังกล่าวกับแอปพลิเคชั่นเวอร์ชั่นของคุณได้\n\nโปรดใช้เวอร์ชันที่เก่ากว่ากับเวอร์ชั่นโปรโตคอล {0} +support.openFile=เปิดไฟล์ที่จะแนบ (ขนาดไฟล์สูงสุด: {0} kb) +support.attachmentTooLarge=ขนาดไฟล์แนบทั้งหมด {0} กิโลไบต์และเกินจำนวนไฟล์สูงสุด ขนาดข้อความที่อนุญาตเท่ากับ {1} kB +support.maxSize=ขนาดไฟล์สูงสุดที่อนุญาตคือ {0} kB +support.attachment=แนบไฟล์ +support.tooManyAttachments=คุณไม่สามารถส่งไฟล์แนบได้มากกว่า 3 ไฟล์ในข้อความเดียว +support.save=บันทึกไฟล์ลงในดิสก์ +support.messages=ข้อความ +support.input.prompt=Enter message... +support.send=ส่ง +support.addAttachments=เพิ่มไฟล์แนบ +support.closeTicket=ยุติคำร้องขอและความช่วยเหลือ +support.attachments=ไฟล์ที่แนบมา: +support.savedInMailbox=ข้อความถูกบันทึกไว้ในกล่องจดหมายของผู้รับ +support.arrived=ข้อความถึงผู้รับแล้ว +support.acknowledged=ข้อความได้รับการยืนยันจากผู้รับแล้ว +support.error=ผู้รับไม่สามารถประมวลผลข้อความได้ ข้อผิดพลาด: {0} +support.buyerAddress=ที่อยู่ของผู้ซื้อ BTC +support.sellerAddress=ที่อยู่ของผู้ขาย BTC +support.role=บทบาท +support.agent=Support agent +support.state=สถานะ +support.chat=Chat +support.closed=ปิดแล้ว +support.open=เปิด +support.process=Process +support.buyerOfferer=BTC ผู้ซื้อ / ผู้สร้าง +support.sellerOfferer= BTC ผู้ขาย/ ผู้สร้าง +support.buyerTaker=BTC ผู้ซื้อ / ผู้รับ +support.sellerTaker=BTC ผู้ขาย / ผู้รับ + +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\nTraders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and suggest a payout of trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. If one or both traders do not agree to the mediator's suggested payout, they can request arbitration.The arbitrator will re-evaluate the situation and, if warranted, personally pay the trader back and request reimbursement for this payment from the Bisq DAO. +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Bisq are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.systemMsg=ระบบข้อความ: {0} +support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nBisq version: {1} +support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nBisq version: {1} +support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq version: {1} +support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=การตั้งค่า +settings.tab.network=ข้อมูลเครือข่าย +settings.tab.about=เกี่ยวกับ + +setting.preferences.general=การตั้งค่าทั่วไป +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=สูงสุด ส่วนเบี่ยงเบนจากราคาตลาด +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=หลีกเลี่ยงโหมดแสตนบายด์ +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=ค่าที่สูงกว่า {0}% ไม่ได้รับอนุญาต +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=ใช้ค่าที่กำหนดเอง +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Ignored peers [onion address:port] +setting.preferences.ignoreDustThreshold=Min. non-dust output value +setting.preferences.currenciesInList=สกุลเงินอยู่ในหน้ารายการราคาตลาด +setting.preferences.prefCurrency=สกุลเงินที่ต้องการ +setting.preferences.displayFiat=แสดงสกุลเงินของประเทศ +setting.preferences.noFiat=ไม่มีสกุลเงินประจำชาติที่เลือกไว้ +setting.preferences.cannotRemovePrefCurrency=คุณไม่สามารถลบสกุลเงินในการแสดงผลที่เลือกไว้ได้ +setting.preferences.displayAltcoins=แสดง altcoins +setting.preferences.noAltcoins=ไม่มี altcoins ที่เลือก +setting.preferences.addFiat=เพิ่มสกุลเงินประจำชาติ +setting.preferences.addAltcoin=เพิ่ม altcoin +setting.preferences.displayOptions=แสดงตัวเลือกเพิ่มเติม +setting.preferences.showOwnOffers=แสดงข้อเสนอของฉันเองในสมุดข้อเสนอ +setting.preferences.useAnimations=ใช้ภาพเคลื่อนไหว +setting.preferences.useDarkMode=Use dark mode +setting.preferences.sortWithNumOffers=จัดเรียงรายการโดยเลขของข้อเสนอ / การซื้อขาย +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=รีเซ็ตทั้งหมด \"ไม่ต้องแสดงอีกครั้ง \" ปักธง +settings.preferences.languageChange=หากต้องการเปลี่ยนภาษากับทุกหน้าต้องทำการรีสตาร์ท +settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. +setting.preferences.daoOptions=ตัวเลือก DAO +setting.preferences.dao.resyncFromGenesis.label=สร้างสถานะ DAO ใหม่จากธุรกรรมต้นกำเนิด +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=ใช้งาน Bisq ในแบบโหนด DAO full node +setting.preferences.dao.rpcUser=ชื่อผู้ใช้ RPC +setting.preferences.dao.rpcPw=รหัส RPC +setting.preferences.dao.blockNotifyPort=Block notify port +setting.preferences.dao.fullNodeInfo=For running Bisq as DAO full node you need to have Bitcoin Core locally running and RPC enabled. All requirements are documented in ''{0}''.\n\nAfter changing the mode you need to restart. +setting.preferences.dao.fullNodeInfo.ok=เปิดหน้าเอกสาร +setting.preferences.dao.fullNodeInfo.cancel=ไม่ ฉันติดกับไลท์โหนดโหมด (lite node mode) +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=ชื่อ +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=เครือข่าย Bitcoin +settings.net.p2pHeader=Bisq network +settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน +settings.net.btcNodesLabel=ใช้โหนดเครือข่าย Bitcoin Core ที่กำหนดเอง +settings.net.bitcoinPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว +settings.net.useTorForBtcJLabel=ใช้ Tor สำหรับเครือข่าย Bitcoin +settings.net.bitcoinNodesLabel=ใช้โหนดเครือข่าย Bitcoin Core เพื่อเชื่อมต่อ +settings.net.useProvidedNodesRadio=ใช้โหนดเครือข่าย Bitcoin ที่ให้มา +settings.net.usePublicNodesRadio=ใช้เครือข่าย Bitcoin สาธารณะ +settings.net.useCustomNodesRadio=ใช้โหนดเครือข่าย Bitcoin Core ที่กำหนดเอง +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=ไม่ ใช้โหนดที่ให้มา +settings.net.warn.usePublicNodes.usePublic=ใช่ ใช้เครือข่ายสาธารณะ +settings.net.warn.useCustomNodes.B2XWarning=โปรดตรวจสอบว่าโหนด Bitcoin ของคุณเป็นโหนด Bitcoin Core ที่เชื่อถือได้!\n\nการเชื่อมต่อกับโหนดที่ไม่ปฏิบัติตามกฎกติกาการยินยอมของ Bitcoin Core อาจทำให้ wallet ของคุณเกิดปัญหาในกระบวนการทางการซื้อขายได้\n\nผู้ใช้ที่เชื่อมต่อกับโหนดที่ละเมิดกฎเป็นเอกฉันท์นั้นจำเป็นต้องรับผิดชอบต่อความเสียหายที่สร้างขึ้น ข้อพิพาทที่เกิดจากการที่จะได้รับการตัดสินใจจาก เน็ตกเวิร์ก Peer คนอื่น ๆ จะไม่มีการสนับสนุนด้านเทคนิคแก่ผู้ใช้ที่ไม่สนใจคำเตือนและกลไกการป้องกันของเรา! +settings.net.warn.invalidBtcConfig=Connection to the Bitcoin network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Bitcoin nodes instead. You will need to restart the application. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว +settings.net.onionAddressColumn=ที่อยู่ Onion +settings.net.creationDateColumn=ที่จัดตั้งขึ้น +settings.net.connectionTypeColumn=เข้า/ออก +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=ไป - กลับ +settings.net.sentBytesColumn=ส่งแล้ว +settings.net.receivedBytesColumn=ได้รับแล้ว +settings.net.peerTypeColumn=ประเภทเน็ตเวิร์ก peer +settings.net.openTorSettingsButton=เปิดการตั้งค่าของ Tor + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Height + +settings.net.needRestart=คุณต้องรีสตาร์ทแอ็พพลิเคชั่นเพื่อทำให้การเปลี่ยนแปลงนั้นเป็นผล\nคุณต้องการทำตอนนี้หรือไม่ +settings.net.notKnownYet=ยังไม่ทราบ ... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[ที่อยู่ IP: พอร์ต | ชื่อโฮสต์: พอร์ต | ที่อยู่ onion: พอร์ต] (คั่นด้วยเครื่องหมายจุลภาค) Port สามารถละเว้นได้ถ้าใช้ค่าเริ่มต้น (8333) +settings.net.seedNode=แหล่งโหนดข้อมูล +settings.net.directPeer=Peer (โดยตรง) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=เน็ตเวิร์ก Peer +settings.net.inbound=ขาเข้า +settings.net.outbound=ขาออก +settings.net.reSyncSPVChainLabel=ซิงค์อีกครั้ง SPV chain +settings.net.reSyncSPVChainButton=ลบไฟล์ SPV และ ซิงค์อีกครั้ง +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=ไฟล์ SPV chain ถูกลบแล้ว โปรดอดใจรอ อาจใช้เวลาสักครู่เพื่อทำการซิงค์ครั้งใหม่กับเครือข่าย +settings.net.reSyncSPVAfterRestartCompleted=การซิงค์เสร็จสมบูรณ์แล้ว โปรดรีสตาร์ทแอ็พพลิเคชั่น +settings.net.reSyncSPVFailed=ไม่สามารถลบไฟล์ SPV chain ได้\nข้อผิดพลาด: {0} +setting.about.aboutBisq=เกี่ยวกับ Bisq +setting.about.about=Bisq เป็นโครงการอิสระโดยอำนวยความสะดวกในการแลกเงินบิตคอยน์กับสกุลเงินของประเทศต่างๆ (และสกุลเงินดิจิตอลอื่นๆ) ผ่านเครือข่ายเข้าถึงกระจายอำนาจอย่างถัดเทียมในรูปแบบการปกป้องข้อมูลส่วนบุคคล เรียนรู้เพิ่มเติมเกี่ยวกับ Bisq ในหน้าเว็บของโปรเจคของเรา +setting.about.web=หน้าเว็บ Bisq +setting.about.code=โค๊ดแหล่งที่มา +setting.about.agpl=ใบอนุญาต AGPL +setting.about.support=สนับสนุน Bisq +setting.about.def=Bisq ไม่ใช่ บริษัท แต่เป็นโปรเจคชุมชนและเปิดให้คนมีส่วนร่วม ถ้าคุณต้องการจะเข้าร่วมหรือสนับสนุน Bisq โปรดทำตามลิงค์ข้างล่างนี้ +setting.about.contribute=สนับสนุน +setting.about.providers=ผู้ให้บริการข้อมูล +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=ราคาตลาดจัดโดย +setting.about.feeEstimation.label=การประมาณค่าธรรมเนียมการขุดโดย +setting.about.versionDetails=รายละเอียดของเวอร์ชั่น +setting.about.version=เวอร์ชั่นของแอปพลิเคชั่น +setting.about.subsystems.label=เวอร์ชั่นของระบบย่อย +setting.about.subsystems.val=เวอร์ชั่นของเครือข่าย: {0}; เวอร์ชั่นข้อความ P2P: {1}; เวอร์ชั่นฐานข้อมูลท้องถิ่น: {2}; เวอร์ชั่นโปรโตคอลการซื้อขาย: {3} + +setting.about.shortcuts=Short cuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigate main menu +setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' + +setting.about.shortcuts.close=Close Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Close popup or dialog window +setting.about.shortcuts.closePopup.value='ESCAPE' key + +setting.about.shortcuts.chatSendMsg=Send trader chat message +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Open dispute +setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} + +setting.about.shortcuts.walletDetails=Open wallet details window + +setting.about.shortcuts.openEmergencyBtcWalletTool=Open emergency wallet tool for BTC wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool for BSQ wallet + +setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN + +setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) +setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} + +setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) +setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} + +setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) + +setting.about.shortcuts.sendFilter=Set Filter (privileged activity) + +setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing +account.info.headline=ยินดีต้อนรับสู่บัญชี Bisq ของคุณ +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). + +account.menu.paymentAccount=บัญชีสกุลเงินของประเทศ +account.menu.altCoinsAccountView=บัญชี Altcoin (เหรียญทางเลือก) +account.menu.password=รหัส Wallet +account.menu.seedWords=รหัสลับ Wallet +account.menu.walletInfo=Wallet info +account.menu.backup=การสำรองข้อมูล +account.menu.notifications=การแจ้งเตือน + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=กุญแจสาธารณะ + +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=เพิกถอน +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.warn.min1Language=คุณต้องตั้งค่าภาษาอย่างน้อย 1 ภาษา\nเราได้เพิ่มภาษาเริ่มต้นให้กับคุณแล้ว +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} + +account.altcoin.yourAltcoinAccounts=บัญชี altcoin (เหรียญทางเลือก) ของคุณ +account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.altcoin.popup.wallet.confirm=ฉันเข้าใจและยืนยันว่าฉันรู้ว่า wallet ใดที่ฉันต้องการใช้ +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN (the receiver needs to be online or at least be online during a certain time frame). \n\nBisq supports only the Grinbox (Wallet713) wallet URL format. \n\nThe GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the latest Grinbox software which supports the transaction proof and that you understand the process of transferring and receiving GRIN as well as how to create the proof. \n\nSee https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more information about the Grinbox proof tool. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the transaction. \n\nBe sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM (the receiver needs to be online or at least be online during a certain time frame). \n\nThe BEAM sender is required to provide proof that they sent BEAM successfully. Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential dispute will be resolved in favor of the BEAM receiver. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Trading L-BTC on Bisq requires that you understand the following:\n\nWhen receiving L-BTC for a trade on Bisq, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-BTC into the Liquid Elements Core wallet, or another L-BTC wallet which allows you to obtain the blinding key for your blinded L-BTC address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-BTC address to the Bisq mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-BTC receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-BTC on Bisq. + +account.fiat.yourFiatAccounts=บัญชีสกุลเงินของคุณ + +account.backup.title=สำรองข้อมูล wallet +account.backup.location=ที่ตั้งการสำรองข้อมูล +account.backup.selectLocation=เลือกตำแหน่งการสำรอง +account.backup.backupNow=สำรองข้อมูลตอนนี้ (สำรองข้อมูลไม่ได้เข้ารหัส!) +account.backup.appDir=สารบบข้อมูลแอ็พพลิเคชั่น +account.backup.openDirectory=เปิดสารบบ +account.backup.openLogFile=เปิดการเข้าสู่ไฟล์ +account.backup.success=สำรองข้อมูลสำเร็จแล้วบันทึกไว้ที่: \n{0} +account.backup.directoryNotAccessible=สารบบที่คุณเลือกไม่สามารถเข้าถึงได้ {0} + +account.password.removePw.button=ลบรหัสผ่าน +account.password.removePw.headline=ลบรหัสผ่านการป้องกันสำหรับ wallet +account.password.setPw.button=ตั้งรหัสผ่าน +account.password.setPw.headline=ตั้งรหัสผ่านการป้องกันสำหรับ wallet +account.password.info=ด้วยระบบป้องกันรหัสผ่าน คุณจะต้องป้อนรหัส ณ จุดเริ่มต้นในแอพพลิเคชั่น เมื่อมีการถอนบิทคอยน์ และการกู้คืนกระเป๋าสตางค์ของคุณจาก seed words + +account.seed.backup.title=สำรองข้อมูล wallet โค้ดของคุณ +account.seed.info=โปรดเขียนรหัสสำรองข้อมูล wallet และวันที่! คุณสามารถกู้ข้อมูล wallet ของคุณได้ทุกเมื่อด้วย รหัสสำรองข้อมูล wallet และวันที่\nรหัสสำรองข้อมูล ใช้ทั้ง BTC และ BSQ wallet\n\nคุณควรเขียนรหัสสำรองข้อมูล wallet ลงบนแผ่นกระดาษและไม่บันทึกไว้ในคอมพิวเตอร์ของคุณ\n\nโปรดทราบว่า รหัสสำรองข้อมูล wallet ไม่ได้แทนการสำรองข้อมูล\nคุณจำเป็นต้องสำรองข้อมูลสารบบแอ็พพลิเคชั่นทั้งหมดที่หน้าจอ \"บัญชี / การสำรองข้อมูล \" เพื่อกู้คืนสถานะแอ็พพลิเคชั่นและข้อมูลที่ถูกต้อง\nการนำเข้ารหัสสำรองข้อมูล wallet เป็นคำแนะนำเฉพาะสำหรับกรณีฉุกเฉินเท่านั้น แอพพลิเคชั่นจะไม่สามารถใช้งานได้หากไม่มีไฟล์สำรองฐานข้อมูลและคีย์ที่ถูกต้อง! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=คุณยังไม่ได้ตั้งรหัสผ่าน wallet ซึ่งจะช่วยป้องกันการแสดงผลของรหัสสำรองข้อมูล wallet \n\nคุณต้องการแสดงรหัสสำรองข้อมูล wallet หรือไม่ +account.seed.warn.noPw.yes=ใช่ และไม่ต้องถามฉันอีก +account.seed.enterPw=ป้อนรหัสผ่านเพื่อดูรหัสสำรองข้อมูล wallet +account.seed.restore.info=Please make a backup before applying restore from seed words. Be aware that wallet restore is only for emergency cases and might cause problems with the internal wallet database.\nIt is not a way for applying a backup! Please use a backup from the application data directory for restoring a previous application state.\n\nAfter restoring the application will shut down automatically. After you have restarted the application it will resync with the Bitcoin network. This can take a while and can consume a lot of CPU, especially if the wallet was older and had many transactions. Please avoid interrupting that process, otherwise you might need to delete the SPV chain file again or repeat the restore process. +account.seed.restore.ok=Ok, do the restore and shut down Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=ติดตั้ง +account.notifications.download.label=ดาวน์โหลดแอปพลิเคชั่นบนมือถือ +account.notifications.waitingForWebCam=กำลังเปิดกล้องเว็บแคม ... +account.notifications.webCamWindow.headline=สแกน QR โค้ดจากโทรศัพท์ +account.notifications.webcam.label=ใช้เว็บแคม +account.notifications.webcam.button=สแกน QR โค้ด +account.notifications.noWebcam.button=ฉันไม่มีเว็บแคม +account.notifications.erase.label=ล้างการแจ้งเตือนบนโทรศัพท์ +account.notifications.erase.title=ล้างการแจ้งเตือน +account.notifications.email.label=การจับคู่โทเค็น +account.notifications.email.prompt=ป้อนคู่โทเค็นที่คุณได้รับทางอีเมล์ +account.notifications.settings.title=ตั้งค่า +account.notifications.useSound.label=เปิดเสียงการแจ้งเตือนบนโทรศัพท์ +account.notifications.trade.label=ได้รับข้อความทางการค้า +account.notifications.market.label=ได้รับการแจ้งเตือนข้อเสนอ +account.notifications.price.label=ได้รับการแจ้งเตือนราคา +account.notifications.priceAlert.title=แจ้งเตือนราคา +account.notifications.priceAlert.high.label=แจ้งเตือนหากราคา BTC สูงกว่า +account.notifications.priceAlert.low.label=แจ้งเตือนหากราคา BTC ต่ำกว่า +account.notifications.priceAlert.setButton=ตั้งค่าการเตือนราคา +account.notifications.priceAlert.removeButton=ลบการเตือนราคา +account.notifications.trade.message.title=การเปลี่ยนแปลงสถานะทางการค้า +account.notifications.trade.message.msg.conf=ธุรกรรมทางการค้าจากผู้ค้า ID {0} ได้รับการยืนยันแล้ว โปรดเปิดแอปพลิเคชัน Bisq ของคุณและเริ่มการรับการชำระเงิน +account.notifications.trade.message.msg.started=ผู้ซื้อ BTC ได้เริ่มต้นการชำระเงินสำหรับผู้ค้าที่มี ID {0} +account.notifications.trade.message.msg.completed=การค้ากับ ID {0} เสร็จสมบูรณ์ +account.notifications.offer.message.title=ข้อเสนอของคุณถูกยอมรับ +account.notifications.offer.message.msg=ข้อเสนอของคุณที่มี ID {0} ถูกยอมรับ +account.notifications.dispute.message.title=มีข้อความใหม่เกี่ยวกับข้อพิพาท +account.notifications.dispute.message.msg=คุณได้รับข้อความการพิพาททางการค้ากับ ID {0} + +account.notifications.marketAlert.title=เสนอการแจ้งเตือน +account.notifications.marketAlert.selectPaymentAccount=เสนอบัญชีการชำระเงินที่ตรงกัน +account.notifications.marketAlert.offerType.label=ประเภทข้อเสนอพิเศษที่ฉันสนใจ +account.notifications.marketAlert.offerType.buy=ซื้อข้อเสนอพิเศษ (ฉันต้องการขาย BTC) +account.notifications.marketAlert.offerType.sell=ข้อเสนอพิเศษในการขาย (ฉันต้องการซื้อ BTC) +account.notifications.marketAlert.trigger=ระดับของราคาที่เสนอ (%) +account.notifications.marketAlert.trigger.info=เมื่อตั้งระดับของราคา คุณจะได้รับการแจ้งเตือนเมื่อมีการเผยแพร่ข้อเสนอที่ตรงกับความต้องการของคุณ (หรือมากกว่า) \nตัวอย่าง: หากคุณต้องการขาย BTC แต่คุณจะขายในราคาที่สูงกว่า 2% จากราคาตลาดปัจจุบันเท่านั้น\n การตั้งค่าฟิลด์นี้เป็น 2% จะทำให้คุณมั่นใจได้ว่าจะได้รับการแจ้งเตือนสำหรับข้อเสนอเฉพาะในราคาที่สูงกว่าราคาตลาดปัจจุบันที่ 2% (หรือมากกว่า) +account.notifications.marketAlert.trigger.prompt=เปอร์เซ็นต์ระดับราคาจากราคาตลาด (เช่น 2.50%, -0.50% ฯลฯ ) +account.notifications.marketAlert.addButton=เพิ่มการแจ้งเตือนข้อเสนอพิเศษ +account.notifications.marketAlert.manageAlertsButton=จัดการการแจ้งเตือนข้อเสนอพิเศษ +account.notifications.marketAlert.manageAlerts.title=จัดการการแจ้งเตือนข้อเสนอพิเศษ +account.notifications.marketAlert.manageAlerts.header.paymentAccount=บัญชีการชำระเงิน +account.notifications.marketAlert.manageAlerts.header.trigger= ราคาเงื่อนไขที่ตั้งไว้ +account.notifications.marketAlert.manageAlerts.header.offerType=ประเภทข้อเสนอ +account.notifications.marketAlert.message.title=แจ้งเตือนข้อเสนอ +account.notifications.marketAlert.message.msg.below=ต่ำกว่า +account.notifications.marketAlert.message.msg.above=สูงกว่า +account.notifications.marketAlert.message.msg=ข้อเสนอใหม่ '{0} {1}' 'ด้วยราคา {2} ({3} {4} ราคาตลาด) และวิธีการชำระเงิน' '{5}' 'ถูกเผยแพร่ลงในหนังสือข้อเสนอ Bisq\nรหัสข้อเสนอพิเศษ: {6} +account.notifications.priceAlert.message.title=การแจ้งเตือนราคาสำหรับ {0} +account.notifications.priceAlert.message.msg=คุณได้รับการแจ้งเตือนราคาของคุณ {0} ราคาปัจจุบันคือ {1} {2} +account.notifications.noWebCamFound.warning=ไม่พบเว็บแคม\n\nโปรดใช้ตัวเลือกอีเมลเพื่อส่งรหัสโทเค็น (รหัสเหรียญ) และคีย์เข้ารหัสจากโทรศัพท์มือถือของคุณไปยังแอพพลิเคชัน Bisq +account.notifications.priceAlert.warning.highPriceTooLow=ราคาที่สูงกว่าต้องเป็นจำนวนที่มากเหนือราคาที่ต่ำกว่า +account.notifications.priceAlert.warning.lowerPriceTooHigh=ราคาที่ต่ำกว่าต้องต่ำกว่าราคาที่สูงขึ้น + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Facts & Figures +dao.tab.bsqWallet=กระเป๋าสตางค์ BSQ +dao.tab.proposals=การกำกับดูแลกิจการ +dao.tab.bonding=ค้ำประกัน +dao.tab.proofOfBurn=ค่าธรรมเนียมในการลงบันทึกรายการทรัพย์สิน/หลักฐานในการทำลายทิ้ง +dao.tab.monitor=Network monitor +dao.tab.news=News + +dao.paidWithBsq=จ่ายโดย BSQ +dao.availableBsqBalance=Available for spending (verified + unconfirmed change outputs) +dao.verifiedBsqBalance=Balance of all verified UTXOs +dao.unconfirmedChangeBalance=Balance of all unconfirmed change outputs +dao.unverifiedBsqBalance=Balance of all unverified transactions (awaiting block confirmation) +dao.lockedForVoteBalance=ถูกใช้สำหรับการโหวต +dao.lockedInBonds=ถูกล็อคไว้ในการค้ำประกัน +dao.availableNonBsqBalance=ยอดคงเหลือที่ไม่ใช่ BSQ (BTC) ซึ่งใช้งานได้ +dao.reputationBalance=Merit Value (not spendable) + +dao.tx.published.success=การทำธุรกรรมของคุณได้รับการเผยแพร่เรียบร้อยแล้ว +dao.proposal.menuItem.make=เสนอคำขอ +dao.proposal.menuItem.browse=เรียกดูข้อเสนอที่เปิด +dao.proposal.menuItem.vote=โหวตข้อเสนอ +dao.proposal.menuItem.result=ผลโหวต +dao.cycle.headline=รอบการลงคะแนนเสียง +dao.cycle.overview.headline=ภาพรวมรอบการโหวด +dao.cycle.currentPhase=ระยะปัจจุบัน +dao.cycle.currentBlockHeight=ความสูงของบล็อกปัจจุบัน +dao.cycle.proposal=ระยะเสนอ +dao.cycle.proposal.next=Next proposal phase +dao.cycle.blindVote=ขั้นตอนการลงคะแนนเสียงแบบไม่ระบุตัวตน +dao.cycle.voteReveal=ขั้นตอนการประกาศผลโหวต +dao.cycle.voteResult=ผลโหวต +dao.cycle.phaseDuration={0} บล็อก (≈{1}); บล็อก {2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Block {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Vote reveal transaction published +dao.voteReveal.txPublished=Your vote reveal transaction with transaction ID {0} was successfully published.\n\nThis happens automatically by the software if you have participated in the DAO voting. + +dao.results.cycles.header=รอบ +dao.results.cycles.table.header.cycle=วงจร +dao.results.cycles.table.header.numProposals=คำขอ +dao.results.cycles.table.header.voteWeight=น้ำหนักโหวต +dao.results.cycles.table.header.issuance=การออกหุ้น + +dao.results.results.table.item.cycle=วงจร {0} เริ่มต้น: {1} + +dao.results.proposals.header=ข้อเสนอของวงจรที่เลือก +dao.results.proposals.table.header.nameLink=Name/link +dao.results.proposals.table.header.details=รายละเอียด +dao.results.proposals.table.header.myVote=การลงคะแนนของฉัน +dao.results.proposals.table.header.result=ผลโหวต +dao.results.proposals.table.header.threshold=Threshold +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=ผลโหวตสำหรับข้อเสนอที่เลือก + +dao.results.exceptions=Vote result exception(s) + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=ไม่ได้กำหนด + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=ค่าธรรมเนียมของผู้สร้าง BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=ค่าธรรมเนียมของผู้รับ BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=ค่าธรรมเนียมของผู้สร้าง BSQ ขั้นต่ำ +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=ค่าธรรมเนียมของผู้รับ BSQ ขั้นต่ำ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=ค่าธรรมเนียมผู้สร้าง BTC +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=ค่าธรรมเนียมผู้รับ BTC +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=ค่าธรรมเนียมของผู้สร้าง BTC ขั้นต่ำ +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=ค่าธรรมเนียมของผู้รับ BTC ขั้นต่ำ +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=ค่าธรรมเนียมข้อเสนอ BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=ค่าธรรมเนียมในการโหวต BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=ยอดรวม BSQ ขั้นต่ำในการยื่นคำร้องขอค่าชดเชย +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=ยอดรวม BSQ ขั้นสูงสุดในการยื่นคำร้องขอค่าชดเชย +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=ยอดรวม BSQ ขั้นต่ำในการยื่นคำร้องขอการชำระเงินคืน +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=ยอดรวม BSQ ขั้นสูงสุดในการยื่นคำร้องขอการชำระเงินคืน + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=องค์ประกอบที่จำเป็นสำหรับข้อเสนอทั่วไปใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=องค์ประกอบที่จำเป็นสำหรับค่าชดเชยใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=องค์ประกอบที่จำเป็นสำหรับการชำระเงินคืนใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=องค์ประกอบที่จำเป็นสำหรับการเปลี่ยนพารามิเตอร์ใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=องค์ประกอบที่จำเป็นสำหรับในการในถอดถอนทรัพย์สินใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=องค์ประกอบที่จำเป็นสำหรับในการยื่นคำร้องต่อการยึดทรัพย์ใน BSQ +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=องค์ประกอบที่จำเป็นสำหรับใน BSQ ในการยื่นคำขอรับประกัน + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับข้อเสนอทั่วไป +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการยื่นคำขอค่าชดเชย +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการชำระเงินคืน +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการเปลี่ยนพารามิเตอร์ +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการถอดถอนสินทรัพย์ +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการยื่นคำร้องของการยึดทรัพย์ +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=องค์ประกอบของเทรสโฮลด์คิดเป็น % สำหรับการร้องขอรับประกัน + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=ที่อยู่ BTC ของผู้รับ: + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=ค่าธรรมเนียมในการลงบันทึกรายการทรัพย์สินต่อวัน +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Min. trade volume for assets + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Lock time for alternative trade payout tx +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Arbitrator fee in BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Max. trade limit in BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Bonded role unit factor in BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Issuance limit per cycle in BSQ + +dao.param.currentValue=มูลค่าปัจจุบัน: {0} +dao.param.currentAndPastValue=Current value: {0} (Value when proposal was made: {1}) +dao.param.blocks={0} บล็อก + +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was not distributed well in the Bisq network.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=ระยะเสนอ +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=พัก 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=ขั้นการลงคะแนนเสียงแบบไม่ระบุตัวตน +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=พัก 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=ขั้นเปิดเผยการลงคะแนน +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=พัก 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=ระยะผลลัพธ์ + +dao.results.votes.table.header.stakeAndMerit=น้ำหนักโหวต +dao.results.votes.table.header.stake=Stake (เหรียญที่ล็อคไว้สำหรับสิทธิ์ในการโหวต) +dao.results.votes.table.header.merit=ที่ได้รับ +dao.results.votes.table.header.vote=โหวต + +dao.bond.menuItem.bondedRoles=บทบาทของการประกัน +dao.bond.menuItem.reputation=กิตติศัพท์ในการค้ำประกัน +dao.bond.menuItem.bonds=สัญญาผูกมัด + +dao.bond.dashboard.bondsHeadline=การประกัน BSQ +dao.bond.dashboard.lockupAmount=เงินทุนที่ล็อค +dao.bond.dashboard.unlockingAmount=การปลดล็อกเงิน (รอจนกว่าเวลาล็อคหมด) + + +dao.bond.reputation.header=ล็อคหลักประกันสำหรับมาตรฐานกิตติศัพท์ความน่าเชื่อถือ +dao.bond.reputation.table.header=หลักประกันกิตติศัพท์ของฉัน +dao.bond.reputation.amount=จำนวน BSQ เพื่อล็อคสิทธิ์ในการโหวต +dao.bond.reputation.time=ปลดล็อกเวลาในบล็อก +dao.bond.reputation.salt=ข้อมูลแบบสุ่ม +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=ล็อค +dao.bond.reputation.lockup.headline=ยืนยันล็อคการทำรายการ +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=ยืนยันการปลดล็อกธุรกรรม +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=การค้ำประกันทั้งหมด + +dao.bond.bondedReputation=กิตติศัพท์ในการค้ำประกัน +dao.bond.bondedRoles=ตำแหน่งในการค้ำประกัน + +dao.bond.details.header=รายละเอียดบทบาทและหน้าที่ +dao.bond.details.role=บทบาท +dao.bond.details.requiredBond=ต้องได้รับการประกัน BSQ +dao.bond.details.unlockTime=ปลดล็อกเวลาในบล็อก +dao.bond.details.link=เชื่อมโยงกับคำอธิบายหลัก +dao.bond.details.isSingleton=สามารถรับได้ในจำนวนที่หลากหลายของผู้ถือหลัก +dao.bond.details.blocks={0} บล็อก + +dao.bond.table.column.name=ชื่อ +dao.bond.table.column.link=ลิงค์ +dao.bond.table.column.bondType=ประเภทของการประกัน +dao.bond.table.column.details=รายละเอียด +dao.bond.table.column.lockupTxId=ล็อคการทำธุรกรรม ID +dao.bond.table.column.bondState=สถานะการประกัน +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=ล็อควันที่ + +dao.bond.table.button.lockup=ล็อค +dao.bond.table.button.unlock=ปลดล็อค +dao.bond.table.button.revoke=เพิกถอน + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=ยังไม่ได้ประกัน +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=รอดำเนินการล็อค +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=การประกันได้ล็อคไว้ +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=รอปลดล็อค +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=ปลดล็อคการดำเนินงานสำเร็จ +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=กำลังปลดล็อกการกู้ยืม +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=การประกันปลดล็อคแล้ว +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=หลักค้ำประกันถูกยึด + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=บทบาทการประกัน +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=กิตติศัพท์ในการประกัน + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=GitHub admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=แอดมินฟอรั่ม +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=แอดมินทวิตเตอร์ +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=ผู้ดูแล Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-fork maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=ผู้ดำเนินงานของเว็บไซต์ +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=ผู้ดำเนินการฟอรั่ม +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=ตัวดำเนินการแหล่งข้อมูลในโหนด +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=ผู้ดำเนินการโหนดด้านราคา +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=ผู้ดำเนินการด้านตลาด +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Mobile notifications relay operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=เจ้าของชื่อโดเมน +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=แอดมิน DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=ผู้ไกล่เกลี่ย +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=ผู้ไกล่เกลี่ย +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC donation address owner + +dao.burnBsq.assetFee=Asset listing +dao.burnBsq.menuItem.assetFee=ค่าธรรมเนียมในการลงบันทึกรายการทรัพย์สิน +dao.burnBsq.menuItem.proofOfBurn=พยานหลักฐานในการทำลายทิ้ง (Proof of burn) +dao.burnBsq.header=ค่าธรรมเนียมสำหรับการลงบันทึกรายการทรัพย์สิน +dao.burnBsq.selectAsset=เลือก Asset +dao.burnBsq.fee=ค่าธรรมเนียม +dao.burnBsq.trialPeriod=ระยะทดลองใช้ +dao.burnBsq.payFee=ชำระค่าธรรมเนียม +dao.burnBsq.allAssets=ทรัพย์สินทั้งหมด +dao.burnBsq.assets.nameAndCode=ชื่อของทรัพย์สิน +dao.burnBsq.assets.state=สถานะ +dao.burnBsq.assets.tradeVolume=ปริมาณการซื้อขาย +dao.burnBsq.assets.lookBackPeriod=ระยะเวลาของการตรวจสอบ +dao.burnBsq.assets.trialFee=ค่าธรรมเนียมสำหรับระยะทดลองใช้ +dao.burnBsq.assets.totalFee=ค่าธรรมเนียมที่ชำระแล้วทั้งหมด +dao.burnBsq.assets.days={0} วัน +dao.burnBsq.assets.toFewDays=ค่าธรรมเนียมสินทรัพย์มีน้อยเกินไป จำนวนระยะเวลาสำหรับการทดลองใช้คือ {0} + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=ในช่วงระยะเวลาทดลองใช้ +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=ซื้อขายอย่างแข่งขัน +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=ถูกถอนออกจากรายชื่อเนื่องจากไม่มีการเคลื่อนไหว +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=ถูกลบออกจากการโหวต + +dao.proofOfBurn.header=พยานหลักฐานในการทำลายทิ้ง (Proof of burn) +dao.proofOfBurn.amount=จำนวน +dao.proofOfBurn.preImage=Pre-image +dao.proofOfBurn.burn=ทำลายทิ้ง (Burn) +dao.proofOfBurn.allTxs=หลักฐานทั้งหมดในธุรกรรมที่ทำลายทิ้ง +dao.proofOfBurn.myItems=หลักฐานการทำลายธุรกรรมทิ้งของฉัน +dao.proofOfBurn.date=วันที่ +dao.proofOfBurn.hash=แฮช +dao.proofOfBurn.txs=การทำธุรกรรม +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Sign a message with key from proof of burn transaction +dao.proofOfBurn.verify.window.title=Verify a message with key from proof of burn transaction +dao.proofOfBurn.copySig=คัดลอกลายเซ็นไปยังคลิปบอร์ด +dao.proofOfBurn.sign=ลงนาม +dao.proofOfBurn.message=ข้อความ +dao.proofOfBurn.sig=ลายเซ็น +dao.proofOfBurn.verify=ยืนยัน +dao.proofOfBurn.verificationResult.ok=การตรวจสอบดำเนินการเรียบร้อยแล้ว +dao.proofOfBurn.verificationResult.failed=การตรวจสอบล้มเหลว + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=ระยะเสนอ +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=พักก่อนการโหวตแบบไม่ระบุตัวตน +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=ขั้นตอนการลงคะแนนเสียงแบบไม่ระบุตัวตน +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=พักเบรกก่อนช่วงการประกาศผลโหวต +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=ขั้นตอนการประกาศผลโหวต +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=พักเบรกก่อนช่วงสรุปผล +# suppress inspection "UnusedProperty" +dao.phase.RESULT=ช่วงผลการลงคะแนนเสียง + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=ระยะเสนอ +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=โหวตแบบไม่ระบุตัวตน +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=การประกาศผลโหวต +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=ผลโหวต + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=คำขอสำหรับค่าสินไหมตอบแทน +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=ยื่นคำขอการชำระเงินคืน +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=ข้อเสนอสำหรับเกณฑ์การประกัน +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=ข้อเสนอสำหรับการถอดถอนสินทรัพย์ +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=ข้อเสนอสำหรับการเปลี่ยนข้อจำกัด +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=ข้อเสนอทั่วไป +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=ข้อเสนอในการริบการประกัน + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=คำขอค่าสินไหมทดแทน +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=ยื่นคำขอการชำระเงินคืน +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=ตำแหน่งการประกัน +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=กำลังลบ altcoin (เหรียญทางเลือก) +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=การเปลี่ยนพารามิเตอร์ +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=ข้อเสนอทั่วไป +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=การริบตัวประกัน + +dao.proposal.details=รายละเอียดข้อเสนอ +dao.proposal.selectedProposal=ข้อเสนอที่เลือก +dao.proposal.active.header=ข้อเสนอของวงจรปัจจุบัน +dao.proposal.active.remove.confirm=คุณแน่ใจหรือไม่ที่จะลบข้อเสนอนั้น ข้อเสนอดังกล่าวที่มีการชำระค่าธรรมเนียมแล้วจะสูญหายหลังจากการลบข้อเสนอ +dao.proposal.active.remove.doRemove=ใช่ ลบข้อเสนอของฉัน +dao.proposal.active.remove.failed=ไม่สามารถลบข้อเสนอได้ +dao.proposal.myVote.title=การโหวต +dao.proposal.myVote.accept=รับข้อเสนอ +dao.proposal.myVote.reject=ปฏิเสธข้อเสนอ +dao.proposal.myVote.removeMyVote=ละเว้นข้อเสนอ +dao.proposal.myVote.merit=น้ำหนักผลคะแนนเสียงจาก BSQ ที่ได้รับ +dao.proposal.myVote.stake=น้ำหนักผลการลงคะแนนเสียงจาก Stake (เหรียญที่ล็อคไว้สำหรับสิทธิ์ในการโหวต) +dao.proposal.myVote.revealTxId=ID ธุรกรรมการแสดงผลการลงคะแนน +dao.proposal.myVote.stake.prompt=Max. available stake for voting: {0} +dao.proposal.votes.header=Set stake for voting and publish your votes +dao.proposal.myVote.button=Publish votes +dao.proposal.myVote.setStake.description=After voting on all proposals you have to set your stake for voting by locking up BSQ. The more BSQ you lock up, the more weight your vote will have. \n\nBSQ locked up for voting will be unlocked again during the vote reveal phase. +dao.proposal.create.selectProposalType=เลือกประเภทข้อเสนอ +dao.proposal.create.phase.inactive=Please wait until the next proposal phase +dao.proposal.create.proposalType=ประเภทข้อเสนอ +dao.proposal.create.new=สร้างข้อเสนอใหม่ +dao.proposal.create.button=เสนอคำขอ +dao.proposal.create.publish=Publish proposal +dao.proposal.create.publishing=Proposal publishing is in progress ... +dao.proposal=ข้อเสนอ +dao.proposal.display.type=ประเภทข้อเสนอ +dao.proposal.display.name=Exact GitHub username +dao.proposal.display.link=Link to detailed info +dao.proposal.display.link.prompt=Link to proposal +dao.proposal.display.requestedBsq=จำนวนที่ต้องการใน BSQ +dao.proposal.display.txId=รหัสธุรกรรมของข้อเสนอ +dao.proposal.display.proposalFee=ค่าธรรมเนียมข้อเสนอ +dao.proposal.display.myVote=การลงคะแนนของฉัน +dao.proposal.display.voteResult=สรุปผลโหวต +dao.proposal.display.bondedRoleComboBox.label=ประเภทของตำแหน่งหลักประกัน +dao.proposal.display.requiredBondForRole.label=การประกันที่จำเป็นโดยหลักการ +dao.proposal.display.option=ตัวเลือก + +dao.proposal.table.header.proposalType=ประเภทข้อเสนอ +dao.proposal.table.header.link=ลิงค์ +dao.proposal.table.header.myVote=การลงคะแนนของฉัน +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=ลบออก +dao.proposal.table.icon.tooltip.removeProposal=ลบข้อเสนอของฉัน +dao.proposal.table.icon.tooltip.changeVote=ผลโหวตปัจจุบัน: ''{0}''. เปลี่ยนโหวตไปยัง: ''{1}'' + +dao.proposal.display.myVote.accepted=ได้รับการยืนยัน +dao.proposal.display.myVote.rejected=ปฏิเสธ +dao.proposal.display.myVote.ignored=ละเว้น +dao.proposal.display.myVote.unCounted=Vote was not included in result +dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Vote was invalid + +dao.proposal.voteResult.success=ได้รับการยืนยัน +dao.proposal.voteResult.failed=ปฏิเสธ +dao.proposal.voteResult.summary=ผลลัพธ์: {0}; เกณฑ์: {1} (ที่กำหนดไว้ต้อง> {2}); องค์ประชุม: {3} (ที่กำหนดไว้ต้อง> {4}) + +dao.proposal.display.paramComboBox.label=เลือกพารามิเตอร์เพื่อเปลี่ยนแปลง +dao.proposal.display.paramValue=ค่าพารามิเตอร์ + +dao.proposal.display.confiscateBondComboBox.label=เลือกรูปแบบการประกัน +dao.proposal.display.assetComboBox.label=ทรัพย์สินที่จะถอนออก + +dao.blindVote=การลงคะแนนเสียงแบบไม่ระบุตัวตน + +dao.blindVote.startPublishing=กำลังเผยแพร่การทำธุรกรรมโหวตแบบไม่ระบุตัวตน ... +dao.blindVote.success=Your blind vote transaction has been successfully published.\n\nPlease note, that you have to be online in the vote reveal phase so that your Bisq application can publish the vote reveal transaction. Without the vote reveal transaction your vote would be invalid! + +dao.wallet.menuItem.send=ส่ง +dao.wallet.menuItem.receive=รับ +dao.wallet.menuItem.transactions=การทำธุรกรรม + +dao.wallet.dashboard.myBalance=ยอดคงเหลือในกระเป๋าสตางค์ของฉัน + +dao.wallet.receive.fundYourWallet=Your BSQ receive address +dao.wallet.receive.bsqAddress=BSQ wallet address (Fresh unused address) + +dao.wallet.send.sendFunds=ส่งเงิน +dao.wallet.send.sendBtcFunds=ส่งเงินทุนที่ไม่ใช่เครือ BSQ (BTC) +dao.wallet.send.amount=จำนวนเงินใน BSQ +dao.wallet.send.btcAmount=ยอดรวมทั้งหมดใน BTC (เงินทุนที่ไม่ใช่ในเครือ BSQ) +dao.wallet.send.setAmount=กำหนดจำนวนเงินที่จะถอน (จำนวนเงินขั้นต่ำคือ {0}) +dao.wallet.send.receiverAddress=ที่อยู่ BSQ ของผู้รับ +dao.wallet.send.receiverBtcAddress=ที่อยู่ BTC ของผู้รับ +dao.wallet.send.setDestinationAddress=กรอกที่อยู่ปลายทางของคุณ +dao.wallet.send.send=ส่งเงิน BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=ส่งเงินทุน BTC +dao.wallet.send.sendFunds.headline=ยืนยันคำขอถอนเงิน +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=บล็อกที่ได้รับการพิสูจน์แล้วล่าสุด: {0} +dao.wallet.chainHeightSyncing=บล็อกที่กำลังรอดำเนินการ... ตรวจสอบแล้ว {0} จากบล็อกทั้งหมด {1} +dao.wallet.tx.type=หมวด + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=ไม่ได้กำหนด +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=จำไม่ได้ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=ธุรกรรม BSQ ที่ไม่ได้รับการยืนยัน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=ธุรกรรม BSQ ไม่ถูกต้อง +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=ธุรกรรมต้นกำเนิด +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=โอน BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=ได้รับ BSQ แล้ว +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=ส่ง BSQ แล้ว +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=ค่าธรรมเนียมการซื้อขาย +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=ค่าธรรมเนียมการขอค่าชดเชย +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=ค่าธรรมเนียมสำหรับการยื่นคำร้องขอการชำระเงินคืน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=ค่าธรรมเนียมสำหรับข้อเสนอ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=ค่าธรรมเนียมการโหวตแบบไม่ระบุตัวตน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=การประกาศผลโหวต +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=ล็อคสิทธิ์ในการประกัน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=ปลดล็อกสิทธิ์การประกัน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=ค่าธรรมเนียมในการลงบันทึกรายการทรัพย์สิน +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=การพิสูจน์หลักฐานในการทำลายทิ้ง (Proof of burn) +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Irregular + +dao.tx.withdrawnFromWallet=BTC withdrawn from wallet +dao.tx.issuanceFromCompReq=คำขอหรือการออกค่าสินไหมทดแทน +dao.tx.issuanceFromCompReq.tooltip=คำขอค่าสินไหมทดแทน ซึ่งนำไปสู่การออก BSQ ใหม่\nวันที่ออก: {0} +dao.tx.issuanceFromReimbursement=การออกคำสั่ง/การยื่นคำร้องขอการชำระเงินคืน +dao.tx.issuanceFromReimbursement.tooltip=การเรียกร้องขอการชำระเงินคืนซึ่งเป็นคำสั่งภายใต้ BSQ ฉบับใหม่\nวันที่เริ่มทำการ: {0} +dao.proposal.create.missingBsqFunds=You don''t have sufficient BSQ funds for creating the proposal. If you have an unconfirmed BSQ transaction you need to wait for a blockchain confirmation because BSQ is validated only if it is included in a block.\nMissing: {0} + +dao.proposal.create.missingBsqFundsForBond=You don''t have sufficient BSQ funds for this role. You can still publish this proposal, but you''ll need the full BSQ amount required for this role if it gets accepted. \nMissing: {0} + +dao.proposal.create.missingMinerFeeFunds=You don''t have sufficient BTC funds for creating the proposal transaction. All BSQ transactions require a miner fee in BTC.\nMissing: {0} + +dao.proposal.create.missingIssuanceFunds=You don''t have sufficient BTC funds for creating the proposal transaction. All BSQ transactions require a miner fee in BTC, and issuance transactions also require BTC for the requested BSQ amount ({0} Satoshis/BSQ).\nMissing: {1} + +dao.feeTx.confirm=ยืนยันการทำรายการ {0} +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=THE BISQ DAO +dao.news.bisqDAO.description=Just as the Bisq exchange is decentralized and censorship-resistant, so is its governance model - and the Bisq DAO and BSQ token are the tools that make it possible. +dao.news.bisqDAO.readMoreLink=Learn More About the Bisq DAO + +dao.news.pastContribution.title=MADE PAST CONTRIBUTIONS? REQUEST BSQ +dao.news.pastContribution.description=If you have contributed to Bisq please use the BSQ address below and make a request for taking part of the BSQ genesis distribution. +dao.news.pastContribution.yourAddress=Your BSQ Wallet Address +dao.news.pastContribution.requestNow=Request now + +dao.news.DAOOnTestnet.title=RUN THE BISQ DAO ON OUR TESTNET +dao.news.DAOOnTestnet.description=The mainnet Bisq DAO is not launched yet but you can learn about the Bisq DAO by running it on our testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Switch to DAO Testnet Mode +dao.news.DAOOnTestnet.firstSection.content=Switch to DAO Testnet from the Settings screen. +dao.news.DAOOnTestnet.secondSection.title=2. Acquire Some BSQ +dao.news.DAOOnTestnet.secondSection.content=Request BSQ on Slack or Buy BSQ on Bisq. +dao.news.DAOOnTestnet.thirdSection.title=3. Participate in a Voting Cycle +dao.news.DAOOnTestnet.thirdSection.content=Making proposals and voting on proposals to change various aspects of Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Explore a BSQ Block Explorer +dao.news.DAOOnTestnet.fourthSection.content=Since BSQ is just bitcoin, you can see BSQ transactions on our bitcoin block explorer. +dao.news.DAOOnTestnet.readMoreLink=Read the full documentation + +dao.monitor.daoState=DAO state +dao.monitor.proposals=Proposals state +dao.monitor.blindVotes=Blind votes state + +dao.monitor.table.peers=Peers +dao.monitor.table.conflicts=Conflicts +dao.monitor.state=สถานะ +dao.monitor.requestAlHashes=Request all hashes +dao.monitor.resync=Resync DAO state +dao.monitor.table.header.cycleBlockHeight=Cycle / block height +dao.monitor.table.cycleBlockHeight=Cycle {0} / block {1} +dao.monitor.table.seedPeers=Seed node: {0} + +dao.monitor.daoState.headline=DAO state +dao.monitor.daoState.table.headline=Chain of DAO state hashes +dao.monitor.daoState.table.blockHeight=Block height +dao.monitor.daoState.table.hash=Hash of DAO state +dao.monitor.daoState.table.prev=Previous hash +dao.monitor.daoState.conflictTable.headline=DAO state hashes from peers in conflict +dao.monitor.daoState.utxoConflicts=UTXO conflicts +dao.monitor.daoState.utxoConflicts.blockHeight=Block height: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Sum of all UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Sum of all BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. After restart the DAO state will resync. + +dao.monitor.proposal.headline=Proposals state +dao.monitor.proposal.table.headline=Chain of proposal state hashes +dao.monitor.proposal.conflictTable.headline=Proposal state hashes from peers in conflict + +dao.monitor.proposal.table.hash=Hash of proposal state +dao.monitor.proposal.table.prev=Previous hash +dao.monitor.proposal.table.numProposals=No. proposals + +dao.monitor.isInConflictWithSeedNode=Your local data is not in consensus with at least one seed node. Please resync the DAO state. +dao.monitor.isInConflictWithNonSeedNode=One of your peers is not in consensus with the network but your node is in sync with the seed nodes. +dao.monitor.daoStateInSync=Your local node is in consensus with the network + +dao.monitor.blindVote.headline=Blind votes state +dao.monitor.blindVote.table.headline=Chain of blind vote state hashes +dao.monitor.blindVote.conflictTable.headline=Blind vote state hashes from peers in conflict +dao.monitor.blindVote.table.hash=Hash of blind vote state +dao.monitor.blindVote.table.prev=Previous hash +dao.monitor.blindVote.table.numBlindVotes=No. blind votes + +dao.factsAndFigures.menuItem.supply=BSQ Supply +dao.factsAndFigures.menuItem.transactions=BSQ Transactions + +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=BSQ ที่ใช้งานได้ทั้งหมด +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt + +dao.factsAndFigures.supply.issued=BSQ issued +dao.factsAndFigures.supply.compReq=คำขอการชดเชย +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=BSQ ที่ได้ดำเนินการในส่วนธุรกรรมทั่วไป +dao.factsAndFigures.supply.compRequestIssueAmount=BSQ ที่ได้ดำเนินการสำหรับการเรียกร้องค่าชดเชย +dao.factsAndFigures.supply.reimbursementAmount=BSQ ที่ได้ดำเนินการสำหรับการเรียกร้องการชำระเงินคืน +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ burnt + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=ปริมาณการซื้อขาย +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=สถานะทั่วโลกของ BSQ ที่ล็อกไว้ +dao.factsAndFigures.supply.totalLockedUpAmount=ถูกล็อคไว้ในการประกัน +dao.factsAndFigures.supply.totalUnlockingAmount=การปลดล็อค BSQ จากการประกัน +dao.factsAndFigures.supply.totalUnlockedAmount=ปลดล็อค BSQ จากการประกันไว้แล้ว +dao.factsAndFigures.supply.totalConfiscatedAmount=ยึด BSQ จากการประกันไว้แล้ว +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=ธุรกรรมต้นกำเนิด +dao.factsAndFigures.transactions.genesisBlockHeight=ความสูงของบล็อกต้นกำเนิด (Genesis block) +dao.factsAndFigures.transactions.genesisTxId=ID การทำธุรกรรมต้นกำเนิด +dao.factsAndFigures.transactions.txDetails=สถิติในการทำธุรกรรม BSQ +dao.factsAndFigures.transactions.allTx=หมายเลขจำนวนธุรกรรม BSQ ทั้งหมด +dao.factsAndFigures.transactions.utxo=หมายเลขจำนวนธุรกรรมที่ยังใช้ไม่หมด (เงินทอน) +dao.factsAndFigures.transactions.compensationIssuanceTx=เลขที่ของการทำธุรกรรมสำหรับยื่นคำขอค่าตอบแทนทั้งหมด +dao.factsAndFigures.transactions.reimbursementIssuanceTx=เลขที่ของการทำธุรกรรมสำหรับการยื่นคำขอการชำระเงินคืนทั้งหมด +dao.factsAndFigures.transactions.burntTx=เลขที่การทำธุรกรรมของการชำระค่าธรรมเนียมทั้งหมด +dao.factsAndFigures.transactions.invalidTx=No. of all invalid transactions +dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=ยอดคงเหลือที่พร้อมใช้งาน + +contractWindow.title=รายละเอียดข้อพิพาท +contractWindow.dates=วันที่เสนอ / วันที่ซื้อขาย +contractWindow.btcAddresses=ที่อยู่ Bitcoin ผู้ซื้อ BTC / ผู้ขาย BTC +contractWindow.onions=ที่อยู่เครือข่ายผู้ซื้อ BTC / ผู้ขาย BTC +contractWindow.accountAge=Account age BTC buyer / BTC seller +contractWindow.numDisputes=เลขที่ข้อพิพาทผู้ซื้อ BTC / ผู้ขาย BTC +contractWindow.contractHash=สัญญา hash + +displayAlertMessageWindow.headline=ข้อมูลสำคัญ! +displayAlertMessageWindow.update.headline=ข้อมูลอัปเดตที่สำคัญ! +displayAlertMessageWindow.update.download=ดาวน์โหลด: +displayUpdateDownloadWindow.downloadedFiles=ไฟล์: +displayUpdateDownloadWindow.downloadingFile=กำลังดาวน์โหลด: {0} +displayUpdateDownloadWindow.verifiedSigs=ลายเซ็นยืนยันด้วยคีย์: +displayUpdateDownloadWindow.status.downloading=กำลังดาวน์โหลดไฟล์ ... +displayUpdateDownloadWindow.status.verifying=กำลังตรวจสอบลายเซ็น ... +displayUpdateDownloadWindow.button.label=ดาวน์โหลดตัวติดตั้งและยืนยันลายเซ็น +displayUpdateDownloadWindow.button.downloadLater=ดาวน์โหลดในภายหลัง +displayUpdateDownloadWindow.button.ignoreDownload=ไม่สนใจเวอร์ชั่นนี้ +displayUpdateDownloadWindow.headline=การอัปเดต Bisq ใหม่พร้อมแล้ว! +displayUpdateDownloadWindow.download.failed.headline=การดาวน์โหลดล้มเหลว +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=ดาวน์โหลดเวอร์ชั่นใหม่เรียบร้อยแล้วและได้รับการยืนยันลายเซ็นแล้ว\n\nโปรดเปิดสารบบดาวน์โหลด หลังจากนั้นปิดโปรแกรมและติดตั้งเวอร์ชั่นใหม่ +displayUpdateDownloadWindow.download.openDir=เปิดสารบบดาวน์โหลด + +disputeSummaryWindow.title=สรุป +disputeSummaryWindow.openDate=วันที่ยื่นการเปิดคำขอและความช่วยเหลือ +disputeSummaryWindow.role=บทบาทของผู้ค้า +disputeSummaryWindow.payout=การจ่ายเงินของจำนวนการซื้อขาย +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} รับการจ่ายเงินของปริมาณการซื้อขาย: +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=การชำระเงินที่กำหนดเอง +disputeSummaryWindow.payoutAmount.buyer=จำนวนเงินที่จ่ายของผู้ซื้อ +disputeSummaryWindow.payoutAmount.seller=จำนวนเงินที่จ่ายของผู้ขาย +disputeSummaryWindow.payoutAmount.invert=ใช้ผู้แพ้เป็นผู้เผยแพร่ +disputeSummaryWindow.reason=เหตุผลในการพิพาท +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=ปัญหา +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=การใช้งาน +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=การละเมิดโปรโตคอล +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=ไม่มีการตอบ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=การหลอกลวง +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=อื่น ๆ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=ธนาคาร +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=สรุปบันทึกย่อ +disputeSummaryWindow.addSummaryNotes=เพิ่มสรุปบันทึกย่อ: +disputeSummaryWindow.close.button=ปิดการยื่นคำขอและความช่วยเหลือ + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=คุณจำเป็นต้องยุติคำขอความช่วยเหลือคู่ค้าด้วย ! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline={0} กระเป๋าสตางค์ฉุกเฉิน +emptyWalletWindow.info=โปรดใช้ในกรณีฉุกเฉินเท่านั้นหากคุณไม่สามารถเข้าถึงเงินจาก UI ได้\n\nโปรดทราบว่าข้อเสนอแบบเปิดทั้งหมดจะถูกปิดโดยอัตโนมัติเมื่อใช้เครื่องมือนี้\n\nก่อนที่คุณจะใช้เครื่องมือนี้โปรดสำรองข้อมูลในสารบบข้อมูลของคุณ คุณสามารถดำเนินการได้ที่ \"บัญชี / การสำรองข้อมูล \" \n\nโปรดรายงานปัญหาของคุณและส่งรายงานข้อบกพร่องเกี่ยวกับ GitHub หรือที่ฟอรัม Bisq เพื่อให้เราสามารถตรวจสอบสิ่งที่เป็นสาเหตุของปัญหาได้ +emptyWalletWindow.balance=ยอดในกระเป๋าสตางค์ที่คงเหลือที่มีอยู่ +emptyWalletWindow.bsq.btcBalance=ยอดดุลของ Non-BSQ Satoshis + +emptyWalletWindow.address=ที่อยู่ปลายทางของคุณ +emptyWalletWindow.button=ส่งเงินทั้งหมด +emptyWalletWindow.openOffers.warn=คุณมีข้อเสนอแบบเปิดซึ่งจะถูกปลดออกในกรณีที่คุณทำให้ กระเป๋าสตางค์ไม่มีเงินเหลืออยู่เลย\nคุณแน่ใจหรือไม่ว่าต้องการให้กระเป๋าสตางค์ของคุณนั้นว่างเปล่า? +emptyWalletWindow.openOffers.yes=ใช่ ฉันแน่ใจ +emptyWalletWindow.sent.success=ยอดคงเหลือในกระเป๋าสตางค์ของคุณได้รับการโอนเรียบร้อยแล้ว + +enterPrivKeyWindow.headline=Enter private key for registration + +filterWindow.headline=แก้ไขรายการตัวกรอง +filterWindow.offers=ข้อเสนอที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=ข้อมูลบัญชีการซื้อขายที่ถูกกรอง: \nรูปแบบ: เครื่องหมายจุลภาค รายการของ [id วิธีการชำระเงิน | ด้านข้อมูล | มูลค่า] +filterWindow.bannedCurrencies=รหัสโค้ดสกุลเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) +filterWindow.bannedPaymentMethods=รหัส ID วิธีการชำระเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=ผู้ไกล่เกลี่ยที่ได้รับการคัดกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) +filterWindow.seedNode=แหล่งข้อมูลในโหนดเครือข่ายที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) +filterWindow.priceRelayNode=โหนดผลัดเปลี่ยนราคาที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) +filterWindow.btcNode=โหนด Bitcoin ที่ได้รับการกรองแล้ว (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ + พอร์ต) +filterWindow.preventPublicBtcNetwork=ป้องกันการใช้เครือข่าย Bitcoin สาธารณะ +filterWindow.disableDao=Disable DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Min. version required for DAO +filterWindow.disableTradeBelowVersion=Min. version required for trading +filterWindow.add=เพิ่มตัวกรอง +filterWindow.remove=ลบตัวกรอง +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=จำนวน BTC ต่ำสุด +offerDetailsWindow.min=(ต่ำสุด. {0}) +offerDetailsWindow.distance=(ระดับราคาจากราคาตลาด: {0}) +offerDetailsWindow.myTradingAccount=บัญชีการซื้อขายของฉัน +offerDetailsWindow.offererBankId=(รหัส ID ธนาคารของผู้สร้าง / BIC / SWIFT) +offerDetailsWindow.offerersBankName=(ชื่อธนาคารของผู้สร้าง) +offerDetailsWindow.bankId=รหัส ID ธนาคาร (เช่น BIC หรือ SWIFT) +offerDetailsWindow.countryBank=ประเทศของธนาคารของผู้สร้าง +offerDetailsWindow.commitment=ข้อผูกมัด +offerDetailsWindow.agree=ฉันเห็นด้วย +offerDetailsWindow.tac=ข้อตกลงและเงื่อนไข +offerDetailsWindow.confirm.maker=ยืนยัน: ยื่นข้อเสนอไปยัง{0} บิทคอยน์ +offerDetailsWindow.confirm.taker=ยืนยัน: รับข้อเสนอไปยัง {0} บิทคอยน์ +offerDetailsWindow.creationDate=วันที่สร้าง +offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=คำขอชำระเงิน: \n{0} + +selectDepositTxWindow.headline=เลือกรายการเงินฝากสำหรับกรณีพิพาท +selectDepositTxWindow.msg=ธุรกรรมเงินฝากไม่ได้เก็บไว้ในการซื้อขาย\nโปรดเลือกหนึ่งในธุรกรรม multisig (การรองรับหลายลายเซ็น) ที่มีอยู่จากกระเป๋าสตางค์ของคุณซึ่งเป็นรายการฝากเงินที่ใช้ในการซื้อขายที่มีเกิดความผิดพลาด\n\nคุณสามารถค้นหารายการที่ถูกต้องได้โดยการเปิดหน้าต่างรายละเอียดทางการซื้อขาย (คลิกที่ ID การซื้อขายในรายการ) และทำรายการธุรกรรมการชำระเงินค่าธรรมเนียมการซื้อขายต่อไปยังรายการถัดไปที่คุณเห็นรายการเงินฝาก multisig (ที่อยู่เริ่มต้นด้วย 3) ID ธุรกรรมนี้ควรปรากฏในรายการที่นำเสนอที่นี่ เมื่อคุณพบรายการถูกต้องเลือกรายการที่นี่และดำเนินต่อไป\n\nขออภัยในความไม่สะดวก แต่กรณีข้อผิดพลาดดังกล่าวควรเกิดขึ้นน้อยมากและในอนาคตเราจะพยายามหาวิธีที่ดีกว่าในการแก้ไข +selectDepositTxWindow.select=เลือกรายการเงินฝาก + +sendAlertMessageWindow.headline=ส่งการแจ้งเตือนทั่วโลก +sendAlertMessageWindow.alertMsg=ข้อความแจ้งเตือน +sendAlertMessageWindow.enterMsg=ใส่ข้อความ +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=หมายเลขเวอร์ชชั่นรุ่นใหม่ +sendAlertMessageWindow.send=ส่งการแจ้งเตือน +sendAlertMessageWindow.remove=นำการแจ้งเตือนออก + +sendPrivateNotificationWindow.headline=ส่งข้อความส่วนตัว +sendPrivateNotificationWindow.privateNotification=การแจ้งเตือนส่วนตัว +sendPrivateNotificationWindow.enterNotification=ป้อนการแจ้งเตือน +sendPrivateNotificationWindow.send=ส่งการแจ้งเตือนส่วนตัว + +showWalletDataWindow.walletData=ข้อมูล Wallet  +showWalletDataWindow.includePrivKeys=รวมคีย์ส่วนตัว + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=ข้อตกลงการใช้ +tacWindow.agree=ฉันเห็นด้วย +tacWindow.disagree=ฉันไม่เห็นด้วยและออก +tacWindow.arbitrationSystem=Dispute resolution + +tradeDetailsWindow.headline=ซื้อขาย +tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท: +tradeDetailsWindow.tradeDate=วันที่ซื้อขาย +tradeDetailsWindow.txFee=ค่าธรรมเนียมการขุด +tradeDetailsWindow.tradingPeersOnion=ที่อยู่ของ onion คู่ค้า +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=สถานะการค้า +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=ป้อนรหัสผ่านเพื่อปลดล็อก + +torNetworkSettingWindow.header=ตั้งค่าเครือข่าย Tor +torNetworkSettingWindow.noBridges=อย่าใช้สะพาน +torNetworkSettingWindow.providedBridges=เชื่อมต่อกับสะพานที่ให้ไว้ +torNetworkSettingWindow.customBridges=ป้อนสะพานที่กำหนดเอง +torNetworkSettingWindow.transportType=ประเภทการขนส่ง +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (แนะนำ) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=ป้อนหนึ่งสะพานผลัดเปลี่ยนหรือหลายรายการ (หนึ่งรายการต่อบรรทัด) +torNetworkSettingWindow.enterBridgePrompt=ที่อยู่ประเภท: พอร์ต +torNetworkSettingWindow.restartInfo=คุณต้องรีสตาร์ทใหม่เพื่อใช้การเปลี่ยนแปลง +torNetworkSettingWindow.openTorWebPage=เปิดหน้าเว็บของโครงการ Tor +torNetworkSettingWindow.deleteFiles.header=กำลังเจอปัญหาการเชื่อมต่ออยู่หรือ +torNetworkSettingWindow.deleteFiles.info=ถ้าคุณมีปัญหาการเชื่อมต่อซ้ำเมื่อเริ่มต้น การลบไฟล์ Tor ที่ล้าสมัยอาจช่วยได้ เมื่อต้องการทำเช่นนั้นคลิกที่ปุ่มด้านล่างและรีสตาร์ทใหม่ +torNetworkSettingWindow.deleteFiles.button=ลบไฟล์ Tor ที่ล้าสมัยออกแล้วปิดลง +torNetworkSettingWindow.deleteFiles.progress=ปิด Tor ที่กำลังดำเนินอยู่ +torNetworkSettingWindow.deleteFiles.success=ไฟล์ Tor ที่ล้าสมัยถูกลบแล้ว โปรดรีสตาร์ท +torNetworkSettingWindow.bridges.header=Tor ถูกบล็อกหรือไม่ +torNetworkSettingWindow.bridges.info=ถ้า Tor ถูกปิดกั้นโดยผู้ให้บริการอินเทอร์เน็ตหรือประเทศของคุณ คุณสามารถลองใช้ Tor bridges\nไปที่หน้าเว็บของ Tor ที่ https://bridges.torproject.org/bridges เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับสะพานและการขนส่งแบบ pluggable + +feeOptionWindow.headline=เลือกสกุลเงินสำหรับการชำระค่าธรรมเนียมการซื้อขาย +feeOptionWindow.info=คุณสามารถเลือกที่จะชำระค่าธรรมเนียมทางการค้าใน BSQ หรือใน BTC แต่ถ้าคุณเลือก BSQ คุณจะได้รับส่วนลดค่าธรรมเนียมการซื้อขาย +feeOptionWindow.optionsLabel=เลือกสกุลเงินสำหรับการชำระค่าธรรมเนียมการซื้อขาย +feeOptionWindow.useBTC=ใช้ BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=การแจ้งเตือน +popup.headline.instruction=โปรดทราบ: +popup.headline.attention=ฟังทางนี้ด้วย +popup.headline.backgroundInfo=ข้อมูลพื้นฐาน +popup.headline.feedback=เสร็จสิ้น +popup.headline.confirmation=การยืนยัน +popup.headline.information=ข้อมูล +popup.headline.warning=คำเตือน +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 bisq.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} + +error.spvFileCorrupted=เกิดข้อผิดพลาดขณะอ่านไฟล์ chain SPV \nอาจเป็นเพราะไฟล์ chain SPV เสียหาย\n\nเกิดข้อผิดพลาด: {0} \n\nคุณต้องการลบและเริ่มการซิงค์ใหม่หรือไม่ +error.deleteAddressEntryListFailed=ไม่สามารถลบไฟล์ AddressEntryList ได้\nข้อผิดพลาด: {0} +error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. +error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. + +popup.warning.walletNotInitialized=wallet ยังไม่ได้เริ่มต้น +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=คุณอาจมีเวอร์ชั่น Bisq ไม่เหมาะสำหรับคอมพิวเตอร์นี้\nสถาปัตยกรรมคอมพิวเตอร์ของคุณคือ: {0} .\nเลขฐานสอง Bisq ที่คุณติดตั้งคือ: {1} .\nโปรดปิดตัวลงและติดตั้งรุ่นที่ถูกต้องอีกครั้ง ({2}) +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq กำลังทำงานอยู่ คุณไม่สามารถเรียกใช้ Bisq พร้อมกันได้ +popup.warning.tradePeriod.halfReached=การซื้อขายของคุณที่มีรหัส ID {0} ได้ถึงครึ่งหนึ่งของจำนวนสูงสุดแล้ว อนุญาตให้ซื้อขายได้และยังไม่สมบูรณ์\n\nช่วงเวลาการซื้อขายสิ้นสุดวันที่ {1} \n\nโปรดตรวจสอบสถานะการค้าของคุณที่ \"Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" สำหรับข้อมูลเพิ่มเติม +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the mediator. +popup.warning.noTradingAccountSetup.headline=คุณยังไม่ได้ตั้งค่าบัญชีการซื้อขาย +popup.warning.noTradingAccountSetup.msg=คุณต้องตั้งค่าสกุลเงินประจำชาติหรือบัญชี altcoin (เหรียญทางเลือก) ก่อนจึงจะสามารถสร้างข้อเสนอได้\nคุณต้องการตั้งค่าบัญชีหรือไม่ +popup.warning.noArbitratorsAvailable=ไม่มีผู้ไกล่เกลี่ยสำหรับทำการ +popup.warning.noMediatorsAvailable=There are no mediators available. +popup.warning.notFullyConnected=คุณต้องรอจนกว่าคุณจะเชื่อมต่อกับเครือข่ายอย่างสมบูรณ์\nอาจใช้เวลาประมาณ 2 นาทีเมื่อเริ่มต้น +popup.warning.notSufficientConnectionsToBtcNetwork=คุณต้องรอจนกว่าจะมีการเชื่อมต่อกับเครือข่าย Bitcoin อย่างน้อย {0} รายการ +popup.warning.downloadNotComplete=คุณต้องรอจนกว่าการดาวน์โหลดบล็อค Bitcoin ที่ขาดหายไปจะเสร็จสมบูรณ์ +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=คุณแน่ใจหรือไม่ว่าต้องการนำข้อเสนอนั้นออก\nค่าธรรมเนียมของผู้สร้าง {0} จะสูญหายไปหากคุณนำข้อเสนอนั้นออก +popup.warning.tooLargePercentageValue=คุณไม่สามารถกำหนดเปอร์เซ็นต์เป็น 100% หรือมากกว่าได้ +popup.warning.examplePercentageValue=โปรดป้อนตัวเลขเปอร์เซ็นต์เช่น \"5.4 \" เป็น 5.4% +popup.warning.noPriceFeedAvailable=ไม่มีฟีดราคาสำหรับสกุลเงินดังกล่าว คุณไม่สามารถใช้ราคาตามเปอร์เซ็นต์ได้\nโปรดเลือกราคาที่ถูกกำหนดไว้แแล้ว +popup.warning.sendMsgFailed=การส่งข้อความไปยังคู่ค้าของคุณล้มเหลว\nโปรดลองอีกครั้งและหากยังคงเกิดขึ้นขึ้นเนื่อง โปรดรายงานข้อผิดพลาดต่อไป +popup.warning.insufficientBtcFundsForBsqTx=คุณไม่มีเงินทุน BTC เพียงพอสำหรับการจ่ายค่าธรรมเนียมขุดสำหรับการทำธุรกรรมดังกล่าว\nกรุณาใส่เงินในกระเป๋าสตางค์ BTC ของคุณ\nเงินขาดไป: {0} +popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. +popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=กระเป๋าสตางค์ BSQ ของคุณไม่มีจำนวนเงินทุนที่มากพอสำหรับการชำระการเทรดใน BSQ +popup.warning.messageTooLong=ข้อความของคุณเกินขีดจำกัดสูงสุดที่อนุญาต โปรดแบ่งส่งเป็นหลายส่วนหรืออัปโหลดไปยังบริการเช่น https://pastebin.com +popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=ราคาผลัดเปลี่ยน +popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล +popup.warning.mandatoryUpdate.trading=Please update to the latest Bisq version. A mandatory update was released which disables trading for old versions. Please check out the Bisq Forum for more information. +popup.warning.mandatoryUpdate.dao=Please update to the latest Bisq version. A mandatory update was released which disables the Bisq DAO and BSQ for old versions. Please check out the Bisq Forum for more information. +popup.warning.disable.dao=The Bisq DAO and BSQ are temporary disabled. Please check out the Bisq Forum for more information. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 BTC to transfer. + +popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Bitcoin 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 Bisq support channel at the Bisq Keybase team. + +popup.warning.trade.txRejected.tradeFee=trade fee +popup.warning.trade.txRejected.deposit=deposit +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.info.securityDepositInfo=เพื่อให้แน่ใจว่าเทรดเดอร์ทั้งคู่นั้นได้ปฏิบัติตามข้อสนธิสัญญาในการค้า เทรดเดอร์จำเป็นต้องทำการชำระค่าประกัน\n\nค่าประกันนี้คือถูกเก็บไว้ในกระเป๋าสตางค์การเทรดของคุณจนกว่าการเทรดของคุณจะดำเนินการสำเร็จ และคุณจะได้รับมันคืนหลังจากนั้น \n\nโปรดทราบ: หากคุณกำลังสร้างข้อเสนอขึ้นมาใหม่ Bisq จำเป็นที่ต้องดำเนินงานต่อเนื่องไปยังเทรดเดอร์รายอื่น และเพื่อที่สถานะข้อเสนอทางออนไลน์ของคุณจะยังคงอยู่ Bisq จะยังคงดำเนินงานต่อเนื่อง และโปรดมั่นใจว่าเครื่องคอมพิวเตอร์นี้กำลังออนไลน์อยู่ด้วยเช่นกัน (ยกตัวอย่างเช่น ตรวจเช็คว่าสวิทช์ไฟไม่ได้อยู่ในโหมดแสตนบายด์...หน้าจอแสตนบายด์คือปกติดี) + +popup.info.cashDepositInfo=โปรดตรวจสอบว่าคุณมีสาขาธนาคารในพื้นที่ของคุณเพื่อสามารถฝากเงินได้\nรหัสธนาคาร (BIC / SWIFT) ของธนาคารผู้ขายคือ: {0} +popup.info.cashDepositInfo.confirm=ฉันยืนยันว่าฉันสามารถฝากเงินได้ +popup.info.shutDownWithOpenOffers=Bisq คือกำลังจะปิดลง แต่ยังคงมีการเปิดขายข้อเสนอปกติ\nข้อเสนอเหล่านี้จะไม่ใข้งานได้บนเครือข่าย P2P network ในขณะที่ Bisq ปิดตัวลง แต่จะมีการเผยแพร่บนเครือข่าย P2P ครั้งถัดไปเมื่อคุณมีการเริ่มใช้งาน Bisq.\n\nในการคงสถานะข้อเสนอแบบออนไลน์ คือเปิดใข้งาน Bisq และทำให้มั่นใจว่าคอมพิวเตอร์เครื่องนี้กำลังออนไลน์อยู่ด้วยเช่นกัน (เช่น ตรวจสอบว่าคอมพิวเตอร์ไม่ได้อยู่ในโหมดแสตนบายด์...หน้าจอแสตนบายด์ไม่มีปัญหา) +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=การแจ้งเตือนส่วนตัวที่สำคัญ! + +popup.securityRecommendation.headline=ข้อเสนอแนะด้านความปลอดภัยที่สำคัญ +popup.securityRecommendation.msg=เราขอแจ้งเตือนให้คุณพิจารณาใช้การป้องกันด้วยรหัสผ่านสำหรับ wallet ของคุณ หากยังไม่ได้เปิดใช้งาน\n\nขอแนะนำให้เขียนรหัสลับป้องกัน wallet รหัสลับเหล่านี้เหมือนกับรหัสผ่านหลักสำหรับการกู้คืน Bitcoin wallet ของคุณ\nไปที่ \"กระเป๋าสตางค์ \" คุณจะพบข้อมูลเพิ่มเติม\n\nนอกจากนี้คุณควรสำรองโฟลเดอร์ข้อมูลแอ็พพลิเคชั่นทั้งหมดไว้ที่ส่วน \"สำรองข้อมูล \" + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=การปิดระบบอยู่ระหว่างดำเนินการ +popup.shutDownInProgress.msg=การปิดแอพพลิเคชั่นอาจใช้เวลาสักครู่\nโปรดอย่าขัดจังหวะกระบวนการนี้ + +popup.attention.forTradeWithId=ต้องให้ความสำคัญสำหรับการซื้อขายด้วย ID {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available +popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. + +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.signAll=Sign all payment methods +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=การแจ้งเตือนการซื้อขายด้วย ID {0} +notification.ticket.headline=ศูนย์ช่วยเหลือสนับสนุนการซื้อขายด้วย ID {0} +notification.trade.completed=การค้าเสร็จสิ้นแล้วและคุณสามารถถอนเงินของคุณได้ +notification.trade.accepted=ข้อเสนอของคุณได้รับการยอมรับจาก BTC {0} แล้ว +notification.trade.confirmed=การซื้อขายของคุณมีการยืนยัน blockchain อย่างน้อยหนึ่งรายการ\nคุณสามารถเริ่มการชำระเงินได้เลย +notification.trade.paymentStarted=ผู้ซื้อ BTC ได้เริ่มการชำระเงินแล้ว +notification.trade.selectTrade=เลือกการซื้อขาย +notification.trade.peerOpenedDispute=เครือข่ายทางการค้าของคุณได้เริ่มต้นเปิดที่ {0} +notification.trade.disputeClosed={0} ถูกปิดแล้ว +notification.walletUpdate.headline=อัพเดตกระเป๋าสตางค์การซื้อขาย +notification.walletUpdate.msg=กระเป๋าสตางค์ของคุณได้รับเงินเพียงพอ\nจำนวนเงิน: {0} +notification.takeOffer.walletUpdate.msg=wallet ของคุณได้รับการสนับสนุนจากการเสนอราคาก่อนหน้านี้\nจำนวนเงิน: {0} +notification.tradeCompleted.headline=การื้อขายเสร็จสิ้น +notification.tradeCompleted.msg=คุณสามารถถอนเงินของคุณตอนนี้ไปยังแหล่งเงินกระเป๋าสตางค์นอก Bitcoin ของคุณหรือโอนเงินไปที่กระเป๋าสตางค์ของ Bisq + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=แสดงหน้าต่างแอ็พพลิเคชั่น +systemTray.hide=ซ่อนหน้าต่างแอ็พพลิเคชั่น +systemTray.info=ข้อมูลเกี่ยวกับ Bisq +systemTray.exit=ออก +systemTray.tooltip=Bisq: A decentralized bitcoin exchange network + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=บัญชีการค้าที่บันทึกไว้ในเส้นทาง: \n{0} +guiUtil.accountExport.noAccountSetup=คุณไม่มีบัญชีการซื้อขายที่ตั้งค่าไว้สำหรับการส่งออก +guiUtil.accountExport.selectPath=เลือกเส้นทางไปที่ {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=บัญชีการซื้อขายด้วยรหัส ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=เราไม่ได้นำเข้าบัญชีการซื้อขายที่มี id {0} เนื่องจากมีอยู่แล้วในระบบ\n +guiUtil.accountExport.exportFailed=การส่งออกไปยัง CSV ล้มเหลวเนื่องจากข้อผิดพลาด\nข้อผิดพลาด = {0} +guiUtil.accountExport.selectExportPath=เลือกเส้นทางการส่งออก +guiUtil.accountImport.imported=บัญชีการซื้อขายที่นำเข้าจากเส้นทาง: \n{0} \n\nบัญชีที่นำเข้า: \n{1} +guiUtil.accountImport.noAccountsFound=ไม่พบบัญชีการค้าที่ส่งออกระหว่างทาง: {0} .\nชื่อไฟล์คือ {1} " +guiUtil.openWebBrowser.warning=คุณกำลังจะเปิดเว็บเพจในเว็บเบราเซอร์ของระบบ\nคุณต้องการเปิดเว็บเพจตอนนี้หรือไม่\n\nหากคุณไม่ได้ใช้ \"Tor Browser \" เป็นเว็บเบราเซอร์ระบบเริ่มต้นของคุณ คุณจะเชื่อมต่อกับเว็บเพจโดยไม่ต้องแจ้งให้ทราบล่วงหน้า\n\nURL: \"{0} \" +guiUtil.openWebBrowser.doOpen=เปิดหน้าเว็บและไม่ต้องถามอีก +guiUtil.openWebBrowser.copyUrl=คัดลอก URL และยกเลิก +guiUtil.ofTradeAmount=ของปริมาณการซื้อขาย +guiUtil.requiredMinimum=(required minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=เลือกสกุลเงิน +list.currency.showAll=แสดงทั้งหมด +list.currency.editList=แก้ไขรายการสกุลเงิน + +table.placeholder.noItems=ขณะนี้ไม่มี {0} พร้อมใช้งาน +table.placeholder.noData=ขณะนี้ไม่มีข้อมูลที่พร้อมใช้งาน +table.placeholder.processingData=Processing data... + + +peerInfoIcon.tooltip.tradePeer=การซื้อขายของระบบเน็ตเวิร์ก peer +peerInfoIcon.tooltip.maker=ของผู้สร้าง +peerInfoIcon.tooltip.trade.traded={0} ที่อยู่ onion : {1} \nคุณได้ทำการซื้อขาย {2} ครั้ง(หลาย)แล้วด้วย peer นั้น\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} ที่อยู่ onion: {1} \nคุณยังไม่เคยทำการซื้อขายกับคู่ peer นั้นมาก่อน\n{2} +peerInfoIcon.tooltip.age=บัญชีการชำระเงินที่สร้างขึ้น {0} ที่ผ่านมา +peerInfoIcon.tooltip.unknownAge=อายุบัญชีการชำระเงินที่ไม่รู้จัก + +tooltip.openPopupForDetails=เปิดป๊อปอัปเพื่ออ่านรายละเอียด +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=เปิดตัวสำรวจ blockchain ภายนอกตามที่อยู่: {0} +tooltip.openBlockchainForTx=เปิดตัวสำรวจ blockchain ภายนอกสำหรับธุรกรรม: {0} + +confidence.unknown=สถานะธุรกรรมที่ไม่รู้จัก +confidence.seen=เห็นโดย {0} peer (s) / 0 การยืนยัน +confidence.confirmed=ยืนยันใน {0} บล็อก(หลายอัน) +confidence.invalid=ธุรกรรมไม่ถูกต้อง + +peerInfo.title=ข้อมูล Peer +peerInfo.nrOfTrades=จำนวนการซื้อขายที่เสร็จสิ้นแล้ว +peerInfo.notTradedYet=คุณยังไม่เคยซื้อขายกับผู้ใช้รายนั้น +peerInfo.setTag=ตั้งค่าแท็กสำหรับ peer นั้น +peerInfo.age.noRisk=อายุบัญชีการชำระเงิน +peerInfo.age.chargeBackRisk=Time since signing +peerInfo.unknownAge=อายุ ที่ไม่ที่รู้จัก + +addressTextField.openWallet=เปิดกระเป๋าสตางค์ Bitcoin เริ่มต้นของคุณ +addressTextField.copyToClipboard=คัดลอกที่อยู่ไปยังคลิปบอร์ด +addressTextField.addressCopiedToClipboard=ที่อยู่ถูกคัดลอกไปยังคลิปบอร์ดแล้ว +addressTextField.openWallet.failed=การเปิดแอปพลิเคชั่นเริ่มต้นกระเป๋าสตางค์ Bitcoin ล้มเหลว บางทีคุณอาจยังไม่ได้ติดตั้งไว้ + +peerInfoIcon.tooltip={0} \nแท็ก: {1} + +txIdTextField.copyIcon.tooltip=คัดลอก ID ธุรกรรมไปยังคลิปบอร์ด +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"บัญชี\" +navigation.account.walletSeed=\ "บัญชี / รหัสลับป้องกันกระเป๋าสตางค์\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"แฟ้มผลงาน / ข้อเสนอของฉัน \" +navigation.portfolio.pending=\"แฟ้มผลงาน / เปิดการซื้อขาย \" +navigation.portfolio.closedTrades=\"แฟ้มผลงาน/ประวัติ\" +navigation.funds.depositFunds=\"เงิน / รับเงิน \" +navigation.settings.preferences=\ "การตั้งค่า / สิ่งที่ชอบ \" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"เงิน / ธุรกรรม\" +navigation.support=\"ช่วยเหลือและสนับสนุน\" +navigation.dao.wallet.receive=\ "Wallet DAO / BSQ / การรับ \" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} จำนวนยอด{1} +formatter.makerTaker=ผู้สร้าง เป็น {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.fiat=คุณกำลังสร้างข้อเสนอให้ {0} {1} +formatter.youAreCreatingAnOffer.altcoin=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) +formatter.asMaker={0} {1} ในฐานะผู้สร้าง +formatter.asTaker={0} {1} ในฐานะคนรับ + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=ปี +time.month=เดือน +time.week=สัปดาห์ +time.day=วัน +time.hour=ชั่วโมง +time.minute10=10 นาที +time.hours=ชั่วโมง +time.days=วัน +time.1hour=1 ชั่วโมง +time.1day=1 วัน +time.minute=นาที +time.second=วินาที +time.minutes=นาที +time.seconds=วินาที + + +password.enterPassword=ใส่รหัสผ่าน +password.confirmPassword=ยืนยันรหัสผ่าน +password.tooLong=รหัสผ่านต้องมีอักขระไม่เกิน 500 ตัว +password.deriveKey=ดึงข้อมูลจากรหัสผ่าน +password.walletDecrypted=กระเป๋าสตางค์ถูกถอดรหัสสำเร็จและการป้องกันรหัสผ่านได้มีการออกแล้ว +password.wrongPw=คุณป้อนรหัสผ่านไม่ถูกต้อง\n\nโปรดลองป้อนรหัสผ่านอีกครั้งโดยละเอียด เพื่อตรวจสอบความผิดพลาดในการพิมพ์หรือสะกด +password.walletEncrypted=เปิดใช้งานกระเป๋าสตางค์ที่เข้ารหัสแล้วและเปิดใช้งานการป้องกันด้วยรหัสผ่านแล้ว +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=รหัสผ่าน 2 รายการที่คุณป้อนไม่ตรงกัน +password.forgotPassword=ลืมรหัสผ่านหรือเปล่า? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=รหัสลับป้องกันกระเป๋าสตางค์ +seed.enterSeedWords=ป้อนรหัสลับกระเป๋าสตางค์ +seed.date=วันที่ในกระเป๋าสตางค์ +seed.restore.title=เรียกคืนกระเป๋าสตางค์จากรหัสลับ +seed.restore=เรียกกระเป๋าสตางค์คืน +seed.creationDate=วันที่สร้าง +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=ฉันต้องการเรียกคืนอีกครั้ง +seed.warn.walletNotEmpty.emptyWallet=ฉันจะทำให้กระเป๋าสตางค์ของฉันว่างเปล่าก่อน +seed.warn.notEncryptedAnymore=กระเป๋าสตางค์ของคุณได้รับการเข้ารหัสแล้ว\n\nหลังจากเรียกคืน wallets จะไม่ได้รับการเข้ารหัสและคุณต้องตั้งรหัสผ่านใหม่\n\nคุณต้องการดำเนินการต่อหรือไม่ +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=กระเป๋าสตางค์ได้รับการกู้คืนข้อมูลด้วยรหัสลับเพื่อป้องกันและกู้คืนกระเป๋าสตางค์ด้วยรหัสลับใหม่แล้ว\n\nคุณจำเป็นต้องปิดและรีสตาร์ทแอ็พพลิเคชั่น +seed.restore.error=เกิดข้อผิดพลาดขณะกู้คืนกระเป๋าสตางค์ด้วยรหัสลับ {0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=บัญชี +payment.account.no=หมายเลขบัญชี +payment.account.name=ชื่อบัญชี +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=ชื่อเต็มของเจ้าของบัญชี +payment.account.fullName=ชื่อเต็ม (ชื่อจริง, ชื่อกลาง, นามสกุล) +payment.account.state=รัฐ / จังหวัด / ภูมิภาค +payment.account.city=เมือง +payment.bank.country=ประเทศของธนาคาร +payment.account.name.email=ชื่อเต็มของเจ้าของบัญชี / อีเมล +payment.account.name.emailAndHolderId=ชื่อเต็มของเจ้าของบัญชี / อีเมล / {0} +payment.bank.name=ชื่อธนาคาร +payment.select.account=เลือกประเภทบัญชี +payment.select.region=เลือกภูมิภาค +payment.select.country=เลือกประเทศ +payment.select.bank.country=เลือกประเทศของธนาคาร +payment.foreign.currency=คุณแน่ใจหรือไม่ว่าต้องการเลือกสกุลเงินอื่นที่ไม่ใช่สกุลเงินเริ่มต้นของประเทศ +payment.restore.default=ไม่ เรียกคืนสกุลเงินเริ่มต้น +payment.email=อีเมล +payment.country=ประเทศ +payment.extras=ข้อกำหนดเพิ่มเติม +payment.email.mobile=อีเมลหรือหมายเลขโทรศัพท์มือถือ +payment.altcoin.address=ที่อยู่ Altcoin (เหรียญทางเลือก) +payment.altcoin.tradeInstantCheckbox=Trade instant (within 1 hour) with this Altcoin +payment.altcoin.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able to complete the trade in less than 1 hour.\n\nIf you have offers open and you are not available please disable those offers under the 'Portfolio' screen. +payment.altcoin=Altcoin (เหรียญทางเลือก) +payment.select.altcoin=Select or search Altcoin +payment.secret=คำถามลับ +payment.answer=คำตอบ +payment.wallet=ID กระเป๋าสตางค์ +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=ชื่อผู้ใช้ หรือ อีเมล หรือ หมายเลขโทรศัพท์ +payment.moneyBeam.accountId=อีเมลหรือหมายเลขโทรศัพท์ +payment.venmo.venmoUserName=ชื่อผู้ใช้ Venmo +payment.popmoney.accountId=อีเมลหรือหมายเลขโทรศัพท์ +payment.promptPay.promptPayId=รหัสบัตรประชาชน/รหัสประจำตัวผู้เสียภาษี หรือเบอร์โทรศัพท์ +payment.supportedCurrencies=สกุลเงินที่ได้รับการสนับสนุน +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=ข้อจำกัด +payment.salt=ข้อมูลแบบสุ่มสำหรับการตรวจสอบอายุบัญชี +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=ยอมรับการซื้อขายจากประเทศยุโรปเหล่านี้ +payment.accept.nonEuro=ยอมรับการซื้อขายจากประเทศนอกสหภาพยุโรปเหล่านี้ +payment.accepted.countries=ประเทศที่ยอมรับ +payment.accepted.banks=ธนาคารที่ยอมรับ (ID) +payment.mobile=เบอร์มือถือ +payment.postal.address=รหัสไปรษณีย์ +payment.national.account.id.AR=หมายเลข CBU +shared.accountSigningState=Account signing status + +#new +payment.altcoin.address.dyn={0} ที่อยู่ +payment.altcoin.receiver.address=ที่อยู่เหรียญทางเลือกของผู้รับ +payment.accountNr=หมายเลขบัญชี +payment.emailOrMobile=อีเมลหรือหมายเลขโทรศัพท์มือถือ +payment.useCustomAccountName=ใช้ชื่อบัญชีที่กำหนดเอง +payment.maxPeriod=ระยะเวลาสูงสุดการค้าที่อนุญาต +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} +payment.maxPeriodAndLimitCrypto=ระยะเวลาสูงสุดทางการค้า: {0} / ขีดจำกัดสูงสุดทางการค้า: {1} +payment.currencyWithSymbol=สกุลเงิน: {0} +payment.nameOfAcceptedBank=ชื่อธนาคารที่ได้รับการยอมรับ +payment.addAcceptedBank=เพิ่มธนาคารที่ได้รับการยอมรับ +payment.clearAcceptedBanks=เคลียร์ธนาคารที่ได้รับการยอมรับ +payment.bank.nameOptional=ชื่อธนาคาร (สามารถเลือกได้) +payment.bankCode=รหัสธนาคาร +payment.bankId=รหัสธนาคาร (BIC / SWIFT) +payment.bankIdOptional=รหัสธนาคาร (BIC / SWIFT) (ไม่จำเป็นต้องกรอก) +payment.branchNr=เลขที่สาขา +payment.branchNrOptional=เลขที่สาขา (สามารถเลือกได้) +payment.accountNrLabel=เลขที่บัญชี (IBAN) +payment.accountType=ประเภทบัญชี +payment.checking=การตรวจสอบ +payment.savings=ออมทรัพย์ +payment.personalId=รหัส ID ประจำตัวบุคคล +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 ผู้ซื้อ BTC จำเป็นต้องส่งรหัส Halcash ให้กับผู้ขายทางข้อความโทรศัพท์มือถือ\n\nโปรดตรวจสอบว่าไม่เกินจำนวนเงินสูงสุดที่ธนาคารของคุณอนุญาตให้คุณส่งด้วย HalCash จำนวนเงินขั้นต่ำในการเบิกถอนคือ 10 EUR และสูงสุดในจำนวนเงิน 600 EUR สำหรับการถอนซ้ำเป็น 3000 EUR ต่อผู้รับและต่อวัน และ 6000 EUR ต่อผู้รับและต่อเดือน โปรดตรวจสอบข้อจำกัดจากทางธนาคารคุณเพื่อให้มั่นใจได้ว่าทางธนาคารได้มีการใช้มาตรฐานข้อกำหนดเดียวกันกับดังที่ระบุไว้ ณ ที่นี่\n\nจำนวนเงินที่ถอนจะต้องเป็นจำนวนเงินหลาย 10 EUR เนื่องจากคุณไม่สามารถถอนเงินอื่น ๆ ออกจากตู้เอทีเอ็มได้ UI ในหน้าจอสร้างข้อเสนอและรับข้อเสนอจะปรับจำนวนเงิน BTC เพื่อให้จำนวนเงิน EUR ถูกต้อง คุณไม่สามารถใช้ราคาตลาดเป็นจำนวนเงิน EUR ซึ่งจะเปลี่ยนแปลงไปตามราคาที่มีการปรับเปลี่ยน\n\nในกรณีที่มีข้อพิพาทผู้ซื้อ BTC ต้องแสดงหลักฐานว่าได้ส่ง 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/Account_limits]. + +payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=ข้อมูลติดต่อ +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=ข้อมูลติดต่อ +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/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.japan.bank=ธนาคาร +payment.japan.branch=Branch +payment.japan.account=บัญชี +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=การโอนเงินผ่านธนาคารแห่งชาติ +SAME_BANK=โอนเงินผ่านธนาคารเดียวกัน +SPECIFIC_BANKS=การโอนเงินกับธนาคารเฉพาะ +US_POSTAL_MONEY_ORDER=US Postal Money Order ใบสั่งซื้อทางไปรษณีย์ของสหรัฐฯ +CASH_DEPOSIT=ฝากเงินสด +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=เห็นหน้ากัน (แบบตัวต่อตัว) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=ธนาคารแห่งชาติ +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=ธนาคารเดียวกัน +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=ธนาคารเฉพาะ +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=US Money Order ใบสั่งทางการเงินของสหรัฐฯ +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=ฝากเงินสด +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=การชำระเงินทันใจของ SEPA +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=ไม่อนุญาตให้ใส่ข้อมูลที่ว่างเปล่า +validation.NaN=การป้อนข้อมูลไม่ใช่ตัวเลขที่ถูกต้อง +validation.notAnInteger=ค่าที่ป้อนไม่ใช่ค่าจำนวนเต็ม +validation.zero=ไม่อนุญาตให้ป้อนข้อมูลเป็น 0 +validation.negative=ไม่อนุญาตให้ใช้ค่าลบ +validation.fiat.toSmall=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดเล็กกว่าจำนวนเป็นไปได้ต่ำสุด +validation.fiat.toLarge=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดใหญ่กว่าจำนวนสูงสุดที่เป็นไปได้ +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=ไม่อนุญาตให้ป้อนข้อมูลขนาดใหญ่กว่า {0} +validation.btc.toSmall=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดเล็กกว่า {0} +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=รหัสผ่านที่คุณป้อนยาวเกินไป ต้องมีความยาวไม่เกิน 50 ตัว +validation.sortCodeNumber={0} ต้องประกอบด้วย {1} ตัวเลข +validation.sortCodeChars={0} ต้องประกอบด้วย {1} ตัวอักษร +validation.bankIdNumber={0} ต้องประกอบด้วย {1} ตัวเลข +validation.accountNr=หมายเลขบัญชีต้องประกอบด้วย {0} ตัวเลข +validation.accountNrChars=หมายเลขบัญชีต้องประกอบด้วย {0} ตัวอักษร +validation.btc.invalidAddress=ที่อยู่ไม่ถูกต้อง โปรดตรวจสอบแบบฟอร์มที่อยู่ +validation.integerOnly=โปรดป้อนตัวเลขจำนวนเต็มเท่านั้น +validation.inputError=การป้อนข้อมูลของคุณเกิดข้อผิดพลาด: \n{0} +validation.bsq.insufficientBalance=ยอดคงเหลือของคุณ {0} +validation.btc.exceedsMaxTradeLimit=ขีดจำกัดการเทรดของคุณคือ {0} +validation.bsq.amountBelowMinAmount=จำนวนเงินต่ำสุด {0} +validation.nationalAccountId={0} ต้องประกอบด้วย {1} ตัวเลข + +#new +validation.invalidInput=ใส่ข้อมูลไม่ถูกต้อง: {0} +validation.accountNrFormat=หมายเลขบัญชีต้องเป็นรูปแบบ: {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=การตรวจสอบความถูกต้องของที่อยู่ล้มเหลวเนื่องจากไม่ตรงกับโครงสร้างของที่อยู่ {0} +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=ที่อยู่ไม่ใช่ที่อยู่ {0} ที่ถูกต้อง! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=รหัสธนาคารและรหัสประเทศต้องเป็นตัวอักษร +validation.bic.invalidLocationCode=BIC มีรหัสตำแหน่งไม่ถูกต้อง +validation.bic.invalidBranchCode=BIC มีรหัสสาขาไม่ถูกต้อง +validation.bic.sepaRevolutBic=บัญชี Revolut Sepa ไม่รองรับ +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=ที่อยู่ไม่ถูกต้อง +validation.iban.invalidCountryCode=รหัสประเทศไม่ถูกต้อง +validation.iban.checkSumNotNumeric=Checksum ต้องเป็นตัวเลข +validation.iban.nonNumericChars=ไม่พบอักขระที่เป็นตัวเลขและตัวอักษร +validation.iban.checkSumInvalid=การตรวจสอบ IBAN ไม่ถูกต้อง +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=รหัสพื้นที่ที่ไม่ใช่แคนาดา +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=ต้องประกอบด้วยตัวอักษร ตัวเลข เว้นวรรค และ/หรือ สัญลักษณ์ ' _ , . ? - +validation.interacETransfer.invalidAnswer=ต้องเป็นคำเดียว และประกอบด้วยตัวอักษร ตัวเลข และ/หรือ สัญลักษณ์ - เท่านั้น +validation.inputTooLarge=ข้อมูลที่ป้อนต้องไม่เป็นจำนวนที่มากกว่า {0} +validation.inputTooSmall=การป้อนเข้าจะต้องมีจำนวนมากกว่า {0} +validation.inputToBeAtLeast=Input has to be at least {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=ความยาวจะต้องอยู่ระหว่าง {0} และ {1} +validation.fixedLength=Length must be {0} +validation.pattern=การป้อนเข้าจะต้องเป็นรูปแบบ {0} +validation.noHexString=การป้อนเข้านั้นคือไม่ใช่รูปแบบของ HEX +validation.advancedCash.invalidFormat=จะต้องเป็นอีเมลหรือรหัสกระเป๋าสตางค์ที่ใช้งานได้: X000000000000 +validation.invalidUrl=This is not a valid URL +validation.mustBeDifferent=Your input must be different from the current value +validation.cannotBeChanged=Parameter cannot be changed +validation.numberFormatException=Number format exception {0} +validation.mustNotBeNegative=Input must not be negative +validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.invalidCharacters=Phone number {0} contains invalid characters +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Must be comma separated list of valid addresses diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties new file mode 100644 index 0000000000..85d6be85f5 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=Đọc thêm +shared.openHelp=Mở Trợ giúp +shared.warning=Cảnh báo +shared.close=Đóng +shared.cancel=Hủy +shared.ok=OK +shared.yes=Có +shared.no=Không +shared.iUnderstand=Tôi hiểu +shared.na=Không áp dụng +shared.shutDown=Đóng +shared.reportBug=Report bug on GitHub +shared.buyBitcoin=Mua bitcoin +shared.sellBitcoin=Bán bitcoin +shared.buyCurrency=Mua {0} +shared.sellCurrency=Bán {0} +shared.buyingBTCWith=đang mua BTC với {0} +shared.sellingBTCFor=đang bán BTC với {0} +shared.buyingCurrency=đang mua {0} (đang bán BTC) +shared.sellingCurrency=đang bán {0} (đang mua BTC) +shared.buy=mua +shared.sell=bán +shared.buying=đang mua +shared.selling=đang bán +shared.P2P=P2P +shared.oneOffer=chào giá +shared.multipleOffers=nhiều chào giá +shared.Offer=Chào giá +shared.offerVolumeCode={0} Offer Volume +shared.openOffers=Các lệnh đang mở +shared.trade=giao dịch +shared.trades=nhiều giao dịch +shared.openTrades=Các giao dịch mở +shared.dateTime=Ngày/Giờ +shared.price=Giá +shared.priceWithCur=Giá bằng {0} +shared.priceInCurForCur=Giá bằng {0} với 1 {1} +shared.fixedPriceInCurForCur=Giá cố định bằng {0} với 1 {1} +shared.amount=Số tiền +shared.txFee=Transaction Fee +shared.tradeFee=Trade Fee +shared.buyerSecurityDeposit=Buyer Deposit +shared.sellerSecurityDeposit=Seller Deposit +shared.amountWithCur=Thành tiền bằng {0} +shared.volumeWithCur=Khối lượng bằng {0} +shared.currency=Tiền tệ +shared.market=Thị trường +shared.deviation=Deviation +shared.paymentMethod=Hình thức thanh toán +shared.tradeCurrency=Loại tiền tệ giao dịch +shared.offerType=Loại chào giá +shared.details=Thông tin chi tiết +shared.address=Địa chỉ +shared.balanceWithCur=Số dư bằng {0} +shared.utxo=Unspent transaction output +shared.txId=ID giao dịch +shared.confirmations=Xác nhận +shared.revert=Khôi phục Tx +shared.select=Chọn +shared.usage=Sử dụng +shared.state=Trạng thái +shared.tradeId=ID giao dịch +shared.offerId=ID chào giá +shared.bankName=Tên ngân hàng +shared.acceptedBanks=Các NH được chấp nhận +shared.amountMinMax=Số tiền (min - max) +shared.amountHelp=Nếu mức chào giá được đặt mức tối thiểu và tối đa, bạn có thể giao dịch với bất kỳ số tiền nào trong phạm vi này +shared.remove=Xoá +shared.goTo=Đi đến {0} +shared.BTCMinMax=BTC (min - max) +shared.removeOffer=Bỏ chào giá +shared.dontRemoveOffer=Không được bỏ chào giá +shared.editOffer=Chỉnh sửa chào giá +shared.openLargeQRWindow=Open large QR code window +shared.tradingAccount=Tài khoản giao dịch +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í Bisq +shared.fundFromExternalWalletButton=Mở ví ngoài để nộp tiền +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? +shared.belowInPercent=Thấp hơn % so với giá thị trường +shared.aboveInPercent=Cao hơn % so với giá thị trường +shared.enterPercentageValue=Nhập giá trị % +shared.OR=HOẶC +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. +shared.waitingForFunds=Đợi nộp tiền... +shared.depositTransactionId=ID giao dịch gửi tiền +shared.TheBTCBuyer=Người mua BTC +shared.You=Bạn +shared.sendingConfirmation=Gửi xác nhận... +shared.sendingConfirmationAgain=Hãy gửi lại xác nhận +shared.exportCSV=Export to CSV +shared.exportJSON=Truy xuất ra JSON +shared.summary=Show summary +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=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin 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 +shared.language=Ngôn ngữ +shared.country=Quốc gia +shared.applyAndShutDown=Áp dụng và tắt +shared.selectPaymentMethod=Chọn phương thức thanh toán +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. +shared.askConfirmDeleteAccount=Bạn có thực sự muốn xóa tài khoản được chọn không? +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). +shared.noAccountsSetupYet=Chưa có tài khoản nào được thiết lập +shared.manageAccounts=Quản lý tài khoản +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.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 +shared.errorMessageInline=\nThông báo lỗi: {0} +shared.errorMessage=Thông báo lỗi +shared.information=Thông tin +shared.name=Tên +shared.id=ID +shared.dashboard=Bảng thống kê +shared.accept=Chấp nhận +shared.balance=Số dư +shared.save=Lưu +shared.onionAddress=Địa chỉ Onion +shared.supportTicket=vé hỗ trợ +shared.dispute=tranh chấp +shared.mediationCase=mediation case +shared.seller=người bán +shared.buyer=người mua +shared.allEuroCountries=Tất cả các nước Châu ÂU +shared.acceptedTakerCountries=Các quốc gia tiếp nhận được chấp nhận +shared.tradePrice=Giá giao dịch +shared.tradeAmount=Khoản tiền giao dịch +shared.tradeVolume=Khối lượng giao dịch +shared.invalidKey=Mã khóa bạn vừa nhập không đúng. +shared.enterPrivKey=Nhập Private key để mở khóa +shared.makerFeeTxId=ID giao dịch thu phí của người tạo +shared.takerFeeTxId=ID giao dịch thu phí của người nhận +shared.payoutTxId=ID giao dịch chi trả +shared.contractAsJson=Hợp đồng định dạng JSON +shared.viewContractAsJson=Xem hợp đồng định dạng JSON +shared.contract.title=Hợp đồng giao dịch có ID: {0} +shared.paymentDetails=Thông tin thanh toán BTC {0} +shared.securityDeposit=Tiền đặt cọc +shared.yourSecurityDeposit=Tiền đặt cọc của bạn +shared.contract=Hợp đồng +shared.messageArrived=Tin nhắn đến. +shared.messageStoredInMailbox=Tin nhắn lưu trong hộp thư. +shared.messageSendingFailed=Tin nhắn chưa gửi được. Lỗi: {0} +shared.unlock=Mở khóa +shared.toReceive=nhận +shared.toSpend=chi +shared.btcAmount=Số lượng BTC +shared.yourLanguage=Ngôn ngữ của bạn +shared.addLanguage=Thêm ngôn ngữ +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.makerTxFee=Người tạo: {0} +shared.takerTxFee=Người nhận: {0} +shared.iConfirm=Tôi xác nhận +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=Mở {0} +shared.fiat=Tiền pháp định +shared.crypto=Tiền mã hóa +shared.all=Tất cả +shared.edit=Chỉnh sửa +shared.advancedOptions=Tùy chọn nâng cao +shared.interval=Khoảng thời gian +shared.actions=Hoạt động +shared.buyerUpperCase=Người mua +shared.sellerUpperCase=Người bán +shared.new=MỚI +shared.blindVoteTxId=Mã giao dịch bỏ phiếu mù +shared.proposal=Đề xuất +shared.votes=Các phiếu bầu +shared.learnMore=Tìm hiểu thêm +shared.dismiss=Hủy +shared.selectedArbitrator=Trọng tài được chọn +shared.selectedMediator=Selected mediator +shared.selectedRefundAgent=Trọng tài được chọn +shared.mediator=Người hòa giải +shared.arbitrator=Trọng tài +shared.refundAgent=Trọng tài +shared.refundAgentForSupportStaff=Refund agent +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to +shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=Enabled + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=Thị trường +mainView.menu.buyBtc=Mua BTC +mainView.menu.sellBtc=Bán BTC +mainView.menu.portfolio=Danh Mục +mainView.menu.funds=Số tiền +mainView.menu.support=Hỗ trợ +mainView.menu.settings=Cài đặt +mainView.menu.account=Tài khoản +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=Giá thị trường theo {0} +mainView.marketPrice.bisqInternalPrice=Giá giao dịch Bisq gần nhất +mainView.marketPrice.tooltip.bisqInternalPrice=Không có giá thị trường từ nhà cung cấp bên ngoài.\nGiá hiển thị là giá giao dịch Bisq gần nhất với đồng tiền này. +mainView.marketPrice.tooltip=Giá thị trường được cung cấp bởi {0}{1}\nCập nhật mới nhất: {2}\nURL nút nhà cung cấp: {3} +mainView.balance.available=Số dư hiện có +mainView.balance.reserved=Phần được bảo lưu trong báo giá +mainView.balance.locked=Khóa trong giao dịch +mainView.balance.reserved.short=Bảo lưu +mainView.balance.locked.short=Bị khóa + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(Máy chủ nội bộ) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=Đang kết nối với mạng Bitcoin +mainView.footer.bsqInfo.synchronizing=/ Đang đồng bộ hóa DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=Đang kết nối với +mainView.footer.btcInfo.connectionFailed=Connection failed to +mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1} +mainView.footer.daoFullNode=Full node DAO + +mainView.bootstrapState.connectionToTorNetwork=(1/4) Kết nối với mạng ... +mainView.bootstrapState.torNodeCreated=(2/4) Nút Tor được tạo +mainView.bootstrapState.hiddenServicePublished=(3/4) Dịch vụ ẩn được công bố +mainView.bootstrapState.initialDataReceived=(4/4) Nhận dữ liệu ban đầu + +mainView.bootstrapWarning.noSeedNodesAvailable=Không có seed nodes khả dụng +mainView.bootstrapWarning.noNodesAvailable=Không có seed nodes và đối tác ngang hàng khả dụng +mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Bisq network failed + +mainView.p2pNetworkWarnMsg.noNodesAvailable=Không có seed nodes hay đối tác ngang hàng để yêu cầu dữ liệu.\nVui lòng kiểm tra kết nối internet hoặc thử khởi động lại ứng dụng. +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Bisq network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. + +mainView.walletServiceErrorMsg.timeout=Kết nối tới mạng Bitcoin không thành công do hết thời gian chờ. +mainView.walletServiceErrorMsg.connectionError=Kết nối tới mạng Bitcoin không thành công do lỗi: {0} + +mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} + +mainView.networkWarning.allConnectionsLost=Mất kết nối tới tất cả mạng ngang hàng {0}.\nCó thể bạn mất kết nối internet hoặc máy tính đang ở chế độ standby. +mainView.networkWarning.localhostBitcoinLost=Mất kết nối tới nút Bitcoin máy chủ nội bộ.\nVui lòng khởi động lại ứng dụng Bisq để nối với nút Bitcoin khác hoặc khởi động lại nút Bitcoin máy chủ nội bộ. +mainView.version.update=(Có cập nhật) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=Danh mục chào giá +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=Các giao dịch + +# OfferBookChartView +market.offerBook.buyAltcoin=Mua {0} (bán {1}) +market.offerBook.sellAltcoin=Bán {0} (mua {1}) +market.offerBook.buyWithFiat=Mua {0} +market.offerBook.sellWithFiat=Bán {0} +market.offerBook.sellOffersHeaderLabel=Bán {0} cho +market.offerBook.buyOffersHeaderLabel=Mua {0} từ +market.offerBook.buy=Tôi muốn mua bitcoin +market.offerBook.sell=Tôi muốn bán bitcoin + +# SpreadView +market.spread.numberOfOffersColumn=Tất cả chào giá ({0}) +market.spread.numberOfBuyOffersColumn=Mua BTC ({0}) +market.spread.numberOfSellOffersColumn=Bán BTC ({0}) +market.spread.totalAmountColumn=Tổng số BTC ({0}) +market.spread.spreadColumn=Chênh lệch giá +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=Các giao dịch: {0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=Mở: +market.trades.tooltip.candle.close=Đóng: +market.trades.tooltip.candle.high=Cao: +market.trades.tooltip.candle.low=Thấp: +market.trades.tooltip.candle.average=Trung bình: +market.trades.tooltip.candle.median=Median: +market.trades.tooltip.candle.date=Ngày: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +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.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} +offerbook.offerersBankSeat=Quốc gia có ngân hàng của người tạo: {0} +offerbook.offerersAcceptedBankSeatsEuro=Các quốc gia có ngân hàng được chấp thuận (người nhận): Tất cả các nước Châu Âu +offerbook.offerersAcceptedBankSeats=Các quốc gia có ngân hàng được chấp thuận (người nhận):\n {0} +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.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 +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) +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.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.ageDays={0} ngày +offerbook.timeSinceSigning.notSigned.noNeed=Không áp dụng +shared.notSigned=This account has not been signed yet and was created {0} days ago +shared.notSigned.noNeed=This account type does not require signing +shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago +shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging + +offerbook.nrOffers=Số chào giá: {0} +offerbook.volume={0} (min - max) +offerbook.deposit=Deposit BTC (%) +offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. + +offerbook.createOfferToBuy=Tạo chào giá mua mới {0} +offerbook.createOfferToSell=Tạo chào giá bán mới {0} +offerbook.createOfferToBuy.withFiat=Tạo chào giá mua {0} bằng {1} +offerbook.createOfferToSell.forFiat=Tạo chào giá bán {0} lấy {1} +offerbook.createOfferToBuy.withCrypto=Tạo chào giá bán {0} (mua {1}) +offerbook.createOfferToSell.forCrypto=Tạo chào giá mua {0} (bán {1}) + +offerbook.takeOfferButton.tooltip=Nhận chào giá cho {0} +offerbook.yesCreateOffer=Vâng, tạo lệnh +offerbook.setupNewAccount=Thiết lập tài khoản giao dịch mới +offerbook.removeOffer.success=Xoá nh thành công. +offerbook.removeOffer.failed=Xoá lệnh không thành công:\n{0} +offerbook.deactivateOffer.failed=Huỷ kích hoạt chào giá không thành công:\n{0} +offerbook.activateOffer.failed=Công bố lệnh không thành công:\n{0} +offerbook.withdrawFundsHint=Bạn có thể rút bạn đã thanh toán từ màn hình {0}. + +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? + +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + +offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} + +offerbook.warning.wrongTradeProtocol=Lệnh này cần phiên bản giao thức khác với được sử dụng trong phiên bản phần mềm của bạn.\n\nHãy kiểm tra xem bạn đã cài đặt phiên bản mới nhất chưa, nếu không người dùng đã tạo lệnh đã sử dụng phiên bản cũ.\n\nNgười dùng không thể giao dịch với phiên bản giao thức giao dịch không tương thích. +offerbook.warning.userIgnored=Bạn đã thêm địa chỉ onion của người dùng vào danh sách bỏ qua. +offerbook.warning.offerBlocked=Báo giá này bị chặn bởi các lập trình viên Bisq.\nCó thể có sự cố chưa được xử lý dẫn tới vấn đề khi nhận lệnh này. +offerbook.warning.currencyBanned=Loại tiền sử dụng trong báo giá này bị chặn bởi các lập trình viên Bisq.\nTruy cập diễn đàn Bisq để biết thêm thông tin. +offerbook.warning.paymentMethodBanned=Phương thức thanh toán sử dụng trong báo giá này bị chặn bởi các lập trình viên Bisq.\nTruy cập diễn đàn Bisq để biết thêm thông tin. +offerbook.warning.nodeBlocked=Địa chỉ onion của Thương gia bị chặn bởi các lập trình viên Bisq.\nCó thể có sự cố chưa được xử lý dẫn tới vấn đề khi nhận báo giá từ Thương gia này. +offerbook.warning.requireUpdateToNewVersion=Your version of Bisq is not compatible for trading anymore.\nPlease update to the latest Bisq version at [HYPERLINK:https://bisq.network/downloads]. +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.info.sellAtMarketPrice=Bạn sẽ bán với giá thị trường (cập nhật mỗi phút). +offerbook.info.buyAtMarketPrice=Bạn sẽ mua với giá thị trường (cập nhật mỗi phút). +offerbook.info.sellBelowMarketPrice=Bạn sẽ nhận {0} thấp hơn so với giá thị trường hiện tại (cập nhật mỗi phút). +offerbook.info.buyAboveMarketPrice=Bạn sẽ trả {0} cao hơn so với giá thị trường hiện tại (cập nhật mỗi phút). +offerbook.info.sellAboveMarketPrice=Bạn sẽ nhận {0} cao hơn so với giá thị trường hiện tại (cập nhật mỗi phút). +offerbook.info.buyBelowMarketPrice=Bạn sẽ trả {0} thấp hơn so với giá thị trường hiện tại (cập nhật mỗi phút). +offerbook.info.buyAtFixedPrice=Bạn sẽ mua với giá cố định này. +offerbook.info.sellAtFixedPrice=Bạn sẽ bán với giá cố định này. +offerbook.info.noArbitrationInUserLanguage=Trong trường hợp có tranh chấp, xin lưu ý rằng việc xử lý tranh chấp sẽ được thực hiện bằng tiếng{0}. Ngôn ngữ hiện tại là tiếng{1}. +offerbook.info.roundedFiatVolume=Số lượng đã được làm tròn để tăng tính bảo mật cho giao dịch + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=Nhập số tiền bằng BTC +createOffer.price.prompt=Nhập giá +createOffer.volume.prompt=Nhập số tiền bằng {0} +createOffer.amountPriceBox.amountDescription=Số tiền BTC đến {0} +createOffer.amountPriceBox.buy.volumeDescription=Số tiền {0} để chi trả +createOffer.amountPriceBox.sell.volumeDescription=Số tiền {0} để nhận +createOffer.amountPriceBox.minAmountDescription=Số tiền BTC nhỏ nhất +createOffer.securityDeposit.prompt=Tiền đặt cọc +createOffer.fundsBox.title=Nộp tiền cho chào giá của bạn +createOffer.fundsBox.offerFee=Phí giao dịch +createOffer.fundsBox.networkFee=Phí đào +createOffer.fundsBox.placeOfferSpinnerInfo=Báo giá đang được công bố +createOffer.fundsBox.paymentLabel=giao dịch Bisq với ID {0} +createOffer.fundsBox.fundsStructure=({0} tiền đặt cọc, {1} phí giao dịch, {2} phí đào) +createOffer.fundsBox.fundsStructure.BSQ=({0} tiền đặt cọc, {1} phí đào)+ {2} phí giao dịch +createOffer.success.headline=Chào giá của bạn đã được công bố +createOffer.success.info=Bạn có thể quản lý báo giá hiện hành của bạn tại \"Portfolio/ báo giá hiện tại của bạn\". +createOffer.info.sellAtMarketPrice=Bạn sẽ luôn bán với giá thị trường vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.info.buyAtMarketPrice=Bạn sẽ luôn mua với giá thị trường vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.info.sellAboveMarketPrice=Bạn sẽ luôn nhận {0}% cao hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.info.buyBelowMarketPrice=Bạn sẽ luôn trả {0}% thấp hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.warning.sellBelowMarketPrice=Bạn sẽ luôn nhận {0}% thấp hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.warning.buyAboveMarketPrice=Bạn sẽ luôn trả {0}% cao hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. +createOffer.tradeFee.descriptionBTCOnly=Phí giao dịch +createOffer.tradeFee.descriptionBSQEnabled=Chọn loại tiền trả phí giao dịch + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +createOffer.placeOfferButton=Kiểm tra:: Đặt báo giá cho {0} bitcoin +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í Bisq 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. + +# 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. +createOffer.setAmountPrice=Đặt số tiền và giá +createOffer.warnCancelOffer=Bạn đã nộp tiền cho báo giá.\nNếu bạn hủy bây giờ, số tiền của bạn sẽ chuyển sang ví Bisq nội bộ của bạn và sẵn sàng để rút tại màn hình \"Vốn/Gửi vốn\".\nBạn có chắc muốn hủy? +createOffer.timeoutAtPublishing=Lỗi hết thời gian xảy ra khi công bố lệnh +createOffer.errorInfo=\n\nPhí người tạo đã được thanh toán. Trong trường hợp xấu nhất bạn mất phí này.\nHã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 không. +createOffer.tooLowSecDeposit.warning=Bạn đã cài đặt tiền gửi đặt cọc thấp hơn giá trị mặc định khuyến cáo {0}.\nBạn có chắc bạn muốn sử dụng tiền gửi đặt cọc thấp hơn không? +createOffer.tooLowSecDeposit.makerIsSeller=Bạn ít được bảo vệ hơn khi đối tác giao dịch không tuân thủ giao thức giao dịch. +createOffer.tooLowSecDeposit.makerIsBuyer=Đối tác giao dịch ít được bảo vệ hơn khi bạn tuân thủ giao thức giao dịch vì bạn có ít tiền đặt cọc chịu rủi ro. Người dùng khác có thể muốn nhận chào giá khác thay cho chào giá của bạn. +createOffer.resetToDefault=Không, cài đặt lại giá trị mặc định +createOffer.useLowerValue=Vâng, sử dụng giá trị thấp hơn +createOffer.priceOutSideOfDeviation=Giá bạn vừa nhập ngoài sai lệch cho phép tối đa so với giá thị trường.\nSai lệch cho phép tối đa là {0} và có thể điều chỉnh trong quyền ưu tiên. +createOffer.changePrice=Thay đổi giá +createOffer.tac=Với việc công bố chào giá này, tôi đồng ý giao dịch với bất cứ Thương gia nào đáp ứng các điều kiện nêu rõ trên màn hình này. +createOffer.currencyForFee=Phí giao dịch +createOffer.setDeposit=Cài đặt tiền đặt cọc của người mua (%) +createOffer.setDepositAsBuyer=Cài đặt tiền đặt cọc của tôi với vai trò người mua (%) +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 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=Nhập giá trị bằng BTC +takeOffer.amountPriceBox.buy.amountDescription=Số lượng BTC bán +takeOffer.amountPriceBox.sell.amountDescription=Số lượng BTC mua +takeOffer.amountPriceBox.priceDescription=Giá mỗi bitcoin bằng {0} +takeOffer.amountPriceBox.amountRangeDescription=Số lượng khả dụng +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=Giá trị bạn vừa nhập vượt quá số ký tự thập phân cho phép.\nGiá trị phải được điều chỉnh về 4 số thập phân. +takeOffer.validation.amountSmallerThanMinAmount=Số tiền không được nhỏ hơn giá trị nhỏ nhất của lệnh. +takeOffer.validation.amountLargerThanOfferAmount=Số tiền nhập không được cao hơn số tiền cao nhất của lệnh +takeOffer.validation.amountLargerThanOfferAmountMinusFee=Giá trị nhập này sẽ làm thay đổi đối với người bán BTC. +takeOffer.fundsBox.title=Nộp tiền cho giao dịch của bạn +takeOffer.fundsBox.isOfferAvailable=Kiểm tra xem có chào giá không ... +takeOffer.fundsBox.tradeAmount=Số tiền để bán +takeOffer.fundsBox.offerFee=Phí giao dịch +takeOffer.fundsBox.networkFee=Tổng phí đào +takeOffer.fundsBox.takeOfferSpinnerInfo=Đang nhận chào giá ... +takeOffer.fundsBox.paymentLabel=giao dịch Bisq có ID {0} +takeOffer.fundsBox.fundsStructure=({0} tiền gửi đại lý, {1} phí giao dịch, {2} phí đào) +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} + +# new entries +takeOffer.takeOfferButton=Rà soát: Nhận báo giá cho {0} bitcoin +takeOffer.noPriceFeedAvailable=Bạn không thể nhận báo giá này do sử dụng giá phần trăm dựa trên giá thị trường nhưng không có giá cung cấp. +takeOffer.takeOfferFundWalletInfo.headline=Nộp tiền cho giao dịch của bạn +# suppress inspection "TrailingSpacesInProperty" +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- Tổng phí đào: {4}\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í Bisq (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.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 +takeOffer.alreadyFunded.askCancel=Bạn đã nộp tiền cho chào giá này.\nNếu bạn hủy bây giờ, số tiền của bạn sẽ được chuyển sang ví Bisq nội bộ của bạn và sẵn sàng để rút tại màn hình Màn hình \"Vốn/Gửi vốn\".\nBạn có chắc muốn hủy? +takeOffer.failed.offerNotAvailable=Nhận yêu cầu chào giá không thành công do chào giá không còn tồn tại. Có thể trong lúc chờ đợi, Thương gia khác đã nhận chào giá này. +takeOffer.failed.offerTaken=Bạn không thể nhận chào giá này vì đã được nhận bởi Thương gia khác. +takeOffer.failed.offerRemoved=Bạn không thể nhận chào giá này vì trong lúc chời đợi, chào giá này đã bị gỡ bỏ. +takeOffer.failed.offererNotOnline=Nhận báo giá không thành công vì người tạo không còn online. +takeOffer.failed.offererOffline=Bạn không thể nhận báo giá vì người tạo đã offline. +takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. + +takeOffer.error.noFundsLost=\n\nVí của bạn không còn tiền.\nHã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. +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n\n +takeOffer.error.depositPublished=\n\nGiao dịch đặt cọc đã được công bố.\nHã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.\nNếu vấn đề vẫn không được xử lý, hãy liên hệ các lập trình viên để được hỗ trợ. +takeOffer.error.payoutPublished=\n\nGiao dịch hoàn tiền đã được công bố.\nHã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.\nNếu vấn đề vẫn không được xử lý, hãy liên hệ các lập trình viên để được hỗ trợ. +takeOffer.tac=Bằng cách nhận báo giá này, tôi đồng ý với các điều khoản giao dịch nêu trong màn hình này. + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=Giá khởi phát +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=Cài đặt giá +editOffer.confirmEdit=Xác nhận: Chỉnh sửa báo giá +editOffer.publishOffer=Công bố báo giá. +editOffer.failed=Chỉnh sửa báo giá không thành công:\n{0} +editOffer.success=Báo giá của bạn đã được chỉnh sửa thành công. +editOffer.invalidDeposit=Số tiền đặt cọc cho người mua không nằm trong giới hạn quy định bởi DAO Bisq và không thể chỉnh sửa được nữa + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=Các báo giá mở của tôi +portfolio.tab.pendingTrades=Các giao dịch mở +portfolio.tab.history=Lịch sử +portfolio.tab.failed=Không thành công +portfolio.tab.editOpenOffer=Chỉnh sửa báo giá + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=Đợi xác nhận blockchain +portfolio.pending.step2_buyer.startPayment=Bắt đầu thanh toán +portfolio.pending.step2_seller.waitPaymentStarted=Đợi đến khi bắt đầu thanh toán +portfolio.pending.step3_buyer.waitPaymentArrived=Đợi đến khi khoản thanh toán đến +portfolio.pending.step3_seller.confirmPaymentReceived=Xác nhận đã nhận được thanh toán +portfolio.pending.step5.completed=Hoàn thành + +portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status +portfolio.pending.autoConf=Auto-confirmed +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} +portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} +portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key +portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. +# 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.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. + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=Hãy chuyển từ ví ngoài {0} của bạn\n{1} cho người bán BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=Hãy đến ngân hàng và thanh toán {0} cho người bán BTC.\n\n +portfolio.pending.step2_buyer.cash.extra=YÊU CẦU QUAN TRỌNG:\nSau khi bạn đã thanh toán xong hãy viết lên giấy biên nhận: KHÔNG HOÀN TRẢ.\nSau đó xé thành 2 phần, chụp ảnh và gửi tới email của người bán BTC. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Vui lòng trả {0} cho người bán BTC qua MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=Yêu cầu quan trọng:\nSau khi bạn hoàn thành chi trả hãy gửi Số xác thực và hình chụp hoá đơn qua email cho người bán BTC.\nHoá đơn phải chỉ rõ họ tên đầy đủ, quốc gia, tiểu bang và số tiền. Email người bán là: {0}. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=Hãy thanh toán {0} cho người bán BTC bằng cách sử dụng Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=YÊU CẦU QUAN TRỌNG:\nSau khi bạn đã thanh toán xong hãy gửi MTCN (số theo dõi) và ảnh giấy biên nhận bằng email cho người bán BTC.\nGiấy biên nhận phải ghi rõ họ tên của người bán, thành phố, quốc gia và số tiền. Địa chỉ email của người bán là: {0}. + +# 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 BTC.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=Vui lòng liên hệ người bán BTC và cung cấp số liên hệ và sắp xếp cuộc hẹn để thanh toán {0}.\n\n +portfolio.pending.step2_buyer.startPaymentUsing=Thanh toán bắt đầu sử dụng {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} +portfolio.pending.step2_buyer.amountToTransfer=Số tiền chuyển +portfolio.pending.step2_buyer.sellersAddress=Địa chỉ của người bán {0} +portfolio.pending.step2_buyer.buyerAccount=Tài khoản thanh toán sẽ sử dụng +portfolio.pending.step2_buyer.paymentStarted=Bắt đầu thanh toán +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. +portfolio.pending.step2_buyer.paperReceipt.headline=Bạn đã gửi giấy biên nhận cho người bán BTC chưa? +portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\nBạn cần phải viết trên giấy biên nhận: KHÔNG HOÀN TRẢ.\nSau đó xé thành 2 phần, chụp ảnh và gửi đến email của người bán BTC. +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Gửi số xác nhận và hoá đơn +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Bạn cần gửi số xác thực và ảnh chụp của hoá đơn qua email đến người bán BTC.\nHoá đơn phải ghi rõ họ tên đầy đủ người bán, quốc gia, tiểu bang và số lượng. Email người bán là: {0}.\n\nBạn đã gửi số xác thực và hợp đồng cho người bán? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Gửi MTCN và biên nhận +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Bạn cần phải gửi MTCN (số theo dõi) và ảnh chụp giấy biên nhận bằng email cho người bán BTC.\nGiấy biên nhận phải nêu rõ họ tên, thành phố, quốc gia của người bán và số tiền. Địa chỉ email của người bán là: {0}.\n\nBạn đã gửi MTCN và hợp đồng cho người bán chưa? +portfolio.pending.step2_buyer.halCashInfo.headline=Gửi mã HalCash +portfolio.pending.step2_buyer.halCashInfo.msg=Bạn cần nhắn tin mã HalCash và mã giao dịch ({0}) tới người bán BTC. \nSố điện thoại của người bán là {1}.\n\nBạn đã gửi mã tới người bán chưa? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Bisq clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). +portfolio.pending.step2_buyer.confirmStart.headline=Xác nhận rằng bạn đã bắt đầu thanh toán +portfolio.pending.step2_buyer.confirmStart.msg=Bạn đã kích hoạt thanh toán {0} cho Đối tác giao dịch của bạn chưa? +portfolio.pending.step2_buyer.confirmStart.yes=Có, tôi đã bắt đầu thanh toán +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the BTC as soon the XMR has been received.\nBeside that, Bisq requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value +portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway +portfolio.pending.step2_seller.waitPayment.headline=Đợi thanh toán +portfolio.pending.step2_seller.f2fInfo.headline=Thông tin liên lạc của người mua +portfolio.pending.step2_seller.waitPayment.msg=Giao dịch đặt cọc có ít nhất một xác nhận blockchain.\nBạn cần phải đợi cho đến khi người mua BTC bắt đầu thanh toán {0}. +portfolio.pending.step2_seller.warn=Người mua BTC vẫn chưa thanh toán {0}.\nBạn cần phải đợi cho đến khi người mua bắt đầu thanh toán.\nNếu giao dịch không được hoàn thành vào {1} trọng tài sẽ điều tra. +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. +tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Bisq (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +message.state.SENT=Tin nhắn được gửi +# suppress inspection "UnusedProperty" +message.state.ARRIVED=Tin nhắn đã nhận +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=Người nhận xác nhận tin nhắn +# suppress inspection "UnusedProperty" +message.state.FAILED=Gửi tin nhắn không thành công + +portfolio.pending.step3_buyer.wait.headline=Đợi người bán BTC xác nhận thanh toán +portfolio.pending.step3_buyer.wait.info=Đợi người bán BTC xác nhận đã nhận thanh toán {0}. +portfolio.pending.step3_buyer.wait.msgStateInfo.label=Thông báo trạng thái thanh toán đã bắt đầu +portfolio.pending.step3_buyer.warn.part1a=trên blockchain {0} +portfolio.pending.step3_buyer.warn.part1b=tại nhà cung cấp thanh toán của bạn (VD: ngân hàng) +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=Đối tác giao dịch của bạn đã xác nhận rằng họ đã kích hoạt thanh toán {0}.\n\n +portfolio.pending.step3_seller.altcoin.explorer=Trên trình duyệt blockchain explorer {0} ưa thích của bạn +portfolio.pending.step3_seller.altcoin.wallet=Trên ví {0} của bạn +portfolio.pending.step3_seller.altcoin={0}Vui lòng kiểm tra {1} xem giao dịch tới địa chỉ nhận của bạn \n{2}\nđã nhận được đủ xác nhận blockchain hay chưa.\nSố tiền thanh toán phải là {3}\n\nBạn có thể copy & paste địa chỉ {4} của bạn từ màn hình chính sau khi đóng cửa sổ này. +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=Vì thanh toán được thực hiện qua Tiền gửi tiền mặt nên người mua BTC phải viết rõ \"KHÔNG HOÀN LẠI\" trên giấy biên nhận, xé làm 2 phần và gửi ảnh cho bạn qua email.\n\nĐể tránh bị đòi tiền lại, chỉ xác nhận bạn đã nhận được email và bạn chắc chắn giấy biên nhận là có hiệu lực.\nNếu bạn không chắc chắn, {0} +portfolio.pending.step3_seller.moneyGram=Người mua phải gửi mã số xác nhận và ảnh chụp của hoá đơn qua email.\nHoá đơn cần ghi rõ họ tên đầy đủ, quốc gia, tiêu bang và số lượng. Vui lòng kiểm tra email nếu bạn nhận được số xác thực.\n\nSau khi popup đóng, bạn sẽ thấy tên người mua BTC và địa chỉ để nhận tiền từ MoneyGram.\n\nChỉ xác nhận hoá đơn sau khi bạn hoàn thành việc nhận tiền. +portfolio.pending.step3_seller.westernUnion=Người mua phải gửi cho bạn MTCN (số theo dõi) và ảnh giấy biên nhận qua email.\nGiấy biên nhận phải ghi rõ họ tên của bạn, thành phố, quốc gia và số tiền. Hãy kiểm tra email xem bạn đã nhận được MTCN chưa.\n\nSau khi đóng cửa sổ này, bạn sẽ thấy tên và địa chỉ của người mua BTC để nhận tiền từ Western Union.\n\nChỉ xác nhận giấy biên nhận sau khi bạn đã nhận tiền thành công! +portfolio.pending.step3_seller.halCash=Người mua phải gửi mã HalCash cho bạn bằng tin nhắn. Ngoài ra, bạn sẽ nhận được một tin nhắn từ HalCash với thông tin cần thiết để rút EUR từ một máy ATM có hỗ trợ HalCash. \n\nSau khi nhận được tiền từ ATM vui lòng xác nhận lại biên lai thanh toán tại đây! +portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. + +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.confirmPaymentReceipt=Xác nhận đã nhận được thanh toán +portfolio.pending.step3_seller.amountToReceive=Số tiền nhận được +portfolio.pending.step3_seller.yourAddress=Địa chỉ {0} của bạn +portfolio.pending.step3_seller.buyersAddress=Địa chỉ {0} của người mua +portfolio.pending.step3_seller.yourAccount=tài khoản giao dịch của bạn +portfolio.pending.step3_seller.xmrTxHash=ID giao dịch +portfolio.pending.step3_seller.xmrTxKey=Transaction key +portfolio.pending.step3_seller.buyersAccount=Buyers account data +portfolio.pending.step3_seller.confirmReceipt=Xác nhận đã nhận được thanh toán +portfolio.pending.step3_seller.buyerStartedPayment=Người mua BTC đã bắt đầu thanh toán {0}.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Kiểm tra xác nhận blockchain ở ví altcoin của bạn hoặc block explorer và xác nhận thanh toán nếu bạn nhận được đủ xác nhận blockchain. +portfolio.pending.step3_seller.buyerStartedPayment.fiat=Kiểm tra tại tài khoản giao dịch của bạn (VD: Tài khoản ngân hàng) và xác nhận khi bạn đã nhận được thanh toán. +portfolio.pending.step3_seller.warn.part1a=trên {0} blockchain +portfolio.pending.step3_seller.warn.part1b=tại nhà cung cấp thanh toán của bạn (VD: ngân hàng) +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=Bạn đã nhận được thanh toán {0} từ Đối tác giao dịch của bạn?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Xác nhận rằng bạn đã nhận được thanh toán +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Vâng, tôi đã nhận được thanh toán +portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. + +portfolio.pending.step5_buyer.groupTitle=Tóm tắt giao dịch đã hoàn thành +portfolio.pending.step5_buyer.tradeFee=Phí giao dịch +portfolio.pending.step5_buyer.makersMiningFee=Phí đào +portfolio.pending.step5_buyer.takersMiningFee=Tổng phí đào +portfolio.pending.step5_buyer.refunded=tiền gửi đặt cọc được hoàn lại +portfolio.pending.step5_buyer.withdrawBTC=Rút bitcoin của bạn +portfolio.pending.step5_buyer.amount=Số tiền được rút +portfolio.pending.step5_buyer.withdrawToAddress=rút tới địa chỉ +portfolio.pending.step5_buyer.moveToBisqWallet=Keep funds in Bisq wallet +portfolio.pending.step5_buyer.withdrawExternal=rút tới ví ngoài +portfolio.pending.step5_buyer.alreadyWithdrawn=Số tiền của bạn đã được rút.\nVui lòng kiểm tra lịch sử giao dịch. +portfolio.pending.step5_buyer.confirmWithdrawal=Xác nhận yêu cầu rút +portfolio.pending.step5_buyer.amountTooLow=Số tiền chuyển nhỏ hơn phí giao dịch và giá trị tx tối thiểu (dust). +portfolio.pending.step5_buyer.withdrawalCompleted.headline=Rút hoàn tất +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Các giao dịch đã hoàn thành của bạn được lưu trong \"Portfolio/Lịch sử\".\nBạn có thể xem lại tất cả giao dịch bitcoin của bạn tại \"Vốn/Giao dịch\" +portfolio.pending.step5_buyer.bought=Bạn đã mua +portfolio.pending.step5_buyer.paid=Bạn đã thanh toán + +portfolio.pending.step5_seller.sold=Bạn đã bán +portfolio.pending.step5_seller.received=Bạn đã nhận + +tradeFeedbackWindow.title=Chúc mừng giao dịch của bạn được hoàn thành. +tradeFeedbackWindow.msg.part1=Chúng tôi rất vui lòng được nghe phản hồi của bạn. Điều đó sẽ giúp chúng tôi cải thiện phần mềm và hoàn thiện trải nghiệm người dùng. Nếu bạn muốn cung cấp phản hồi, vui lòng điền vào cuộc khảo sát ngắn dưới đây (không yêu cầu đăng nhập) ở: +tradeFeedbackWindow.msg.part2=nếu bạn có câu hỏi hay vấn đề, vui lòng liên hệ với người dùng khác và các nhàn đóng góp qua Bisq forum ở: +tradeFeedbackWindow.msg.part3=Cám ơn bạn đã sử dụng Bisq! + +portfolio.pending.role=Vai trò của tôi +portfolio.pending.tradeInformation=Thông tin giao dịch +portfolio.pending.remainingTime=Thời gian còn lại +portfolio.pending.remainingTimeDetail={0} (cho đến khi {1}) +portfolio.pending.tradePeriodInfo=Sau xác nhận blockchain đầu tiên, thời gian giao dịch bắt đầu. Phương thức thanh toán áp dụng khác nhau sẽ có thời gian giao dịch tối đa cho phép khác nhau. +portfolio.pending.tradePeriodWarning=Nếu quá thời gian giao dịch, cả hai Thương gia đều có thể mở khiếu nại. +portfolio.pending.tradeNotCompleted=giao dịch không được hoàn thành đúng thời gian (cho đến khi {0}) +portfolio.pending.tradeProcess=Quá trình giao dịch +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Bisq forum at [HYPERLINK:https://bisq.community]. +portfolio.pending.openAgainDispute.button=Mở khiếu nại lần nữa +portfolio.pending.openSupportTicket.headline=Mở vé hỗ trợ +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. + +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. +portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. +portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. +portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Bisq community at https://bisq.community. If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Open Trader Chat +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Trade period is over + +portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested +portfolio.pending.openSupport=Mở đơn hỗ trợ +portfolio.pending.supportTicketOpened=Đơn hỗ trợ đã mở +portfolio.pending.communicateWithArbitrator=Vui lòng liên lạc với trong tài qua màn hình \"Hỗ trợ\". +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. +portfolio.pending.disputeOpenedMyUser=Bạn đã mở một khiếu nại.\n{0} +portfolio.pending.disputeOpenedByPeer=Đối tác giao dịch của bạn đã mở một khiếu nại\n{0} +portfolio.pending.noReceiverAddressDefined=Không có địa chỉ người nhận + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=View proposed resolution +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted + +portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/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 fiat or altcoin payment to the BTC 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 Bisq 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.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. +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades +portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade +portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending=Move trade to open trades + +portfolio.closed.completed=Hoàn thành +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated +portfolio.closed.canceled=Đã hủy +portfolio.failed.Failed=Không thành công +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. +portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=Nhận vốn +funds.tab.withdrawal=Gửi vốn +funds.tab.reserved=Vốn bảo toàn +funds.tab.locked=Vốn bị khóa +funds.tab.transactions=Giao dịch + +funds.deposit.unused=Không sử dụng +funds.deposit.usedInTx=Sử dụng trong {0} giao dịch +funds.deposit.fundBisqWallet=Nộp tiền ví Bisq +funds.deposit.noAddresses=Chưa có địa chỉ nộp tiền được tạo +funds.deposit.fundWallet=Nộp tiền cho ví của bạn +funds.deposit.withdrawFromWallet=Chuyển tiền từ ví +funds.deposit.amount=Số tiền bằng BTC (tùy chọn) +funds.deposit.generateAddress=Tạo địa chỉ mới +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) +funds.deposit.selectUnused=Vui lòng chọn địa chỉ chưa sử dụng từ bảng trên hơn là tạo một địa chỉ mới. + +funds.withdrawal.arbitrationFee=Phí trọng tài +funds.withdrawal.inputs=Lựa chọn số liệu đầu vào +funds.withdrawal.useAllInputs=Sử dụng tất cả số liệu đầu vào sẵn có +funds.withdrawal.useCustomInputs=Sử dụng số liệu đầu vào thông dụng +funds.withdrawal.receiverAmount=Số tiền của người nhận +funds.withdrawal.senderAmount=Số tiền của người gửi +funds.withdrawal.feeExcluded=Số tiền không bao gồm phí đào +funds.withdrawal.feeIncluded=Số tiền bao gồm phí đào +funds.withdrawal.fromLabel=Rút từ địa chỉ +funds.withdrawal.toLabel=rút tới địa chỉ +funds.withdrawal.memoLabel=Withdrawal memo +funds.withdrawal.memo=Optionally fill memo +funds.withdrawal.withdrawButton=Rút được chọn +funds.withdrawal.noFundsAvailable=Không còn tiền để rút +funds.withdrawal.confirmWithdrawalRequest=Xác nhận yêu cầu rút +funds.withdrawal.withdrawMultipleAddresses=Rút từ nhiều địa chỉ ({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=Rút từ nhiều địa chỉ:\n{0} +funds.withdrawal.notEnoughFunds=Bạn không còn đủ tiền trong ví. +funds.withdrawal.selectAddress=Chọn địa chỉ nguồn từ bảng +funds.withdrawal.setAmount=Cài đặt số tiền được rút +funds.withdrawal.fillDestAddress=Điền địa chỉ đến của bạn +funds.withdrawal.warn.noSourceAddressSelected=Bạn cần chọn địa chỉ nguồn ở bảng trên. +funds.withdrawal.warn.amountExceeds=Bạn không có đủ tiền từ địa chỉ được chọn.\nXem xét chọn nhiều địa chỉ ở bảng trên hoặc thay đổi cài đặt phí để bao gồm phí đào. + +funds.reserved.noFunds=Không có tiền dự trữ trong báo giá mở +funds.reserved.reserved=Dự trữ trong ví nội bộ để chào giá với ID: {0} + +funds.locked.noFunds=Không có tiền bị khóa trong giao dịch +funds.locked.locked=Khóa trong multisig để giao dịch với ID: {0} + +funds.tx.direction.sentTo=Gửi đến: +funds.tx.direction.receivedWith=Nhận với: +funds.tx.direction.genesisTx=Từ giao dịch gốc: +funds.tx.txFeePaymentForBsqTx=Phí đào cho giao dịch BSQ +funds.tx.createOfferFee=Người tạo và phí tx: {0} +funds.tx.takeOfferFee=Người nhận và phí tx: {0} +funds.tx.multiSigDeposit=Tiền gửi Multisig: {0} +funds.tx.multiSigPayout=Tiền trả Multisig: {0} +funds.tx.disputePayout=Tiền trả khiếu nại: {0} +funds.tx.disputeLost=Vụ khiếu nại bị thua: {0} +funds.tx.collateralForRefund=Refund collateral: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} +funds.tx.unknown=Không rõ lý do: {0} +funds.tx.noFundsFromDispute=KHÔNG HOÀN LẠI từ khiếu nại +funds.tx.receivedFunds=Vốn đã nhận +funds.tx.withdrawnFromWallet=rút từ ví +funds.tx.withdrawnFromBSQWallet=BTC được rút từ ví BSQ +funds.tx.memo=Memo +funds.tx.noTxAvailable=Không có giao dịch nào +funds.tx.revert=Khôi phục +funds.tx.txSent=GIao dịch đã gửi thành công tới địa chỉ mới trong ví Bisq nội bộ. +funds.tx.direction.self=Gửi cho chính bạn +funds.tx.daoTxFee=Phí đào cho giao dịch BSQ +funds.tx.reimbursementRequestTxFee=Yêu cầu bồi hoàn +funds.tx.compensationRequestTxFee=Yêu cầu bồi thường +funds.tx.dustAttackTx=Số dư nhỏ đã nhận +funds.tx.dustAttackTx.popup=Giao dịch này đang gửi một lượng BTC rất nhỏ vào ví của bạn và có thể đây là cách các công ty phân tích chuỗi đang tìm cách theo dõi ví của bạn.\nNếu bạn sử dụng đầu ra giao dịch đó cho một giao dịch chi tiêu, họ sẽ phát hiện ra rằng rất có thể bạn cũng là người sở hửu cái ví kia (nhập coin). \n\nĐể bảo vệ quyền riêng tư của bạn, ví Bisq sẽ bỏ qua các đầu ra có số dư nhỏ dành cho mục đích chi tiêu cũng như hiển thị số dư. Bạn có thể thiết lập ngưỡng khi một đầu ra được cho là có số dư nhỏ trong phần cài đặt. + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.legacyArbitration.support=Legacy Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets +support.filter=Search disputes +support.filter.prompt=Nhập ID giao dịch, ngày tháng, địa chỉ onion hoặc dữ liệu tài khoản + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Thông báo riêng tư +support.reportButton.label=Report +support.fullReportButton.label=All disputes +support.noTickets=Không có đơn hỗ trợ được mở +support.sendingMessage=Đang gửi tin nhắn... +support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. +support.sendMessageError=Gửi tin nhắn thất bại. Lỗi: {0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=Báo giá trong khiếu nại này được tạo với phiên bản Bisq cũ.\nBạn không thể đóng khiếu nại với phiên bản ứng dụng của bạn.\n\nVui lòng sử dụng phiên bản giao thức cũ hơn {0} +support.openFile=Mở file để đính kèm (dung lượng file tối đa: {0} kb) +support.attachmentTooLarge=Tổng dung lượng file đính kèm là {0} kb và vượt quá dung lượng tối đa cho phép {1} kB. +support.maxSize=Dung lượng file tối đa cho phép là {0} kB. +support.attachment=File đính kèm +support.tooManyAttachments=Bạn không thể gửi quá 3 file đính kèm trong một tin. +support.save=Lưu file vào đĩa +support.messages=Tin nhắn +support.input.prompt=Enter message... +support.send=Gửi +support.addAttachments=Thêm file đính kèm +support.closeTicket=Đóng đơn hỗ trợ +support.attachments=File đính kèm: +support.savedInMailbox=Tin nhắn lưu trong hộp thư của người nhận +support.arrived=Tin nhắn đã đến người nhận +support.acknowledged=xác nhận tin nhắn đã được gửi bởi người nhận +support.error=Người nhận không thể tiến hành gửi tin nhắn: Lỗi: {0} +support.buyerAddress=Địa chỉ người mua BTC +support.sellerAddress=Địa chỉ người bán BTC +support.role=Vai trò +support.agent=Support agent +support.state=Trạng thái +support.chat=Chat +support.closed=Đóng +support.open=Mở +support.process=Process +support.buyerOfferer=Người mua BTC/Người tạo +support.sellerOfferer=Người bán BTC/Người tạo +support.buyerTaker=Người mua BTC/Người nhận +support.sellerTaker=Người bán BTC/Người nhận + +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\nTraders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and suggest a payout of trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. If one or both traders do not agree to the mediator's suggested payout, they can request arbitration.The arbitrator will re-evaluate the situation and, if warranted, personally pay the trader back and request reimbursement for this payment from the Bisq DAO. +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Bisq are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.systemMsg=Tin nhắn hệ thống: {0} +support.youOpenedTicket=Bạn đã mở yêu cầu hỗ trợ.\n\n{0}\n\nPhiên bản Bisq: {1} +support.youOpenedDispute=Bạn đã mở yêu cầu giải quyết tranh chấp.\n\n{0}\n\nPhiên bản Bisq: {1} +support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq version: {1} +support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} +support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=Tham khảo +settings.tab.network=Thông tin mạng +settings.tab.about=Về + +setting.preferences.general=Tham khảo chung +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer +setting.preferences.deviation=Sai lệch tối đa so với giá thị trường +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate +setting.preferences.avoidStandbyMode=Tránh để chế độ chờ +setting.preferences.autoConfirmXMR=XMR auto-confirm +setting.preferences.autoConfirmEnabled=Enabled +setting.preferences.autoConfirmRequiredConfirmations=Required confirmations +setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) +setting.preferences.deviationToLarge=Giá trị không được phép lớn hơn {0}%. +setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) +setting.preferences.useCustomValue=Sử dụng giá trị thông dụng +setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte +setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. +setting.preferences.ignorePeers=Bỏ qua đối tác[địa chỉ onion:cổng] +setting.preferences.ignoreDustThreshold=Giá trị đầu ra tối thiểu không phải số dư nhỏ +setting.preferences.currenciesInList=Tiền tệ trong danh sách cung cấp giá thị trường +setting.preferences.prefCurrency=Tiền tệ ưu tiên +setting.preferences.displayFiat=Hiển thị tiền tệ các nước +setting.preferences.noFiat=Không có tiền tệ nước nào được chọn +setting.preferences.cannotRemovePrefCurrency=Bạn không thể gỡ bỏ tiền tệ ưu tiên được chọn +setting.preferences.displayAltcoins=Hiển thị altcoin +setting.preferences.noAltcoins=Không có altcoin nào được chọn +setting.preferences.addFiat=Bổ sung tiền tệ các nước +setting.preferences.addAltcoin=Bổ sung altcoin +setting.preferences.displayOptions=Hiển thị các phương án +setting.preferences.showOwnOffers=Hiển thị Báo giá của tôi trong danh mục Báo giá +setting.preferences.useAnimations=Sử dụng hoạt ảnh +setting.preferences.useDarkMode=Use dark mode +setting.preferences.sortWithNumOffers=Sắp xếp danh sách thị trường với số chào giá/giao dịch +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=Cài đặt lại tất cả nhãn \"Không hiển thị lại\" +settings.preferences.languageChange=Áp dụng thay đổi ngôn ngữ cho tất cả màn hình yêu cầu khởi động lại. +settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. +setting.preferences.daoOptions=Tùy chọn DAO +setting.preferences.dao.resyncFromGenesis.label=Tái dựng trạng thái DAO từ giao dịch genesis +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\nIf you proceed, after an application restart the Bisq network governance data will be reloaded from the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown +setting.preferences.dao.isDaoFullNode=Chạy ứng dụng Bisq như một full node DAO +setting.preferences.dao.rpcUser=Tên người dùng RPC +setting.preferences.dao.rpcPw=Mật khẩu RPC +setting.preferences.dao.blockNotifyPort=Cổng thông báo chặn +setting.preferences.dao.fullNodeInfo=Để chạy Bisq như một DAO full node, bạn cần phải chạy Bitcoin Core trên máy tính của mình và cho phép RPC. Tất cả các yêu cầu khác được nêu rõ tại ''{0}''.\n\nSau khi thay đổi chế độ bạn cần phải khởi động lại. +setting.preferences.dao.fullNodeInfo.ok=Mở trang docs +setting.preferences.dao.fullNodeInfo.cancel=Không, tôi sẽ tiếp tục dùng chế độ lite node +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen=Chosen explorer settings +settings.preferences.editCustomExplorer.name=Tên +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL + +settings.net.btcHeader=Mạng Bitcoin +settings.net.p2pHeader=Bisq network +settings.net.onionAddressLabel=Địa chỉ onion của tôi +settings.net.btcNodesLabel=Sử dụng nút Bitcoin Core thông dụng +settings.net.bitcoinPeersLabel=Các đối tác được kết nối +settings.net.useTorForBtcJLabel=Sử dụng Tor cho mạng Bitcoin +settings.net.bitcoinNodesLabel=nút Bitcoin Core để kết nối +settings.net.useProvidedNodesRadio=Sử dụng các nút Bitcoin Core đã cung cấp +settings.net.usePublicNodesRadio=Sử dụng mạng Bitcoin công cộng +settings.net.useCustomNodesRadio=Sử dụng nút Bitcoin Core thông dụng +settings.net.warn.usePublicNodes=If you use the public Bitcoin network you are exposed to a severe privacy problem caused by the broken bloom filter design and implementation which is used for SPV wallets like BitcoinJ (used in Bisq). Any full node you are connected to could find out that all your wallet addresses belong to one entity.\n\nPlease read more about the details at [HYPERLINK:https://bisq.network/blog/privacy-in-bitsquare].\n\nAre you sure you want to use the public nodes? +settings.net.warn.usePublicNodes.useProvided=Không, sử dụng nút được cung cấp +settings.net.warn.usePublicNodes.usePublic=Vâng, sử dụng nút công cộng +settings.net.warn.useCustomNodes.B2XWarning=Vui lòng chắc chắn rằng nút Bitcoin của bạn là nút Bitcoin Core đáng tin cậy!\n\nKết nối với nút không tuân thủ nguyên tắc đồng thuận Bitcoin Core có thể làm hỏng ví của bạn và gây ra các vấn đề trong quá trình giao dịch.\n\nNgười dùng kết nối với nút vi phạm nguyên tắc đồng thuận chịu trách nhiệm đối với các thiệt hại mà việc này gây ra. Các khiếu nại do điều này gây ra sẽ được quyết định theo hướng có lợi cho đối tác bên kia. Sẽ không có hỗ trợ về mặt kỹ thuật nào cho người dùng không tuân thủ cơ chế cảnh báo và bảo vệ của chúng tôi! +settings.net.warn.invalidBtcConfig=Connection to the Bitcoin network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Bitcoin nodes instead. You will need to restart the application. +settings.net.localhostBtcNodeInfo=Background information: Bisq looks for a local Bitcoin node when starting. If it is found, Bisq will communicate with the Bitcoin network exclusively through it. +settings.net.p2PPeersLabel=Các đối tác được kết nối +settings.net.onionAddressColumn=Địa chỉ onion +settings.net.creationDateColumn=Đã thiết lập +settings.net.connectionTypeColumn=Vào/Ra +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height +settings.net.roundTripTimeColumn=Khứ hồi +settings.net.sentBytesColumn=Đã gửi +settings.net.receivedBytesColumn=Đã nhận +settings.net.peerTypeColumn=Kiểu đối tác +settings.net.openTorSettingsButton=Mở cài đặt Tor + +settings.net.versionColumn=Version +settings.net.subVersionColumn=Subversion +settings.net.heightColumn=Height + +settings.net.needRestart=Bạn cần khởi động lại ứng dụng để thay đổi.\nBạn có muốn khởi động bây giờ không? +settings.net.notKnownYet=Chưa biết... +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=[Địa chỉ IP:tên cổng | máy chủ:cổng | Địa chỉ onion:cổng] (tách bằng dấu phẩy). Cổng có thể bỏ qua nếu sử dụng mặc định (8333). +settings.net.seedNode=nút cung cấp thông tin +settings.net.directPeer=Đối tác (trực tiếp) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=Đối tác +settings.net.inbound=chuyến về +settings.net.outbound=chuyến đi +settings.net.reSyncSPVChainLabel=Đồng bộ hóa lại SPV chain +settings.net.reSyncSPVChainButton=Xóa file SPV và đồng bộ hóa lại +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=File chuỗi SPV đã được xóa. Vui lòng đợi, có thể mất một lúc để đồng bộ hóa với mạng. +settings.net.reSyncSPVAfterRestartCompleted=Đồng bộ hóa đã xong. Vui lòng khởi động lại ứng dụng. +settings.net.reSyncSPVFailed=Không thể xóa SPV chain file.\nLỗi: {0} +setting.about.aboutBisq=Về Bisq +setting.about.about=Bisq là một phần mềm mã nguồn mở nhằm hỗ trợ quá trình trao đổi giữa bitcoin và tiền tệ quốc gia (và các loại tiền crypto khác) thông qua một mạng lưới ngang hàng phi tập trung hoạt động trên cơ sở bảo vệ tối đa quyền riêng tư của người dùng. Vui lòng tìm hiểu thêm về Bisq trên trang web dự án của chúng tôi. +setting.about.web=Trang web Bisq +setting.about.code=Mã nguồn +setting.about.agpl=Giấy phép AGPL +setting.about.support=Hỗ trợ Bisq +setting.about.def=Bisq không phải là một công ty mà là một dự án mở cho cả cộng đồng. Nếu bạn muốn tham gia hoặc hỗ trợ Bisq, vui lòng truy cập link dưới đây. +setting.about.contribute=Góp vốn +setting.about.providers=Nhà cung cấp dữ liệu +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.pricesProvided=Giá thị trường cung cấp bởi +setting.about.feeEstimation.label=Ước tính phí đào cung cấp bởi +setting.about.versionDetails=Thông tin về phiên bản +setting.about.version=Phiên bản ứng dụng +setting.about.subsystems.label=Các phiên bản của hệ thống con +setting.about.subsystems.val=Phiên bản mạng: {0}; Phiên bản tin nhắn P2P: {1}; Phiên bản DB nội bộ: {2}; Phiên bản giao thức giao dịch: {3} + +setting.about.shortcuts=Short cuts +setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' + +setting.about.shortcuts.menuNav=Navigate main menu +setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' + +setting.about.shortcuts.close=Close Bisq +setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' + +setting.about.shortcuts.closePopup=Close popup or dialog window +setting.about.shortcuts.closePopup.value='ESCAPE' key + +setting.about.shortcuts.chatSendMsg=Send trader chat message +setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' + +setting.about.shortcuts.openDispute=Open dispute +setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} + +setting.about.shortcuts.walletDetails=Open wallet details window + +setting.about.shortcuts.openEmergencyBtcWalletTool=Open emergency wallet tool for BTC wallet + +setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool for BSQ wallet + +setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN + +setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx + +setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) +setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} + +setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) +setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} + +setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) + +setting.about.shortcuts.sendFilter=Set Filter (privileged activity) + +setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} + +setting.info.headline=New XMR auto-confirm Feature +setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Bisq can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Bisq uses explorer nodes run by Bisq contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of BTC per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Bisq wiki [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades] +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing +account.info.headline=Chào mừng đến với tài khoản Bisq của bạn +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). + +account.menu.paymentAccount=Tài khoản tiền tệ quốc gia +account.menu.altCoinsAccountView=Tài khoản Altcoin +account.menu.password=Password ví +account.menu.seedWords=Mã sao lưu dự phòng ví +account.menu.walletInfo=Wallet info +account.menu.backup=Dự phòng +account.menu.notifications=Thông báo + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=Public key (địa chỉ ví) + +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=Hủy +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.warn.min1Language=Bạn cần cài đặt ít nhất 1 ngôn ngữ.\nChúng tôi thêm ngôn ngữ mặc định cho bạn. +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} + +account.altcoin.yourAltcoinAccounts=Tài khoản altcoin của bạn +account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.altcoin.popup.wallet.confirm=Tôi hiểu và xác nhận rằng tôi đã biết loại ví mình cần sử dụng. +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=Trading UPX on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=Trading XMR on Bisq requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://bisq.wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Bisq now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades]. +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN yêu cầu một quá trình tương tác giữa người gửi và người nhận để thực hiện một giao dịch. Vui lòng làm theo hướng dẫn từ trang web của dự án GRIN để gửi và nhận GRIN đúng cách. (người nhận cần phải trực tuyến hoặc ít nhất là trực tuyến trong một khung thời gian nhất định).\n\nBisq chỉ hỗ trợ ví Grinbox(wallet713) theo định dạng URL.\n\nNgười gửi GRIN phải cung cấp bằng chứng là họ đã gửi GRIN thành công. Nếu ví không thể cung cấp bằng chứng đó, nếu có tranh chấp thì sẽ được giải quyết theo hướng có lợi cho người nhận GRIN. Vui lòng đảm bảo rằng bạn sử dụng phần mềm Grinbox mới nhất có hỗ trợ bằng chứng giao dịch và bạn hiểu quy trình chuyển và nhận GRIN cũng như tạo bằng chứng. \n\nXem https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only để biết thêm thông tin về công cụ bằng chứng Grinbox. +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM yêu cầu một quá trình tương tác giữa người gửi và người nhận để thực hiện một giao dịch. \n\nVui lòng làm theo hướng dẫn từ trang web của dự án BEAM để gửi và nhận BEAM đúng cách. (người nhận cần phải trực tuyến hoặc ít nhất là trực tuyến trong một khung thời gian nhất định).\n\nNgười gửi BEAM phải cung cấp bằng chứng là họ đã gửi BEAM thành công. Vui lòng đảm bảo là bạn sử dụng phần mềm ví có thể tạo ra một bằng chứng như vậy. Nếu ví không thể cung cấp bằng chứng đó, nếu có tranh chấp thì sẽ được giải quyết theo hướng có lợi cho người nhận BEAM. +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=Trading ParsiCoin on Bisq requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Bisq, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=Trading L-BTC on Bisq requires that you understand the following:\n\nWhen receiving L-BTC for a trade on Bisq, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-BTC into the Liquid Elements Core wallet, or another L-BTC wallet which allows you to obtain the blinding key for your blinded L-BTC address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-BTC address to the Bisq mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-BTC receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-BTC on Bisq. + +account.fiat.yourFiatAccounts=Các tài khoản tiền tệ quốc gia của bạn + +account.backup.title=Ví dự phòng +account.backup.location=Vị trí sao lưu +account.backup.selectLocation=Chọn vị trí dự phòng +account.backup.backupNow=Dự phòng bây giờ (dự phòng không được mã hóa!) +account.backup.appDir=Thư mục dữ liệu ứng dụng +account.backup.openDirectory=Mở thư mục +account.backup.openLogFile=Mở tệp nhật ký +account.backup.success=Dự phòng đã lưu thành công tại:\n{0} +account.backup.directoryNotAccessible=Thư mục bạn vừa chọn không thể truy cập được. {0} + +account.password.removePw.button=Gỡ bỏ mật khẩu +account.password.removePw.headline=Gỡ bỏ bảo vệ mật khẩu cho ví +account.password.setPw.button=Cài đặt mật khẩu +account.password.setPw.headline=Cài đặt bảo vệ mật khẩu cho ví +account.password.info=Bằng cách bảo vệ với mật khẩu, bạn cần nhập mật khẩu khi khởi động ứng dụng, khi rút bitcoin ra khỏi ví hoặc khi bạn muốn khôi phục ví từ các từ khởi tạo. + +account.seed.backup.title=Sao lưu dự phòng từ khởi tạo ví của bạn +account.seed.info=Hãy viết ra từ khởi tạo ví của bạn và ngày! Bạn có thể khôi phục ví của bạn bất cứ lúc nào với các từ khởi tạo và ngày này.\nTừ khởi tạo được sử dụng chung cho cả ví BTC và BSQ.\n\nBạn nên viết các từ khởi tạo ra tờ giấy. Không được lưu trên máy tính.\n\nLưu ý rằng từ khởi tạo KHÔNG PHẢI là phương án thay thế cho sao lưu dự phòng.\nBạn cần sao lưu dự phòng toàn bộ thư mục của ứng dụng tại màn hình \"Tài khoản/Sao lưu dự phòng\" để khôi phục trạng thái và dữ liệu ứng dụng.\nNhập từ khởi tạo chỉ được thực hiện trong tình huống khẩn cấp. Ứng dụng sẽ không hoạt động mà không có dự phòng các file dữ liệu và khóa phù hợp! +account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page [HYPERLINK:https://bisq.wiki/Backing_up_application_data] for extended info. +account.seed.warn.noPw.msg=Bạn đã tạo mật khẩu ví để bảo vệ tránh hiển thị Seed words.\n\nBạn có muốn hiển thị Seed words? +account.seed.warn.noPw.yes=Có và không hỏi lại +account.seed.enterPw=Nhập mật khẩu để xem seed words +account.seed.restore.info=Vui lòng tạo sao lưu dự phòng trước khi tiến hành khôi phục ví từ các từ khởi tạo. Phải hiểu rằng việc khôi phục ví chỉ nên thực hiện trong các trường hợp khẩn cấp và có thể gây sự cố với cơ sở dữ liệu ví bên trong.\nĐây không phải là một cách sao lưu dự phòng! Vui lòng sử dụng sao lưu dự phòng từ thư mục dữ liệu của ứng dụng để khôi phục trạng thái ban đầu của ứng dụng.\n\nSau khi khôi phục ứng dụng sẽ tự động tắt. Sau khi bạn khởi động lại, ứng dụng sẽ tái đồng bộ với mạng Bitcoin. Quá trình này có thể mất một lúc và tiêu tốn khá nhiều CPU, đặc biệt là khi ví đã cũ và có nhiều giao dịch. Vui lòng không làm gián đoạn quá trình này, nếu không bạn có thể sẽ phảỉ xóa file chuỗi SPV một lần nữa hoặc lặp lại quy trình khôi phục. +account.seed.restore.ok=Được, hãy thực hiện khôi phục và tắt ứng dụng Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=Cài đặt +account.notifications.download.label=Tải ứng dụng di động +account.notifications.waitingForWebCam=Đang chờ webcam... +account.notifications.webCamWindow.headline=Quét mã QR từ điện thoại +account.notifications.webcam.label=Dùng webcam +account.notifications.webcam.button=Quét mã QR +account.notifications.noWebcam.button=Tôi không có webcam +account.notifications.erase.label=xóa thông báo trên điện thoại +account.notifications.erase.title=Xóa thông báo +account.notifications.email.label=Mã tài sản đảm bảo +account.notifications.email.prompt=Nhập mã tài sản đảm bảo bạn nhận được từ email +account.notifications.settings.title=Cài đặt +account.notifications.useSound.label=Bật âm thanh thông báo trên điện thoại +account.notifications.trade.label=Nhận tin nhắn giao dịch +account.notifications.market.label=Nhận thông báo chào hàng +account.notifications.price.label=Nhận thông báo về giá +account.notifications.priceAlert.title=Thông báo về giá +account.notifications.priceAlert.high.label=Thông báo nếu giá BTC cao hơn +account.notifications.priceAlert.low.label=Thông báo nếu giá BTC thấp hơn +account.notifications.priceAlert.setButton=Đặt thông báo giá +account.notifications.priceAlert.removeButton=Gỡ thông báo giá +account.notifications.trade.message.title=Trạng thái giao dịch thay đổi +account.notifications.trade.message.msg.conf=Lệnh nạp tiền cho giao dịch có mã là {0} đã được xác nhận. Vui lòng mở ứng dụng Bisq và bắt đầu thanh toán. +account.notifications.trade.message.msg.started=Người mua BTC đã tiến hành thanh toán cho giao dịch có mã là {0}. +account.notifications.trade.message.msg.completed=Giao dịch có mã là {0} đã hoàn thành. +account.notifications.offer.message.title=Chào giá của bạn đã được chấp nhận +account.notifications.offer.message.msg=Chào giá mã {0} của bạn đã được chấp nhận +account.notifications.dispute.message.title=Tin nhắn tranh chấp mới +account.notifications.dispute.message.msg=Bạn nhận được một tin nhắn tranh chấp trong giao dịch có mã {0} + +account.notifications.marketAlert.title=Thông báo chào giá +account.notifications.marketAlert.selectPaymentAccount=Tài khoản thanh toán khớp với chào giá +account.notifications.marketAlert.offerType.label=Loại chào giátôi muốn +account.notifications.marketAlert.offerType.buy=Chào mua (Tôi muốn bán BTC) +account.notifications.marketAlert.offerType.sell=Chào bán (Tôi muốn mua BTC) +account.notifications.marketAlert.trigger=Khoảng cách giá chào (%) +account.notifications.marketAlert.trigger.info=Khi đặt khoảng cách giá, bạn chỉ nhận được thông báo khi có một chào giá bằng (hoặc cao hơn) giá bạn yêu cầu được đăng lên. Ví dụ: Bạn muốn bán BTC, nhưng chỉ bán với giá cao hơn thị trường hiện tại 2%. Đặt trường này ở mức 2% sẽ đảm bảo là bạn chỉ nhận được thông báo từ những chào giá cao hơn 2%(hoặc hơn) so với giá thị trường hiện tại. +account.notifications.marketAlert.trigger.prompt=Khoảng cách phần trăm so với giá thị trường (vd: 2.50%, -0.50%, ...) +account.notifications.marketAlert.addButton=Thêm thông báo chào giá +account.notifications.marketAlert.manageAlertsButton=Quản lý thông báo chào giá +account.notifications.marketAlert.manageAlerts.title=Quản lý thông báo chào giá +account.notifications.marketAlert.manageAlerts.header.paymentAccount=tài khoản thanh toán +account.notifications.marketAlert.manageAlerts.header.trigger=Giá khởi phát +account.notifications.marketAlert.manageAlerts.header.offerType=Loại chào giá +account.notifications.marketAlert.message.title=Thông báo chào giá +account.notifications.marketAlert.message.msg.below=cao hơn +account.notifications.marketAlert.message.msg.above=thấp hơn +account.notifications.marketAlert.message.msg=một ''{0} {1}'' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán ''{5}''đã được đăng lên danh mục chào giá của Bisq.\nMã chào giá: {6}. +account.notifications.priceAlert.message.title=Thông báo giá cho {0} +account.notifications.priceAlert.message.msg=Thông báo giá của bạn đã được kích hoạt. Giá {0} hiện tại là {1} {2} +account.notifications.noWebCamFound.warning=Không tìm thấy webcam.\n\nVui lòng sử dụng lựa chọn email để gửi mã bảo mật và khóa mã hóa từ điện thoại di động của bạn tới ứng dùng Bisq. +account.notifications.priceAlert.warning.highPriceTooLow=Giá cao hơn phải lớn hơn giá thấp hơn. +account.notifications.priceAlert.warning.lowerPriceTooHigh=Giá thấp hơn phải nhỏ hơn giá cao hơn. + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=Thông tin & Số liệu +dao.tab.bsqWallet=Ví BSQ +dao.tab.proposals=Đề xuất +dao.tab.bonding=Tài sản đảm bảo +dao.tab.proofOfBurn=Phí niêm yết tài sản/ Bằng chứng đốt +dao.tab.monitor=Giám sát mạng +dao.tab.news=Tin tức + +dao.paidWithBsq=thanh toán bằng BSQ +dao.availableBsqBalance=Số dư khả dụng (những dầu ra thay đổi đã và chưa xác nhận) +dao.verifiedBsqBalance=Số dư tất cả các đầu ra giao dịch chưa sử dụng đã xác minh +dao.unconfirmedChangeBalance=Số dư tất cả các đầu ra thay đổi chưa xác nhận +dao.unverifiedBsqBalance=Số dư tất cả các giao dịch chưa xác minh (đang chờ xác nhận) +dao.lockedForVoteBalance=Dùng để bỏ phiếu +dao.lockedInBonds=Bị khóa trong hợp đồng +dao.availableNonBsqBalance=Số dư không phải BSQ có sẵn (tính bằng BTC) +dao.reputationBalance=Merit Value (not spendable) + +dao.tx.published.success=Giao dịch của bạn đã được công bố thành công. +dao.proposal.menuItem.make=Tạo đề nghị +dao.proposal.menuItem.browse=Xem những chào giá đang mở +dao.proposal.menuItem.vote=Bỏ phiếu chọn đề xuất +dao.proposal.menuItem.result=Kết quả bỏ phiếu +dao.cycle.headline=Vòng bỏ phiếu +dao.cycle.overview.headline=Tổng quan vòng bỏ phiếu +dao.cycle.currentPhase=Giai đoạn hiện tại +dao.cycle.currentBlockHeight=Chiều cao khối hiện tại +dao.cycle.proposal=Giai đoạn đề xuất +dao.cycle.proposal.next=Giai đoạn đề xuất tiếp theo +dao.cycle.blindVote=Mở để bỏ phiếu +dao.cycle.voteReveal=Mở để công khai khóa bỏ phiếu +dao.cycle.voteResult=Kết quả bỏ phiếu +dao.cycle.phaseDuration={0} khối(≈{1}); Khối{2} - {3} (≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=Khối {0} - {1} (≈{2} - ≈{3}) + +dao.voteReveal.txPublished.headLine=Giao dịch công khai phiếu bầu đã công bố +dao.voteReveal.txPublished=Giao dịch công khai phiếu bầu của bạn với ID giao dịch {0} dã được công bố thành công.\n\nQuá trình này xả ra tự động bở phần mềm nếu bạn đã tham gia bầu chọn DAO. + +dao.results.cycles.header=Các vòng +dao.results.cycles.table.header.cycle=Vòng +dao.results.cycles.table.header.numProposals=Đề xuất +dao.results.cycles.table.header.voteWeight=Sức nặng của phiếu bầu +dao.results.cycles.table.header.issuance=Phát hành + +dao.results.results.table.item.cycle=Vòng {0} bắt đầu: {1} + +dao.results.proposals.header=Các đề xuất của vòng được chọn +dao.results.proposals.table.header.nameLink=Name/link +dao.results.proposals.table.header.details=Thông tin chi tiết +dao.results.proposals.table.header.myVote=Phiếu bầu của tôi +dao.results.proposals.table.header.result=Kết quả bầu chọn +dao.results.proposals.table.header.threshold=Threshold +dao.results.proposals.table.header.quorum=Quorum + +dao.results.proposals.voting.detail.header=Kết quả của đề xuất được chọn + +dao.results.exceptions=(Các) kết quả bỏ phiếu ngoại lệ + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=Không xác định + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=Phí maker BSQ +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=Phí taker BSQ +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=Phí maker BSQ tối thiểu +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=Phí taker BSQ tối thiểu +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=Phí maker BTC +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=Phí taker BTC +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=Phí maker BTC tối thiểu +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=Phí taker BTC tối thiểu +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=Phí đề xuất tính bằng BSQ +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=Phí bỏ phiếu tính bằng BSQ + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=Lượng BSQ yêu cầu bồi thường tối thiểu +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=Lượng BSQ yêu cầu bồi thường tối đa +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=Lượng BSQ yêu cầu bồi hoàn tối thiểu +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=Lượng BSQ yêu cầu bồi hoàn tối đa + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=Số đại biểu tính theo BSQ cần cho một đề xuất chung +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=Số đại biểu tính theo BSQ cần cho một yêu cầu bồi thường +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=Số đại biểu tính theo BSQ cần cho một yêu cầu bồi hoàn +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=Số đại biểu tính theo BSQ cần để thay đổi một thông số +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=Số đại biểu tính theo BSQ cần để gỡ bỏ một tài sản +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=Số đại biểu tính theo BSQ cần cho một yêu cầu tịch thu +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=Số đại biểu tính theo BSQ cần cho những yêu cầu về vai trò đảm bảo + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=Ngưỡng cần thiết tính theo % cho một yêu cầu chung +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=Ngưỡng cần thiết tính theo % cho một yêu cầu bồi thường +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=Ngưỡng cần thiết tính theo % cho một yêu cầu bồi hoàn +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=Ngưỡng cần thiết tính theo % để thay đổi một thông số +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=Ngưỡng cần thiết tính theo % để gỡ bỏ một tài sản +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=Ngưỡng cần thiết tính theo % cho một yêu cầu tịch thu +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=Ngưỡng cần thiết tính theo % cho các yêu cầu về vai trò đảm bảo + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=Địa chỉ BTC của người nhận + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=Phí niêm yết tài sản/ ngày +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Khối lượng giao dịch tối thiểu cho tài sản + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=Thời gian khóa dành cho tx trả phí giao dịch thay thế +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=Phí trọng tài tính bằng BTC + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=Giới giạn giao dịch tối đa tính bằng BTC + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=Yếu tố đơn vị của vai trò đảm bảo tính bằng BSQ +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=Giới hạn phát hành trên mỗi vòng tính bằng BSQ + +dao.param.currentValue=Giá trị hiện tại: {0} +dao.param.currentAndPastValue=Current value: {0} (Value when proposal was made: {1}) +dao.param.blocks={0} khối + +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was not distributed well in the Bisq network.\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=Giai đoạn đề xuất +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=Tạm dừng 1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=Mở để bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=Tạm dừng 2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=Mở để công khai khóa bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=Tạm dừng 3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=Giai đoạn kết quả + +dao.results.votes.table.header.stakeAndMerit=Sức nặng của phiếu bầu +dao.results.votes.table.header.stake=Tiền đầu tư +dao.results.votes.table.header.merit=Kiếm được +dao.results.votes.table.header.vote=Bỏ phiếu + +dao.bond.menuItem.bondedRoles=Vai trò đảm bảo +dao.bond.menuItem.reputation=Danh tiếng tài sản đảm bảo +dao.bond.menuItem.bonds=Tài sản đảm bảo + +dao.bond.dashboard.bondsHeadline=Tài sản đảm bảo BSQ +dao.bond.dashboard.lockupAmount=Quỹ khóa +dao.bond.dashboard.unlockingAmount=Quỹ đang mở khóa (chờ tới hết thời gian khóa) + + +dao.bond.reputation.header=Khóa tài sản để tạo danh tiếng +dao.bond.reputation.table.header=Tài sản đảm bảo danh tiếng của tôi +dao.bond.reputation.amount=Lượng BSQ để khóa +dao.bond.reputation.time=Thời gian mở khóa tính bằng khối +dao.bond.reputation.salt=Salt +dao.bond.reputation.hash=Hash +dao.bond.reputation.lockupButton=Khóa +dao.bond.reputation.lockup.headline=Xác nhận giao dịch khóa +dao.bond.reputation.lockup.details=Lockup amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? +dao.bond.reputation.unlock.headline=Xác nhận giao dịch mở khóa +dao.bond.reputation.unlock.details=Unlock amount: {0}\nUnlock time: {1} block(s) (≈{2})\n\nMining fee: {3} ({4} Satoshis/vbyte)\nTransaction vsize: {5} Kb\n\nAre you sure you want to proceed? + +dao.bond.allBonds.header=Tất cả cách tài sản đảm bảo + +dao.bond.bondedReputation=Danh tiếng được đảm bảo bằng tài sản +dao.bond.bondedRoles=Vai trò đảm bảo + +dao.bond.details.header=Chi tiết về vai trò +dao.bond.details.role=Vai trò +dao.bond.details.requiredBond=Tài sản đảm bảo BSQ yêu cầu +dao.bond.details.unlockTime=Thời gian mở khóa tính bằng khối +dao.bond.details.link=Đường dẫn tới mô tả vai trò +dao.bond.details.isSingleton=Có thể được thực hiện bởi nhiều người với vai trò khác nhau +dao.bond.details.blocks={0} khối + +dao.bond.table.column.name=Tên +dao.bond.table.column.link=đường dẫn +dao.bond.table.column.bondType=Loại tài sản đảm bảo +dao.bond.table.column.details=Thông tin chi tiết +dao.bond.table.column.lockupTxId=Mã giao dịch khóa +dao.bond.table.column.bondState=Trạng thái tài sản đảm bảo +dao.bond.table.column.lockTime=Unlock time +dao.bond.table.column.lockupDate=Ngày khóa + +dao.bond.table.button.lockup=Khóa +dao.bond.table.button.unlock=Mở khóa +dao.bond.table.button.revoke=Hủy + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=Chưa có tài sản đảm bảo +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=Chờ khóa +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=Tài sản đảm bảo đã khóa +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=Chờ mở khóa +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=Giao dịch mở khóa đã xác nhận +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=Tài sản đảm bảo đang mở khóa +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=Tài sản đảm bảo đã mở khóa +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=Tài sản đảm bảo đã bị tịch thu + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=Vai trò đảm bảo +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=Danh tiếng tài sản đảm bảo + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Quản trị Github +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=Quản trị diễn đàn +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Quản trị Twitter +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Quản trị Youtube +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Nhân viên bảo trì Bisq +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=Bên bảo trì BitcoinJ-fork +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Bên bảo trì Netlayer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Điều hành website +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Điều hành diễn đàn +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Người chạy seed node +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Điều hành giá node +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Điều hành thị trường +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Điều hành rơ-le thông báo điện thoại +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Người giữ tên miền +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=Quản trị DNS +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Người hòa giải +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Trọng tài +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=Chủ địa chỉ ví BTC hiến tặng + +dao.burnBsq.assetFee=Niêm yết tài sản +dao.burnBsq.menuItem.assetFee=Phí niêm yết tài sản +dao.burnBsq.menuItem.proofOfBurn=Bằng chứng đốt +dao.burnBsq.header=Phí để niêm yết tài sản +dao.burnBsq.selectAsset=Chọn tài sản +dao.burnBsq.fee=Phí +dao.burnBsq.trialPeriod=Giai đoạn dùng thử +dao.burnBsq.payFee=Trả phí +dao.burnBsq.allAssets=Tất cả tài sản +dao.burnBsq.assets.nameAndCode=Tên tài sản +dao.burnBsq.assets.state=Trạng thái +dao.burnBsq.assets.tradeVolume=Khối lượng giao dịch +dao.burnBsq.assets.lookBackPeriod=Giai đoạn xác minh +dao.burnBsq.assets.trialFee=Phí cho giai đoạn dùng thử +dao.burnBsq.assets.totalFee=Tổng số phí đã trả +dao.burnBsq.assets.days={0} ngày +dao.burnBsq.assets.toFewDays=Phí tài sản quá thấp. Số ngày tối thiểu cho giai đoạn dùng thử là {0}. + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=Đang trong giai đoạn dùng thử +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Đang được giao dịch tích cực +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=Bị gỡ bỏ vì ít hoạt động +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Bị gỡ bỏ thông qua bỏ phiếu + +dao.proofOfBurn.header=Bằng chứng đốt +dao.proofOfBurn.amount=Số tiền +dao.proofOfBurn.preImage=Pre-image +dao.proofOfBurn.burn=Đốt +dao.proofOfBurn.allTxs=Tất cả bằng chứng của các giao dịch đốt +dao.proofOfBurn.myItems=Bằng chứng giao dịch đốt của tôi +dao.proofOfBurn.date=Ngày +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Giao dịch +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Ký một thông điệp với khóa lấy từ bằng chứng giao dịch đót +dao.proofOfBurn.verify.window.title=Xác minh một thông điệp với khóa lấy từ bằng chứng giao dịch đốt +dao.proofOfBurn.copySig=Sao chép chữ ký tới clipboard +dao.proofOfBurn.sign=Ký +dao.proofOfBurn.message=Tin nhắn +dao.proofOfBurn.sig=Chữ ký +dao.proofOfBurn.verify=Xác minh +dao.proofOfBurn.verificationResult.ok=Xác minh thành công +dao.proofOfBurn.verificationResult.failed=Xác minh thất bại + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=Giai đoạn đề xuất +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=Ngừng trước khi bắt đầu bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=Mở để bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=Ngừng trước khi bắt đầu xác nhận bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=Mở để công khai khóa bỏ phiếu +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=Ngừng trước khi bắt đầu giai đoạn phát hành +# suppress inspection "UnusedProperty" +dao.phase.RESULT=giai đoạn kết quả bỏ phiếu + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=Giai đoạn đề xuất +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=Bỏ phiếu mù +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=Công khai phiếu bầu +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=Kết quả bỏ phiếu + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=Yêu cầu bồi thường +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=Yêu cầu bồi hoàn +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=Đề xuất một vai trò đảm bảo +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=Đề xuất gỡ bỏ một tài sản +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=Đề xuất thay đổi một thông số +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=Đề xuất chung +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=Đề xuất tịch thu tài sản đảm bảo + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=Yêu cầu bồi thường +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=Yêu cầu bồi hoàn +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=Vai trò đảm bảo +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=Gỡ bỏ một altcoin +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=Thay đổi một thông số +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=Đề xuất chung +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=Tịch thu tài sản đảm bảo + +dao.proposal.details=Thông tin về đề xuất +dao.proposal.selectedProposal=Đề xuất được chọn +dao.proposal.active.header=Các đề xuất có hiệu lực +dao.proposal.active.remove.confirm=Bạn có chắc bạn muốn gỡ bỏ đề xuất này?\nPhí đề xuất đã trả sẽ bị mất. +dao.proposal.active.remove.doRemove=Vâng, xin hãy gở đề xuất của tôi +dao.proposal.active.remove.failed=Không thể gỡ bỏ đề xuất. +dao.proposal.myVote.title=Bỏ phiếu +dao.proposal.myVote.accept=Chấp nhận đề xuất +dao.proposal.myVote.reject=Từ chối đề xuất +dao.proposal.myVote.removeMyVote=Bỏ qua đề xuất +dao.proposal.myVote.merit=Trọng lượng phiếu bầu từ BSQ kiếm được +dao.proposal.myVote.stake=Tiền đầu tư BSQ để bỏ phiếu +dao.proposal.myVote.revealTxId=Mã giao dịch công khai phiếu bầu +dao.proposal.myVote.stake.prompt=Số tiền cọc khả dụng tối đa để bỏ phiếu: {0} +dao.proposal.votes.header=Cài đặt tiền cọc dành cho bỏ phiếu và công bố các phiếu bầu +dao.proposal.myVote.button=Công bố phiếu bầu +dao.proposal.myVote.setStake.description=Sau khi bỏ phiếu chọn các đề xuất bạn phải cài đặt số tiền cọc dành cho việc bỏ phiếu bằng cách khóa BSQ lại. Khóa càng nhiều BSQ phiếu bầu của bạn càng có nhiều trọng lượng.\n\nBSQ bị khóa trong quá trình bỏ phiếu sẽ được mở khóa lại trong giai đoạn công khai phiếu bầu. +dao.proposal.create.selectProposalType=Chọn kiểu đề xuất +dao.proposal.create.phase.inactive=Vui lòng chờ tới giai đoạn đề xuất tiếp theo +dao.proposal.create.proposalType=Kiểu đề xuất +dao.proposal.create.new=Tạo đề xuất mới +dao.proposal.create.button=Tạo đề nghị +dao.proposal.create.publish=Công bố đề xuất +dao.proposal.create.publishing=Đang tiến hành công bố đề xuất... +dao.proposal=đề xuất +dao.proposal.display.type=Kiểu đề xuất +dao.proposal.display.name=Exact GitHub username +dao.proposal.display.link=Link to detailed info +dao.proposal.display.link.prompt=Link to proposal +dao.proposal.display.requestedBsq=Số lượng yêu cầu tính theo BSQ +dao.proposal.display.txId=ID giao dịch đề xuất +dao.proposal.display.proposalFee=Phí đề xuất +dao.proposal.display.myVote=Phiếu bầu của tôi +dao.proposal.display.voteResult=Tóm tắt kết quả bỏ phiếu +dao.proposal.display.bondedRoleComboBox.label=Loại vi trò được đảm bảo +dao.proposal.display.requiredBondForRole.label=Tài sản đảm bảo BSQ yêu cầu cho vai trò +dao.proposal.display.option=Tùy chọn + +dao.proposal.table.header.proposalType=Kiểu đề xuất +dao.proposal.table.header.link=đường dẫn +dao.proposal.table.header.myVote=Phiếu bầu của tôi +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=Xoá +dao.proposal.table.icon.tooltip.removeProposal=Gỡ bỏ đề xuất của tôi +dao.proposal.table.icon.tooltip.changeVote=phiếu bầu hiện tại: ''{0}''. Thay đổi phiếu bầu tới: ''{1}'' + +dao.proposal.display.myVote.accepted=Chấp nhận +dao.proposal.display.myVote.rejected=Từ chối +dao.proposal.display.myVote.ignored=Bỏ qua +dao.proposal.display.myVote.unCounted=Vote was not included in result +dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}) {4} +dao.proposal.myVote.invalid=Phiếu bầu không hợp lệ + +dao.proposal.voteResult.success=Chấp nhận +dao.proposal.voteResult.failed=Từ chối +dao.proposal.voteResult.summary=Kết quả: {0}; Ngưỡng: {1} (yêu cầu > {2}); Đại biểu: {3} (yêu cầu > {4}) + +dao.proposal.display.paramComboBox.label=Hãy chọn thông số cần thay đổi +dao.proposal.display.paramValue=Giá trị thông số + +dao.proposal.display.confiscateBondComboBox.label=Chọn tài sản đảm bảo +dao.proposal.display.assetComboBox.label=Tài sản cần gỡ bỏ + +dao.blindVote=bỏ phiếu mù + +dao.blindVote.startPublishing=Công bố giao dịch bỏ phiếu mù... +dao.blindVote.success=Giao dịch bỏ phiếu mù của bạn đã được công bố thành công.\n\nVui lòng chú ý rằng, bạn phải trực tuyến trong giai đoạn công khai phiếu bầu thì ứng dụng Bisq của bạn mới có thể công bố giao dịch công khai phiếu bầu. Nếu không có giao dịch công khai phiếu bầu, phiếu của bạn sẽ không hợp lệ! + +dao.wallet.menuItem.send=Gửi +dao.wallet.menuItem.receive=Nhận +dao.wallet.menuItem.transactions=Giao dịch + +dao.wallet.dashboard.myBalance=Số dư trong ví của tôi + +dao.wallet.receive.fundYourWallet=Địa chỉ nhận BSQ của bạn +dao.wallet.receive.bsqAddress=Đại chỉ ví BSQ (Địa chỉ mới chưa sử dụng) + +dao.wallet.send.sendFunds=Gửi vốn +dao.wallet.send.sendBtcFunds=Gửi các loại tiền không phải BSQ (BTC) +dao.wallet.send.amount=Số tiền tính bằng BQS +dao.wallet.send.btcAmount=Số tiền tính bằng BTC (các loại tiền không phải BSQ) +dao.wallet.send.setAmount=Cài đặt số tiền được rút (số tiền tối thiểu là {0}) +dao.wallet.send.receiverAddress=Địa chỉ BSQ của người nhận +dao.wallet.send.receiverBtcAddress=Địa chỉ BTC của người nhận +dao.wallet.send.setDestinationAddress=Điền địa chỉ đến của bạn +dao.wallet.send.send=Gửi vốn BSQ +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=Gửi vốn BTC +dao.wallet.send.sendFunds.headline=Xác nhận yêu cầu rút +dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? +dao.wallet.chainHeightSynced=Khối đã xác minh mới nhất: {0} +dao.wallet.chainHeightSyncing=Đang chờ khối mới... Đã xác nhận{0} / {1} khối +dao.wallet.tx.type=Loại + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=Không xác định +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=Không xác định +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=Giao dịch BSQ chưa xác minh +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=Giao dịch BSQ không có hiệu lực +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=Giao dịch chung +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=Chuyển BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=BSQ đã nhận +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=BSQ đã gửi +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=Phí giao dịch +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=Phí yêu cầu bồi thường +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=Phí trả cho yêu cầu bồi hoàn +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=Phí đề xuất +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=Phí cho phiếu không +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=Tiết lộ phiếu bầu +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=Tài sản khóa +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=Tài sản mở khóa +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Phí niêm yết tài sản +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Bằng chứng đốt +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=Không theo quy cách + +dao.tx.withdrawnFromWallet=BTC rút từ ví +dao.tx.issuanceFromCompReq=Yêu cầu bồi thường/ban hành +dao.tx.issuanceFromCompReq.tooltip=Yêu cầu bồi thường dẫn đến ban hành BSQ mới.\nNgày ban hành: {0} +dao.tx.issuanceFromReimbursement=Yêu cầu/ Phát hành bồi hoàn +dao.tx.issuanceFromReimbursement.tooltip=Yêu cầu bồi hoàn dẫn đến ban hành BSQ mới.\nNgày ban hành: {0} +dao.proposal.create.missingBsqFunds=Bạn không có đủ BSQ để tạo đề xuất. Nếu bạn đang có một giao dịch BSQ chưa xác nhận, bạn phải chờ một xác nhận blockchain bởi vì BSQ chỉ được xác thực khi nó được bao gồm vào một khối. \nCòn thiếu: {0} + +dao.proposal.create.missingBsqFundsForBond=Bạn không có đủ BSQ cho vai trò này. Bạn vẫn có thể công bố đề xuất ngày, nhưng bạn sẽ cần đủ lượng BSQ yêu cầu cho vai trò này nếu nó được chấp nhận.\nCòn thiếu: {0} + +dao.proposal.create.missingMinerFeeFunds=Bạn không đủ BTC để tạo giao dịch đề xuất này. Tất cả các giao dịch BSQ đều yêu cầu phí đào bằng BTC.\nCòn thiếu: {0} + +dao.proposal.create.missingIssuanceFunds=Bạn không có đủ BTC để tạo giao dịch đề xuất này. Tất cả các giao dịch BSQ đều yêu cầu phí đào bằng BTC, và các giao dịch phát hành cũng yêu cầu BTC cho lượng BSQ yêu cầu ({0} Satoshis/BSQ).\nCòn thiếu: {1} + +dao.feeTx.confirm=Xác nhận {0} giao dịch +dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nAre you sure you want to publish the {5} transaction? + +dao.feeTx.issuanceProposal.confirm.details={0} fee: {1}\nBTC needed for BSQ issuance: {2} ({3} Satoshis/BSQ)\nMining fee: {4} ({5} Satoshis/vbyte)\nTransaction vsize: {6} vKb\n\nIf your request is approved, you will receive the amount you requested net of the 2 BSQ proposal fee.\n\nAre you sure you want to publish the {7} transaction? + +dao.news.bisqDAO.title=DAO BISQ +dao.news.bisqDAO.description=Vì BIsq là sàn giao dịch phi tập trung và không bị kiểm duyệt, bởi vậy mô hình vận hành của nó, DAO Bisq và đồng BSQ là công cụ giúp điều này trở thành hiện thực. +dao.news.bisqDAO.readMoreLink=Tìm hiểu thêm về DAO Bisq + +dao.news.pastContribution.title=BẠN ĐÃ THAM GIA ĐÓNG GÓP? YÊU CẦU BSQ +dao.news.pastContribution.description=Nếu như bạn đã tham giao đóng góp cho Bisq, vui lòng sử dụng ví BSQ phía dưới và thực hiện một yêu cầu tham gia vào sự kiện phát hành BSQ genesis. +dao.news.pastContribution.yourAddress=Ví BSQ của bạn +dao.news.pastContribution.requestNow=Yêu cầu ngay + +dao.news.DAOOnTestnet.title=CHẠY DAO BISQ TRÊN TESTNET CỦA CHÚNG TÔI +dao.news.DAOOnTestnet.description=Mainnet DAO Bisq chưa ra mắt nhưng bạn vẫn có thể tìm hiểu về DAO Bisq bằng cách chạy nó trên testnet. +dao.news.DAOOnTestnet.firstSection.title=1. Chuyển qua chế độ Testnet DAO +dao.news.DAOOnTestnet.firstSection.content=1. Chuyển qua chế độ Testnet DAO từ màn hình cài đặt +dao.news.DAOOnTestnet.secondSection.title=2. Kiếm BSQ +dao.news.DAOOnTestnet.secondSection.content=Yêu cầu BSQ trên Slack hoặc Mua BSQ trên Bisq +dao.news.DAOOnTestnet.thirdSection.title=3. Tham gia một vòng bỏ phiếu +dao.news.DAOOnTestnet.thirdSection.content=Tạo đề xuất và bỏ phiếu cho đề xuất để thanh đổi nhiều khía cạnh của Bisq. +dao.news.DAOOnTestnet.fourthSection.title=4. Tìm hiểu về BSQ Block Explorer +dao.news.DAOOnTestnet.fourthSection.content=Vì BSQ chỉa là bitcoin, bạn có thể thấy các giao dịch BSQ trên trình duyện bitcoin Block Explorer của chúng tôi. +dao.news.DAOOnTestnet.readMoreLink=Đọc tài liệu đầy đủ + +dao.monitor.daoState=Trạng thái DAO +dao.monitor.proposals=Trạng thái đề xuất +dao.monitor.blindVotes=Trạng thái bỏ phiếu mù + +dao.monitor.table.peers=Đối tác +dao.monitor.table.conflicts=Xung đột +dao.monitor.state=Trạng thái +dao.monitor.requestAlHashes=Yêu cầu tất cả các Hash +dao.monitor.resync=Đồng bộ lại trạng thái DAO +dao.monitor.table.header.cycleBlockHeight=Chiêu cao khói/vòng +dao.monitor.table.cycleBlockHeight=Vòng{0} / khối{1} +dao.monitor.table.seedPeers=Seed node: {0} + +dao.monitor.daoState.headline=Trạng thái DAO +dao.monitor.daoState.table.headline=Chuỗi Hash trạng thái DAO +dao.monitor.daoState.table.blockHeight=Chiều cao khối +dao.monitor.daoState.table.hash=Hash của trạng thái DAO +dao.monitor.daoState.table.prev=Hash trước đó +dao.monitor.daoState.conflictTable.headline=Hash trạng thái DAO từ đối tác đang trong xung dột +dao.monitor.daoState.utxoConflicts=Xung đột đầu ra giao dịch chưa sử dụng +dao.monitor.daoState.utxoConflicts.blockHeight=Chiều cao khối: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Tỏng đầu ra giao dịch chưa sử dụng: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Tổng BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. After restart the DAO state will resync. + +dao.monitor.proposal.headline=Trạng thái đề xuất +dao.monitor.proposal.table.headline=Chuỗi Hash trạng thái đề xuất +dao.monitor.proposal.conflictTable.headline=Hash trạng thái đề xuất từ đối tác đang trong xung dột + +dao.monitor.proposal.table.hash=Hash trạng thái đề xuất +dao.monitor.proposal.table.prev=Hash trước đó +dao.monitor.proposal.table.numProposals=Số đề xuất + +dao.monitor.isInConflictWithSeedNode=Dữ liệu trên máy bạn không đồng bộ với ít nhất một seed node. Vui lòng đồng bộ lại trạng thái DAO. +dao.monitor.isInConflictWithNonSeedNode=Một trong các đối tác của bạn không đồng bộ với mạng nhưng node của bạn vẫn đang đồng bộ với các seed node. +dao.monitor.daoStateInSync=Node trên máy tính của bạn đang dồng bộ với mạng + +dao.monitor.blindVote.headline=Trạng thái bỏ phiếu mù +dao.monitor.blindVote.table.headline=Chuỗi hash trạng thái bỏ phiếu mù +dao.monitor.blindVote.conflictTable.headline=Hash trạng thái bỏ phiếu mù từ đối tác đang trong xung dột +dao.monitor.blindVote.table.hash=Hash trạng thái bỏ phiếu mù +dao.monitor.blindVote.table.prev=Hash trước đó +dao.monitor.blindVote.table.numBlindVotes=Số lượng phiếu mù + +dao.factsAndFigures.menuItem.supply=Lượng cung BSQ +dao.factsAndFigures.menuItem.transactions=Giao dịch BSQ + +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=Tổng lượng BSQ hiện có +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt + +dao.factsAndFigures.supply.issued=BSQ đã phát hành +dao.factsAndFigures.supply.compReq=Các yêu cầu bồi thường +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=Lượng BSQ phát hành tại giao dịch Genesis +dao.factsAndFigures.supply.compRequestIssueAmount=Lượng BSQ phát hành dành cho yêu cầu bồi thường +dao.factsAndFigures.supply.reimbursementAmount=Lượng BSQ phát hành dành cho yêu cầu bồi hoàn +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ đã đốt + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Khối lượng giao dịch +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=Trạng thái toàn cầu của BSQ đã khóa +dao.factsAndFigures.supply.totalLockedUpAmount=Bị khóa làm tải sản đảm bảo +dao.factsAndFigures.supply.totalUnlockingAmount=Đang mở khóa BSQ từ tài sản đảm bảo +dao.factsAndFigures.supply.totalUnlockedAmount=BSQ đã được mở khóa từ tài sản đảm bảo +dao.factsAndFigures.supply.totalConfiscatedAmount=Lượng BSQ đã tịch thu từ tài sản đảm bảo +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=Giao dịch chung +dao.factsAndFigures.transactions.genesisBlockHeight=Chiều cao khối Genesis +dao.factsAndFigures.transactions.genesisTxId=ID giao dịch Genesis +dao.factsAndFigures.transactions.txDetails=Thống kê giao dịch BSQ +dao.factsAndFigures.transactions.allTx=Tổng số giao dịch BSQ +dao.factsAndFigures.transactions.utxo=Tổng đầu ra giao dịch chưa sử dụng +dao.factsAndFigures.transactions.compensationIssuanceTx=Tổng số lượng giao dịch phát hành yêu cầu bồi thường +dao.factsAndFigures.transactions.reimbursementIssuanceTx=Tổng số lượng giao dịch phát hành yêu cầu bồi hoàn +dao.factsAndFigures.transactions.burntTx=Tổng số lượng giao dịch thanh toán phí +dao.factsAndFigures.transactions.invalidTx=Số lượng tất cả các giao dịch không hợp lệ +dao.factsAndFigures.transactions.irregularTx=Số lượng tất cả các giao dịch lạ + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=Số dư hiện có + +contractWindow.title=Thông tin khiếu nại +contractWindow.dates=Ngày chào giá / Ngày giao dịch +contractWindow.btcAddresses=Địa chỉ Bitcoin người mua BTC / người bán BTC +contractWindow.onions=Địa chỉ mạng người mua BTC / người bán BTC +contractWindow.accountAge=Tuổi tài khoản người mua BTC/người bán BTC +contractWindow.numDisputes=Số khiếu nại người mua BTC / người bán BTC +contractWindow.contractHash=Hash của hợp đồng + +displayAlertMessageWindow.headline=Thông tin quan trọng! +displayAlertMessageWindow.update.headline=Thông tin cập nhật quan trọng! +displayAlertMessageWindow.update.download=Download: +displayUpdateDownloadWindow.downloadedFiles=Files: +displayUpdateDownloadWindow.downloadingFile=Đang downloading: {0} +displayUpdateDownloadWindow.verifiedSigs=Xác minh chữ ký có khóa: +displayUpdateDownloadWindow.status.downloading=Đang downloading files... +displayUpdateDownloadWindow.status.verifying=Xác minh chữ ký ... +displayUpdateDownloadWindow.button.label=Download chương trình cài đặt và xác minh chữ ký +displayUpdateDownloadWindow.button.downloadLater=Download sau +displayUpdateDownloadWindow.button.ignoreDownload=Bỏ qua phiên bản này +displayUpdateDownloadWindow.headline=Hiện có một cập nhật Bisq mới! +displayUpdateDownloadWindow.download.failed.headline=Download không thành công +displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://bisq.network/downloads] +displayUpdateDownloadWindow.success=Phiên bản mới đã được download thành công và chữ ký đã được xác minh.\n\nVui lòng mở thư mục download, tắt ứng dụng và cài đặt phiên bản mới. +displayUpdateDownloadWindow.download.openDir=Mở thư mục download + +disputeSummaryWindow.title=Tóm tắt +disputeSummaryWindow.openDate=Ngày mở đơn +disputeSummaryWindow.role=Vai trò của người giao dịch +disputeSummaryWindow.payout=Khoản tiền giao dịch hoàn lại +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} nhận được khoản tiền giao dịch hoàn lại +disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} +disputeSummaryWindow.payout.custom=Thuế hoàn lại +disputeSummaryWindow.payoutAmount.buyer=Khoản tiền hoàn lại của người mua +disputeSummaryWindow.payoutAmount.seller=Khoản tiền hoàn lại của người bán +disputeSummaryWindow.payoutAmount.invert=Sử dụng người thua như người công bố +disputeSummaryWindow.reason=Lý do khiếu nại +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Sự cố +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Khả năng sử dụng +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Vi phạm giao thức +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Không có phản hồi +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Khác +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Ngân hàng +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + +disputeSummaryWindow.summaryNotes=Lưu ý tóm tắt +disputeSummaryWindow.addSummaryNotes=Thêm lưu ý tóm tắt +disputeSummaryWindow.close.button=Đóng đơn + +# 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 closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for BTC buyer: {6}\nPayout amount for BTC seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\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=\nNext steps:\nOpen trade and accept or reject suggestion from mediator +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +disputeSummaryWindow.close.closePeer=Bạn cũng cần phải đóng Đơn Đối tác giao dịch! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? + +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + +emptyWalletWindow.headline=Công cụ ví khẩn cấp +emptyWalletWindow.info=Vui lòng chỉ sử dụng trong trường hợp khẩn cấp nếu bạn không thể truy cập vốn của bạn từ UI.\n\nLưu ý rằng tất cả Báo giá mở sẽ được tự động đóng khi sử dụng công cụ này.\n\nTrước khi sử dụng công cụ này, vui lòng sao lưu dự phòng thư mục dữ liệu của bạn. Bạn có thể sao lưu tại \"Tài khoản/Sao lưu dự phòng\".\n\nVui lòng báo với chúng tôi vấn đề của bạn và lập báo cáo sự cố trên GitHub hoặc diễn đàn Bisq để chúng tôi có thể điều tra điều gì gây nên vấn đề đó. +emptyWalletWindow.balance=Số dư ví hiện tại của bạn +emptyWalletWindow.bsq.btcBalance=Số dư tính bằng Satoshi của tài sản không phải là BSQ + +emptyWalletWindow.address=Địa chỉ đến của bạn +emptyWalletWindow.button=Gửi tất cả vốn +emptyWalletWindow.openOffers.warn=Bạn có chào giá mở sẽ được gỡ bỏ khi bạn rút hết trong ví.\nBạn có chắc chắn muốn rút hết ví của bạn? +emptyWalletWindow.openOffers.yes=Vâng, tôi chắc chắn +emptyWalletWindow.sent.success=Số dư trong ví của bạn đã được chuyển thành công. + +enterPrivKeyWindow.headline=Enter private key for registration + +filterWindow.headline=Chỉnh sửa danh sách lọc +filterWindow.offers=Chào giá đã lọc (cách nhau bằng dấu phẩy) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=Dữ liệu tài khoản giao dịch đã lọc:\nĐịnh dạng: cách nhau bằng dấu phẩy danh sách [ID phương thức thanh toán | trường dữ liệu | giá trị] +filterWindow.bannedCurrencies=Mã tiền tệ đã lọc (cách nhau bằng dấu phẩy) +filterWindow.bannedPaymentMethods=ID phương thức thanh toán đã lọc (cách nhau bằng dấu phẩy) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) +filterWindow.arbitrators=Các trọng tài đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) +filterWindow.seedNode=Node cung cấp thông tin đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) +filterWindow.priceRelayNode=nút rơle giá đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) +filterWindow.btcNode=nút Bitcoin đã lọc (địa chỉ cách nhau bằng dấu phẩy + cửa) +filterWindow.preventPublicBtcNetwork=Ngăn sử dụng mạng Bitcoin công cộng +filterWindow.disableDao=Tắt DAO +filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) +filterWindow.disableDaoBelowVersion=Phiên bản tối thiểu yêu cầu cho DAO +filterWindow.disableTradeBelowVersion=Phiên bản tối thiể yêu cầu cho giao dịch +filterWindow.add=Thêm bộ lọc +filterWindow.remove=Gỡ bỏ bộ lọc +filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=Giá trị BTC tối thiểu +offerDetailsWindow.min=(min. {0}) +offerDetailsWindow.distance=(chênh lệch so với giá thị trường: {0}) +offerDetailsWindow.myTradingAccount=itài khoản giao dịch của tôi +offerDetailsWindow.offererBankId=(ID/BIC/SWIFT ngân hàng của người tạo) +offerDetailsWindow.offerersBankName=(tên ngân hàng của người tạo) +offerDetailsWindow.bankId=ID ngân hàng (VD: BIC hoặc SWIFT) +offerDetailsWindow.countryBank=Quốc gia ngân hàng của người tạo +offerDetailsWindow.commitment=Cam kết +offerDetailsWindow.agree=Tôi đồng ý +offerDetailsWindow.tac=Điều khoản và điều kiện +offerDetailsWindow.confirm.maker=Xác nhận: Đặt chào giá cho {0} bitcoin +offerDetailsWindow.confirm.taker=Xác nhận: Nhận chào giáo cho {0} bitcoin +offerDetailsWindow.creationDate=Ngày tạo +offerDetailsWindow.makersOnion=Địa chỉ onion của người tạo + +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. +qRCodeWindow.request=Yêu cầu thanh toán:\n{0} + +selectDepositTxWindow.headline=Chọn giao dịch tiền gửi để khiếu nại +selectDepositTxWindow.msg=Giao dịch tiền gửi không được lưu trong danh mục giao dịch.\nVui lòng chọn một trong các giao dịch multisig hiện có từ ví của bạn là giao dịch tiền gửi sử dụng trong danh mục giao dịch không thành công.\n\nBạn có thể tìm đúng giao dịch bằng cách mở cửa sổ thông tin giao dịch (nhấp vào ID giao dịch trong danh sách) và tuân thủ yêu cầu thanh toán phí giao dịch cho giao dịch tiếp theo khi bạn thấy giao dịch tiền gửi multisig (địa chỉ bắt đầu bằng số 3). ID giao dịch sẽ thấy trong danh sách tại đây. Khi bạn thấy đúng giao dịch, chọn giao dịch đó và tiếp tục.\n\nXin lỗi vì sự bất tiện này nhưng thỉnh thoảng sẽ có lỗi xảy ra và trong tương lai chúng tôi sẽ tìm cách tốt hơn để xử lý vấn đề này. +selectDepositTxWindow.select=Chọn giao dịch tiền gửi + +sendAlertMessageWindow.headline=Gửi thông báo toàn cầu +sendAlertMessageWindow.alertMsg=Tin nhắn cảnh báo +sendAlertMessageWindow.enterMsg=Nhận tin nhắn +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=Phiên bản mới số +sendAlertMessageWindow.send=Gửi thông báo +sendAlertMessageWindow.remove=Gỡ bỏ thông báo + +sendPrivateNotificationWindow.headline=Gửi tin nhắn riêng tư +sendPrivateNotificationWindow.privateNotification=Thông báo riêng tư +sendPrivateNotificationWindow.enterNotification=Nhập thông báo +sendPrivateNotificationWindow.send=Gửi thông báo riêng tư + +showWalletDataWindow.walletData=Dữ liệu ví +showWalletDataWindow.includePrivKeys=Bao gồm khóa cá nhân + +setXMRTxKeyWindow.headline=Prove sending of XMR +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=Thỏa thuận người dùng +tacWindow.agree=Tôi đồng ý +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.tradeDate=Ngày giao dịch +tradeDetailsWindow.txFee=Phí đào +tradeDetailsWindow.tradingPeersOnion=Địa chỉ onion Đối tác giao dịch +tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash +tradeDetailsWindow.tradeState=Trạng thái giao dịch +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=Nhập mật khẩu để mở khóa + +torNetworkSettingWindow.header=Cài đặt mạng Tor +torNetworkSettingWindow.noBridges=Không sử dụng cầu nối +torNetworkSettingWindow.providedBridges=Nối với cầu nối được cung cấp +torNetworkSettingWindow.customBridges=Nhập cầu nối thông dụng +torNetworkSettingWindow.transportType=Loại hình vận chuyển +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4 (khuyến cáo) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=Nhập một hoặc nhiều chuyển tiếp cầu (một trên một dòng) +torNetworkSettingWindow.enterBridgePrompt=địa chỉ loại:cổng +torNetworkSettingWindow.restartInfo=Bạn cần phải khởi động lại để thay đổi +torNetworkSettingWindow.openTorWebPage=Mở trang web dự án Tor +torNetworkSettingWindow.deleteFiles.header=Sự cố kết nối? +torNetworkSettingWindow.deleteFiles.info=Nếu sự cố kết nối lặp lại khi khởi động, có thể xóa các file Tor đã hết hạn. Để xóa, ấn vào nút dưới đây và sau đó khởi động lại. +torNetworkSettingWindow.deleteFiles.button=Xóa các file Tor đã hết hạn và tắt +torNetworkSettingWindow.deleteFiles.progress=Đang tắt Tor +torNetworkSettingWindow.deleteFiles.success=Các file Tor lỗi thời đã xóa thành công. Vui lòng khởi động lại. +torNetworkSettingWindow.bridges.header=Tor bị khoá? +torNetworkSettingWindow.bridges.info=Nếu Tor bị kẹt do nhà cung cấp internet hoặc quốc gia của bạn, bạn có thể thử dùng cầu nối Tor.\nTruy cập trang web Tor tại: https://bridges.torproject.org/bridges để biết thêm về cầu nối và phương tiện vận chuyển kết nối được. + +feeOptionWindow.headline=Chọn đồng tiền để thanh toán phí giao dịch +feeOptionWindow.info=Bạn có thể chọn thanh toán phí giao dịch bằng BSQ hoặc BTC. Nếu bạn chọn BSQ, bạn sẽ được khấu trừ phí giao dịch. +feeOptionWindow.optionsLabel=Chọn đồng tiền để thanh toán phí giao dịch +feeOptionWindow.useBTC=Sử dụng BTC +feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=Thông báo +popup.headline.instruction=Lưu ý rằng: +popup.headline.attention=Chú ý +popup.headline.backgroundInfo=Thông tin cơ bản +popup.headline.feedback=Đã hoàn thành +popup.headline.confirmation=Xác nhận +popup.headline.information=Thông tin +popup.headline.warning=Cảnh báo +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 bisq.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} + +error.spvFileCorrupted=Có lỗi xảy ra khi đọc SPV chain file.\nCó thể SPV chain file bị hỏng.\n\nTin nhắn lỗi: {0}\n\nBạn có muốn xóa và bắt đầu đồng bộ hóa? +error.deleteAddressEntryListFailed=Không thể xóa AddressEntryList file.\nError: {0} +error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. +error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. + +popup.warning.walletNotInitialized=Ví chưa được kích hoạt +popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Bisq uses Java) causes a popup warning in macOS ('Bisq would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Bisq' from the list on the right side.\n\nBisq will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. +popup.warning.wrongVersion=Có thể máy tính của bạn có phiên bản Bisq không đúng.\nCấu trúc máy tính của bạn là: {0}.\nHệ nhị phân Bisq bạn cài đặt là: {1}.\nVui lòng tắt máy và cài đặt lại phiên bản đúng ({2}). +popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Bisq installed.\nYou can download it at: [HYPERLINK:https://bisq.network/downloads].\n\nPlease restart the application. +popup.warning.startupFailed.twoInstances=Bisq đã chạy. Bạn không thể chạy hai chương trình Bisq. +popup.warning.tradePeriod.halfReached=giao dịch của bạn với ID {0} đã qua một nửa thời gian giao dịch cho phép tối đa và vẫn chưa hoàn thành.\n\nThời gian giao dịch kết thúc vào {1}\n\nVui lòng kiểm tra trạng thái giao dịch của bạn tại \"Portfolio/Các giao dịch mở\" để biết thêm thông tin. +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the mediator. +popup.warning.noTradingAccountSetup.headline=Bạn chưa thiết lập tài khoản giao dịch +popup.warning.noTradingAccountSetup.msg=Bạn cần thiết lập tiền tệ quốc gia hoặc tài khoản altcoin trước khi tạo Báo giá.\nDBạn có muốn thiết lập tài khoản? +popup.warning.noArbitratorsAvailable=Hiện không có trọng tài nào +popup.warning.noMediatorsAvailable=There are no mediators available. +popup.warning.notFullyConnected=Bạn cần phải đợi cho đến khi kết nối hoàn toàn với mạng.\nĐiều này mất khoảng 2 phút khi khởi động. +popup.warning.notSufficientConnectionsToBtcNetwork=Bạn cần phải đợi cho đến khi bạn có ít nhất {0} kết nối với mạng Bitcoin. +popup.warning.downloadNotComplete=Bạn cần phải đợi cho đến khi download xong các block Bitcoin còn thiếu. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\nYou can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] +popup.warning.removeOffer=Bạn có chắc bạn muốn gỡ bỏ Báo giá này?\nPhí người khởi tạo {0} sẽ bị mất nếu bạn gỡ bỏ Báo giá. +popup.warning.tooLargePercentageValue=Bạn không thể cài đặt phần trăm là 100% hoặc cao hơn. +popup.warning.examplePercentageValue=Vui lòng nhập số phần trăm như \"5.4\" cho 5,4% +popup.warning.noPriceFeedAvailable=Không có giá cung cấp cho tiền tệ này. Bạn không thể sử dụng giá dựa trên tỷ lệ.\nVui lòng chọn giá cố định. +popup.warning.sendMsgFailed=Gửi tin nhắn Đối tác giao dịch không thành công.\nVui lòng thử lại và nếu tiếp tục không thành công thì báo cáo sự cố. +popup.warning.insufficientBtcFundsForBsqTx=Bạn không có đủ vốn BTC để thanh toán phí đào cho giao dịch BSQ này.\nVui lòng nộp tiền vào ví BTC của bạn để có thể chuyển giao BSQ.\nSố tiền còn thiếu: {0} +popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. +popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. + +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} +popup.warning.noBsqFundsForBtcFeePayment=Ví BSQ của bạn không đủ tiền để trả phí giao dịch bằng BSQ. +popup.warning.messageTooLong=Tin nhắn của bạn vượt quá kích cỡ tối đa cho phép. Vui lòng gửi thành nhiều lần hoặc tải lên mạng như https://pastebin.com. +popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=One of the {0} nodes got banned. +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=rơle giá +popup.warning.seed=seed +popup.warning.mandatoryUpdate.trading=Please update to the latest Bisq version. A mandatory update was released which disables trading for old versions. Please check out the Bisq Forum for more information. +popup.warning.mandatoryUpdate.dao=Please update to the latest Bisq version. A mandatory update was released which disables the Bisq DAO and BSQ for old versions. Please check out the Bisq Forum for more information. +popup.warning.disable.dao=The Bisq DAO and BSQ are temporary disabled. Please check out the Bisq Forum for more information. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=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 đủ BTC để chuyển. + +popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Bitcoin 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 Bisq support channel at the Bisq Keybase team. + +popup.warning.trade.txRejected.tradeFee=trade fee +popup.warning.trade.txRejected.deposit=deposit +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.info.securityDepositInfo=Để đảm bảo cả hai người giao dịch đều tuân thủ giao thức giao dịch, cả hai cần phải trả một khoản tiền cọc. \n\nSố tiền cọc này được giữ ở ví giao dịch cho đến khi giao dịch của bạn được hoàn thành, sau đó nó sẽ được trả lại cho bạn. \nXin lưu ý: Nếu bạn tạo một chào giá mới, ứng dụng Bisq cần phải chạy để người giao dịch khác có thể nhận chào giá đó. Để giữ cho chào giá của bạn online, để Bisq chạy và đảm bảo là máy tính của bạn cũng online (nghĩa là đảm bảo là máy tính của bạn không chuyển qua chế độ standby, nếu màn hình chuyển qua chế độ standby thì không sao). + +popup.info.cashDepositInfo=Chắc chắn rằng khu vực của bạn có chi nhánh ngân hàng có thể gửi tiền mặt.\nID (BIC/SWIFT) ngân hàng của bên bán là: {0}. +popup.info.cashDepositInfo.confirm=Tôi xác nhận tôi đã gửi tiền +popup.info.shutDownWithOpenOffers=Bisq đang đóng, nhưng vẫn có các chào giá đang mở. \n\nNhững chào giá này sẽ không có tại mạng P2P khi Bisq đang đóng, nhưng chúng sẽ được công bố lại trên mạng P2P vào lần tiếp theo bạn khởi động Bisq.\nĐể giữ các chào giá luôn trực tuyến, vui lòng để Bisq chạy và đảm bảo là máy tính của bạn cũng đang trực tuyến(có nghĩa là đảm bảo là máy tính của bạn không chuyển về chế độ chờ...nếu màn hình về chế độ chờ thì không sao). +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\nPlease make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=Thông báo riêng tư quan trọng! + +popup.securityRecommendation.headline=Khuyến cáo an ninh quan trọng +popup.securityRecommendation.msg=Chúng tôi muốn nhắc nhở bạn sử dụng bảo vệ bằng mật khẩu cho ví của bạn nếu bạn vẫn chưa sử dụng.\n\nChúng tôi cũng khuyên bạn nên viết Seed words ví của bạn ra giấy. Các Seed words này như là mật khẩu chủ để khôi phục ví Bitcoin của bạn.\nBạn có thể xem thông tin ở mục \"Wallet Seed\".\n\nNgoài ra bạn nên sao lưu dự phòng folder dữ liệu ứng dụng đầy đủ ở mục \"Backup\". + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=Đang tắt ứng dụng +popup.shutDownInProgress.msg=Tắt ứng dụng sẽ mất vài giây.\nVui lòng không gián đoạn quá trình này. + +popup.attention.forTradeWithId=Cần chú ý khi giao dịch có ID {0} +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=Có sẵn nhiều tài khoản thanh toán +popup.info.multiplePaymentAccounts.msg=Bạn có sẵn nhiều tài khoản thanh toán cho chào giá này. Vui lòng đảm bảo là bạn chọn đúng tài khoản. + +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.signAll=Sign all payment methods +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! +popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=Thông báo với giao dịch có ID {0} +notification.ticket.headline=Vé hỗ trợ cho giao dịch có ID {0} +notification.trade.completed=giao dịch đã hoàn thành và bạn có thể rút tiền. +notification.trade.accepted=Chào giá của bạn đã được chấp thuận bởi BTC {0}. +notification.trade.confirmed=giao dịch của bạn có ít nhất một xác nhận blockchain.\nBạn có thể bắt đầu thanh toán bây giờ. +notification.trade.paymentStarted=Người mua BTC đã bắt đầu thanh toán. +notification.trade.selectTrade=Lựa chọn giao dịch +notification.trade.peerOpenedDispute=Đối tác giao dịch của bạn đã mở một {0}. +notification.trade.disputeClosed={0} đã đóng. +notification.walletUpdate.headline=Cập nhật ví giao dịch +notification.walletUpdate.msg=Ví giao dịch của bạn không được nạp đủ tiền.\nSố tiền: {0} +notification.takeOffer.walletUpdate.msg=Ví giao dịch của bạn đã được nạp đủ tiền từ lần nhận báo giá trước.\nSố tiền: {0} +notification.tradeCompleted.headline=giao dịch đã hoàn thành +notification.tradeCompleted.msg=Bạn có thể rút tiền từ ví Bitcoin ngoài của bạn hoặc chuyển vào ví Bisq. + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=Hiển thị cửa sổ ứng dung +systemTray.hide=Ẩn cửa sổ ứng dụng +systemTray.info=Thông tin về Bisq +systemTray.exit=Thoát +systemTray.tooltip=Bisq: A decentralized bitcoin exchange network + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. + +guiUtil.accountExport.savedToPath=tài khoản giao dịch được lưu vào đường dẫn:\n{0} +guiUtil.accountExport.noAccountSetup=Bạn không có tài khoản giao dịch được thiết lập để truy xuất. +guiUtil.accountExport.selectPath=Chọn đường dẫn đến {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=tài khoản giao dịch với ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=Chúng tôi không truy nhập tài khoản giao dịch với ID {0} do nó đã tồn tại.\n +guiUtil.accountExport.exportFailed=Truy xuất tới CSV không thành công do có lỗi.\nError = {0} +guiUtil.accountExport.selectExportPath=Lựa chọn đường dẫn truy xuất +guiUtil.accountImport.imported=tài khoản giao dịch truy nhập từ đường dẫn:\n{0}\n\nTài khoản truy nhập:\n{1} +guiUtil.accountImport.noAccountsFound=Không tìm thấy tài khoản giao dịch truy xuất tại đường dẫn: {0}.\nTên file là {1}." +guiUtil.openWebBrowser.warning=Bạn sẽ mở một trang web trong trình duyệt trang web của hệ thống.\nBạn có muốn mở trang web bây giờ?\n\nNếu bạn không sử dụng \"Tor Browser\" là trình duyệt web hệ thống mặc định bạn sẽ kết nối với trang web tại clear net.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.doOpen=Mở trang web và không hỏi lại +guiUtil.openWebBrowser.copyUrl=Copy URL và hủy +guiUtil.ofTradeAmount=Giá trị giao dịch +guiUtil.requiredMinimum=(required minimum) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=Lựa chọn tiền tệ +list.currency.showAll=Hiển thị tất cả +list.currency.editList=Chỉnh sửa danh sách tiền tệ + +table.placeholder.noItems=Hiện không có {0} nào +table.placeholder.noData=Hiện không có dữ liệu nào +table.placeholder.processingData=Processing data... + + +peerInfoIcon.tooltip.tradePeer=Đối tác giao dịch +peerInfoIcon.tooltip.maker=Người tạo +peerInfoIcon.tooltip.trade.traded={0} Địa chỉ onion: {1}\nBạn đã giao dịch {2} lần với đối tác này\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} Địa chỉ onion: {1}\nBạn chưa từng giao dịch với đối tác này.\n{2} +peerInfoIcon.tooltip.age=Tài khoản thanh toán được tạo cách đây {0}. +peerInfoIcon.tooltip.unknownAge=Tuổi tài khoản thanh toán chưa biết. + +tooltip.openPopupForDetails=Mở cửa sổ để xem chi tiết +tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information +tooltip.openBlockchainForAddress=Mở blockchain explorer ngoài để xem địa chỉ: {0} +tooltip.openBlockchainForTx=Mở blockchain explorer ngoài để xem giao dịch: {0} + +confidence.unknown=Trạng thái giao dịch chưa biết +confidence.seen=Đã xem bởi {0} đối tác / 0 xác nhận +confidence.confirmed=Xác nhận tại {0} block +confidence.invalid=Giao dịch không có hiệu lực + +peerInfo.title=Thông tin đối tác +peerInfo.nrOfTrades=Số giao dịch đã hoàn thành +peerInfo.notTradedYet=Bạn chưa từng giao dịch với người dùng này. +peerInfo.setTag=Đặt nhãn cho đối tác này +peerInfo.age.noRisk=Tuổi tài khoản thanh toán +peerInfo.age.chargeBackRisk=Time since signing +peerInfo.unknownAge=Tuổi chưa biết + +addressTextField.openWallet=Mở ví Bitcoin mặc định của bạn +addressTextField.copyToClipboard=Copy địa chỉ vào clipboard +addressTextField.addressCopiedToClipboard=Địa chỉ đã được copy vào clipboard +addressTextField.openWallet.failed=Mở ứng dụng ví Bitcoin mặc định không thành công. Có lẽ bạn chưa cài đặt? + +peerInfoIcon.tooltip={0}\nTag: {1} + +txIdTextField.copyIcon.tooltip=Copy ID giao dịch vào clipboard +txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID +txIdTextField.missingTx.warning.tooltip=Missing required transaction + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=\"Tài khoản\" +navigation.account.walletSeed=\"Tài khoản/Khởi tạo ví\" +navigation.funds.availableForWithdrawal=\"Funds/Send funds\" +navigation.portfolio.myOpenOffers=\"Portfolio/Các Báo giá mở của tôi\" +navigation.portfolio.pending=\"Portfolio/Các giao dịch mở\" +navigation.portfolio.closedTrades=\"Portfolio/Lịch sử\" +navigation.funds.depositFunds=\"Vốn/Nhận vốn\" +navigation.settings.preferences=\"Cài đặt/Tham khảo\" +# suppress inspection "UnusedProperty" +navigation.funds.transactions=\"Vốn/Giao dịch\" +navigation.support=\"Hỗ trợ\" +navigation.dao.wallet.receive=\"DAO/Ví BSQ/Nhận\" + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} giá trị {1} +formatter.makerTaker=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.fiat=Bạn đang tạo một chào giá đến {0} {1} +formatter.youAreCreatingAnOffer.altcoin=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 + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=Bitcoin Mainnet +# suppress inspection "UnusedProperty" +BTC_TESTNET=Bitcoin Testnet +# suppress inspection "UnusedProperty" +BTC_REGTEST=Bitcoin Regtest +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=Bitcoin DAO Testnet (không tán thành) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Regtest + +time.year=Năm +time.month=Tháng +time.week=Tuần +time.day=Ngày +time.hour=Giờ +time.minute10=10 Phút +time.hours=giờ +time.days=ngày +time.1hour=1 giờ +time.1day=1 ngày +time.minute=phút +time.second=giây +time.minutes=phút +time.seconds=giây + + +password.enterPassword=Nhập mật khẩu +password.confirmPassword=Xác nhận mật khẩu +password.tooLong=Mật khẩu phải ít hơn 500 ký tự. +password.deriveKey=Lấy khóa từ mật khẩu +password.walletDecrypted=Ví đã giải mã thành công và bảo vệ bằng mật khẩu bị gỡ bỏ. +password.wrongPw=Bạn nhập sai mật khẩu.\n\nVui lòng nhập lại mật khẩu, kiểm tra lỗi do gõ phí hoặc lỗi chính tả cẩn thận. +password.walletEncrypted=Ví đã được mã hóa thành công và bảo vệ bằng mật khẩu được kích hoạt. +password.walletEncryptionFailed=Wallet password could not be set. You may have imported seed words which do not match the wallet database. Please contact the developers on Keybase ([HYPERLINK:https://keybase.io/team/bisq]). +password.passwordsDoNotMatch=2 mật khẩu bạn nhập không khớp. +password.forgotPassword=Quên mật khẩu? +password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! +password.backupWasDone=I have already made a backup +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=Seed words ví +seed.enterSeedWords=Nhập seed words ví +seed.date=Ngày ví +seed.restore.title=Khôi phục vú từ Seed words +seed.restore=Khôi phục ví +seed.creationDate=Ngày tạo +seed.warn.walletNotEmpty.msg=Your Bitcoin wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\nIn case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.restore=Tôi muốn khôi phục +seed.warn.walletNotEmpty.emptyWallet=Tôi sẽ làm trống ví trước +seed.warn.notEncryptedAnymore=Ví của bạn đã được mã hóa.\n\nSau khi khôi phục, ví sẽ không còn được mã hóa và bạn phải cài đặt mật khẩu mới.\n\nBạn có muốn tiếp tục? +seed.warn.walletDateEmpty=As you have not specified a wallet date, bisq will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in bisq on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? +seed.restore.success=Ví khôi phục thành công với từ khởi tạo mới.\n\nBạn cần phải tắt và khởi động lại ứng dụng. +seed.restore.error=Có lỗi xảy ra khi khôi phục ví với Seed words.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=Tài khoản +payment.account.no=Tài khoản số +payment.account.name=Tên tài khoản +payment.account.userName=User name +payment.account.phoneNr=Phone number +payment.account.owner=Họ tên chủ tài khoản +payment.account.fullName=Họ tên (họ, tên lót, tên) +payment.account.state=Bang/Tỉnh/Vùng +payment.account.city=Thành phố +payment.bank.country=Quốc gia của ngân hàng +payment.account.name.email=Họ tên / email của chủ tài khoản +payment.account.name.emailAndHolderId=Họ tên / email / {0} của chủ tài khoản +payment.bank.name=Tên ngân hàng +payment.select.account=Chọn loại tài khoản +payment.select.region=Chọn vùng +payment.select.country=Chọn quốc gia +payment.select.bank.country=Chọn quốc gia của ngân hàng +payment.foreign.currency=Bạn có muốn còn tiền tệ khác tiền tệ mặc định của quốc gia không? +payment.restore.default=Không, khôi phục tiền tệ mặc định +payment.email=Email +payment.country=Quốc gia +payment.extras=Yêu cầu thêm +payment.email.mobile=Email hoặc số điện thoại +payment.altcoin.address=Địa chỉ Altcoin +payment.altcoin.tradeInstantCheckbox=Giao dịch ngay với Altcoin này (trong 1 giờ) +payment.altcoin.tradeInstant.popup=Để giao dịch ngay, cả hai đối tác giao dịch phải cùng trực tuyến để hoàn thành giao dịch trong vòng ít hơn 1 giờ. \n\nNếu bạn có chào giá đang mở mà bạn không trực tuyến, vui lòng tắt chúng ở phần 'Danh mục'. +payment.altcoin=Altcoin +payment.select.altcoin=Select or search Altcoin +payment.secret=Câu hỏi bí mật +payment.answer=Trả lời +payment.wallet=ID ví +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=Tên người dùng hoặc email hoặc số điện thoại +payment.moneyBeam.accountId=Email hoặc số điện thoại +payment.venmo.venmoUserName=Tên người dùng Venmo +payment.popmoney.accountId=Email hoặc số điện thoại +payment.promptPay.promptPayId=ID công dân/ ID thuế hoặc số điện thoại +payment.supportedCurrencies=Tiền tệ hỗ trợ +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=Hạn chế +payment.salt=Salt để xác minh tuổi tài khoản +payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). +payment.accept.euro=Chấp nhận giao dịch từ các nước Châu Âu này +payment.accept.nonEuro=Chấp nhận giao dịch từ các nước không thuộc Châu Âu này +payment.accepted.countries=Các nước được chấp nhận +payment.accepted.banks=Các ngân hàng được chấp nhận (ID) +payment.mobile=Số điện thoại +payment.postal.address=Địa chỉ bưu điện +payment.national.account.id.AR=Số CBU +shared.accountSigningState=Account signing status + +#new +payment.altcoin.address.dyn=Địa chỉ {0}  +payment.altcoin.receiver.address=Địa chỉ altcoin của người nhận +payment.accountNr=Số tài khoản +payment.emailOrMobile=Email hoặc số điện thoại +payment.useCustomAccountName=Sử dụng tên tài khoản thông dụng +payment.maxPeriod=Thời gian giao dịch cho phép tối đa +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} +payment.maxPeriodAndLimitCrypto=Thời gian giao dịch tối đa: {0} / Giới hạn giao dịch tối đa: {1} +payment.currencyWithSymbol=Tiền tệ: {0} +payment.nameOfAcceptedBank=Tên NH được chấp nhận +payment.addAcceptedBank=Thêm NH được chấp nhận +payment.clearAcceptedBanks=Xóa NH được chấp nhận +payment.bank.nameOptional=Tên ngân hàng (không bắt buộc) +payment.bankCode=Mã ngân hàng +payment.bankId=ID (BIC/SWIFT) ngân hàng +payment.bankIdOptional=ID ngân hàng (BIC/SWIFT) (không bắt buộc) +payment.branchNr=Chi nhánh số +payment.branchNrOptional=Chi nhánh số (không bắt buộc) +payment.accountNrLabel=Tài khoản số (IBAN) +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 BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.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 Bisq 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 Bisq. +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 Bisq 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 BTC buyer has to send the Authorisation number and a photo of the receipt by email to the BTC 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 BTC buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the BTC 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 BTC cần phải gửi cho người bán BTC 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 BTC 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, Bisq 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://bisq.wiki/Account_limits]. +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=To limit chargeback risk, Bisq 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://bisq.wiki/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. + +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account + +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 Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC 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- BTC buyers must send the USPMO to the BTC 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 Bisq 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 Bisq. + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=thông tin liên hệ +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=thông tin liên hệ +payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) +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 BTC 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.bisq.network/trading-rules.html#f2f-trading] +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.japan.bank=Ngân hàng +payment.japan.branch=Branch +payment.japan.account=Tài khoản +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=Chuyển khoản ngân hàng trong nước +SAME_BANK=Chuyển khoản cùng ngân hàng +SPECIFIC_BANKS=Chuyển khoản với ngân hàng cụ thể +US_POSTAL_MONEY_ORDER=Thư chuyển tiền US +CASH_DEPOSIT=Tiền gửi tiền mặt +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=Western Union +F2F=Giao dịch trực tiếp (gặp mặt) +JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=Ngân hàng trong nước +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=Cùng ngân hàng +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=Ngân hàng cụ thể +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=Thư chuyển tiền US +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=Tiền gửi tiền mặt +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=Western Union +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA Instant Payments +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=Faster Payments +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle (ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoin ngay tức thì + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam (N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=AliPay +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=WeChat Pay +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=Faster Payments +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=Altcoins +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=Amazon eGift Card +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoin ngay tức thì + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=Không cho phép nhập trống. +validation.NaN=Giá trị nhập là số không có hiệu lực. +validation.notAnInteger=Giá trị nhập không phải là một số nguyên. +validation.zero=Không cho phép nhập giá trị 0. +validation.negative=Không cho phép nhập giá trị âm. +validation.fiat.toSmall=Không cho phép giá trị nhập nhỏ hơn giá trị có thể nhỏ nhất. +validation.fiat.toLarge=Không cho phép giá trị nhập lớn hơn giá trị có thể lớn nhất. +validation.btc.fraction=Input will result in a bitcoin value of less than 1 satoshi +validation.btc.toLarge=Không cho phép giá trị nhập lớn hơn {0}. +validation.btc.toSmall=Không cho phép giá trị nhập nhỏ hơn {0}. +validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. +validation.passwordTooLong=Mật khẩu bạn vừa nhập quá dài. Không được quá 50 ký tự. +validation.sortCodeNumber={0} phải có {1} số. +validation.sortCodeChars={0} phải có {1} ký tự. +validation.bankIdNumber={0} phải có {1} số. +validation.accountNr=Số tài khoản phải có {0} số. +validation.accountNrChars=Số tài khoản phải có {0} ký tự. +validation.btc.invalidAddress=Địa chỉ không đúng. Vui lỏng kiểm tra lại định dạng địa chỉ. +validation.integerOnly=Vui lòng chỉ nhập số nguyên. +validation.inputError=Giá trị nhập của bạn gây lỗi:\n{0} +validation.bsq.insufficientBalance=Số dư hiện tại của bạn là {0}. +validation.btc.exceedsMaxTradeLimit=Giới hạn giao dịch của bạn là {0}. +validation.bsq.amountBelowMinAmount=Giá trị nhỏ nhất là {0} +validation.nationalAccountId={0} phải có {1} số. + +#new +validation.invalidInput=Giá trị nhập không hợp lệ: {0} +validation.accountNrFormat=Số tài khoản phải có định dạng : {0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=Xác nhận địa chỉ không thành công vì không khớp với cấu trúc của {0} địa chỉ. +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=Địa chỉ không phải là địa chỉ {0} hợp lệ! {1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. +validation.bic.invalidLength=Input length must be 8 or 11 +validation.bic.letters=Mã NH và quốc gia phải là chữ +validation.bic.invalidLocationCode=BIC chứa mã vị trí không hợp lệ +validation.bic.invalidBranchCode=BIC chứa mã chi nhánh không hợp lệ +validation.bic.sepaRevolutBic=Tài khoản Revolut Sepa không được hỗ trợ. +validation.btc.invalidFormat=Invalid format for a Bitcoin address. +validation.bsq.invalidFormat=Invalid format for a BSQ address. +validation.email.invalidAddress=Địa chỉ không hợp lệ +validation.iban.invalidCountryCode=Mã quốc gia không hợp lệ +validation.iban.checkSumNotNumeric=Mã kiểm tra phải là số +validation.iban.nonNumericChars=Phát hiện ký tự không phải kiểu chữ-số +validation.iban.checkSumInvalid=Mã kiểm tra IBAN không hợp lệ +validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.interacETransfer.invalidAreaCode=Mã vùng không phải Canada +validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidQuestion=Chỉ được chứa chữ cái, số, khoảng trắng, và/hoặc các ký tự ' _ , . ? - +validation.interacETransfer.invalidAnswer=Phải được viết liền và chỉ bao gồm chữ cái, số, và/hoặc ký tự - +validation.inputTooLarge=Giá trị nhập không được lớn hơn {0} +validation.inputTooSmall=Giá trị nhập phải lớn hơn {0} +validation.inputToBeAtLeast=Giá trị nhập tối thiểu phải bằng {0} +validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. +validation.length=Chiều dài phải nằm trong khoảng từ {0} đến {1} +validation.fixedLength=Length must be {0} +validation.pattern=Giá trị nhập phải có định dạng: {0} +validation.noHexString=Giá trị nhập không ở định dạng HEX +validation.advancedCash.invalidFormat=Phải là một địa chỉ email hợp lệ hoặc là ID ví với định dạng: X000000000000 +validation.invalidUrl=Đây không phải là URL hợp lệ +validation.mustBeDifferent=Your input must be different from the current value +validation.cannotBeChanged=Thông số không thể thay đổi +validation.numberFormatException=Ngoại lệ cho định dạng số {0} +validation.mustNotBeNegative=Giá trị nhập không được là số âm +validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.invalidCharacters=Phone number {0} contains invalid characters +validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number +validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number +validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. +validation.invalidAddressList=Must be comma separated list of valid addresses diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties new file mode 100644 index 0000000000..26ea5d9a74 --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=阅读更多 +shared.openHelp=打开帮助 +shared.warning=警告 +shared.close=关闭 +shared.cancel=取消 +shared.ok=好的 +shared.yes=是 +shared.no=否 +shared.iUnderstand=我了解 +shared.na=N/A +shared.shutDown=完全关闭 +shared.reportBug=在 Github 报告错误 +shared.buyBitcoin=买入比特币 +shared.sellBitcoin=卖出比特币 +shared.buyCurrency=买入 {0} +shared.sellCurrency=卖出 {0} +shared.buyingBTCWith=用 {0} 买入 BTC +shared.sellingBTCFor=卖出 BTC 为 {0} +shared.buyingCurrency=买入 {0}(卖出 BTC) +shared.sellingCurrency=卖出 {0}(买入 BTC) +shared.buy=买 +shared.sell=卖 +shared.buying=买入 +shared.selling=卖出 +shared.P2P=P2P +shared.oneOffer=报价 +shared.multipleOffers=报价 +shared.Offer=报价 +shared.offerVolumeCode={0} 报价量 +shared.openOffers=可用报价 +shared.trade=交易 +shared.trades=交易 +shared.openTrades=进行中的交易 +shared.dateTime=日期/时间 +shared.price=价格 +shared.priceWithCur={0} 价格 +shared.priceInCurForCur=1 {1} 的 {0} 价格 +shared.fixedPriceInCurForCur=1 {1} 的 {0} 的固定价格 +shared.amount=数量 +shared.txFee=交易手续费 +shared.tradeFee=交易手续费 +shared.buyerSecurityDeposit=买家保证金 +shared.sellerSecurityDeposit=卖家保证金 +shared.amountWithCur={0} 数量 +shared.volumeWithCur={0} 总量 +shared.currency=货币类型 +shared.market=交易项目 +shared.deviation=偏差 +shared.paymentMethod=付款方式 +shared.tradeCurrency=交易货币 +shared.offerType=报价类型 +shared.details=详情 +shared.address=地址 +shared.balanceWithCur={0} 余额 +shared.utxo=Unspent transaction output +shared.txId=交易记录 ID +shared.confirmations=审核 +shared.revert=还原 Tx +shared.select=选择 +shared.usage=使用状况 +shared.state=状态 +shared.tradeId=交易 ID +shared.offerId=报价 ID +shared.bankName=银行名称 +shared.acceptedBanks=接受的银行 +shared.amountMinMax=总额(最小 - 最大) +shared.amountHelp=如果报价包含最小和最大限制,那么您可以在这个范围内的任意数量进行交易。 +shared.remove=移除 +shared.goTo=前往 {0} +shared.BTCMinMax=BTC(最小 - 最大) +shared.removeOffer=移除报价 +shared.dontRemoveOffer=不要移除报价 +shared.editOffer=编辑报价 +shared.openLargeQRWindow=打开放大二维码窗口 +shared.tradingAccount=交易账户 +shared.faq=访问 FAQ 页面 +shared.yesCancel=是的,取消 +shared.nextStep=下一步 +shared.selectTradingAccount=选择交易账户 +shared.fundFromSavingsWalletButton=从 Bisq 钱包资金划转 +shared.fundFromExternalWalletButton=从您的外部钱包充值 +shared.openDefaultWalletFailed=打开默认的比特币钱包应用程序失败了。您确定您安装了吗? +shared.belowInPercent=低于市场价格 % +shared.aboveInPercent=高于市场价格 % +shared.enterPercentageValue=输入 % 值 +shared.OR=或者 +shared.notEnoughFunds=您的 Bisq 钱包中没有足够的资金去支付这一交易 需要{0} 您可用余额为 {1}。\n\n请从外部比特币钱包注入资金或在“资金/存款”充值到您的 Bisq 钱包。 +shared.waitingForFunds=等待资金充值... +shared.depositTransactionId=存款交易 ID +shared.TheBTCBuyer=BTC 买家 +shared.You=您 +shared.sendingConfirmation=发送确认... +shared.sendingConfirmationAgain=请再次发送确认 +shared.exportCSV=导出保存为 .csv +shared.exportJSON=导出保存至 JSON +shared.summary=Show summary +shared.noDateAvailable=没有可用数据 +shared.noDetailsAvailable=没有可用详细 +shared.notUsedYet=尚未使用 +shared.date=日期 +shared.sendFundsDetailsWithFee=发送:{0}\n来自:{1}\n接收地址:{2}\n要求的最低交易费:{3}({4} 聪/byte)\n交易大小:{5} Kb\n\n收款方将收到:{6}\n\n您确定您想要提现吗? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq 检测到,该交易将产生一个低于最低零头阈值的输出(不被比特币共识规则所允许)。相反,这些零头({0}satoshi{1})将被添加到挖矿手续费中。 +shared.copyToClipboard=复制到剪贴板 +shared.language=语言 +shared.country=国家或地区 +shared.applyAndShutDown=同意并关闭 +shared.selectPaymentMethod=选择付款方式 +shared.accountNameAlreadyUsed=这个账户名称已经被已保存的账户占用。\n请使用另外一个名称。 +shared.askConfirmDeleteAccount=您确定想要删除被选定的账号吗? +shared.cannotDeleteAccount=您不能删除这个账户,因为它正在被使用于报价或交易中。 +shared.noAccountsSetupYet=还没有建立帐户。 +shared.manageAccounts=管理账户 +shared.addNewAccount=添加新的账户 +shared.ExportAccounts=导出账户 +shared.importAccounts=导入账户 +shared.createNewAccount=创建新的账户 +shared.saveNewAccount=保存新的账户 +shared.selectedAccount=选中的账户 +shared.deleteAccount=删除账户 +shared.errorMessageInline=\n错误信息:{0} +shared.errorMessage=错误信息 +shared.information=资料 +shared.name=名称 +shared.id=ID +shared.dashboard=仪表盘 +shared.accept=接受 +shared.balance=余额 +shared.save=保存 +shared.onionAddress=匿名地址 +shared.supportTicket=帮助话题 +shared.dispute=纠纷 +shared.mediationCase=调解事件 +shared.seller=卖家 +shared.buyer=买家 +shared.allEuroCountries=所有欧元国家 +shared.acceptedTakerCountries=接受的买家国家 +shared.tradePrice=交易价格 +shared.tradeAmount=交易金额 +shared.tradeVolume=交易总量 +shared.invalidKey=您输入的密码不正确。 +shared.enterPrivKey=输入私钥解锁 +shared.makerFeeTxId=挂单费交易 ID +shared.takerFeeTxId=买单费交易 ID +shared.payoutTxId=支出交易 ID +shared.contractAsJson=JSON 格式的合同 +shared.viewContractAsJson=查看 JSON 格式的合同 +shared.contract.title=交易 ID:{0} 的合同 +shared.paymentDetails=BTC {0} 支付详情 +shared.securityDeposit=保证金 +shared.yourSecurityDeposit=你的保证金 +shared.contract=合同 +shared.messageArrived=消息送达。 +shared.messageStoredInMailbox=消息保存在邮箱中。 +shared.messageSendingFailed=消息发送失败。错误:{0} +shared.unlock=解锁 +shared.toReceive=接收 +shared.toSpend=花费 +shared.btcAmount=BTC 总额 +shared.yourLanguage=你的语言 +shared.addLanguage=添加语言 +shared.total=合计 +shared.totalsNeeded=需要资金 +shared.tradeWalletAddress=交易钱包地址 +shared.tradeWalletBalance=交易钱包余额 +shared.makerTxFee=卖家:{0} +shared.takerTxFee=买家:{0} +shared.iConfirm=我确认 +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=打开 {0} +shared.fiat=法定货币 +shared.crypto=加密 +shared.all=全部 +shared.edit=编辑 +shared.advancedOptions=高级选项 +shared.interval=取消 +shared.actions=操作 +shared.buyerUpperCase=买家 +shared.sellerUpperCase=买家 +shared.new=新 +shared.blindVoteTxId=匿名投票交易 ID +shared.proposal=建议 +shared.votes=投票 +shared.learnMore=了解更多 +shared.dismiss=忽略 +shared.selectedArbitrator=选中的仲裁者 +shared.selectedMediator=选择调解员 +shared.selectedRefundAgent=选中的仲裁者 +shared.mediator=调解员 +shared.arbitrator=仲裁员 +shared.refundAgent=仲裁员 +shared.refundAgentForSupportStaff=退款助理 +shared.delayedPayoutTxId=延迟支付交易 ID +shared.delayedPayoutTxReceiverAddress=延迟交易交易已发送至 +shared.unconfirmedTransactionsLimitReached=你现在有过多的未确认交易。请稍后尝试 +shared.numItemsLabel=实体数:{0} +shared.filter=过滤 +shared.enabled=启用 + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=交易项目 +mainView.menu.buyBtc=买入 BTC +mainView.menu.sellBtc=卖出 BTC +mainView.menu.portfolio=业务 +mainView.menu.funds=资金 +mainView.menu.support=帮助 +mainView.menu.settings=设置 +mainView.menu.account=账户 +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=交易所价格提供商:{0} +mainView.marketPrice.bisqInternalPrice=最新 Bisq 交易的价格 +mainView.marketPrice.tooltip.bisqInternalPrice=外部交易所供应商没有可用的市场价格。\n显示的价格是该货币的最新 Bisq 交易价格。 +mainView.marketPrice.tooltip=交易所价格提供者 {0}{1}\n最后更新:{2}\n提供者节点 URL:{3} +mainView.balance.available=可用余额 +mainView.balance.reserved=保证金 +mainView.balance.locked=冻结余额 +mainView.balance.reserved.short=保证 +mainView.balance.locked.short=冻结 + +mainView.footer.usingTor=(通过 Tor) +mainView.footer.localhostBitcoinNode=(本地主机) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ 矿工手费率:{0} 聪/字节 +mainView.footer.btcInfo.initializing=连接至比特币网络 +mainView.footer.bsqInfo.synchronizing=正在同步 DAO +mainView.footer.btcInfo.synchronizingWith=正在通过{0}同步区块:{1}/{2} +mainView.footer.btcInfo.synchronizedWith=已通过{0}同步至区块{1} +mainView.footer.btcInfo.connectingTo=连接至 +mainView.footer.btcInfo.connectionFailed=连接失败: +mainView.footer.p2pInfo=比特币网络节点:{0} / Bisq 网络节点:{1} +mainView.footer.daoFullNode=DAO 全节点 + +mainView.bootstrapState.connectionToTorNetwork=(1/4) 连接至 Tor 网络... +mainView.bootstrapState.torNodeCreated=(2/4) Tor 节点已创建 +mainView.bootstrapState.hiddenServicePublished=(3/4) 隐藏的服务已发布 +mainView.bootstrapState.initialDataReceived=(4/4) 初始数据已接收 + +mainView.bootstrapWarning.noSeedNodesAvailable=没有可用的种子节点 +mainView.bootstrapWarning.noNodesAvailable=没有可用的种子节点和节点 +mainView.bootstrapWarning.bootstrappingToP2PFailed=启动 Bisq 网络失败 + +mainView.p2pNetworkWarnMsg.noNodesAvailable=没有可用种子节点或永久节点可请求数据。\n请检查您的互联网连接或尝试重启应用程序。 +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=连接至 Bisq 网络失败(错误报告:{0})。\n请检查您的互联网连接或尝试重启应用程序。 + +mainView.walletServiceErrorMsg.timeout=比特币网络连接超时。 +mainView.walletServiceErrorMsg.connectionError=错误:{0} 比特币网络连接失败。 + +mainView.walletServiceErrorMsg.rejectedTxException=交易被网络拒绝。\n\n{0} + +mainView.networkWarning.allConnectionsLost=您失去了所有与 {0} 网络节点的连接。\n您失去了互联网连接或您的计算机处于待机状态。 +mainView.networkWarning.localhostBitcoinLost=您丢失了与本地主机比特币节点的连接。\n请重启 Bisq 应用程序连接到其他比特币节点或重新启动主机比特币节点。 +mainView.version.update=(有更新可用) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=报价列表 +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=行情图 + +# OfferBookChartView +market.offerBook.buyAltcoin=我想要买入 {0}(卖出 {1}) +market.offerBook.sellAltcoin=我想要卖出 {0}(买入 {1}) +market.offerBook.buyWithFiat=购买 {0} +market.offerBook.sellWithFiat=出售 {0} +market.offerBook.sellOffersHeaderLabel=出售 {0} 到 +market.offerBook.buyOffersHeaderLabel=购买 {0} 以 +market.offerBook.buy=我想要买入比特币 +market.offerBook.sell=我想要卖出比特币 + +# SpreadView +market.spread.numberOfOffersColumn=所有报价({0}) +market.spread.numberOfBuyOffersColumn=买入 BTC({0}) +market.spread.numberOfSellOffersColumn=卖出 BTC({0}) +market.spread.totalAmountColumn=总共 BTC({0}) +market.spread.spreadColumn=差价 +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=交易:{0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=打开: +market.trades.tooltip.candle.close=关闭: +market.trades.tooltip.candle.high=高: +market.trades.tooltip.candle.low=低: +market.trades.tooltip.candle.average=平均: +market.trades.tooltip.candle.median=调解员: +market.trades.tooltip.candle.date=日期: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=创建报价 +offerbook.takeOffer=接受报价 +offerbook.takeOfferToBuy=接受报价来收购 {0} +offerbook.takeOfferToSell=接受报价来出售 {0} +offerbook.trader=商人 +offerbook.offerersBankId=卖家的银行 ID(BIC/SWIFT):{0} +offerbook.offerersBankName=卖家的银行名称:{0} +offerbook.offerersBankSeat=卖家的银行所在国家或地区:{0} +offerbook.offerersAcceptedBankSeatsEuro=接受的银行所在国家(买家):所有欧元国家 +offerbook.offerersAcceptedBankSeats=接受的银行所在国家(买家):\n {0} +offerbook.availableOffers=可用报价 +offerbook.filterByCurrency=以货币筛选 +offerbook.filterByPaymentMethod=以支付方式筛选 +offerbook.matchingOffers=Offers matching my accounts +offerbook.timeSinceSigning=账户信息 +offerbook.timeSinceSigning.info=此账户已验证,{0} +offerbook.timeSinceSigning.info.arbitrator=由仲裁员验证,并可以验证伙伴账户 +offerbook.timeSinceSigning.info.peer=由对方验证,等待%d天限制被解除 +offerbook.timeSinceSigning.info.peerLimitLifted=由对方验证,限制被取消 +offerbook.timeSinceSigning.info.signer=由对方验证,并可验证对方账户(限制已取消) +offerbook.timeSinceSigning.info.banned=账户已被封禁 +offerbook.timeSinceSigning.daysSinceSigning={0} 天 +offerbook.timeSinceSigning.daysSinceSigning.long=自验证{0} +offerbook.xmrAutoConf=是否开启自动确认 + +offerbook.timeSinceSigning.help=当您成功地完成与拥有已验证付款帐户的伙伴交易时,您的付款帐户已验证。\n{0} 天后,最初的 {1} 的限制解除以及你的账户可以验证其他人的付款账户。 +offerbook.timeSinceSigning.notSigned=尚未验证 +offerbook.timeSinceSigning.notSigned.ageDays={0} 天 +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=此账户还没有被验证以及在{0}前创建 +shared.notSigned.noNeed=此账户类型不适用验证 +shared.notSigned.noNeedDays=此账户类型不适用验证且在{0}天创建 +shared.notSigned.noNeedAlts=数字货币不适用账龄与签名 + +offerbook.nrOffers=报价数量:{0} +offerbook.volume={0}(最小 - 最大) +offerbook.deposit=BTC 保证金(%) +offerbook.deposit.help=交易双方均已支付保证金确保这个交易正常进行。这会在交易完成时退还。 + +offerbook.createOfferToBuy=创建新的报价来买入 {0} +offerbook.createOfferToSell=创建新的报价来卖出 {0} +offerbook.createOfferToBuy.withFiat=创建新的报价用 {1} 购买 {0} +offerbook.createOfferToSell.forFiat=创建新的报价以 {1} 出售 {0} +offerbook.createOfferToBuy.withCrypto=创建新的卖出报价 {0} (买入 {1}) +offerbook.createOfferToSell.forCrypto=创建新的买入报价 {0}(卖出 {1}) + +offerbook.takeOfferButton.tooltip=下单买入 {0} +offerbook.yesCreateOffer=是的,创建报价 +offerbook.setupNewAccount=设置新的交易账户 +offerbook.removeOffer.success=撤销报价成功。 +offerbook.removeOffer.failed=撤销报价失败:\n{0} +offerbook.deactivateOffer.failed=报价停用失败:\n{0} +offerbook.activateOffer.failed=报价发布失败:\n{0} +offerbook.withdrawFundsHint=您可以从 {0} 中撤回您支付的资金。 + +offerbook.warning.noTradingAccountForCurrency.headline=选择的货币没有支付账户 +offerbook.warning.noTradingAccountForCurrency.msg=您选择的货币还没有建立支付账户。\n\n你想要用其他货币创建一个报价吗? +offerbook.warning.noMatchingAccount.headline=没有匹配的支付账户。 +offerbook.warning.noMatchingAccount.msg=这个报价使用了您未创建过的支付方式。\n\n你现在想要创建一个新的支付账户吗? + +offerbook.warning.counterpartyTradeRestrictions=由于交易伙伴的交易限制,这个报价不能接受 + +offerbook.warning.newVersionAnnouncement=使用这个版本的软件,交易伙伴可以验证和验证彼此的支付帐户,以创建一个可信的支付帐户网络。\n\n交易成功后,您的支付帐户将被验证以及交易限制将在一定时间后解除(此时间基于验证方法)。\n\n有关验证帐户的更多信息,请参见文档 https://docs.bisq.network/payment-methods#account-signing + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=基于以下标准的安全限制,允许的交易金额限制为 {0}:\n- 买方的帐目没有由仲裁员或伙伴验证\n- 买方帐户自验证之日起不足30天\n- 本报价的付款方式被认为存在银行退款的风险\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=基于以下标准的安全限制,允许的交易金额限制为{0}:\n- 你的买家帐户没有由仲裁员或伙伴验证\n- 自验证你的帐户以来的时间少于30天\n- 本报价的付款方式被认为存在银行退款的风险\n\n{1} + +offerbook.warning.wrongTradeProtocol=该报价要求的软件版本与您现在运行的版本不一致。\n\n请检查您是否运行最新版本,或者是该报价用户在使用一个旧的版本。\n用户不能与不兼容的交易协议版本进行交易。 +offerbook.warning.userIgnored=您已添加该用户的匿名地址在您的忽略列表里。 +offerbook.warning.offerBlocked=该报价被 Bisq 开发人员限制。\n接受该报价时,可能有一个未处理的漏洞导致了问题。 +offerbook.warning.currencyBanned=该报价中使用的货币被 Bisq 开发人员阻止。\n请访问 Bisq 论坛了解更多信息。 +offerbook.warning.paymentMethodBanned=该报价中使用的付款方式被 Bisq 开发人员阻止。\n请访问 Bisq 论坛了解更多信息。 +offerbook.warning.nodeBlocked=该交易者的匿名地址被 Bisq 开发人员限制。\n当获取来自该交易者的报价,可能有一个未处理的漏洞导致了问题。 +offerbook.warning.requireUpdateToNewVersion=您的 Bisq 版本不再兼容交易。\n请通过 https://bisq.network/downloads 更新到最新的 Bisq 版本。 +offerbook.warning.offerWasAlreadyUsedInTrade=您不能吃单因为您已经完成了该操作。可能是你之前的吃单尝试导致了交易失败。 + +offerbook.info.sellAtMarketPrice=您会以市场价格进行出售(每分钟更新) +offerbook.info.buyAtMarketPrice=您将以市场价格进行购买(每分钟更新)。 +offerbook.info.sellBelowMarketPrice=您将以低于市场价 {0} 的价格进行出售(每分钟更新) +offerbook.info.buyAboveMarketPrice=您将以高于市场价 {0} 的价格进行支付(每分钟更新) +offerbook.info.sellAboveMarketPrice=您将以高于市场价 {0} 的价格进行出售(每分钟更新) +offerbook.info.buyBelowMarketPrice=您将以低于市场价 {0} 的价格进行支付(每分钟更新) +offerbook.info.buyAtFixedPrice=您会以这个固定价格购买。 +offerbook.info.sellAtFixedPrice=您会以这个固定价格出售。 +offerbook.info.noArbitrationInUserLanguage=如有任何争议,请注意此报价的仲裁将在 {0} 内处理。语言目前设置为{1}。 +offerbook.info.roundedFiatVolume=金额四舍五入是为了增加您的交易隐私。 + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=输入 BTC 数量 +createOffer.price.prompt=输入价格 +createOffer.volume.prompt=输入 {0} 金额 +createOffer.amountPriceBox.amountDescription=比特币数量 {0} +createOffer.amountPriceBox.buy.volumeDescription=花费 {0} 数量 +createOffer.amountPriceBox.sell.volumeDescription=接收 {0} 数量 +createOffer.amountPriceBox.minAmountDescription=最小 BTC 数量 +createOffer.securityDeposit.prompt=保证金 +createOffer.fundsBox.title=为您的报价充值 +createOffer.fundsBox.offerFee=挂单费 +createOffer.fundsBox.networkFee=矿工手续费 +createOffer.fundsBox.placeOfferSpinnerInfo=正在发布报价中... +createOffer.fundsBox.paymentLabel=Bisq 交易 ID {0} +createOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) +createOffer.fundsBox.fundsStructure.BSQ=({0} 保证金,{1} 采矿费)+ {2} 交易费 +createOffer.success.headline=你的报价已经发布 +createOffer.success.info=你可以在“业务/未完成报价”页面内管理您的未完成报价。 +createOffer.info.sellAtMarketPrice=由于您的价格是持续更新的,因此您将始终以市场价格进行出售。 +createOffer.info.buyAtMarketPrice=由于您的价格是持续更新的,因此您将始终以市场价格进行购买。 +createOffer.info.sellAboveMarketPrice=由于您的价格是持续更新的,因此您将始终按照高于市场价 {0}% 的价格出售。 +createOffer.info.buyBelowMarketPrice=由于您的价格是持续更新的,因此您将始终支付低于市场价 {0}% 的价格。 +createOffer.warning.sellBelowMarketPrice=由于您的价格是持续更新的,因此您将始终按照低于市场价 {0}% 的价格出售。 +createOffer.warning.buyAboveMarketPrice=由于您的价格是持续更新的,因此您将始终支付高于市场价 {0}% 的价格。 +createOffer.tradeFee.descriptionBTCOnly=挂单费 +createOffer.tradeFee.descriptionBSQEnabled=选择手续费币种 + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +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- 使用您的 Bisq 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 + +# 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请检查您的互联网连接或尝试重启应用程序。 +createOffer.setAmountPrice=设置数量和价格 +createOffer.warnCancelOffer=您已经为该报价充值了。\n如果您想立即取消,您的资金将划转到您的本地 Bisq 钱包并在“资金/提现”界面可以提现。\n您确定要取消吗? +createOffer.timeoutAtPublishing=发布报价时产生了一个错误。 +createOffer.errorInfo=\n\n挂单费已经支付,在最坏的情况下,你会失去了这笔费用。 我们很抱歉,但请记住,这是一个很小的数量。\n请尝试重新启动应用程序并检查您的网络连接,看看是否可以解决问题。 +createOffer.tooLowSecDeposit.warning=您设置的保证金低于推荐默认值 {0}。\n您确定要使用较低的保证金吗? +createOffer.tooLowSecDeposit.makerIsSeller=在交易对手不遵循交易协议时,这给予您较少的保护。 +createOffer.tooLowSecDeposit.makerIsBuyer=对于遵守交易协议的交易对象,您的保护金额较低,因为风险存款较小。 其他用户可能更喜欢采取其他报价,而不是您的。 +createOffer.resetToDefault=不,恢复默认值 +createOffer.useLowerValue=是的,使用我较低的值 +createOffer.priceOutSideOfDeviation=您输入的价格超过了市场价差价的最大值。\n最大值为 {0},您可以在偏好中进行调整。 +createOffer.changePrice=改变价格 +createOffer.tac=发布该报价,我同意与满足该条件的任何交易者进行交易。 +createOffer.currencyForFee=挂单费 +createOffer.setDeposit=设置买家的保证金(%) +createOffer.setDepositAsBuyer=设置自己作为买家的保证金(%) +createOffer.setDepositForBothTraders=设置双方的保证金比例(%) +createOffer.securityDepositInfo=您的买家的保证金将会是 {0} +createOffer.securityDepositInfoAsBuyer=您作为买家的保证金将会是 {0} +createOffer.minSecurityDepositUsed=已使用最低买家保证金 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=输入 BTC 数量 +takeOffer.amountPriceBox.buy.amountDescription=卖出比特币数量 +takeOffer.amountPriceBox.sell.amountDescription=买入比特币数量 +takeOffer.amountPriceBox.priceDescription=每个比特币的 {0} 价格 +takeOffer.amountPriceBox.amountRangeDescription=可用数量范围 +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=你输入的数量超过允许的小数位数。\n数量已被调整为4位小数。 +takeOffer.validation.amountSmallerThanMinAmount=数量不能比报价内设置的最小数量小。 +takeOffer.validation.amountLargerThanOfferAmount=数量不能比报价提供的总量大。 +takeOffer.validation.amountLargerThanOfferAmountMinusFee=该输入数量可能会给卖家造成比特币碎片。 +takeOffer.fundsBox.title=为交易充值 +takeOffer.fundsBox.isOfferAvailable=检查报价是否有效... +takeOffer.fundsBox.tradeAmount=卖出数量 +takeOffer.fundsBox.offerFee=挂单费 +takeOffer.fundsBox.networkFee=总共挖矿手续费 +takeOffer.fundsBox.takeOfferSpinnerInfo=正在下单... +takeOffer.fundsBox.paymentLabel=Bisq 交易 ID {0} +takeOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) +takeOffer.success.headline=你已成功下单一个报价。 +takeOffer.success.info=你可以在“业务/未完成交易”页面内查看您的未完成交易。 +takeOffer.error.message=下单时发生了一个错误。\n\n{0} + +# new entries +takeOffer.takeOfferButton=复审:报价下单 {0} 比特币 +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- 矿工手续费:{4}\n\n您有两种选项可以充值您的交易:\n- 使用您的 Bisq 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 +takeOffer.alreadyPaidInFunds=如果你已经支付,你可以在“资金/提现”提现它。 +takeOffer.paymentInfo=付款信息 +takeOffer.setAmountPrice=设置数量 +takeOffer.alreadyFunded.askCancel=您已经为该报价充值了。\n如果您想立即取消,您的资金将划转到您的本地 Bisq 钱包并在“资金/提现”界面可以提现。\n您确定要取消吗? +takeOffer.failed.offerNotAvailable=请求失败,由于报价不再可用。 也许有交易者在此期间已经下单。 +takeOffer.failed.offerTaken=您不能对该报价下单,因为该报价已经被其他交易者下单。 +takeOffer.failed.offerRemoved=您不能对该报价下单,因为该报价已经在此期间被删除。 +takeOffer.failed.offererNotOnline=下单失败,因为卖家已经不在线。 +takeOffer.failed.offererOffline=您不能下单,因为卖家已经下线。 +takeOffer.warning.connectionToPeerLost=您与卖家失去连接。\n因为太多连接,他或许已经下线或者关掉了与您的连接。\n\n如果您还是能在报价列表中看到他的报价,您可以再次尝试下单。 + +takeOffer.error.noFundsLost=\n\n你的钱包里还没有钱。 \n请尝试重启您的应用程序或者检查您的网络连接。 +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n!\n +takeOffer.error.depositPublished=\n\n您的保证金转账已经发布。\n请尝试重启您的应用程序或者检查您的网络连接。\n如果始终存在问题,请到帮助界面联系开发者。 +takeOffer.error.payoutPublished=\n\n您的支付转账已经发布。\n请尝试重启您的应用程序或者检查您的网络连接。\n如果始终存在问题,请到帮助界面联系开发者。 +takeOffer.tac=接受该报价,意味着我同意这交易界面中的条件。 + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=触发价格 +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=设定价格 +editOffer.confirmEdit=确认:编辑报价 +editOffer.publishOffer=发布您的报价。 +editOffer.failed=报价编辑失败:\n{0} +editOffer.success=您的报价已成功编辑。 +editOffer.invalidDeposit=买方保证金不符合 Bisq DAO 规定,不能再次编辑。 + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=我的未完成报价 +portfolio.tab.pendingTrades=未完成交易 +portfolio.tab.history=历史记录 +portfolio.tab.failed=失败 +portfolio.tab.editOpenOffer=编辑报价 + +portfolio.closedTrades.deviation.help=与市场价格偏差百分比 + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=等待区块链确认 +portfolio.pending.step2_buyer.startPayment=开始付款 +portfolio.pending.step2_seller.waitPaymentStarted=等待直到付款 +portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到达 +portfolio.pending.step3_seller.confirmPaymentReceived=确定收到付款 +portfolio.pending.step5.completed=完成 + +portfolio.pending.step3_seller.autoConf.status.label=自动确认状态。 +portfolio.pending.autoConf=自动确认 +portfolio.pending.autoConf.blocks=XMR 确认数:{0} / 需求量:{2} +portfolio.pending.autoConf.state.xmr.txKeyReused=交易密钥已重复使用。请发起纠纷处理。 +portfolio.pending.autoConf.state.confirmations=XMR 确认:{0}/{1} +portfolio.pending.autoConf.state.txNotFound=交易并未在内存池中检索。 +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=无有效交易 ID / 交易密钥 +portfolio.pending.autoConf.state.filterDisabledFeature=由开发者禁用 + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=自动确认功能已禁用。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=交易金额超过自动确认金额限制。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=对等点提供不可用数据。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支付交易已经发布 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=已发起纠纷。该交易的自动确认已被禁用 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=交易证明申请已经开始 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=成功结果:{0}/{1} ;{2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=所有服务都已被证明。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=您请求的服务发生了错误。没有自动确认。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FAILED=服务返回失败。没有自动确认。 + +portfolio.pending.step1.info=存款交易已经发布。\n开始付款之前,{0} 需要等待至少一个区块链确认。 +portfolio.pending.step1.warn=保证金交易仍未得到确认。这种情况可能会发生在外部钱包转账时使用的交易手续费用较低造成的。 +portfolio.pending.step1.openForDispute=保证金交易仍未得到确认。请联系调解员协助。 + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=请从您的外部 {0} 钱包划转\n{1} 到 BTC 卖家。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=请到银行并支付 {0} 给 BTC 卖家。\n\n +portfolio.pending.step2_buyer.cash.extra=重要要求:\n完成付款后在纸质收据上写下:不退款。\n然后将其撕成2份,拍照片并发送给 BTC 卖家的电子邮件地址。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=请使用 MoneyGram 向 BTC 卖家支付 {0}。\n\n +portfolio.pending.step2_buyer.moneyGram.extra=重要要求:\n完成支付后,请通过电邮发送授权编号和照片给 BTC 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=请使用 Western Union 向 BTC 卖家支付 {0}。\n\n +portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付后,请通过电邮发送 MTCN(追踪号码)和照片给 BTC 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。 + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} 给 BTC 卖家。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=请通过提供的联系人与 BTC 卖家联系,并安排会议支付 {0}。\n\n +portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 开始付款 +portfolio.pending.step2_buyer.recipientsAccountData=接受 {0} +portfolio.pending.step2_buyer.amountToTransfer=划转数量 +portfolio.pending.step2_buyer.sellersAddress=卖家的 {0} 地址 +portfolio.pending.step2_buyer.buyerAccount=您的付款帐户将被使用 +portfolio.pending.step2_buyer.paymentStarted=付款开始 +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=你还没有完成你的 {0} 付款!\n请注意,交易必须在 {1} 之前完成。 +portfolio.pending.step2_buyer.openForDispute=您还没有完成您的付款!\n最大交易期限已过。请联系调解员寻求帮助。 +portfolio.pending.step2_buyer.paperReceipt.headline=您是否将纸质收据发送给 BTC 卖家? +portfolio.pending.step2_buyer.paperReceipt.msg=请牢记:\n完成付款后在纸质收据上写下:不退款。\n然后将其撕成2份,拍照片并发送给 BTC 卖家的电子邮件地址。 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=发送授权编号和收据 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=请通过电邮发送授权编号和照片给 BTC 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。\n\n您把授权编号和合同发给卖方了吗? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=发送 MTCN 和收据 +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=请通过电邮发送 MTCN(追踪号码)和照片给 BTC 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。\n\n您把 MTCN 和合同发给卖方了吗? +portfolio.pending.step2_buyer.halCashInfo.headline=请发送 HalCash 代码 +portfolio.pending.step2_buyer.halCashInfo.msg=您需要向 BTC 卖家发送带有 HalCash 代码和交易 ID({0})的文本消息。\n\n卖方的手机号码是 {1} 。\n\n您是否已经将代码发送至卖家? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=有些银行可能会要求接收方的姓名。在较旧的 Bisq 客户端创建的快速支付帐户没有提供收款人的姓名,所以请使用交易聊天来获得收款人姓名(如果需要)。 +portfolio.pending.step2_buyer.confirmStart.headline=确定您已经付款 +portfolio.pending.step2_buyer.confirmStart.msg=您是否向您的交易伙伴发起 {0} 付款? +portfolio.pending.step2_buyer.confirmStart.yes=是的,我已经开始付款 +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=你没有提供任何付款证明 +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=您还没有输入交易 ID 以及交易密钥\n\n如果不提供此数据您的交易伙伴无法在收到 XMR 后使用自动确认功能以快速释放 BTC。\n另外,Bisq 要求 XMR 发送者在发生纠纷的时候能够向调解员和仲裁员提供这些信息。\n更多细节在 Bisq Wiki:https://bisq.wiki/Trading_Monero#Auto-confirming_trades +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=输入并不是一个 32 字节的哈希值 +portfolio.pending.step2_buyer.confirmStart.warningButton=忽略并继续 +portfolio.pending.step2_seller.waitPayment.headline=等待付款 +portfolio.pending.step2_seller.f2fInfo.headline=买家的合同信息 +portfolio.pending.step2_seller.waitPayment.msg=存款交易至少有一个区块链确认。\n您需要等到 BTC 买家开始 {0} 付款。 +portfolio.pending.step2_seller.warn=BTC 买家仍然没有完成 {0} 付款。\n你需要等到他开始付款。\n如果 {1} 交易尚未完成,仲裁员将进行调查。 +portfolio.pending.step2_seller.openForDispute=BTC 买家尚未开始付款!\n允许的最长交易期限已经过去了。你可以继续等待给予交易双方更多时间,或联系仲裁员以争取解决纠纷。 +tradeChat.chatWindowTitle=使用 ID “{0}” 进行交易的聊天窗口 +tradeChat.openChat=打开聊天窗口 +tradeChat.rules=您可以与您的伙伴沟通,以解决该交易的潜在问题。\n在聊天中不强制回复。\n如果交易员违反了下面的任何规则,打开纠纷并向调解员或仲裁员报告。\n聊天规则:\n\n\t●不要发送任何链接(有恶意软件的风险)。您可以发送交易 ID 和区块资源管理器的名称。\n\t●不要发送还原密钥、私钥、密码或其他敏感信息!\n\t●不鼓励 Bisq 以外的交易(无安全保障)。\n\t●不要参与任何形式的危害社会安全的计划。\n\t●如果对方没有回应,也不愿意通过聊天进行沟通,那就尊重对方的决定。\n\t●将谈话范围限制在行业内。这个聊天不是一个社交软件替代品或troll-box。\n\t●保持友好和尊重的交谈。 + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +message.state.SENT=发出信息 +# suppress inspection "UnusedProperty" +message.state.ARRIVED=消息已抵达 +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=已发送但尚未被对方接收的付款信息 +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=对方确认消息回执 +# suppress inspection "UnusedProperty" +message.state.FAILED=发送消息失败 + +portfolio.pending.step3_buyer.wait.headline=等待 BTC 卖家付款确定 +portfolio.pending.step3_buyer.wait.info=等待 BTC 卖家确认收到 {0} 付款。 +portfolio.pending.step3_buyer.wait.msgStateInfo.label=支付开始消息状态 +portfolio.pending.step3_buyer.warn.part1a=在 {0} 区块链 +portfolio.pending.step3_buyer.warn.part1b=在您的支付供应商(例如:银行) +portfolio.pending.step3_buyer.warn.part2=BTC 卖家仍然没有确认您的付款。如果付款发送成功,请检查 {0}。 +portfolio.pending.step3_buyer.openForDispute=BTC 卖家还没有确认你的付款!最大交易期限已过。您可以等待更长时间,并给交易伙伴更多时间或请求调解员的帮助。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=您的交易伙伴已经确认他们已经发起了 {0} 付款。\n\n +portfolio.pending.step3_seller.altcoin.explorer=在您最喜欢的 {0} 区块链浏览器 +portfolio.pending.step3_seller.altcoin.wallet=在您的 {0} 钱包 +portfolio.pending.step3_seller.altcoin={0} 请检查 {1} 是否交易已经到您的接收地址\n{2}\n已经有足够的区块链确认了\n支付金额必须为 {3}\n\n关闭该弹出窗口后,您可以从主界面复制并粘贴 {4} 地址。 +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=因为付款是通过现金存款完成的,BTC 买家必须在纸质收据上写“不退款”,将其撕成2份,并通过电子邮件向您发送照片。\n\n为避免退款风险,请仅确认您是否收到电子邮件,如果您确定收据有效。\n如果您不确定,{0} +portfolio.pending.step3_seller.moneyGram=买方必须发送授权编码和一张收据的照片。\n收据必须清楚地显示您的全名、城市、国家或地区、数量。如果您收到授权编码,请查收邮件。\n\n关闭弹窗后,您将看到 BTC 买家的姓名和在 MoneyGram 的收款地址。\n\n只有在您成功收到钱之后,再确认收据! +portfolio.pending.step3_seller.westernUnion=买方必须发送 MTCN(跟踪号码)和一张收据的照片。\n收据必须清楚地显示您的全名、城市、国家或地区、数量。如果您收到 MTCN,请查收邮件。\n\n关闭弹窗后,您将看到 BTC 买家的姓名和在 Western Union 的收款地址。\n\n只有在您成功收到钱之后,再确认收据! +portfolio.pending.step3_seller.halCash=买方必须将 HalCash代码 用短信发送给您。除此之外,您将收到来自 HalCash 的消息,其中包含从支持 HalCash 的 ATM 中提取欧元所需的信息\n从 ATM 取款后,请在此确认付款收据! +portfolio.pending.step3_seller.amazonGiftCard=BTC 买家已经发送了一张亚马逊电子礼品卡到您的邮箱或手机短信。请现在立即兑换亚马逊电子礼品卡到您的亚马逊账户中以及确认交易信息。 + +portfolio.pending.step3_seller.bankCheck=\n\n还请确认您的银行对帐单中的发件人姓名与委托合同中的发件人姓名相符:\n发件人姓名:{0}\n\n如果名称与此处显示的名称不同,则 {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=请不要确认,而是通过键盘组合键“alt + o”或“option + o”来打开纠纷。 +portfolio.pending.step3_seller.confirmPaymentReceipt=确定付款收据 +portfolio.pending.step3_seller.amountToReceive=接收数量: +portfolio.pending.step3_seller.yourAddress=您的 {0} 地址 +portfolio.pending.step3_seller.buyersAddress=卖家的 {0} 地址 +portfolio.pending.step3_seller.yourAccount=您的交易账户 +portfolio.pending.step3_seller.xmrTxHash=交易记录 ID +portfolio.pending.step3_seller.xmrTxKey=交易密钥 +portfolio.pending.step3_seller.buyersAccount=买方账号数据 +portfolio.pending.step3_seller.confirmReceipt=确定付款收据 +portfolio.pending.step3_seller.buyerStartedPayment=BTC 买家已经开始 {0} 的付款。\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=检查您的数字货币钱包或块浏览器的区块链确认,并确认付款时,您有足够的块链确认。 +portfolio.pending.step3_seller.buyerStartedPayment.fiat=检查您的交易账户(例如银行帐户),并确认您何时收到付款。 +portfolio.pending.step3_seller.warn.part1a=在 {0} 区块链 +portfolio.pending.step3_seller.warn.part1b=在您的支付供应商(例如:银行) +portfolio.pending.step3_seller.warn.part2=你还没有确认收到款项。如果您已经收到款项,请检查 {0}。 +portfolio.pending.step3_seller.openForDispute=您尚未确认付款的收据!\n最大交易期已过\n请确认或请求调解员的协助。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=您是否收到了您交易伙伴的 {0} 付款?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=还请确认您的银行对帐单中的发件人姓名与委托合同中的发件人姓名相符:\n每个交易合约的发送者姓名:{0}\n\n如果名称与此处显示的名称不一致,请不要通过确认付款,而是通过“alt + o”或“option + o”打开纠纷。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=请注意,一旦您确认收到,冻结交易金额将被发放给 BTC 买家,保证金将被退还。 +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=确定您已经收到付款 +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=是的,我已经收到付款。 +portfolio.pending.step3_seller.onPaymentReceived.signer=重要提示:通过确认收到付款,你也验证了对方的账户,并获得验证。因为对方的账户还没有验证,所以你应该尽可能的延迟付款的确认,以减少退款的风险。 + +portfolio.pending.step5_buyer.groupTitle=完成交易的概要 +portfolio.pending.step5_buyer.tradeFee=挂单费 +portfolio.pending.step5_buyer.makersMiningFee=矿工手续费 +portfolio.pending.step5_buyer.takersMiningFee=总共挖矿手续费 +portfolio.pending.step5_buyer.refunded=退还保证金 +portfolio.pending.step5_buyer.withdrawBTC=提现您的比特币 +portfolio.pending.step5_buyer.amount=提现数量 +portfolio.pending.step5_buyer.withdrawToAddress=提现地址 +portfolio.pending.step5_buyer.moveToBisqWallet=在 Bisq 钱包中保留资金 +portfolio.pending.step5_buyer.withdrawExternal=提现到外部钱包 +portfolio.pending.step5_buyer.alreadyWithdrawn=您的资金已经提现。\n请查看交易历史记录。 +portfolio.pending.step5_buyer.confirmWithdrawal=确定提现请求 +portfolio.pending.step5_buyer.amountTooLow=转让金额低于交易费用和最低可能的tx值(零头)。 +portfolio.pending.step5_buyer.withdrawalCompleted.headline=提现完成 +portfolio.pending.step5_buyer.withdrawalCompleted.msg=您完成的交易存储在“业务/历史记录”下。\n您可以查看“资金/交易”下的所有比特币交易 +portfolio.pending.step5_buyer.bought=您已经买入 +portfolio.pending.step5_buyer.paid=您已经支付 + +portfolio.pending.step5_seller.sold=您已经卖出 +portfolio.pending.step5_seller.received=您已经收到 + +tradeFeedbackWindow.title=恭喜您完成交易 +tradeFeedbackWindow.msg.part1=我们很想听听您的体验如何。这将帮助我们改进软件,优化体验不好的地方。如欲提供意见,请填写这份简短的问卷(无需注册),网址: +tradeFeedbackWindow.msg.part2=如果您有任何疑问或遇到任何问题,请通过 Bisq 论坛与其他用户和贡献者联系: +tradeFeedbackWindow.msg.part3=感谢使用 Bisq + +portfolio.pending.role=我的角色 +portfolio.pending.tradeInformation=交易信息 +portfolio.pending.remainingTime=剩余时间 +portfolio.pending.remainingTimeDetail={0}(直到 {1} ) +portfolio.pending.tradePeriodInfo=在第一次区块链确认之后,交易周期开始。根据所使用的付款方法,采用不同的最大允许交易周期。 +portfolio.pending.tradePeriodWarning=如果超过了这个周期,双方均可以提出纠纷。 +portfolio.pending.tradeNotCompleted=交易不会及时完成(直到 {0} ) +portfolio.pending.tradeProcess=交易流程 +portfolio.pending.openAgainDispute.msg=如果您不确定发送给调解员或仲裁员的消息是否已送达(例如,如果您在1天后没有收到回复),请放心使用 Cmd/Ctrl+o 再次打开纠纷。你也可以在 Bisq 论坛上寻求额外的帮助,网址是 https://bisq.community。 +portfolio.pending.openAgainDispute.button=再次出现纠纷 +portfolio.pending.openSupportTicket.headline=创建帮助话题 +portfolio.pending.openSupportTicket.msg=请仅在紧急情况下使用此功能,如果您没有看到“提交支持”或“提交纠纷”按钮。\n\n当您发出工单时,交易将被中断并由调解员或仲裁员进行处理。 + +portfolio.pending.timeLockNotOver=你必须等到≈{0}(还需等待{1}个区块)才能提交纠纷。 +portfolio.pending.error.depositTxNull=保证金交易无效。没有有效的保证金交易,你使用创建纠纷。请到“设置/网络信息”进行 SPV 重新同步。\n \n如需更多帮助,请联系 Bisq Keybase 团队的 Support 频道。 +portfolio.pending.mediationResult.error.depositTxNull=保证金交易为空。你可以移动该交易至失败的交易。 +portfolio.pending.mediationResult.error.delayedPayoutTxNull=延迟支付交易为空。你可以移动该交易至失败的交易。 +portfolio.pending.error.depositTxNotConfirmed=保证金交易未确认。未经确认的存款交易不能发起纠纷或仲裁请求。请耐心等待,直到它被确认或进入“设置/网络信息”进行 SPV 重新同步。\n\n如需更多帮助,请联系 Bisq Keybase 团队的 Support 频道。 + +portfolio.pending.support.headline.getHelp=需要帮助? +portfolio.pending.support.text.getHelp=如果您有任何问题,您可以尝试在交易聊天中联系交易伙伴,或在 https://bisq.community 询问 Bisq 社区。如果您的问题仍然没有解决,您可以向调解员取得更多的帮助。 +portfolio.pending.support.button.getHelp=开启交易聊天 +portfolio.pending.support.headline.halfPeriodOver=确认付款 +portfolio.pending.support.headline.periodOver=交易期结束 + +portfolio.pending.mediationRequested=已请求调解员协助 +portfolio.pending.refundRequested=已请求退款 +portfolio.pending.openSupport=创建帮助话题 +portfolio.pending.supportTicketOpened=帮助话题已经创建 +portfolio.pending.communicateWithArbitrator=请在“帮助”界面上与仲裁员联系。 +portfolio.pending.communicateWithMediator=请在“支持”页面中与调解员进行联系。 +portfolio.pending.disputeOpenedMyUser=您创建了一个纠纷。\n{0} +portfolio.pending.disputeOpenedByPeer=您的交易对象创建了一个纠纷。\n{0} +portfolio.pending.noReceiverAddressDefined=没有定义接收地址 + +portfolio.pending.mediationResult.headline=调解费用的支出 +portfolio.pending.mediationResult.info.noneAccepted=通过接受调解员关于交易的建议的支出来完成交易。 +portfolio.pending.mediationResult.info.selfAccepted=你已经接受了调解员的建议。等待伙伴接受。 +portfolio.pending.mediationResult.info.peerAccepted=你的伙伴已经接受了调解员的建议。你也接受吗? +portfolio.pending.mediationResult.button=查看建议的解决方案 +portfolio.pending.mediationResult.popup.headline=调解员在交易 ID:{0}上的建议 +portfolio.pending.mediationResult.popup.headline.peerAccepted=你的伙伴已经接受了调解员的建议 +portfolio.pending.mediationResult.popup.info=调解员建议的支出如下:\n你将支付:{0}\n你的交易伙伴将支付:{1}\n\n你可以接受或拒绝这笔调解费支出。\n\n通过接受,你验证了合约的支付交易。如果你的交易伙伴也接受和验证,支付将完成,交易将关闭。\n\n如果你们其中一人或双方都拒绝该建议,你将必须等到(2)({3}区块)与仲裁员展开第二轮纠纷讨论,仲裁员将再次调查该案件,并根据他们的调查结果进行支付。\n\n仲裁员可以收取少量费用(费用上限:交易的保证金)作为其工作的补偿。两个交易者都同意调解员的建议是愉快的路径请求仲裁是针对特殊情况的,比如如果一个交易者确信调解员没有提出公平的赔偿建议(或者如果另一个同伴没有回应)。\n\n关于新的仲裁模型的更多细节:https://docs.bisq.network/trading-rules.html#arbitration +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=您已经接受了调解员的建议支付但是似乎您的交易对手并没有接受。\n\n一旦锁定时间到{0}(区块{1})您可以打开第二轮纠纷让仲裁员重新研究该案件并重新作出支出决定。\n\n您可以找到更多关于仲裁模型的信息在:\nhttps://docs.bisq.network/trading-rules.html#arbitration +portfolio.pending.mediationResult.popup.openArbitration=拒绝并请求仲裁 +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这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Bisq 调解员处得到建议。\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.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您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=将交易移至失败交易 +portfolio.pending.failedTrade.warningIcon.tooltip=点击打开该交易的问题细节 +portfolio.failed.revertToPending.popup=您想要将该交易移至未完成交易吗 +portfolio.failed.revertToPending=将交易移至未完成交易 + +portfolio.closed.completed=完成 +portfolio.closed.ticketClosed=已仲裁 +portfolio.closed.mediationTicketClosed=已调解 +portfolio.closed.canceled=已取消 +portfolio.failed.Failed=失败 +portfolio.failed.unfail=再继续之前,请保证你有一份根目录的备份!\n您想要将此交易移至未完成的交易吗?\n这是一个解锁卡在失败交易的资金的方法 +portfolio.failed.cantUnfail=目前该交易暂无法移至未完成的交易。\n请在完成交易后重试{0} +portfolio.failed.depositTxNull=交易无法恢复至未完成交易。保证金交易为空。 +portfolio.failed.delayedPayoutTxNull=交易无法恢复至未完成交易。延迟支付交易为空。 + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=存款 +funds.tab.withdrawal=提现 +funds.tab.reserved=保证金 +funds.tab.locked=冻结资金 +funds.tab.transactions=交易记录 + +funds.deposit.unused=尚未使用 +funds.deposit.usedInTx=用在 {0} 交易 +funds.deposit.fundBisqWallet=充值 Bisq 钱包 +funds.deposit.noAddresses=尚未生成存款地址 +funds.deposit.fundWallet=充值您的钱包 +funds.deposit.withdrawFromWallet=从钱包转出资金 +funds.deposit.amount=BTC 数量(可选) +funds.deposit.generateAddress=生成新的地址 +funds.deposit.generateAddressSegwit=原生 segwit 格式(Bech32) +funds.deposit.selectUnused=请从上表中选择一个未使用的地址,而不是生成一个新地址。 + +funds.withdrawal.arbitrationFee=仲裁费用 +funds.withdrawal.inputs=充值选择 +funds.withdrawal.useAllInputs=使用所有可用的充值地址 +funds.withdrawal.useCustomInputs=使用自定义充值地址 +funds.withdrawal.receiverAmount=接收者的数量 +funds.withdrawal.senderAmount=发送者的数量 +funds.withdrawal.feeExcluded=不含挖矿费的金额 +funds.withdrawal.feeIncluded=包含挖矿费的金额 +funds.withdrawal.fromLabel=从源地址提现 +funds.withdrawal.toLabel=提现地址 +funds.withdrawal.memoLabel=提现备注 +funds.withdrawal.memo=可选备注 +funds.withdrawal.withdrawButton=选定提现 +funds.withdrawal.noFundsAvailable=没有可用资金提现 +funds.withdrawal.confirmWithdrawalRequest=确定提现请求 +funds.withdrawal.withdrawMultipleAddresses=从多个地址提现({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=从多个地址提现:\n{0} +funds.withdrawal.notEnoughFunds=您钱包里没有足够的资金。 +funds.withdrawal.selectAddress=从列表中选一个源地址 +funds.withdrawal.setAmount=设置提现数量 +funds.withdrawal.fillDestAddress=输入您的目标地址 +funds.withdrawal.warn.noSourceAddressSelected=您需要从上面列表中选一个源地址。 +funds.withdrawal.warn.amountExceeds=您的金额超过所选地址的可用金额。\n请考虑在上表中选择多个地址或调整手续费设置,来支付手续费。 + +funds.reserved.noFunds=未完成报价中没有已用资金 +funds.reserved.reserved=报价 ID:{0} 接收在本地钱包中 + +funds.locked.noFunds=交易中没有冻结资金 +funds.locked.locked=多重验证冻结交易 ID:{0} + +funds.tx.direction.sentTo=发送至: +funds.tx.direction.receivedWith=接收到: +funds.tx.direction.genesisTx=从初始 tx: +funds.tx.txFeePaymentForBsqTx=BSQ tx 的矿工手续费支付 +funds.tx.createOfferFee=挂单和tx费用:{0} +funds.tx.takeOfferFee=下单和tx费用:{0} +funds.tx.multiSigDeposit=多重验证保证金:{0} +funds.tx.multiSigPayout=多重验证花费:{0} +funds.tx.disputePayout=纠纷花费:{0} +funds.tx.disputeLost=失败的纠纷案件:{0} +funds.tx.collateralForRefund=押金退款:{0} +funds.tx.timeLockedPayoutTx=距锁定锁定支付 tx 的时间: {0} +funds.tx.refund=仲裁退款:{0} +funds.tx.unknown=未知原因:{0} +funds.tx.noFundsFromDispute=没有退款的纠纷 +funds.tx.receivedFunds=收到的资金: +funds.tx.withdrawnFromWallet=从钱包提现 +funds.tx.withdrawnFromBSQWallet=BTC 已从 BSQ 钱包中取出 +funds.tx.memo=备注 +funds.tx.noTxAvailable=没有可用交易 +funds.tx.revert=还原 +funds.tx.txSent=交易成功发送到本地 Bisq 钱包中的新地址。 +funds.tx.direction.self=内部钱包交易 +funds.tx.daoTxFee=BSQ tx 的矿工手续费支付 +funds.tx.reimbursementRequestTxFee=退还申请 +funds.tx.compensationRequestTxFee=报偿申请 +funds.tx.dustAttackTx=接受零头 +funds.tx.dustAttackTx.popup=这笔交易是发送一个非常小的比特币金额到您的钱包,可能是区块链分析公司尝试监控您的交易。\n\n如果您在交易中使用该交易输出,他们将了解到您很可能也是其他地址的所有者(资金归集)。\n\n为了保护您的隐私,Bisq 钱包忽略了这种零头的消费和余额显示。可以在设置中将输出视为零头时设置阈值量。 + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=调解 +support.tab.arbitration.support=仲裁 +support.tab.legacyArbitration.support=历史仲裁 +support.tab.ArbitratorsSupportTickets={0} 的工单 +support.filter=查找纠纷 +support.filter.prompt=输入 交易 ID、日期、洋葱地址或账户信息 + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=如果向在 DAO 发送赔偿请求,您需要在 Github 上粘贴您的赔偿请求中的调解和仲裁过程的摘要消息。要使此声明可验证,任何用户都可以使用此工具检查调解或仲裁人员的签名是否与摘要消息匹配。 +support.sigCheck.popup.header=确认纠纷结果签名 +support.sigCheck.popup.msg.label=总结消息 +support.sigCheck.popup.msg.prompt=复制粘贴纠纷总结消息 +support.sigCheck.popup.result=验证结果 +support.sigCheck.popup.success=签名有效 +support.sigCheck.popup.failed=签名验证失败 +support.sigCheck.popup.invalidFormat=消息并不是正确的格式。请复制粘贴纠纷总结消息。 + +support.reOpenByTrader.prompt=您确定想要重新开启纠纷? +support.reOpenButton.label=重新打开 +support.sendNotificationButton.label=私人通知 +support.reportButton.label=报告 +support.fullReportButton.label=所有纠纷 +support.noTickets=没有创建的话题 +support.sendingMessage=发送消息... +support.receiverNotOnline=收件人未在线。消息被保存到他们的邮箱。 +support.sendMessageError=发送消息失败。错误:{0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=纠纷中的订单创建于一个旧版本的 Bisq。\n您不能在当前版本关闭这个纠纷。\n\n请您使用旧版本/协议版本: {0} +support.openFile=打开附件文件(文件最大大小:{0} kb) +support.attachmentTooLarge=您的附件的总大小为 {0} kb,并超过最大值。 允许消息大小为 {1} kB。 +support.maxSize=文件允许的最大大小 {0} kB。 +support.attachment=附件 +support.tooManyAttachments=您不能在一个消息里发送超过3个附件。 +support.save=保存文件到磁盘 +support.messages=消息 +support.input.prompt=输入消息... +support.send=发送 +support.addAttachments=添加附件 +support.closeTicket=关闭话题 +support.attachments=附件: +support.savedInMailbox=消息保存在收件人的信箱中 +support.arrived=消息抵达收件人 +support.acknowledged=收件人已确认接收消息 +support.error=收件人无法处理消息。错误:{0} +support.buyerAddress=BTC 买家地址 +support.sellerAddress=BTC 卖家地址 +support.role=角色 +support.agent=Support agent +support.state=状态 +support.chat=Chat +support.closed=关闭 +support.open=打开 +support.process=Process +support.buyerOfferer=BTC 买家/挂单者 +support.sellerOfferer=BTC 卖家/挂单者 +support.buyerTaker=BTC 买家/买单者 +support.sellerTaker=BTC 卖家/买单者 + +support.backgroundInfo=Bisq 不是一家公司,所以它处理纠纷的方式不同。\n\n交易双方可以在应用程序中通过未完成交易页面上的安全聊天进行通信,以尝试自行解决争端。如果这还不够,调解员可以介入帮助。调解员将对情况进行评估,并对交易资金的支出提出建议。如果两个交易者都接受这个建议,那么支付交易就完成了,交易也结束了。如果一方或双方不同意调解员的建议,他们可以要求仲裁。仲裁员将重新评估情况,如果有必要,将亲自向交易员付款,并要求 Bisq DAO 对这笔付款进行补偿。 +support.initialInfo=请在下面的文本框中输入您的问题描述。添加尽可能多的信息,以加快解决纠纷的时间。\n\n以下是你应提供的资料核对表:\n\t●如果您是 BTC 买家:您是否使用法定货币或其他加密货币转账?如果是,您是否点击了应用程序中的“支付开始”按钮?\n\t●如果您是 BTC 卖家:您是否收到法定货币或其他加密货币的付款了?如果是,你是否点击了应用程序中的“已收到付款”按钮?\n\t●您使用的是哪个版本的 Bisq?\n\t●您使用的是哪种操作系统?\n\t●如果遇到操作执行失败的问题,请考虑切换到新的数据目录。\n\t有时数据目录会损坏,并导致奇怪的错误。\n详见:https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\n请熟悉纠纷处理的基本规则:\n\t●您需要在2天内答复 {0} 的请求。\n\t●调解员会在2天之内答复,仲裁员会在5天之内答复。\n\t●纠纷的最长期限为14天。\n\t●你需要与仲裁员合作,提供他们为你的案件所要求的信息。\n\t●当您第一次启动应用程序时,您接受了用户协议中争议文档中列出的规则。\n\n您可以通过 {2} 了解有关纠纷处理的更多信息 +support.systemMsg=系统消息:{0} +support.youOpenedTicket=您创建了帮助请求。\n\n{0}\n\nBisq 版本:{1} +support.youOpenedDispute=您创建了一个纠纷请求。\n\n{0}\n\nBisq 版本:{1} +support.youOpenedDisputeForMediation=您创建了一个调解请求。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedTicket=对方因技术问题请求获取帮助。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedDispute=对方创建了一个纠纷请求。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedDisputeForMediation=对方创建了一个调解请求。\n\n{0}\n\nBisq 版本:{1} +support.mediatorsDisputeSummary=系统消息:\n调解纠纷总结:\n{0} +support.mediatorsAddress=仲裁员的节点地址:{0} +support.warning.disputesWithInvalidDonationAddress=延迟支付交易已经被用于一个不可用接受者地址。它与有效捐赠地址的任何 DAO 中参数值均不匹配。\n\n这可能是一个骗局。请将该事件通知开发者,在问题解决之前不要关闭该案件!\n\n纠纷所用的地址:{0}\n\n所有 DAO 参数中捐赠地址:{1}\n\n交易:{2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\n您确定一定要关闭纠纷吗? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n您不能进行支付。 +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=偏好 +settings.tab.network=网络信息 +settings.tab.about=关于我们 + +setting.preferences.general=通用偏好 +setting.preferences.explorer=比特币区块浏览器 +setting.preferences.explorer.bsq=Bisq 区块浏览器 +setting.preferences.deviation=与市场价格最大差价 +setting.preferences.bsqAverageTrimThreshold=BSQ 率已超过阈值 +setting.preferences.avoidStandbyMode=避免待机模式 +setting.preferences.autoConfirmXMR=XMR 自动确认 +setting.preferences.autoConfirmEnabled=启用 +setting.preferences.autoConfirmRequiredConfirmations=已要求确认 +setting.preferences.autoConfirmMaxTradeSize=最大交易量(BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer 链接(使用Tor,但本地主机,LAN IP地址和 *.local 主机名除外) +setting.preferences.deviationToLarge=值不允许大于30% +setting.preferences.txFee=提现交易手续费(聪/字节) +setting.preferences.useCustomValue=使用自定义值 +setting.preferences.txFeeMin=交易手续费必须至少为{0} 聪/字节 +setting.preferences.txFeeTooLarge=您输入的数额超过可接受值(>5000 聪/字节)。交易手续费一般在 50-400 聪/字节、 +setting.preferences.ignorePeers=忽略节点 [洋葱地址:端口] +setting.preferences.ignoreDustThreshold=最小无零头输出值 +setting.preferences.currenciesInList=市场价的货币列表 +setting.preferences.prefCurrency=首选货币 +setting.preferences.displayFiat=显示国家货币 +setting.preferences.noFiat=没有选定国家货币 +setting.preferences.cannotRemovePrefCurrency=您不能删除您选定的首选货币 +setting.preferences.displayAltcoins=显示数字货币 +setting.preferences.noAltcoins=没有选定数字货币 +setting.preferences.addFiat=添加法定货币 +setting.preferences.addAltcoin=添加数字货币 +setting.preferences.displayOptions=显示选项 +setting.preferences.showOwnOffers=在报价列表中显示我的报价 +setting.preferences.useAnimations=使用动画 +setting.preferences.useDarkMode=使用夜间模式 +setting.preferences.sortWithNumOffers=使用“报价ID/交易ID”筛选列表 +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=重置所有“不再提示”的提示 +settings.preferences.languageChange=同意重启请求以更换语言 +settings.preferences.supportLanguageWarning=如有任何争议,请注意调解在 {0} 处理,仲裁在 {1} 处理。 +setting.preferences.daoOptions=DAO 选项 +setting.preferences.dao.resyncFromGenesis.label=从初始 tx 重构 DAO 状态 +setting.preferences.dao.resyncFromResources.label=从指定资源重新构建 DAO 状态 +setting.preferences.dao.resyncFromResources.popup=应用程序重新启动后,Bisq 网络治理数据将从种子节点重新加载,而 BSQ 同步状态将从创始交易中重新构建。 +setting.preferences.dao.resyncFromGenesis.popup=从创始交易中出现同步会消耗大量时间以及 CPU 资源。您确定要重新同步吗?通常,从最新资源文件进行重新同步就足够了,而且速度更快。\n\n应用程序重新启动后,Bisq 网络治理数据将从种子节点重新加载,而 BSQ 同步状态将从初始交易中重新构建。 +setting.preferences.dao.resyncFromGenesis.resync=从创始区块重新同步并关闭 +setting.preferences.dao.isDaoFullNode=以 DAO 全节点运行 Bisq +setting.preferences.dao.rpcUser=RPC 用户名 +setting.preferences.dao.rpcPw=PRC 密码 +setting.preferences.dao.blockNotifyPort=区块通知端口 +setting.preferences.dao.fullNodeInfo=如果要将 Bisq 以 DAO 全节点运行,您需要在本地运行比特币核心并启用 RPC 。所有的需求都记录在“ {0} ”中。 +setting.preferences.dao.fullNodeInfo.ok=打开文档页面 +setting.preferences.dao.fullNodeInfo.cancel=不,我坚持使用轻节点模式 +settings.preferences.editCustomExplorer.headline=浏览设置。 +settings.preferences.editCustomExplorer.description=从左侧列表中选择一个系统默认浏览器,或使用您偏好的自定义设置。 +settings.preferences.editCustomExplorer.available=可用浏览器 +settings.preferences.editCustomExplorer.chosen=已选择的浏览器设置 +settings.preferences.editCustomExplorer.name=名称 +settings.preferences.editCustomExplorer.txUrl=交易 URL +settings.preferences.editCustomExplorer.addressUrl=地址 URL + +settings.net.btcHeader=比特币网络 +settings.net.p2pHeader=Bisq 网络 +settings.net.onionAddressLabel=我的匿名地址 +settings.net.btcNodesLabel=使用自定义比特币主节点 +settings.net.bitcoinPeersLabel=已连接节点 +settings.net.useTorForBtcJLabel=使用 Tor 连接比特币网络 +settings.net.bitcoinNodesLabel=需要连接比特币核心 +settings.net.useProvidedNodesRadio=使用公共比特币核心节点 +settings.net.usePublicNodesRadio=使用公共比特币网络 +settings.net.useCustomNodesRadio=使用自定义比特币主节点 +settings.net.warn.usePublicNodes=如果你使用公共比特币网络,你就会面临严重的隐私问题,这是由损坏的 bloom filter 设计和实现造成的,它适用于像 BitcoinJ 这样的 SPV 钱包(在 Bisq 中使用)。您所连接的任何完整节点都可以发现您的所有钱包地址都属于一个实体。\n\n详情请浏览: https://bisq.network/blog/privacy-in-bitsquare 。\n\n您确定要使用公共节点吗? +settings.net.warn.usePublicNodes.useProvided=不,使用给定的节点 +settings.net.warn.usePublicNodes.usePublic=使用公共网络 +settings.net.warn.useCustomNodes.B2XWarning=请确保您的比特币节点是一个可信的比特币核心节点!\n\n连接到不遵循比特币核心共识规则的节点可能会损坏您的钱包,并在交易过程中造成问题。\n\n连接到违反共识规则的节点的用户应对任何由此造成的损害负责。任何由此产生的纠纷都将有利于另一方。对于忽略此警告和保护机制的用户,不提供任何技术支持! +settings.net.warn.invalidBtcConfig=由于您的配置无效,无法连接至比特币网络。\n\n您的配置已经被重置为默认比特币节点。你需要重启 Bisq。 +settings.net.localhostBtcNodeInfo=背景信息:Bisq 在启动时会在本地查找比特币节点。如果有,Bisq 将只通过它与比特币网络进行通信。 +settings.net.p2PPeersLabel=已连接节点 +settings.net.onionAddressColumn=匿名地址 +settings.net.creationDateColumn=已建立连接 +settings.net.connectionTypeColumn=入/出 +settings.net.sentDataLabel=统计数据已发送 +settings.net.receivedDataLabel=统计数据已接收 +settings.net.chainHeightLabel=最新 BTC 区块高度 +settings.net.roundTripTimeColumn=延迟 +settings.net.sentBytesColumn=发送 +settings.net.receivedBytesColumn=接收 +settings.net.peerTypeColumn=节点类型 +settings.net.openTorSettingsButton=打开 Tor 设置 + +settings.net.versionColumn=版本 +settings.net.subVersionColumn=子版本 +settings.net.heightColumn=高度 + +settings.net.needRestart=您需要重启应用程序以同意这次变更。\n您需要现在重启吗? +settings.net.notKnownYet=至今未知... +settings.net.sentData=已发送数据 {0},{1} 条消息,{2} 条消息/秒 +settings.net.receivedData=已接收数据 {0},{1} 条消息,{2} 条消息/秒 +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=添加逗号分隔的 IP 地址及端口,如使用8333端口可不填写。 +settings.net.seedNode=种子节点 +settings.net.directPeer=节点(直连) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=节点 +settings.net.inbound=接收数据包 +settings.net.outbound=发送数据包 +settings.net.reSyncSPVChainLabel=重新同步 SPV 链 +settings.net.reSyncSPVChainButton=删除 SPV 链文件并重新同步 +settings.net.reSyncSPVSuccess=您确定要进行 SPV 重新同步?如果您继续,SPV 链文件将会在下一次启动前删除。\n\n重新启动后,可能需要一段时间才能与网络重新同步,只有重新同步完成后才会看到所有的交易。\n\n根据交易的数量和钱包账龄,重新同步可能会花费几个小时,并消耗100%的 CPU。不要打断这个过程,否则你会不断地重复它。 +settings.net.reSyncSPVAfterRestart=SPV 链文件已被删除。请耐心等待,与网络重新同步可能需要一段时间。 +settings.net.reSyncSPVAfterRestartCompleted=重新同步刚刚完成,请重启应用程序。 +settings.net.reSyncSPVFailed=无法删除 SPV 链文件。\n错误:{0} +setting.about.aboutBisq=关于 Bisq +setting.about.about=Bisq 是一款开源软件,它通过分散的对等网络促进了比特币与各国货币(以及其他加密货币)的交易,严格保护了用户隐私的方式。请到我们项目的网站阅读更多关于 Bisq 的信息。 +setting.about.web=Bisq 网站 +setting.about.code=源代码 +setting.about.agpl=AGPL 协议 +setting.about.support=支持 Bisq +setting.about.def=Bisq 不是一个公司,而是一个社区项目,开放参与。如果您想参与或支持 Bisq,请点击下面连接。 +setting.about.contribute=贡献 +setting.about.providers=数据提供商 +setting.about.apisWithFee=Bisq 使用 Bisq 价格指数来表示法币与虚拟货币的市场价格,并使用 Bisq 内存池节点来估算采矿费。 +setting.about.apis=Bisq 使用 Bisq 价格指数来表示法币与数字货币的市场价格。 +setting.about.pricesProvided=交易所价格提供商 +setting.about.feeEstimation.label=矿工手续费估算提供商 +setting.about.versionDetails=版本详情 +setting.about.version=应用程序版本 +setting.about.subsystems.label=子系统版本 +setting.about.subsystems.val=网络版本:{0};P2P 消息版本:{1};本地数据库版本:{2};交易协议版本:{3} + +setting.about.shortcuts=快捷键 +setting.about.shortcuts.ctrlOrAltOrCmd=“Ctrl + {0}”或“alt + {0}”或“cmd + {0}” + +setting.about.shortcuts.menuNav=主页面 +setting.about.shortcuts.menuNav.value=使用“Ctrl”或“Alt”或“cmd” + 数字键“1-9”来切换不同的主页面 + +setting.about.shortcuts.close=关闭 Bisq +setting.about.shortcuts.close.value=“Ctrl + {0}”或“cmd + {0}”或“Ctrl + {1}”或“cmd + {1}” + +setting.about.shortcuts.closePopup=关闭弹窗以及对话框 +setting.about.shortcuts.closePopup.value=‘释放’ 键 + +setting.about.shortcuts.chatSendMsg=发送信息到交易伙伴 +setting.about.shortcuts.chatSendMsg.value=“Ctrl + ENTER”或“alt + ENTER”或“cmd + ENTER” + +setting.about.shortcuts.openDispute=创建纠纷 +setting.about.shortcuts.openDispute.value=选择未完成交易并点击:{0} + +setting.about.shortcuts.walletDetails=打开钱包详情窗口 + +setting.about.shortcuts.openEmergencyBtcWalletTool=打开应急 BTC 钱包工具 + +setting.about.shortcuts.openEmergencyBsqWalletTool=打开应急 BSQ 钱包工具 + +setting.about.shortcuts.showTorLogs=在 DEBUG 与 WARN 之间切换 Tor 日志等级 + +setting.about.shortcuts.manualPayoutTxWindow=打开窗口手动支付双重验证存款交易 + +setting.about.shortcuts.reRepublishAllGovernanceData=重新推送 DAO 众议厅数据(包括提案以及投票) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=注册仲裁员(仅限调解员/仲裁员) +setting.about.shortcuts.registerArbitrator.value=切换至账户页面并按下:{0} + +setting.about.shortcuts.registerMediator=注册调解员(仅限调解员/仲裁员) +setting.about.shortcuts.registerMediator.value=切换至账户页面并按下:{0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=打开账龄验证窗口(仅限仲裁员) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=切换至仲裁页面并按下:{0} + +setting.about.shortcuts.sendAlertMsg=发送警报或更新消息(需要权限) + +setting.about.shortcuts.sendFilter=设置过滤器(需要权限) + +setting.about.shortcuts.sendPrivateNotification=发送私人通知到对等点(需要权限) +setting.about.shortcuts.sendPrivateNotification.value=点击交易伙伴头像并按下:{0} 以显示更多信息 + +setting.info.headline=新 XMR 自动确认功能 +setting.info.msg=当你完成 BTC/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Bisq 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Bisq 使用由 Bisq 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置''中将每笔交易的最大 BTC 数量设置为自动确认以及所需确认的数量。\n\n在 Bisq Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://bisq.wiki/Trading_Monero#Auto-confirming_trades +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=调解员注册 +account.tab.refundAgentRegistration=退款助理注册 +account.tab.signing=验证中 +account.info.headline=欢迎来到 Bisq 账户 +account.info.msg=在这里你可以设置交易账户的法定货币及数字货币,选择仲裁员和备份你的钱包及账户数据。\n\n当你开始运行 Bisq 就已经创建了一个空的比特币钱包。\n\n我们建议你在充值之前写下你比特币钱包的还原密钥(在左边的列表)和考虑添加密码。在“资金”选项中管理比特币存入和提现。\n\n隐私 & 安全:\nBisq 是一个去中心化的交易所 – 意味着您的所有数据都保存在您的电脑上,没有服务器,我们无法访问您的个人信息,您的资金,甚至您的 IP 地址。如银行账号、数字货币、比特币地址等数据只分享给与您交易的人,以实现您发起的交易(如果有争议,仲裁员将会看到您的交易数据)。 + +account.menu.paymentAccount=法定货币账户 +account.menu.altCoinsAccountView=数字货币账户 +account.menu.password=钱包密码 +account.menu.seedWords=钱包密钥 +account.menu.walletInfo=钱包信息 +account.menu.backup=备份 +account.menu.notifications=通知 + +account.menu.walletInfo.balance.headLine=钱包余额 +account.menu.walletInfo.balance.info=这里包括内部钱包余额包括未确认交易。\n对于 BTC,下方显示的内部钱包的余额将会是窗口右上方的“可用”与“保留”余额的总和。 +account.menu.walletInfo.xpub.headLine=监控密钥(xpub keys) +account.menu.walletInfo.walletSelector={0} {1} 钱包 +account.menu.walletInfo.path.headLine=HD 密钥链路径 +account.menu.walletInfo.path.info=如果您导入其他钱包(例如 Electrum)的种子词,你需要去确认路径。这个操作只能用于你失去 Bisq 钱包和数据目录的控制的紧急情况。\n请记住使用非 Bisq 钱包的资金可能会打乱 Bisq 内部与之相连的钱包数据结构,这可能导致交易失败。\n\n请不要将 BSQ 发送至非 Bisq 钱包,因为这可能让您的 BSQ 交易记录失效以及损失 BSQ. + +account.menu.walletInfo.openDetails=显示原始钱包详情与私钥 + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=公钥 + +account.arbitratorRegistration.register=注册 +account.arbitratorRegistration.registration={0} 注册 +account.arbitratorRegistration.revoke=撤销 +account.arbitratorRegistration.info.msg=请注意,撤销后需要保留15天,因为可能有交易正在以你作为 {0}。最大允许的交易期限为8天,纠纷过程最多可能需要7天。 +account.arbitratorRegistration.warn.min1Language=您需要设置至少1种语言。\n我们已经为您添加了默认语言。 +account.arbitratorRegistration.removedSuccess=您已从 Bisq 网络成功删除仲裁员注册信息。 +account.arbitratorRegistration.removedFailed=无法删除仲裁员。{0} +account.arbitratorRegistration.registerSuccess=您已从 Bisq 网络成功注册您的仲裁员。 +account.arbitratorRegistration.registerFailed=无法注册仲裁员。{0} + +account.altcoin.yourAltcoinAccounts=您的数字货币账户 +account.altcoin.popup.wallet.msg=请确保您按照 {1} 网页上所述使用 {0} 钱包的要求。\n使用集中式交易所的钱包,您无法控制密钥或使用不兼容的钱包软件,可能会导致交易资金的流失!\n调解员或仲裁员不是 {2} 专家,在这种情况下不能帮助。 +account.altcoin.popup.wallet.confirm=我了解并确定我知道我需要哪种钱包。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=在 Bisq 上交易 UPX 需要您了解并满足以下要求:\n\n要发送 UPX ,您需要使用官方的 UPXmA GUI 钱包或启用 store-tx-info 标志的 UPXmA CLI 钱包(在新版本中是默认的)。请确保您可以访问Tx密钥,因为在纠纷状态时需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\n在普通的区块链浏览器中,这种交易是不可验证的。\n\n如有纠纷,你须向仲裁员提供下列资料:\n \n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,UPX 发送方负责向仲裁员提供 UPX 转账的验证。\n\n不需要支付 ID,只需要普通的公共地址。\n \n如果您对该流程不确定,请访问 UPXmA Discord 频道(https://discord.gg/vhdNSrV)或 Telegram 交流群(https://t.me/uplexaOfficial)了解更多信息。\n\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=在 Bisq 上交易 ARQ 需要您了解并满足以下要求:\n\n要发送 ARQ ,您需要使用官方的 ArQmA GUI 钱包或启用 store-tx-info 标志的 ArQmA CLI 钱包(在新版本中是默认的)。请确保您可以访问Tx密钥,因为在纠纷状态时需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\n在普通的区块链浏览器中,这种交易是不可验证的。\n\n如有纠纷,你须向调解员或仲裁员提供下列资料:\n\n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,ARQ 发送方负责向调解员或仲裁员提供 ARQ 转账的验证。\n\n不需要交易 ID,只需要普通的公共地址。\n\n如果您对该流程不确定,请访问 ArQmA Discord 频道(https://discord.gg/s9BQpJT)或 ArQmA 论坛(https://labs.arqma.com)了解更多信息。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=在 Bisq 上交易 XMR 需要你理解并满足以下要求。\n\n如果您出售 XMR,当您在纠纷中您必须要提供下列信息给调解员或仲裁员:\n- 交易密钥(Tx 公钥,Tx密钥,Tx私钥)\n- 交易 ID(Tx ID 或 Tx 哈希)\n- 交易目标地址(接收者地址)\n\n在 wiki 中查看更多关于 Monero 钱包的信息:\nhttps://bisq.wiki/Trading_Monero#Proving_payments\n\n如未能提供要求的交易数据将在纠纷中直接判负\n\n还要注意,Bisq 现在提供了自动确认 XMR 交易的功能,以使交易更快,但是您需要在设置中启用它。\n\n有关自动确认功能的更多信息,请参见 Wiki:\nhttps://bisq.wiki/Trading_Monero#Auto-confirming_trades +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=区块链浏览器在 Bisq 上交易 XMR 需要您了解并满足以下要求:\n\n发送MSR时,您需要使用官方的 Masari GUI 钱包、启用store-tx-info标记的Masari CLI钱包(默认启用)或Masari 网页钱包(https://wallet.getmasari.org)。请确保您可以访问的 tx 密钥,因为如果发生纠纷这是需要的。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\nMasari 网页钱包(前往 帐户->交易历史和查看您发送的交易细节)\n\n验证可以在钱包中完成。\nmonero-wallet-cli:使用命令(check_tx_key)。\nmonero-wallet-gui:在高级>证明/检查页面\n验证可以在区块浏览器中完成\n打开区块浏览器(https://explorer.getmasari.org),使用搜索栏查找您的事务哈希。\n一旦找到交易,滚动到底部的“证明发送”区域,并填写所需的详细信息。\n如有纠纷,你须向调解员或仲裁员提供下列资料:\n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n不需要交易 ID,只需要正常的公共地址。\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,XMR 发送方负责向调解员或仲裁员提供 XMR 转账的验证。\n\n如果您对该流程不确定,请访问官方的 Masari Discord(https://discord.gg/sMCwMqs)上寻求帮助。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=在 Bisq 上交易 BLUR 需要你了解并满足以下要求:\n\n要发送匿名信息你必须使用匿名网络 CLI 或 GUI 钱包。\n如果您正在使用 CLI 钱包,在传输发送后将显示交易哈希(tx ID)。您必须保存此信息。在发送传输之后,您必须立即使用“get_tx_key”命令来检索交易私钥。如果未能执行此步骤,以后可能无法检索密钥。\n\n如果您使用 Blur Network GUI 钱包,可以在“历史”选项卡中方便地找到交易私钥和交易 ID。发送后立即定位感兴趣的交易。单击包含交易的框的右下角的“?”符号。您必须保存此信息。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1.)交易ID,2.)交易私钥,3.)收件人地址。调解或仲裁程序将使用 BLUR 事务查看器(https://blur.cash/#tx-viewer)验证 BLUR 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有争议的情况下,匿名发送方承担100%的责任来向调解员或仲裁员核实交易。\n\n如果你不了解这些要求,不要在 Bisq 上交易。首先,在 Blur Network Discord 中寻求帮助(https://discord.gg/dMWaqVW)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=在 Bisq 上交易 Solo 需要您了解并满足以下要求:\n\n要发送 Solo,您必须使用 Solo CLI 网络钱包版本 5.1.3 或更高。\n\n如果您使用的是CLI钱包,则在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,您必须立即使用'get_tx_key'命令来检索交易密钥。如果未能执行此步骤,则以后可能无法检索密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1)交易 ID,、2)交易密钥,3)收件人的地址。调解员或仲裁员将使用 Solo 区块资源管理器(https://explorer.Solo.org)搜索交易然后使用“发送证明”功能(https://explorer.minesolo.com/)\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,QWC 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Bisq 上交易。首先,在 Solo Discord 中寻求帮助(https://discord.minesolo.com/)。\n\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=在 Bisq 上交易 CASH2 需要您了解并满足以下要求:\n\n要发送 CASH2,您必须使用 CASH2 钱包版本 3 或更高。\n\n在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,必须立即在 simplewallet 中使用命令“getTxKey”来检索交易密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1)交易 ID,2)交易密钥,3)收件人的 CASH2 地址。调解员或仲裁员将使用 CASH2 区块资源管理器(https://blocks.cash2.org)验证 CASH2 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,CASH2 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Bisq 上交易。首先,在 Cash2 Discord 中寻求帮助(https://discord.gg/FGfXAYN)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=在 Bisq 上交易 Qwertycoin 需要您了解并满足以下要求:\n\n要发送 Qwertycoin,您必须使用 Qwertycoin 钱包版本 5.1.3 或更高。\n\n在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,必须立即在 simplewallet 中使用命令“get_Tx_Key”来检索交易密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息::1)交易 ID,、2)交易密钥,3)收件人的 QWC 地址。调解员或仲裁员将使用 QWC 区块资源管理器(https://explorer.qwertycoin.org)验证 QWC 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,QWC 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Bisq 上交易。首先,在 QWC Discord 中寻求帮助(https://discord.gg/rUkfnpC)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=在 Bisq 上交易 Dragonglass 需要您了解并满足以下要求:\n\n由于 Dragonglass 提供了隐私保护,所以交易不能在公共区块链上验证。如果需要,您可以通过使用您的 TXN-Private-Key 来证明您的付款。\nTXN-Private 密匙是自动生成的一次性密匙,用于只能从 DRGL 钱包中访问的每个交易。\n要么通过 DRGL-wallet GUI(内部交易细节对话框),要么通过 Dragonglass CLI simplewallet(使用命令“get_tx_key”)。\n\n两者都需要 DRGL 版本的“Oathkeeper”或更高版本。\n\n如有争议,你必须向调解员或仲裁员提供下列资料:\n\n- txn-Privite-ket\n- 交易哈希 \n- 接收者的公开地址\n\n付款验证可以使用上面的数据作为输入(http://drgl.info/#check_txn)。\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。Dragonglass 发送方负责在发生争议时向调解员或仲裁员提供 DRGL 转账的验证。不需要使用付款 ID。\n\n如果您对这个过程的任何部分都不确定,请访问(http://discord.drgl.info)上的 Dragonglass 寻求帮助。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=当使用 Zcash 时,您只能使用透明地址(以 t 开头),而不能使用 z 地址(私有),因为调解员或仲裁员无法使用 z 地址验证交易。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=在使用 Zcoin 时,您只能使用透明的(可跟踪的)地址,而不能使用不可跟踪的地址,因为调解员或仲裁员无法在区块资源管理器中使用不可跟踪的地址验证交易。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN 需要发送方和接收方之间的交互过程来创建交易。请确保遵循 GRIN 项目网页中的说明,以可靠地发送和接收 GRIN(接收方需要在线,或至少在一定时间内在线)。\n \nBisq 只支持 Grinbox(Wallet713)钱包 URL 格式。\n\nGRIN 发送者需要提供他们已成功发送 GRIN 的证明。如果钱包不能提供证明,一个潜在的纠纷将被解决,有利于露齿微笑的接受者。请确保您使用了最新的支持交易证明的 Grinbox 软件,并且您了解传输和接收 GRIN 的过程以及如何创建证明。\n请参阅 https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only,以获得关于 Grinbox 证明工具的更多信息。\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM 需要发送方和接收方之间的交互过程来创建交易。\n\n\n确保遵循 BEAM 项目网页的指示可靠地发送和接收 BEAM(接收方需要在线,或者至少在一定的时间范围内在线)。\n\nBEAM 发送者需要提供他们成功发送 BEAM 的证明。一定要使用钱包软件,可以产生这样的证明。如果钱包不能提供证据,一个潜在的纠纷将得到解决,有利于 BEAM 接收者。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=在 Bisq 上交易 ParsiCoin 需要您了解并满足以下要求:\n\n要发送 PARS ,您必须使用官方 ParsiCoin Wallet 版本 3.0.0 或更高。\n\n您可以在 GUI 钱包(ParsiPay)的交易部分检查您的交易哈希和交易键,您需要右键单击“交易”,然后单击“显示详情”。\n\n如果仲裁是 100% 必要的,您必须向调解员或仲裁员提供以下内容:1)交易哈希,2)交易密钥,以及3)接收方的 PARS 地址。调解员或仲裁员将使用 ParsiCoin 区块链浏览器 (http://explorer.parsicoin.net/#check_payment)验证 PARS 传输。\n\n如果你不了解这些要求,不要在 Bisq 上交易。首先,在 ParsiCoin Discord 寻求帮助(https://discord.gg/c7qmFNh)。 + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=要交易烧毁的货币,你需要知道以下几点:\n\n烧毁的货币是不能花的。要在 Bisq 上交易它们,输出脚本需要采用以下形式:OP_RETURN OP_PUSHDATA,后跟相关的数据字节,这些字节经过十六进制编码后构成地址。例如,地址为666f6f(在UTF-8中的"foo")的烧毁的货币将有以下脚本:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\n要创建烧毁的货币,您可以使用“烧毁”RPC命令,它在一些钱包可用。\n\n对于可能的情况,可以查看 https://ibo.laboratorium.ee\n\n因为烧毁的货币是不能用的,所以不能重新出售。“出售”烧毁的货币意味着焚烧初始的货币(与目的地地址相关联的数据)。\n\n如果发生争议,BLK 卖方需要提供交易哈希。 + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=在 Bisq 上交易 L-BTC 你必须理解下述条款:\n\n当你在 Bisq 上接受 L-BTC 交易时,你不能使用手机 Blockstream Green Wallet 或者是一个托管/交易钱包。你必须只接收 L-BTC 到 Liquid Elements Core 钱包,或另一个 L-BTC 钱包且允许你获得匿名的 L-BTC 地址以及密钥。\n\n在需要进行调解的情况下,或者如果发生了交易纠纷,您必须将接收 L-BTC地址的安全密钥披露给 Bisq 调解员或退款代理,以便他们能够在他们自己的 Elements Core 全节点上验证您的匿名交易的细节。\n\n如果你不了解或了解这些要求,不要在 Bisq 上交易 L-BTC。 + +account.fiat.yourFiatAccounts=您的法定货币账户 + +account.backup.title=备份钱包 +account.backup.location=备份路径 +account.backup.selectLocation=选择备份路径 +account.backup.backupNow=立即备份(备份没有被加密!) +account.backup.appDir=应用程序数据目录 +account.backup.openDirectory=打开目录 +account.backup.openLogFile=打开日志文件 +account.backup.success=备份成功保存在:\n{0} +account.backup.directoryNotAccessible=您没有访问选择的目录的权限。 {0} + +account.password.removePw.button=移除密码 +account.password.removePw.headline=移除钱包的密码保护 +account.password.setPw.button=设置密码 +account.password.setPw.headline=设置钱包的密码保护 +account.password.info=使用密码保护,您需要在将比特币从钱包中取出时输入密码,或者要从还原密钥和应用程序启动时查看或恢复钱包时输入密码。 + +account.seed.backup.title=备份您的钱包还原密钥 +account.seed.info=请写下钱包还原密钥和时间!\n您可以通过还原密钥和时间在任何时候恢复您的钱包。\n还原密钥用于 BTC 和 BSQ 钱包。\n\n您应该在一张纸上写下还原密钥并且不要保存它们在您的电脑上。\n请注意还原密钥并不能代替备份。\n您需要备份完整的应用程序目录在”账户/备份“界面去恢复有效的应用程序状态和数据。 +account.seed.backup.warning=请注意种子词不能替代备份。您需要为整个应用目录(在“账户/备份”选项卡中)以恢复应用状态以及数据。\n导入种子词仅在紧急情况时才推荐使用。 如果没有正确备份数据库文件和密钥,该应用程序将无法运行!\n\n在 Bisq wiki 中查看更多信息: https://bisq.wiki/Backing_up_application_data +account.seed.warn.noPw.msg=您还没有设置一个可以保护还原密钥显示的钱包密码。\n\n要显示还原密钥吗? +account.seed.warn.noPw.yes=是的,不要再问我 +account.seed.enterPw=输入密码查看还原密钥 +account.seed.restore.info=请在应用还原密钥还原之前进行备份。请注意,钱包还原仅用于紧急情况,可能会导致内部钱包数据库出现问题。\n这不是应用备份的方法!请使用应用程序数据目录中的备份来恢复以前的应用程序状态。\n恢复后,应用程序将自动关闭。重新启动应用程序后,它将重新与比特币网络同步。这可能需要一段时间,并且会消耗大量CPU,特别是在钱包较旧且有很多交易的情况下。请避免中断该进程,否则可能需要再次删除 SPV 链文件或重复还原过程。 +account.seed.restore.ok=好的,立即执行回复并且关闭 Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=安装 +account.notifications.download.label=下载手机应用 +account.notifications.waitingForWebCam=等待网络摄像头... +account.notifications.webCamWindow.headline=用手机扫描二维码 +account.notifications.webcam.label=使用网络摄像头 +account.notifications.webcam.button=扫描二维码 +account.notifications.noWebcam.button=我没有网络摄像头 +account.notifications.erase.label=在手机上清除通知 +account.notifications.erase.title=清除通知 +account.notifications.email.label=验证码 +account.notifications.email.prompt=输入您通过电子邮件收到的验证码 +account.notifications.settings.title=设置 +account.notifications.useSound.label=在手机上播放提示声音 +account.notifications.trade.label=接收交易信息 +account.notifications.market.label=接收报价提醒 +account.notifications.price.label=接收价格提醒 +account.notifications.priceAlert.title=价格提醒 +account.notifications.priceAlert.high.label=提醒条件:当 BTC 价格高于 +account.notifications.priceAlert.low.label=提醒条件:当 BTC 价格低于 +account.notifications.priceAlert.setButton=设置价格提醒 +account.notifications.priceAlert.removeButton=取消价格提醒 +account.notifications.trade.message.title=交易状态已变更 +account.notifications.trade.message.msg.conf=ID 为 {0} 的交易的存款交易已被确认。请打开您的 Bisq 应用程序并开始付款。 +account.notifications.trade.message.msg.started=BTC 买家已经开始支付 ID 为 {0} 的交易。 +account.notifications.trade.message.msg.completed=ID 为 {0} 的交易已完成。 +account.notifications.offer.message.title=您的报价已被接受 +account.notifications.offer.message.msg=您的 ID 为 {0} 的报价已被接受 +account.notifications.dispute.message.title=新的纠纷消息 +account.notifications.dispute.message.msg=您收到了一个 ID 为 {0} 的交易纠纷消息 + +account.notifications.marketAlert.title=报价提醒 +account.notifications.marketAlert.selectPaymentAccount=提供匹配的付款帐户 +account.notifications.marketAlert.offerType.label=我感兴趣的报价类型 +account.notifications.marketAlert.offerType.buy=买入报价(我想要出售 BTC ) +account.notifications.marketAlert.offerType.sell=卖出报价(我想要购买 BTC ) +account.notifications.marketAlert.trigger=报价距离(%) +account.notifications.marketAlert.trigger.info=设置价格区间后,只有当满足(或超过)您的需求的报价发布时,您才会收到提醒。您想卖 BTC ,但你只能以当前市价的 2% 溢价出售。将此字段设置为 2% 将确保您只收到高于当前市场价格 2%(或更多)的报价的提醒。 +account.notifications.marketAlert.trigger.prompt=与市场价格的百分比距离(例如 2.50%, -0.50% 等) +account.notifications.marketAlert.addButton=添加报价提醒 +account.notifications.marketAlert.manageAlertsButton=管理报价提醒 +account.notifications.marketAlert.manageAlerts.title=管理报价提醒 +account.notifications.marketAlert.manageAlerts.header.paymentAccount=支付账户 +account.notifications.marketAlert.manageAlerts.header.trigger=触发价格 +account.notifications.marketAlert.manageAlerts.header.offerType=报价类型 +account.notifications.marketAlert.message.title=报价提醒 +account.notifications.marketAlert.message.msg.below=低于 +account.notifications.marketAlert.message.msg.above=高于 +account.notifications.marketAlert.message.msg=价格为 {2}({3} {4}市场价)和支付方式为 {5} 的报价 {0} {1} 已发布到 Bisq 报价列表。\n报价ID: {6}。 +account.notifications.priceAlert.message.title=价格提醒 {0} +account.notifications.priceAlert.message.msg=您的价格提醒已被触发。当前 {0} 的价格为 {1} {2} +account.notifications.noWebCamFound.warning=未找到网络摄像头。\n\n请使用电子邮件选项将代码和加密密钥从您的手机发送到 Bisq 应用程序。 +account.notifications.priceAlert.warning.highPriceTooLow=较高的价格必须大于较低的价格。 +account.notifications.priceAlert.warning.lowerPriceTooHigh=较低的价格必须低于较高的价格。 + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=确切消息 +dao.tab.bsqWallet=BSQ 钱包 +dao.tab.proposals=管理 +dao.tab.bonding=关系 +dao.tab.proofOfBurn=资产清单挂牌费/烧毁证明 +dao.tab.monitor=网络监视器 +dao.tab.news=新闻 + +dao.paidWithBsq=已用 BSQ 支付 +dao.availableBsqBalance=可用于支出(已验证的+未确认的变更输出) +dao.verifiedBsqBalance=所有已验证的 UTXO 余额 +dao.unconfirmedChangeBalance=已验证的+未确认的变更输出 +dao.unverifiedBsqBalance=所有未验证交易的余额(等待区块确认) +dao.lockedForVoteBalance=用于投票 +dao.lockedInBonds=冻结余额 +dao.availableNonBsqBalance=可用的非 BSQ 余额(BTC) +dao.reputationBalance=声望值(不会花费) + +dao.tx.published.success=你的交易已经成功发布 +dao.proposal.menuItem.make=创建要求 +dao.proposal.menuItem.browse=浏览开启的报偿申请 +dao.proposal.menuItem.vote=为报偿申请投票 +dao.proposal.menuItem.result=投票结果 +dao.cycle.headline=投票周期 +dao.cycle.overview.headline=投票周期总览 +dao.cycle.currentPhase=现阶段 +dao.cycle.currentBlockHeight=当前区块高度: +dao.cycle.proposal=提议阶段 +dao.cycle.proposal.next=下一个提议阶段 +dao.cycle.blindVote=匿名投票阶段 +dao.cycle.voteReveal=投票公示阶段 +dao.cycle.voteResult=投票结果 +dao.cycle.phaseDuration=区块 {0} (≈{1});区块 {2} - {3})(≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=区块 {0} - {1} (≈{2} - ≈{3} ) + +dao.voteReveal.txPublished.headLine=投票公示交易发布 +dao.voteReveal.txPublished=你的交易 ID 为 {0} 投票公示交易已经成功发布。\n\n如果您已经参与了 DAO 投票,那么这将由软件自动完成。 + +dao.results.cycles.header=周期 +dao.results.cycles.table.header.cycle=周期 +dao.results.cycles.table.header.numProposals=请求 +dao.results.cycles.table.header.voteWeight=投票权重 +dao.results.cycles.table.header.issuance=发行 + +dao.results.results.table.item.cycle=周期 {0} 开始于:{1} + +dao.results.proposals.header=选定周期的请求 +dao.results.proposals.table.header.nameLink=名称/链接 +dao.results.proposals.table.header.details=详情 +dao.results.proposals.table.header.myVote=我的投票 +dao.results.proposals.table.header.result=投票结果 +dao.results.proposals.table.header.threshold=阈值 +dao.results.proposals.table.header.quorum=法定人数 + +dao.results.proposals.voting.detail.header=选定提案的投票结果 + +dao.results.exceptions=投票结果异常 + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=未定义 + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQ 挂单费 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQ 买单费 +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=最小 BSQ 挂单费 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=最小 BSQ 买单费 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTC 挂单费 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTC 买单费 +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=最小 BTC 挂单费 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=最小 BTC 买单费 +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=BSQ 提案手续费 +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=BSQ 投票手续费 + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=最低 BSQ 报偿申请数量 +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=最高 BSQ 报偿申请数量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=最低 BSQ 退还申请数量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=最高 BSQ 退还申请数量 + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=BSQ 要求的一般提案的仲裁人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=BSQ 要求的报偿申请的仲裁人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=BSQ 要求的退还申请的仲裁人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=BSQ 要求的改变参数的仲裁人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=BSQ 要求的移除资产要求的人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=BSQ 要求的没收申请的仲裁人数 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=BSQ 要求的绑定角色的仲裁人数 + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=普通提案的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=报偿申请的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=退还申请的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=改变参数的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=移除资产的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=没收申请的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=担保角色的要求百分比 + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=接收方 BTC 地址 + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=资产清单挂牌费每日支付 +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=最小资产交易量 + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=其他交易支出tx的锁定时间 +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=BTC 仲裁费 + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=最高 BTC 交易限额 + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=担保角色对 BSQ 的影响 +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=每个周期的 BSQ 发行限额 + +dao.param.currentValue=当前值:{0} +dao.param.currentAndPastValue=当前余额:{0}(提案时的余额:{1}) +dao.param.blocks={0} 区块 + +dao.results.invalidVotes=在那个投票周期中,我们有无效的投票。如果投票没有在 Bisq 网络中很好地分布,就会发生这种情况。\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=提议阶段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=休息1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=匿名投票阶段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=休息2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=投票公示阶段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=休息3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=结果阶段 + +dao.results.votes.table.header.stakeAndMerit=投票权重 +dao.results.votes.table.header.stake=份额 +dao.results.votes.table.header.merit=获得的 +dao.results.votes.table.header.vote=投票 + +dao.bond.menuItem.bondedRoles=担保角色 +dao.bond.menuItem.reputation=担保的名誉 +dao.bond.menuItem.bonds=担保 + +dao.bond.dashboard.bondsHeadline=担保式 BSQ +dao.bond.dashboard.lockupAmount=锁定资金 +dao.bond.dashboard.unlockingAmount=正在锁定资金(等到锁定时间过后) + + +dao.bond.reputation.header=为名誉锁定担保 +dao.bond.reputation.table.header=我的名誉担保 +dao.bond.reputation.amount=锁定的 BSQ 数量 +dao.bond.reputation.time=在区块中的解锁时间 +dao.bond.reputation.salt=盐 +dao.bond.reputation.hash=哈希 +dao.bond.reputation.lockupButton=锁定 +dao.bond.reputation.lockup.headline=确认锁定交易 +dao.bond.reputation.lockup.details=锁定数量:{0}\n解锁时间:{1} 区块(≈{2})\n\n矿工手续费:{3}({4} 聪/字节)\n交易大小:{5} 字节\n\n你确定想要继续? +dao.bond.reputation.unlock.headline=确认解锁交易 +dao.bond.reputation.unlock.details=解锁金额:{0}\n解锁时间:{1} 区块(≈{2})\n\n挖矿手续费:{3}({4} 聪/Byte)\n交易大小:{5} Kb\n\n你想继续这个操作吗? + +dao.bond.allBonds.header=所有担保 + +dao.bond.bondedReputation=担保的名誉 +dao.bond.bondedRoles=担保角色 + +dao.bond.details.header=交易方详情 +dao.bond.details.role=角色 +dao.bond.details.requiredBond=需要的 BSQ 担保 +dao.bond.details.unlockTime=在区块中的解锁时间 +dao.bond.details.link=链接到交易方描述 +dao.bond.details.isSingleton=是否可由多个交易方担任 +dao.bond.details.blocks={0} 区块 + +dao.bond.table.column.name=名称 +dao.bond.table.column.link=绑定 +dao.bond.table.column.bondType=连接类型 +dao.bond.table.column.details=详情 +dao.bond.table.column.lockupTxId=锁定 Tx ID +dao.bond.table.column.bondState=连接状态 +dao.bond.table.column.lockTime=解锁时间 +dao.bond.table.column.lockupDate=锁定日期 + +dao.bond.table.button.lockup=锁定 +dao.bond.table.button.unlock=解锁 +dao.bond.table.button.revoke=撤销 + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=尚未担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=等待锁定 +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=锁定的担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=等待解锁 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=解锁 Tx 确认 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=正在解锁担保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=担保已解锁 +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=没收的担保 + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=担保角色 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=担保名誉 + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Github 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=论坛管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase 管理员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq 运维人员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-fork 运维人员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer 运维人员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=网站运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=论坛运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=种子节点运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=价格节点运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=比特币节点运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=交易所运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=浏览器运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=移动通知中继运营者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=域名持有者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS 管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=调解员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=仲裁员 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC 赞助地址所有者 + +dao.burnBsq.assetFee=资产清单 +dao.burnBsq.menuItem.assetFee=资产清单挂牌费 +dao.burnBsq.menuItem.proofOfBurn=烧毁证明 +dao.burnBsq.header=资产清单挂牌费 +dao.burnBsq.selectAsset=选择资产 +dao.burnBsq.fee=手续费 +dao.burnBsq.trialPeriod=试用期 +dao.burnBsq.payFee=支付手续费 +dao.burnBsq.allAssets=所有资产 +dao.burnBsq.assets.nameAndCode=资产名称 +dao.burnBsq.assets.state=状态 +dao.burnBsq.assets.tradeVolume=交易量 +dao.burnBsq.assets.lookBackPeriod=确认期 +dao.burnBsq.assets.trialFee=试用期手续费 +dao.burnBsq.assets.totalFee=总共已支付费用 +dao.burnBsq.assets.days={0} 天 +dao.burnBsq.assets.toFewDays=资产清单挂牌费过低。在试用期中最低数量为 {0}。 + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=在试用期 +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=活跃的交易 +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=因不活跃而被取消 +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=移除投票 + +dao.proofOfBurn.header=烧毁证明 +dao.proofOfBurn.amount=数量 +dao.proofOfBurn.preImage=预览 +dao.proofOfBurn.burn=烧毁 +dao.proofOfBurn.allTxs=所有烧毁证明交易 +dao.proofOfBurn.myItems=我的烧毁证明交易 +dao.proofOfBurn.date=日期 +dao.proofOfBurn.hash=哈希 +dao.proofOfBurn.txs=交易记录 +dao.proofOfBurn.pubKey=公钥 +dao.proofOfBurn.signature.window.title=使用烧毁证明交易中的密钥验证消息 +dao.proofOfBurn.verify.window.title=使用烧毁证明交易中的密钥确认消息 +dao.proofOfBurn.copySig=将验证复制到剪贴板 +dao.proofOfBurn.sign=验证 +dao.proofOfBurn.message=消息 +dao.proofOfBurn.sig=验证 +dao.proofOfBurn.verify=确认 +dao.proofOfBurn.verificationResult.ok=确认成功 +dao.proofOfBurn.verificationResult.failed=确认失败 + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=提议阶段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=匿名投票前的休息阶段 +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=匿名投票阶段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=投票公示前的休息阶段 +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=投票公示阶段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=公布结果前的休息阶段 +# suppress inspection "UnusedProperty" +dao.phase.RESULT=投票结果阶段 + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=提议阶段 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=匿名投票 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=投票公示 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=投票结果 + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=报偿申请 +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=退还申请 +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=担保角色的提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=移除资产提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=修改参数的提议 +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=一般提议 +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=没收担保请求 + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=报偿申请 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=退还申请 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=担保角色 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=移除一个数字货币 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=修改参数 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=一般提议 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=没收担保 + +dao.proposal.details=提议细节 +dao.proposal.selectedProposal=选定赔偿要求 +dao.proposal.active.header=当前周期的提案 +dao.proposal.active.remove.confirm=您确定要移除该提案吗?\n如果您删除该提案,提案费将会丢失。 +dao.proposal.active.remove.doRemove=是,移除我的提案 +dao.proposal.active.remove.failed=不能移除提案。 +dao.proposal.myVote.title=投票 +dao.proposal.myVote.accept=接受提议 +dao.proposal.myVote.reject=拒绝提案 +dao.proposal.myVote.removeMyVote=忽略提案 +dao.proposal.myVote.merit=所得的 BSQ 的投票权重 +dao.proposal.myVote.stake=Stake 的投票权重 +dao.proposal.myVote.revealTxId=投票公示的交易 ID +dao.proposal.myVote.stake.prompt=在投票中最大可用份额:{0} +dao.proposal.votes.header=设置投票的份额,并发布您的投票 +dao.proposal.myVote.button=发布投票 +dao.proposal.myVote.setStake.description=在对所有提案进行投票后,您必须锁定BSQ来设置投票的份额。您锁定的 BSQ 越多,你的投票权重就越大。\n\n投票会锁定 BSQ 将在投票显示阶段再次解锁。 +dao.proposal.create.selectProposalType=选择提案类型 +dao.proposal.create.phase.inactive=请等到下一个提案阶段 +dao.proposal.create.proposalType=提议类型 +dao.proposal.create.new=创建新的赔偿要求 +dao.proposal.create.button=创建赔偿要求 +dao.proposal.create.publish=发布提案 +dao.proposal.create.publishing=正在发布提案中... +dao.proposal=提案 +dao.proposal.display.type=提议类型 +dao.proposal.display.name=确切的 GitHub 的用户名 +dao.proposal.display.link=详情的链接 +dao.proposal.display.link.prompt=提案的链接 +dao.proposal.display.requestedBsq=申请的 BSQ 数量 +dao.proposal.display.txId=提案交易 ID +dao.proposal.display.proposalFee=提案手续费 +dao.proposal.display.myVote=我的投票 +dao.proposal.display.voteResult=投票结果总结 +dao.proposal.display.bondedRoleComboBox.label=担保角色类型 +dao.proposal.display.requiredBondForRole.label=角色需要的担保 +dao.proposal.display.option=选项 + +dao.proposal.table.header.proposalType=提议类型 +dao.proposal.table.header.link=绑定 +dao.proposal.table.header.myVote=我的投票 +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=移除 +dao.proposal.table.icon.tooltip.removeProposal=移除我的提案 +dao.proposal.table.icon.tooltip.changeVote=当前投票:“{0}”。更改投票至:“{1}” + +dao.proposal.display.myVote.accepted=已接受 +dao.proposal.display.myVote.rejected=已拒绝 +dao.proposal.display.myVote.ignored=已忽略 +dao.proposal.display.myVote.unCounted=投票结果不包括在内 +dao.proposal.myVote.summary=已投票:{0};投票权重:{1}(获得的:{2} + 奖金:{3})({4}) +dao.proposal.myVote.invalid=投票无效 + +dao.proposal.voteResult.success=已接受 +dao.proposal.voteResult.failed=已拒绝 +dao.proposal.voteResult.summary=结果:{0};阈值:{1}(要求> {2});仲裁人数:{3}(要求> {4}) + +dao.proposal.display.paramComboBox.label=选择需要改变的参数 +dao.proposal.display.paramValue=参数值 + +dao.proposal.display.confiscateBondComboBox.label=选择担保 +dao.proposal.display.assetComboBox.label=需要移除的资产 + +dao.blindVote=匿名投票 + +dao.blindVote.startPublishing=发布匿名投票交易中 +dao.blindVote.success=我们的匿名投票交易已经成功发布。\n\n请注意,您必须在线在投票公示阶段,以便您的 Bisq 应用程序可以发布投票公示交易。没有投票公示交易,您的投票将无效! + +dao.wallet.menuItem.send=发送 +dao.wallet.menuItem.receive=接收 +dao.wallet.menuItem.transactions=交易记录 + +dao.wallet.dashboard.myBalance=我的钱包余额 + +dao.wallet.receive.fundYourWallet=你的 BSQ 接收地址 +dao.wallet.receive.bsqAddress=BSQ 钱包地址(刷新未使用地址) + +dao.wallet.send.sendFunds=提现 +dao.wallet.send.sendBtcFunds=发送非 BSQ 资金(BTC) +dao.wallet.send.amount=BSQ 数量 +dao.wallet.send.btcAmount=BTC 数量(无 BSQ 资金) +dao.wallet.send.setAmount=设置提现数量(最小量 {0}) +dao.wallet.send.receiverAddress=接收者的 BSQ 地址 +dao.wallet.send.receiverBtcAddress=接收者的 BTC 地址 +dao.wallet.send.setDestinationAddress=输入您的目标地址 +dao.wallet.send.send=发送 BSQ 资金 +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=发送 BTC 资金 +dao.wallet.send.sendFunds.headline=确定提现申请 +dao.wallet.send.sendFunds.details=发送:{0}\n来自:{1}\n要求的矿工手续费:{2}({3}比特/节)\n交易大小:{4}字节\n\n接收方会收到:{5}\n\n您确定您想要提现这些数量吗? +dao.wallet.chainHeightSynced=最新确认区块:{0} +dao.wallet.chainHeightSyncing=等待区块... 已确认{0}/{1}区块 +dao.wallet.tx.type=类型 + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=未定义 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=不被认可 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=未验证的 BSQ 交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=无效的 BSQ 交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=初始交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=划转 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=接收 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=发送 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=付交易费记录 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=报偿申请记录 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=退还申请的手续费 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=提案手续费 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=投票记录 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=投票公示 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=锁定担保 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=解锁担保 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=资产清单挂牌费 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=烧毁证明 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=不正常 + +dao.tx.withdrawnFromWallet=BTC 已从钱包中取出 +dao.tx.issuanceFromCompReq=报偿申请/发放 +dao.tx.issuanceFromCompReq.tooltip=导致新 BSQ 发行的报偿请求。\n发行日期:{0} +dao.tx.issuanceFromReimbursement=退还申请/发放 +dao.tx.issuanceFromReimbursement.tooltip=导致新 BSQ 发放的退还申请。\n发放日期: {0} +dao.proposal.create.missingBsqFunds=您没有足够的 BSQ 资金来创建提案。如果您有一个未经确认的 BSQ 交易,您需要等待一个区块链确认,因为 BSQ 只有在包含在一个区块中时才会被验证。\n缺失:{0} + +dao.proposal.create.missingBsqFundsForBond=你没有足够的 BSQ 资金来承担这个角色。您仍然可以发布这个提案,但如果它被接受,您将需要这个角色所需的全部 BSQ 金额。\n缺少:{0} + +dao.proposal.create.missingMinerFeeFunds=您没有足够的BTC资金来支付该提案交易。所有的 BSQ交易需要用 BTC 支付挖矿手续费。\n缺少:{0} + +dao.proposal.create.missingIssuanceFunds=您没有足够的BTC资金来支付该提案交易。所有的 BSQ交易需要用 BTC 支付挖矿手续费以及发起交易也需要用 BTC 支付所需的 BSQ 数量({0} 聪/BSQ)\n缺少:{1} + +dao.feeTx.confirm=确认 {0} 交易 +dao.feeTx.confirm.details={0}手续费:{1}\n矿工手续费:{2}({3} 聪/byte)\n交易大小:{4} Kb\n\n您确定您要发送这个 {5} 交易吗? + +dao.feeTx.issuanceProposal.confirm.details={0}手续费:{1}\n为 BSQ 提案所需要的BTC:{2}({3}聪 / BSQ)\n挖矿手续费:{4}({5}聪 /字节)\n交易大小:{6}Kb\n\n如果你的要求被批准,你将收到你要求数量的 2 个 BSQ 提议的费用。\n\n你确定你想要发布{7}交易? + +dao.news.bisqDAO.title=Bisq DAO +dao.news.bisqDAO.description=正如 Bisq交易是分散的,并且不受审查,它的治理模型也是如此—— Bisq DAO 和 BSQ 是使其成为可能的工具。 +dao.news.bisqDAO.readMoreLink=了解有关 Bisq DAO 的更多信息 + +dao.news.pastContribution.title=过去有所贡献?申请 BSQ +dao.news.pastContribution.description=如果您对 Bisq 有贡献,请使用下面的 BSQ 地址,并申请参与 BSQ 初始分发。 +dao.news.pastContribution.yourAddress=你的 BSQ 钱包地址 +dao.news.pastContribution.requestNow=现在申请 + +dao.news.DAOOnTestnet.title=在我们的测试网络上运行 BISQ DAO +dao.news.DAOOnTestnet.description=核心网络 Bisq DAO 还没有启动,但是您可以通过在我们的测试网络上运行它来了解 Bisq DAO 。 +dao.news.DAOOnTestnet.firstSection.title=1.切换至 DAO 测试网络模式 +dao.news.DAOOnTestnet.firstSection.content=从设置页面切换到 DAO 测试网络。 +dao.news.DAOOnTestnet.secondSection.title=2.获得一些 BSQ +dao.news.DAOOnTestnet.secondSection.content=在 Slack 上申请 BSQ 或在 Bisq 上购买 BSQ 。 +dao.news.DAOOnTestnet.thirdSection.title=3.参与投票周期 +dao.news.DAOOnTestnet.thirdSection.content=就修改 Bisq 的各个方面提出建议并进行表决。 +dao.news.DAOOnTestnet.fourthSection.title=4.探索 BSQ 区块链浏览器 +dao.news.DAOOnTestnet.fourthSection.content=由于 BSQ 只是比特币,你可以看到 BSQ 交易在我们的比特币区块浏览器。 +dao.news.DAOOnTestnet.readMoreLink=阅读完整的文档 + +dao.monitor.daoState=DAO 状态 +dao.monitor.proposals=提案状态 +dao.monitor.blindVotes=匿名投票状态 + +dao.monitor.table.peers=节点 +dao.monitor.table.conflicts=矛盾 +dao.monitor.state=状态 +dao.monitor.requestAlHashes=要求所有哈希 +dao.monitor.resync=重新同步 DAO 状态 +dao.monitor.table.header.cycleBlockHeight=周期/区块高度 +dao.monitor.table.cycleBlockHeight=周期 {0} /区块 {1} +dao.monitor.table.seedPeers=种子节点:{0} + +dao.monitor.daoState.headline=DAO 状态 +dao.monitor.daoState.table.headline=DAO 状态的哈希链 +dao.monitor.daoState.table.blockHeight=区块高度 +dao.monitor.daoState.table.hash=DAO 状态的哈希 +dao.monitor.daoState.table.prev=以前的哈希 +dao.monitor.daoState.conflictTable.headline=来自不同实体的 DAO 状态哈希 +dao.monitor.daoState.utxoConflicts=UTXO 冲突 +dao.monitor.daoState.utxoConflicts.blockHeight=区块高度:{0} +dao.monitor.daoState.utxoConflicts.sumUtxo=所有 UTXO 的总和:{0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=所有 BSQ 的总和:{0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO 状态与网络不同步。重启之后,DAO 状态将重新同步。 + +dao.monitor.proposal.headline=提案状态 +dao.monitor.proposal.table.headline=提案状态的哈希链 +dao.monitor.proposal.conflictTable.headline=来自不同实体的提案状态哈希 + +dao.monitor.proposal.table.hash=提案状态的哈希 +dao.monitor.proposal.table.prev=以前的哈希 +dao.monitor.proposal.table.numProposals=提案编号 + +dao.monitor.isInConflictWithSeedNode=您的本地数据与至少一个种子节点不一致。请重新同步 DAO 状态。 +dao.monitor.isInConflictWithNonSeedNode=您的一个对等节点与网络不一致,但您的节点与种子节点同步。 +dao.monitor.daoStateInSync=您的本地节点与网络一致 + +dao.monitor.blindVote.headline=匿名投票状态 +dao.monitor.blindVote.table.headline=匿名投票状态的哈希链 +dao.monitor.blindVote.conflictTable.headline=来自不同实体的匿名投票状态哈希 +dao.monitor.blindVote.table.hash=匿名投票状态的哈希 +dao.monitor.blindVote.table.prev=以前的哈希 +dao.monitor.blindVote.table.numBlindVotes=匿名投票编号 + +dao.factsAndFigures.menuItem.supply=BSQ 供给 +dao.factsAndFigures.menuItem.transactions=BSQ 交易 + +dao.factsAndFigures.dashboard.avgPrice90=90天平均 BSQ/BTC 交易价格 +dao.factsAndFigures.dashboard.avgPrice30=30天平均 BSQ/BTC 交易价格 +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=总共可用的 BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=已发放的 BSQ 已销毁的 BSQ + +dao.factsAndFigures.supply.issued=已发放的 BSQ +dao.factsAndFigures.supply.compReq=赔偿要求 +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=在初始交易中心有问题的 BSQ +dao.factsAndFigures.supply.compRequestIssueAmount=报偿申请发放的 BSQ +dao.factsAndFigures.supply.reimbursementAmount=退还申请发放的 BSQ +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ 烧毁总量 + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=交易总量 +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=BSQ 全局锁定状态 +dao.factsAndFigures.supply.totalLockedUpAmount=担保的锁定 +dao.factsAndFigures.supply.totalUnlockingAmount=正在从担保解锁 BSQ +dao.factsAndFigures.supply.totalUnlockedAmount=已从担保解锁 BSQ +dao.factsAndFigures.supply.totalConfiscatedAmount=已从担保没收 BSQ +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=创始交易 +dao.factsAndFigures.transactions.genesisBlockHeight=初始区块高度 +dao.factsAndFigures.transactions.genesisTxId=初始交易 ID +dao.factsAndFigures.transactions.txDetails=BSQ 交易统计 +dao.factsAndFigures.transactions.allTx=所有 BSQ 交易记录 +dao.factsAndFigures.transactions.utxo=所有未用交易的量 +dao.factsAndFigures.transactions.compensationIssuanceTx=所有报偿请求问题的交易记录 +dao.factsAndFigures.transactions.reimbursementIssuanceTx=所有退回申请问题的交易记录 +dao.factsAndFigures.transactions.burntTx=所有费用支付记录 +dao.factsAndFigures.transactions.invalidTx=所有无效交易记录 +dao.factsAndFigures.transactions.irregularTx=所有不正常的交易记录: + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=可用余额 + +contractWindow.title=纠纷详情 +contractWindow.dates=报价时间/交易时间 +contractWindow.btcAddresses=BTC 买家/BTC 卖家的比特币地址 +contractWindow.onions=BTC 买家/BTC 卖家的网络地址 +contractWindow.accountAge=BTC 买家/BTC 卖家的账龄 +contractWindow.numDisputes=BTC 买家/BTC 卖家的纠纷编号 +contractWindow.contractHash=合同哈希 + +displayAlertMessageWindow.headline=重要资料! +displayAlertMessageWindow.update.headline=重要更新资料! +displayAlertMessageWindow.update.download=下载: +displayUpdateDownloadWindow.downloadedFiles=下载完成的文件: +displayUpdateDownloadWindow.downloadingFile=正在下载:{0} +displayUpdateDownloadWindow.verifiedSigs=验证验证: +displayUpdateDownloadWindow.status.downloading=下载文件... +displayUpdateDownloadWindow.status.verifying=验证验证中... +displayUpdateDownloadWindow.button.label=下载安装程序并验证验证 +displayUpdateDownloadWindow.button.downloadLater=稍后下载 +displayUpdateDownloadWindow.button.ignoreDownload=忽略这个版本 +displayUpdateDownloadWindow.headline=Bisq 有新的更新! +displayUpdateDownloadWindow.download.failed.headline=下载失败 +displayUpdateDownloadWindow.download.failed=下载失败。\n请到 https://bisq.io/downloads 下载并验证。 +displayUpdateDownloadWindow.installer.failed=无法确定正确的安装程序。请通过 https://bisq.network/downloads 手动下载和验证。 +displayUpdateDownloadWindow.verify.failed=验证失败。\n请到 https://bisq.io/downloads 手动下载和验证。 +displayUpdateDownloadWindow.success=新版本成功下载并验证验证 。\n\n请打开下载目录,关闭应用程序并安装最新版本。 +displayUpdateDownloadWindow.download.openDir=打开下载目录 + +disputeSummaryWindow.title=概要 +disputeSummaryWindow.openDate=工单创建时间 +disputeSummaryWindow.role=交易者的角色 +disputeSummaryWindow.payout=交易金额支付 +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} 获得交易金额支付 +disputeSummaryWindow.payout.getsAll=最大 BTC 支付数 {0} +disputeSummaryWindow.payout.custom=自定义支付 +disputeSummaryWindow.payoutAmount.buyer=买家支付金额 +disputeSummaryWindow.payoutAmount.seller=卖家支付金额 +disputeSummaryWindow.payoutAmount.invert=使用失败者作为发布者 +disputeSummaryWindow.reason=纠纷的原因 +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=可用性 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=违反协议 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=不回复 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=诈骗 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=其他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=银行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=可选交易 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=错误的发送者账号 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=交易伙伴已超时 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=交易已稳定 + +disputeSummaryWindow.summaryNotes=总结说明 +disputeSummaryWindow.addSummaryNotes=添加总结说明 +disputeSummaryWindow.close.button=关闭话题 + +# Do no change any line break or order of tokens as the structure is used for signature verification +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.msg=工单已关闭{0}\n{1} 节点地址:{12}\n\n总结:\n交易 ID:{3}\n货币:{4}\n交易金额:{5}\nBTC 买家支付金额:{6}\nBTC 卖家支付金额:{7}\n\n纠纷原因:{8}\n\n总结:\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=\n\n下一个步骤:\n打开未完成交易,接受或拒绝建议的调解员的建议 +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一个步骤:\n不需要您采取进一步的行动。如果仲裁员做出了对你有利的裁决,你将在 资金/交易 页中看到“仲裁退款”交易 +disputeSummaryWindow.close.closePeer=你也需要关闭交易对象的话题! +disputeSummaryWindow.close.txDetails.headline=发布交易退款 +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=买方收到{0}在地址:{1} +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=卖方收到{0}在地址:{1} +disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}({4}satoshis/byte)\n事务大小:{5} Kb\n\n您确定要发布此交易吗? + +disputeSummaryWindow.close.noPayout.headline=未支付关闭 +disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情况下关闭吗? + +emptyWalletWindow.headline={0} 钱包急救工具 +emptyWalletWindow.info=请在紧急情况下使用,如果您无法从 UI 中访问您的资金。\n\n请注意,使用此工具时,所有未结报价将自动关闭。\n\n在使用此工具之前,请备份您的数据目录。您可以在“帐户/备份”中执行此操作。\n\n请报告我们您的问题,并在 Github 或 Bisq 论坛上提交错误报告,以便我们可以调查导致问题的原因。 +emptyWalletWindow.balance=您的可用钱包余额 +emptyWalletWindow.bsq.btcBalance=非 BSQ 聪余额 + +emptyWalletWindow.address=输入您的目标地址 +emptyWalletWindow.button=发送全部资金 +emptyWalletWindow.openOffers.warn=您有已发布的报价,如果您清空钱包将被删除。\n你确定要清空你的钱包吗? +emptyWalletWindow.openOffers.yes=是的,我确定 +emptyWalletWindow.sent.success=您的钱包的余额已成功转移。 + +enterPrivKeyWindow.headline=输入密钥进行注册 + +filterWindow.headline=编辑筛选列表 +filterWindow.offers=筛选报价(用逗号“,”隔开) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=筛选交易账户数据:\n格式:逗号分割的 [付款方式ID|数据字段|值] +filterWindow.bannedCurrencies=筛选货币代码(用逗号“,”隔开) +filterWindow.bannedPaymentMethods=筛选支付方式 ID(用逗号“,”隔开) +filterWindow.bannedAccountWitnessSignerPubKeys=已过滤的帐户证人签名者公钥(逗号分隔十六进制公钥) +filterWindow.bannedPrivilegedDevPubKeys=已过滤的特权开发者公钥(逗号分隔十六进制公钥) +filterWindow.arbitrators=筛选后的仲裁人(用逗号“,”隔开的洋葱地址) +filterWindow.mediators=筛选后的调解员(用逗号“,”隔开的洋葱地址) +filterWindow.refundAgents=筛选后的退款助理(用逗号“,”隔开的洋葱地址) +filterWindow.seedNode=筛选后的种子节点(用逗号“,”隔开的洋葱地址) +filterWindow.priceRelayNode=筛选后的价格中继节点(用逗号“,”隔开的洋葱地址) +filterWindow.btcNode=筛选后的比特币节点(用逗号“,”隔开的地址+端口) +filterWindow.preventPublicBtcNetwork=禁止使用公共比特币网络 +filterWindow.disableDao=禁用 DAO +filterWindow.disableAutoConf=禁用自动确认 +filterWindow.autoConfExplorers=已过滤自动确认浏览器(逗号分隔地址) +filterWindow.disableDaoBelowVersion=DAO 最低所需要的版本 +filterWindow.disableTradeBelowVersion=交易最低所需要的版本 +filterWindow.add=添加筛选 +filterWindow.remove=移除筛选 +filterWindow.btcFeeReceiverAddresses=比特币手续费接收地址 +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=最小 BTC 数量 +offerDetailsWindow.min=(最小 {0}) +offerDetailsWindow.distance=(与市场价格的差距:{0}) +offerDetailsWindow.myTradingAccount=我的交易账户 +offerDetailsWindow.offererBankId=(卖家的银行 ID/BIC/SWIFT) +offerDetailsWindow.offerersBankName=(卖家的银行名称): +offerDetailsWindow.bankId=银行 ID(例如 BIC 或者 SWIFT ): +offerDetailsWindow.countryBank=卖家银行所在国家或地区 +offerDetailsWindow.commitment=承诺 +offerDetailsWindow.agree=我同意 +offerDetailsWindow.tac=条款和条件 +offerDetailsWindow.confirm.maker=确定:发布报价 {0} 比特币 +offerDetailsWindow.confirm.taker=确定:下单买入 {0} 比特币 +offerDetailsWindow.creationDate=创建时间 +offerDetailsWindow.makersOnion=卖家的匿名地址 + +qRCodeWindow.headline=二维码 +qRCodeWindow.msg=请使用二维码从外部钱包充值至 Bisq 钱包 +qRCodeWindow.request=付款请求:\n{0} + +selectDepositTxWindow.headline=选择纠纷的存款交易 +selectDepositTxWindow.msg=存款交易未存储在交易中。\n请从您的钱包中选择一个现有的多重验证交易,这是在失败的交易中使用的存款交易。\n\n您可以通过打开交易详细信息窗口(点击列表中的交易 ID)并按照交易费用支付交易输出到您看到多重验证存款交易的下一个交易(地址从3开始),找到正确的交易。 该交易 ID 应在此处列出的列表中显示。 一旦您找到正确的交易,请在此处选择该交易并继续\n\n抱歉给您带来不便,但是错误的情况应该非常罕见,将来我们会尝试找到更好的解决方法。 +selectDepositTxWindow.select=选择存款交易 + +sendAlertMessageWindow.headline=发送全球通知 +sendAlertMessageWindow.alertMsg=提醒消息 +sendAlertMessageWindow.enterMsg=输入消息: +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=新版本号 +sendAlertMessageWindow.send=发送通知 +sendAlertMessageWindow.remove=移除通知 + +sendPrivateNotificationWindow.headline=发送私信 +sendPrivateNotificationWindow.privateNotification=私人通知 +sendPrivateNotificationWindow.enterNotification=输入通知 +sendPrivateNotificationWindow.send=发送私人通知 + +showWalletDataWindow.walletData=钱包数据 +showWalletDataWindow.includePrivKeys=包含私钥 + +setXMRTxKeyWindow.headline=证明已发送 XMR +setXMRTxKeyWindow.note=在下面添加 tx 信息可以更快的自动确认交易。更多信息::https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=交易 ID (可选) +setXMRTxKeyWindow.txKey=交易密钥 (可选) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=用户协议 +tacWindow.agree=我同意 +tacWindow.disagree=我不同意并退出 +tacWindow.arbitrationSystem=纠纷解决方案 + +tradeDetailsWindow.headline=交易 +tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID: +tradeDetailsWindow.tradeDate=交易时间 +tradeDetailsWindow.txFee=矿工手续费 +tradeDetailsWindow.tradingPeersOnion=交易伙伴匿名地址 +tradeDetailsWindow.tradingPeersPubKeyHash=交易伙伴公钥哈希值 +tradeDetailsWindow.tradeState=交易状态 +tradeDetailsWindow.agentAddresses=仲裁员/调解员 +tradeDetailsWindow.detailData=详情数据 + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=输入密码解锁 + +torNetworkSettingWindow.header=Tor 网络设置 +torNetworkSettingWindow.noBridges=不使用网桥 +torNetworkSettingWindow.providedBridges=连接到提供的网桥 +torNetworkSettingWindow.customBridges=输入自定义网桥 +torNetworkSettingWindow.transportType=传输类型 +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4(推荐) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=输入一个或多个网桥中继节点(每行一个) +torNetworkSettingWindow.enterBridgePrompt=输入地址:端口 +torNetworkSettingWindow.restartInfo=您需要重新启动以应用更改 +torNetworkSettingWindow.openTorWebPage=打开 Tor Project 网页 +torNetworkSettingWindow.deleteFiles.header=连接问题? +torNetworkSettingWindow.deleteFiles.info=如果您在启动时有重复的连接问题,删除过期的 Tor 文件可能会有所帮助。如果要尝试修复,请单击下面的按钮,然后重新启动。 +torNetworkSettingWindow.deleteFiles.button=删除过期的 Tor 文件并关闭 +torNetworkSettingWindow.deleteFiles.progress=关闭正在运行中的 Tor +torNetworkSettingWindow.deleteFiles.success=过期的 Tor 文件被成功删除。请重新启动。 +torNetworkSettingWindow.bridges.header=Tor 网络被屏蔽? +torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的国家或地区屏蔽,您可以尝试使用 Tor 网桥。\n \n访问 Tor 网页:https://bridges.torproject.org/bridges,了解关于网桥和可插拔传输的更多信息。 + +feeOptionWindow.headline=选择货币支付交易手续费 +feeOptionWindow.info=您可以选择用 BSQ 或 BTC 支付交易费用。如果您选择 BSQ ,您会感谢这些交易手续费折扣。 +feeOptionWindow.optionsLabel=选择货币支付交易手续费 +feeOptionWindow.useBTC=使用 BTC +feeOptionWindow.fee={0}(≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=通知 +popup.headline.instruction=请注意: +popup.headline.attention=注意 +popup.headline.backgroundInfo=背景资料 +popup.headline.feedback=完成 +popup.headline.confirmation=确定 +popup.headline.information=资料 +popup.headline.warning=警告 +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 报告中,如果包含 bisq.log 文件,那么调试就会变得更容易。 + +popup.error.tryRestart=请尝试重启您的应用程序或者检查您的网络连接。 +popup.error.takeOfferRequestFailed=当有人试图接受你的报价时发生了一个错误:\n{0} + +error.spvFileCorrupted=读取 SPV 链文件时发生错误。\n可能是 SPV 链文件被破坏了。\n\n错误消息:{0}\n\n要删除它并开始重新同步吗? +error.deleteAddressEntryListFailed=无法删除 AddressEntryList 文件。\n \n错误:{0} +error.closedTradeWithUnconfirmedDepositTx=交易 ID 为 {0} 的已关闭交易的保证金交易仍未确认。\n \n请在“设置/网络信息”进行 SPV 重新同步,以查看交易是否有效。 +error.closedTradeWithNoDepositTx=交易 ID 为 {0} 的保证金交易已被确认。\n\n请重新启动应用程序来清理已关闭的交易列表。 + +popup.warning.walletNotInitialized=钱包至今未初始化 +popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Bisq 使用Java)会在 MacOS 中引发弹出警告(``Bisq 希望从任何应用程序接收击键'').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Bisq”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Bisq将升级到新的 Java 版本,以避免该问题。 +popup.warning.wrongVersion=您这台电脑上可能有错误的 Bisq 版本。\n您的电脑的架构是:{0}\n您安装的 Bisq 二进制文件是:{1}\n请关闭并重新安装正确的版本({2})。 +popup.warning.incompatibleDB=我们检测到不兼容的数据库文件!\n\n那些数据库文件与我们当前的代码库不兼容:\n{0}\n\n我们对损坏的文件进行了备份,并将默认值应用于新的数据库版本。\n\n备份位于:\n{1}/db/backup_of_corrupted_data。\n\n请检查您是否安装了最新版本的 Bisq\n您可以下载:\nhttps://bisq.network/downloads\n\n请重新启动应用程序。 +popup.warning.startupFailed.twoInstances=Bisq 已经在运行。 您不能运行两个 Bisq 实例。 +popup.warning.tradePeriod.halfReached=您与 ID {0} 的交易已达到最长交易期的一半,且仍未完成。\n\n交易期结束于 {1}\n\n请查看“业务/未完成交易”的交易状态,以获取更多信息。 +popup.warning.tradePeriod.ended=您与 ID {0} 的已达到最长交易期,且未完成。\n\n交易期结束于 {1}\n\n请查看“业务/未完成交易”的交易状态,以从调解员获取更多信息。 +popup.warning.noTradingAccountSetup.headline=您还没有设置交易账户 +popup.warning.noTradingAccountSetup.msg=您需要设置法定货币或数字货币账户才能创建报价。\n您要设置帐户吗? +popup.warning.noArbitratorsAvailable=没有仲裁员可用。 +popup.warning.noMediatorsAvailable=没有调解员可用。 +popup.warning.notFullyConnected=您需要等到您完全连接到网络\n在启动时可能需要2分钟。 +popup.warning.notSufficientConnectionsToBtcNetwork=你需要等待至少有{0}个与比特币网络的连接点。 +popup.warning.downloadNotComplete=您需要等待,直到丢失的比特币区块被下载完毕。 +popup.warning.chainNotSynced=Bisq 钱包区块链高度没有正确地同步。如果最近才打开应用,请等待一个新发布的比特币区块。\n\n你可以检查区块链高度在设置/网络信息。如果经过了一个区块但问题还是没有解决,你应该及时的完成 SPV 链重新同步。https://bisq.wiki/Resyncing_SPV_file +popup.warning.removeOffer=您确定要移除该报价吗?\n如果您删除该报价,{0} 的挂单费将会丢失。 +popup.warning.tooLargePercentageValue=您不能设置100%或更大的百分比。 +popup.warning.examplePercentageValue=请输入百分比数字,如 5.4% 是“5.4” +popup.warning.noPriceFeedAvailable=该货币没有可用的价格。 你不能使用基于百分比的价格。\n请选择固定价格。 +popup.warning.sendMsgFailed=向您的交易对象发送消息失败。\n请重试,如果继续失败报告错误。 +popup.warning.insufficientBtcFundsForBsqTx=你没有足够的 BTC 资金支付这笔交易的挖矿手续费。\n请充值您的 BTC 钱包。\n缺少的资金:{0} +popup.warning.bsqChangeBelowDustException=该交易产生的 BSQ 变化输出低于零头限制(5.46 BSQ),将被比特币网络拒绝。\n\n您需要发送更高的金额以避免更改输出(例如,通过在您的发送金额中添加零头),或者向您的钱包中添加更多的 BSQ 资金,以避免生成零头输出。\n\n零头输出为 {0}。 +popup.warning.btcChangeBelowDustException=该交易创建的更改输出低于零头限制(546 聪),将被比特币网络拒绝。\n\n您需要将零头添加到发送量中,以避免生成零头输出。\n\n零头输出为{0}。 + +popup.warning.insufficientBsqFundsForBtcFeePayment=您需要更多的 BSQ 去完成这笔交易 - 钱包中最后剩余 5.46 BSQ 将无法用于支付交易手续费因为 BTC 协议中的零头限制。\n\n你可以购买更多的 BSQ 或用 BTC支付交易手续费\n\n缺少 BSQ 资金:{0} +popup.warning.noBsqFundsForBtcFeePayment=您的 BSQ 钱包没有足够的资金支付 BSQ 的交易费用。 +popup.warning.messageTooLong=您的信息超过最大允许的大小。请将其分成多个部分发送,或将其上传到 https://pastebin.com 之类的服务器。 +popup.warning.lockedUpFunds=你已经从一个失败的交易中冻结了资金。\n冻结余额:{0}\n存款tx地址:{1}\n交易单号:{2}\n\n请通过选择待处理交易界面中的交易并点击“alt + o”或“option+ o”打开帮助话题。 + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=忽略并继续 + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=价格传递 +popup.warning.seed=种子 +popup.warning.mandatoryUpdate.trading=请更新到最新的 Bisq 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Bisq 论坛。 +popup.warning.mandatoryUpdate.dao=请更新到最新的 Bisq 版本。强制更新禁止了旧版本旧版本的 Bisq DAO 和 BSQ 。更多信息请访问 Bisq 论坛。 +popup.warning.disable.dao=Bisq DAO 和 BSQ 被临时禁用的。更多信息请访问 Bisq 论坛。 +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 BTC 来转账。 + +popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Bisq Keybase 团队的 Support 频道 + +popup.warning.trade.txRejected.tradeFee=交易手续费 +popup.warning.trade.txRejected.deposit=押金 +popup.warning.trade.txRejected=使用 ID {1} 进行交易的 {0} 交易被比特币网络拒绝。\n交易 ID = {2}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Bisq Keybase 团队的 Support 频道 + +popup.warning.openOfferWithInvalidMakerFeeTx=交易 ID 为 {0} 的挂单费交易无效。\n交易 ID = {1}。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Bisq Keybase 团队的 Support 频道 + +popup.info.securityDepositInfo=为了确保双方都遵守交易协议,双方都需要支付保证金。\n\n这笔存款一直保存在您的交易钱包里,直到您的交易成功完成,然后再退还给您。\n\n请注意:如果您正在创建一个新的报价,Bisq 需要运行另一个交易员接受它。为了让您的报价在线,保持 Bisq 运行,并确保这台计算机也在线(即,确保它没有切换到待机模式…显示器可以待机)。 + +popup.info.cashDepositInfo=请确保您在您的地区有一个银行分行,以便能够进行现金存款。\n卖方银行的银行 ID(BIC/SWIFT)为:{0}。 +popup.info.cashDepositInfo.confirm=我确认我可以支付保证金 +popup.info.shutDownWithOpenOffers=Bisq 正在被关闭,但仍有公开的报价。\n\n当 Bisq 关闭时,这些提供将不能在 P2P 网络上使用,但是它们将在您下次启动 Bisq 时重新发布到 P2P 网络上。\n\n为了让您的报价在线,保持 Bisq 运行,并确保这台计算机也在线(即,确保它不会进入待机模式…显示器待机不是问题)。 +popup.info.qubesOSSetupInfo=你似乎好像在 Qubes OS 上运行 Bisq。\n\n请确保您的 Bisq qube 是参考设置指南的说明设置的 https://bisq.wiki/Running_Bisq_on_Qubes +popup.warn.downGradePrevention=不支持从 {0} 版本降级到 {1} 版本。请使用最新的 Bisq 版本。 +popup.warn.daoRequiresRestart=在同步 DAO 状态时发生问题。你需要重启应用以修复此问题。 + +popup.privateNotification.headline=重要私人通知! + +popup.securityRecommendation.headline=重要安全建议 +popup.securityRecommendation.msg=如果您还没有启用,我们想提醒您考虑为您的钱包使用密码保护。\n\n强烈建议你写下钱包还原密钥。 那些还原密钥就是恢复你的比特币钱包的主密码。\n在“钱包密钥”部分,您可以找到更多信息\n\n此外,您应该在“备份”界面备份完整的应用程序数据文件夹。 + +popup.bitcoinLocalhostNode.msg=Bisq 侦测到一个比特币核心节点在本机(本地)运行\n\n请确保:\n- 这个节点已经在运行 Bisq 之前已全部同步\n- 修饰已被禁用 ('prune=0' 在 bitcoin.conf 文件中)\n- Bloom 过滤器已经启用('peerbloomfilters=1' 在 bitcoin.conf 文件中) + +popup.shutDownInProgress.headline=正在关闭 +popup.shutDownInProgress.msg=关闭应用可能会花一点时间。\n请不要打断关闭过程。 + +popup.attention.forTradeWithId=交易 ID {0} 需要注意 +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=多个支付账户可用 +popup.info.multiplePaymentAccounts.msg=您有多个支付帐户在这个报价中可用。请确你做了正确的选择。 + +popup.accountSigning.selectAccounts.headline=选择付款账户 +popup.accountSigning.selectAccounts.description=根据付款方式和时间点,所有与支付给买方的付款发生的争议有关的付款帐户将被选择让您验证。 +popup.accountSigning.selectAccounts.signAll=验证所有付款方式 +popup.accountSigning.selectAccounts.datePicker=选择要验证的帐户的时间点 + +popup.accountSigning.confirmSelectedAccounts.headline=确认选定的付款帐户 +popup.accountSigning.confirmSelectedAccounts.description=根据您的输入,将选择 {0} 支付帐户。 +popup.accountSigning.confirmSelectedAccounts.button=确认付款账户 +popup.accountSigning.signAccounts.headline=确认验证付款账户 +popup.accountSigning.signAccounts.description=根据您的选择,{0} 付款帐户将被验证。 +popup.accountSigning.signAccounts.button=验证付款账户 +popup.accountSigning.signAccounts.ECKey=输入仲裁员密钥 +popup.accountSigning.signAccounts.ECKey.error=不正确的仲裁员 ECKey + +popup.accountSigning.success.headline=恭喜 +popup.accountSigning.success.description=所有 {0} 支付账户已成功验证! +popup.accountSigning.generalInformation=您将在帐户页面找到所有账户的验证状态。\n\n更多信息,请访问https://docs.bisq.network/payment-methods#account-signing. +popup.accountSigning.signedByArbitrator=您的一个付款帐户已被认证以及被仲裁员验证。交易成功后,使用此帐户将自动验证您的交易伙伴的帐户。\n\n{0} +popup.accountSigning.signedByPeer=您的一个付款帐户已经被交易伙伴验证和验证。您的初始交易限额将被取消,您将能够在{0}天后验证其他帐户。 +popup.accountSigning.peerLimitLifted=您其中一个帐户的初始限额已被取消。\n\n{0} +popup.accountSigning.peerSigner=您的一个帐户已足够成熟,可以验证其他付款帐户,您的一个帐户的初始限额已被取消。\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=导入未验证账龄证据 +popup.accountSigning.confirmSingleAccount.headline=确认所选账龄证据 +popup.accountSigning.confirmSingleAccount.selectedHash=已选择证据哈希值 +popup.accountSigning.confirmSingleAccount.button=验证账龄证据 +popup.accountSigning.successSingleAccount.description=证据 {0} 已被验证 +popup.accountSigning.successSingleAccount.success.headline=成功 + +popup.accountSigning.unsignedPubKeys.headline=未验证公钥 +popup.accountSigning.unsignedPubKeys.sign=验证公钥 +popup.accountSigning.unsignedPubKeys.signed=公钥已被验证 +popup.accountSigning.unsignedPubKeys.result.signed=已验证公钥 +popup.accountSigning.unsignedPubKeys.result.failed=未能验证公钥 + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=交易 ID {0} 的通知 +notification.ticket.headline=交易 ID {0} 的帮助话题 +notification.trade.completed=交易现在完成,您可以提取资金。 +notification.trade.accepted=您 BTC {0} 的报价被接受。 +notification.trade.confirmed=您的交易至少有一个区块链确认。\n您现在可以开始付款。 +notification.trade.paymentStarted=BTC 买家已经开始付款。 +notification.trade.selectTrade=选择交易 +notification.trade.peerOpenedDispute=您的交易对象创建了一个 {0}。 +notification.trade.disputeClosed=这个 {0} 被关闭。 +notification.walletUpdate.headline=交易钱包更新 +notification.walletUpdate.msg=您的交易钱包充值成功。\n金额:{0} +notification.takeOffer.walletUpdate.msg=您的交易钱包已经从早期的下单尝试中得到足够的资金支持。\n金额:{0} +notification.tradeCompleted.headline=交易完成 +notification.tradeCompleted.msg=您现在可以提现您的资金到您的外部比特币钱包或者划转它到 Bisq 钱包。 + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=显示应用程序窗口 +systemTray.hide=隐藏应用程序窗口 +systemTray.info=关于 Bisq 信息 +systemTray.exit=退出 +systemTray.tooltip=Bisq:去中心化比特币交易网络 + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=请确保您的外部钱包使用的矿工手续费费用足够高至少为 {0} 聪/字节,以便矿工接受资金交易。\n否则交易所交易无法确认,交易最终将会出现纠纷。 + +guiUtil.accountExport.savedToPath=交易账户保存在路径:\n{0} +guiUtil.accountExport.noAccountSetup=您没有交易账户设置导出。 +guiUtil.accountExport.selectPath=选择路径 {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=交易账户 ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=我们没有导入 ID {0} 的交易账户,因为它已经存在。\n +guiUtil.accountExport.exportFailed=导出 .CSV 失败,因为发生了错误。\n错误 = {0} +guiUtil.accountExport.selectExportPath=选择导出路径 +guiUtil.accountImport.imported=交易账户导入路径:\n{0}\n\n导入账户:\n{1} +guiUtil.accountImport.noAccountsFound=在路径 {0} 找不到导出的交易账户。\n文件名为 {1}。 +guiUtil.openWebBrowser.warning=您将在系统网络浏览器中打开一个网页。\n你现在要打开网页吗?\n\n如果您没有使用“Tor 浏览器”作为默认的系统网络浏览器,则将以默认连接到网页。\n\n网址:“{0}” +guiUtil.openWebBrowser.doOpen=打开网页并且不要再询问我 +guiUtil.openWebBrowser.copyUrl=复制 URL 并取消 +guiUtil.ofTradeAmount=的交易数量 +guiUtil.requiredMinimum=(最低需求量) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=选择币种 +list.currency.showAll=显示全部 +list.currency.editList=编辑币种列表 + +table.placeholder.noItems=最近没有可用的 {0} +table.placeholder.noData=最近没有可用数据 +table.placeholder.processingData=处理数据… + + +peerInfoIcon.tooltip.tradePeer=交易伙伴 +peerInfoIcon.tooltip.maker=制造者 +peerInfoIcon.tooltip.trade.traded={0} 匿名地址:{1}\n您已经与他交易过 {2} 次了\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} 匿名地址:{1}\n你还没有与他交易过。\n{2} +peerInfoIcon.tooltip.age=支付账户在 {0} 前创建。 +peerInfoIcon.tooltip.unknownAge=支付账户账龄未知。 + +tooltip.openPopupForDetails=打开弹出窗口的详细信息 +tooltip.invalidTradeState.warning=这个交易处于不可用状态。打开详情窗口以发现更多细节。 +tooltip.openBlockchainForAddress=使用外部区块链浏览器打开地址:{0} +tooltip.openBlockchainForTx=使用外部区块链浏览器打开交易:{0} + +confidence.unknown=未知交易状态 +confidence.seen=被 {0} 人查看 / 0 确定 +confidence.confirmed=在 {0} 区块中确认 +confidence.invalid=交易无效 + +peerInfo.title=对象资料 +peerInfo.nrOfTrades=已完成交易数量 +peerInfo.notTradedYet=你还没有与他交易过。 +peerInfo.setTag=设置该对象的标签 +peerInfo.age.noRisk=支付账户账龄 +peerInfo.age.chargeBackRisk=自验证 +peerInfo.unknownAge=账龄未知 + +addressTextField.openWallet=打开您的默认比特币钱包 +addressTextField.copyToClipboard=复制地址到剪贴板 +addressTextField.addressCopiedToClipboard=地址已被复制到剪贴板 +addressTextField.openWallet.failed=打开默认的比特币钱包应用程序失败了。或许您没有安装? + +peerInfoIcon.tooltip={0}\n标识:{1} + +txIdTextField.copyIcon.tooltip=复制交易 ID 到剪贴板 +txIdTextField.blockExplorerIcon.tooltip=使用外部区块链浏览器打开这个交易 ID +txIdTextField.missingTx.warning.tooltip=所需的交易缺失 + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=“账户” +navigation.account.walletSeed=“账户/钱包密钥” +navigation.funds.availableForWithdrawal=“资金/提现” +navigation.portfolio.myOpenOffers=“资料/未完成报价” +navigation.portfolio.pending=“业务/未完成交易” +navigation.portfolio.closedTrades=“资料/历史” +navigation.funds.depositFunds=“资金/收到资金” +navigation.settings.preferences=“设置/偏好” +# suppress inspection "UnusedProperty" +navigation.funds.transactions=“资金/交易记录” +navigation.support=“帮助” +navigation.dao.wallet.receive=“DAO/BSQ 钱包/接收” + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} 数量 {1} +formatter.makerTaker=卖家 {0} {1} / 买家 {2} {3} +formatter.youAreAsMaker=您是 {1} {0} 卖家 / 买家是 {3} {2} +formatter.youAreAsTaker=您是 {1} {0} 买家 / 卖家是 {3} {2} +formatter.youAre=您是 {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.fiat=您创建新的报价 {0} {1} +formatter.youAreCreatingAnOffer.altcoin=您正创建报价 {0} {1}({2} {3}) +formatter.asMaker={0} {1} 是卖家 +formatter.asTaker={0} {1} 是买家 + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=比特币主干网络 +# suppress inspection "UnusedProperty" +BTC_TESTNET=比特币测试网络 +# suppress inspection "UnusedProperty" +BTC_REGTEST=比特币回归测试 +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=比特币 DAO 测试网络(弃用) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO 测试网络(比特币主要网络) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=比特币 DAO 回归测试 + +time.year=年线 +time.month=月线 +time.week=周线 +time.day=日线 +time.hour=小时 +time.minute10=10分钟 +time.hours=小时 +time.days=天 +time.1hour=1小时 +time.1day=1天 +time.minute=分钟 +time.second=秒 +time.minutes=分钟 +time.seconds=秒 + + +password.enterPassword=输入密码 +password.confirmPassword=确认密码 +password.tooLong=你输入的密码太长,不要超过 500 个字符。 +password.deriveKey=从密码中提取密钥 +password.walletDecrypted=钱包成功解密并移除密码保护 +password.wrongPw=你输入了错误的密码。\n\n请再次尝试输入密码,仔细检查拼写错误。 +password.walletEncrypted=钱包成功加密并开启密码保护。 +password.walletEncryptionFailed=无法设置钱包密码。您可能导入了与钱包数据库不匹配的还原密钥。请在 Keybase 上联系开发者(https://keybase.io/team/bisq]) +password.passwordsDoNotMatch=这2个密码您输入的不相同 +password.forgotPassword=忘记密码? +password.backupReminder=请注意,设置钱包密码时,所有未加密的钱包的自动创建的备份将被删除。\n\n强烈建议您备份应用程序的目录,并在设置密码之前记下您的还原密钥! +password.backupWasDone=我已备份 +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=钱包密钥 +seed.enterSeedWords=输入钱包密钥 +seed.date=钱包时间 +seed.restore.title=使用还原密钥恢复钱包 +seed.restore=恢复钱包 +seed.creationDate=创建时间 +seed.warn.walletNotEmpty.msg=你的比特币钱包不是空的。\n\n在尝试恢复较旧的钱包之前,您必须清空此钱包,因为将钱包混在一起会导致无效的备份。\n\n请完成您的交易,关闭所有您的未完成报价,并转到资金界面撤回您的比特币。\n如果您无法访问您的比特币,您可以使用紧急工具清空钱包。\n要打开该应急工具,请按“alt + e”或“Cmd/Ctrl + e” 。 +seed.warn.walletNotEmpty.restore=无论如何我要恢复 +seed.warn.walletNotEmpty.emptyWallet=我先清空我的钱包 +seed.warn.notEncryptedAnymore=你的钱包被加密了。\n\n恢复后,钱包将不再加密,您必须设置新的密码。\n\n你要继续吗? +seed.warn.walletDateEmpty=由于您尚未指定钱包日期,因此 Bisq 将必须扫描 2013.10.09(BIP39创始日期)之后的区块链。\n\nBIP39 钱包于 Bisq 于 2017.06.28 首次发布(版本 v0.5)。因此,您可以使用该日期来节省时间。\n\n理想情况下,您应指定创建钱包种子的日期。\n\n\n您确定要继续而不指定钱包日期吗? +seed.restore.success=新的还原密钥成功地恢复了钱包。\n\n您需要关闭并重新启动应用程序。 +seed.restore.error=使用还原密钥恢复钱包时出现错误。{0} +seed.restore.openOffers.warn=您有公开报价,如果您从种子词恢复,则这些报价将被删除。\n您确定要继续吗? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=账户 +payment.account.no=账户编号 +payment.account.name=账户名称 +payment.account.userName=用户昵称 +payment.account.phoneNr=电话号码 +payment.account.owner=账户拥有者姓名: +payment.account.fullName=全称(名,中间名,姓) +payment.account.state=州/省/地区 +payment.account.city=城市 +payment.bank.country=银行所在国家或地区 +payment.account.name.email=账户拥有者姓名/电子邮箱 +payment.account.name.emailAndHolderId=账户拥有者姓名/电子邮箱 / {0} +payment.bank.name=银行名称 +payment.select.account=选择账户类型 +payment.select.region=选择地区 +payment.select.country=选择国家或地区 +payment.select.bank.country=选择银行所在国家或地区 +payment.foreign.currency=你确定想选择一个与此国家或地区默认币种不同的货币? +payment.restore.default=不,恢复默认值 +payment.email=电子邮箱 +payment.country=国家或地区 +payment.extras=额外要求 +payment.email.mobile=电子邮箱或手机号码 +payment.altcoin.address=数字货币地址 +payment.altcoin.tradeInstantCheckbox=使用数字货币进行即时交易( 1 小时内) +payment.altcoin.tradeInstant.popup=对于即时交易,要求交易双方都在线,能够在不到1小时内完成交易。\n \n如果你已经有未完成的报价以及你不能即时完成,请在资料页面禁用这些报价。 +payment.altcoin=数字货币 +payment.select.altcoin=选择或搜索数字货币 +payment.secret=密保问题 +payment.answer=答案 +payment.wallet=钱包 ID +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=用户名或电子邮箱或电话号码 +payment.moneyBeam.accountId=电子邮箱或者电话号码 +payment.venmo.venmoUserName=Venmo 用户名: +payment.popmoney.accountId=电子邮箱或者电话号码 +payment.promptPay.promptPayId=公民身份证/税号或电话号码 +payment.supportedCurrencies=支持的货币 +payment.supportedCurrenciesForReceiver=收款 +payment.limitations=限制条件 +payment.salt=帐户年龄验证盐值 +payment.error.noHexSalt=盐值需要十六进制的。\n如果您想要从旧帐户转移盐值以保留帐龄,只建议编辑盐值字段。帐龄通过帐户盐值和识别帐户数据(例如 IBAN )来验证。 +payment.accept.euro=接受来自这些欧元国家的交易 +payment.accept.nonEuro=接受来自这些非欧元国家的交易 +payment.accepted.countries=接受的国家 +payment.accepted.banks=接受的银行(ID) +payment.mobile=手机号码 +payment.postal.address=邮寄地址 +payment.national.account.id.AR=CBU 号码 +shared.accountSigningState=账户验证状态 + +#new +payment.altcoin.address.dyn={0} 地址 +payment.altcoin.receiver.address=接收者的数字货币地址 +payment.accountNr=账号: +payment.emailOrMobile=电子邮箱或手机号码 +payment.useCustomAccountName=使用自定义名称 +payment.maxPeriod=最大允许交易时限 +payment.maxPeriodAndLimit=最大交易时间:{0}/ 最大买入:{1}/ 最大出售:{2}/账龄:{3} +payment.maxPeriodAndLimitCrypto=最大交易期限:{0}/最大交易限额:{1} +payment.currencyWithSymbol=货币:{0} +payment.nameOfAcceptedBank=接受的银行名称 +payment.addAcceptedBank=添加接受的银行 +payment.clearAcceptedBanks=清除接受的银行 +payment.bank.nameOptional=银行名称(可选) +payment.bankCode=银行代码 +payment.bankId=银行 ID (BIC/SWIFT): +payment.bankIdOptional=银行 ID(BIC/SWIFT)(可选) +payment.branchNr=分行编码 +payment.branchNrOptional=分行编码(可选) +payment.accountNrLabel=账号(IBAN) +payment.accountType=账户类型 +payment.checking=检查 +payment.savings=保存 +payment.personalId=个人 ID +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.info=Zelle是一项转账服务,转账到其他银行做的很好。\n\n1.检查此页面以查看您的银行是否(以及如何)与 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特别注意您的转账限额-汇款限额因银行而异,银行通常分别指定每日,每周和每月的限额。\n\n3.如果您的银行不能使用 Zelle,您仍然可以通过 Zelle 移动应用程序使用它,但是您的转账限额会低得多。\n\n4.您的 Bisq 帐户上指定的名称必须与 Zelle/银行帐户上的名称匹配。 \n\n如果您无法按照贸易合同中的规定完成 Zelle 交易,则可能会损失部分(或全部)保证金。\n\n由于 Zelle 的拒付风险较高,因此建议卖家通过电子邮件或 SMS 与未签名的买家联系,以确认买家确实拥有 Bisq 中指定的 Zelle 帐户。 +payment.fasterPayments.newRequirements.info=有些银行已经开始核实快捷支付收款人的全名。您当前的快捷支付帐户没有填写全名。\n\n请考虑在 Bisq 中重新创建您的快捷支付帐户,为将来的 {0} 买家提供一个完整的姓名。\n\n重新创建帐户时,请确保将银行区号、帐户编号和帐龄验证盐值从旧帐户复制到新帐户。这将确保您现有的帐龄和签名状态得到保留。 +payment.moneyGram.info=使用 MoneyGram 时,BTC 买方必须将授权号码和收据的照片通过电子邮件发送给 BTC 卖方。收据必须清楚地显示卖方的全名、国家或地区、州和金额。买方将在交易过程中显示卖方的电子邮件。 +payment.westernUnion.info=使用 Western Union 时,BTC 买方必须通过电子邮件将 MTCN(运单号)和收据照片发送给 BTC 卖方。收据上必须清楚地显示卖方的全名、城市、国家或地区和金额。买方将在交易过程中显示卖方的电子邮件。 +payment.halCash.info=使用 HalCash 时,BTC 买方需要通过手机短信向 BTC 卖方发送 HalCash 代码。\n\n请确保不要超过银行允许您用半现金汇款的最高金额。每次取款的最低金额是 10 欧元,最高金额是 10 欧元。金额是 600 欧元。对于重复取款,每天每个接收者 3000 欧元,每月每个接收者 6000 欧元。请与您的银行核对这些限额,以确保它们使用与此处所述相同的限额。\n\n提现金额必须是 10 欧元的倍数,因为您不能从 ATM 机提取其他金额。 创建报价和下单屏幕中的 UI 将调整 BTC 金额,使 EUR 金额正确。你不能使用基于市场的价格,因为欧元的数量会随着价格的变化而变化。\n +# suppress inspection "UnusedMessageFormatParameter" +payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Bisq 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Bisq Wiki 查看更多信息[HYPERLINK:https://bisq.wiki/Account_limits]。 +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=为了降低这一风险,Bisq 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Bisq Wiki 上查看更多:\nhttps://bisq.wiki/Account_limits + +payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 + +payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不是像以往的电话号码或电子邮件。 +payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名''以更新您的帐户数据。\n这不会影响您的账龄验证状态。 +payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 + +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=在 Bisq 上交易 US Postal Money Orders (USPMO)您必须理解下述条款:\n\n- BTC 买方必须在发送方和收款人字段中都写上 BTC 卖方的名称,并在发送之前对 USPMO 和信封进行高分辨率照片拍照,并带有跟踪证明。\n- BTC 买方必须将 USPMO 连同交货确认书一起发送给 BTC 卖方。\n\n如果需要调解,或有交易纠纷,您将需要将照片连同 USPMO 编号,邮局编号和交易金额一起发送给 Bisq 调解员或退款代理,以便他们进行验证美国邮局网站上的详细信息。\n\n如未能提供要求的交易数据将在纠纷中直接判负\n\n在所有争议案件中,USPMO 发送方在向调解人或仲裁员提供证据/证明时承担 100% 的责任。\n\n如果您不理解这些要求,请不要在 Bisq 上使用 USPMO 进行交易。 + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=联系方式 +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=联系方式 +payment.f2f.contact.prompt=您希望如何与交易伙伴联系?(电子邮箱、电话号码、…) +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=与网上交易相比,“面对面”交易有不同的规则,也有不同的风险。\n\n主要区别是:\n●交易伙伴需要使用他们提供的联系方式交换关于会面地点和时间的信息。\n●交易双方需要携带笔记本电脑,在会面地点确认“已发送付款”和“已收到付款”。\n●如果交易方有特殊的“条款和条件”,他们必须在账户的“附加信息”文本框中声明这些条款和条件。\n●在发生争议时,调解员或仲裁员不能提供太多帮助,因为通常很难获得有关会面上所发生情况的篡改证据。在这种情况下,BTC 资金可能会被无限期锁定,或者直到交易双方达成协议。\n\n为确保您完全理解“面对面”交易的不同之处,请阅读以下说明和建议:“https://docs.bisq.network/trading-rules.html#f2f-trading” +payment.f2f.info.openURL=打开网页 +payment.f2f.offerbook.tooltip.countryAndCity=国家或地区及城市:{0} / {1} +payment.f2f.offerbook.tooltip.extra=附加信息:{0} + +payment.japan.bank=银行 +payment.japan.branch=分行 +payment.japan.account=账户 +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=国内银行转账 +SAME_BANK=同银行转账 +SPECIFIC_BANKS=转到指定银行 +US_POSTAL_MONEY_ORDER=美国邮政汇票 +CASH_DEPOSIT=现金/ATM 存款 +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=西联汇款 +F2F=面对面(当面交易) +JAPAN_BANK=日本银行汇款 +AUSTRALIA_PAYID=澳大利亚 PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=国内银行 +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=相同银行 +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=指定银行 +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=美国汇票 +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=现金/ATM 存款 +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=西联汇款 +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=支付 ID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam(N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=支付宝 +# suppress inspection "UnusedProperty" +WECHAT_PAY=微信支付 +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA 即时支付 +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=更快的支付方式 +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle(ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=数字货币 +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=亚马逊电子礼品卡 +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam(N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=支付宝 +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=微信支付 +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=更快的支付方式 +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=数字货币 +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=亚马逊电子礼品卡 +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=不允许留空。 +validation.NaN=输入的不是有效数字。 +validation.notAnInteger=输入的不是整数。 +validation.zero=不允许输入0。 +validation.negative=不允许输入负值。 +validation.fiat.toSmall=不允许输入比最小可能值还小的数值。 +validation.fiat.toLarge=不允许输入比最大可能值还大的数值。 +validation.btc.fraction=此充值将会产生小于 1 聪的比特币数量。 +validation.btc.toLarge=不允许充值大于{0} +validation.btc.toSmall=不允许充值小于{0} +validation.passwordTooShort=你输入的密码太短。最少 8 个字符。 +validation.passwordTooLong=你输入的密码太长。最长不要超过50个字符。 +validation.sortCodeNumber={0} 必须由 {1} 个数字构成。 +validation.sortCodeChars={0} 必须由 {1} 个字符构成。 +validation.bankIdNumber={0} 必须由 {1} 个数字构成。 +validation.accountNr=账号必须由 {0} 个数字构成。 +validation.accountNrChars=账户必须由 {0} 个字符构成。 +validation.btc.invalidAddress=地址不正确,请检查地址格式。 +validation.integerOnly=请输入整数。 +validation.inputError=您的输入引起了错误:\n{0} +validation.bsq.insufficientBalance=您的可用钱包余额为 {0}。 +validation.btc.exceedsMaxTradeLimit=您的交易限额为 {0}。 +validation.bsq.amountBelowMinAmount=最小金额为 {0} +validation.nationalAccountId={0} 必须由{1}个数字组成。 + +#new +validation.invalidInput=输入无效:{0} +validation.accountNrFormat=帐号必须是格式:{0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=地址验证失败,因为它与 {0} 地址的结构不匹配。 +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ 地址需要以 L 开头。 不支持以 Z 开头的地址。 +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=LTZ 地址需要以 L 开头。 不支持以 Z 开头的地址。 +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=这个地址不是有效的{0}地址!{1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=不支持本地 segwit 地址(以“lq”开头的地址)。 +validation.bic.invalidLength=输入长度既不是 8 也不是 11 +validation.bic.letters=必须输入银行和国家或地区代码 +validation.bic.invalidLocationCode=BIC 包含无效的地址代码 +validation.bic.invalidBranchCode=BIC 包含无效的分行代码 +validation.bic.sepaRevolutBic=不支持 Revolut Sepa 账户 +validation.btc.invalidFormat=无效格式的比特币地址 +validation.bsq.invalidFormat=无效格式的 BSQ 地址 +validation.email.invalidAddress=无效地址 +validation.iban.invalidCountryCode=国家或地区代码无效 +validation.iban.checkSumNotNumeric=校验必须是数字 +validation.iban.nonNumericChars=检测到非字母数字字符 +validation.iban.checkSumInvalid=IBAN 校验无效 +validation.iban.invalidLength=数字的长度必须为15到34个字符。 +validation.interacETransfer.invalidAreaCode=非加拿大区号 +validation.interacETransfer.invalidPhone=请输入可用的 11 为电话号码(例如 1-123-456-7890)或邮箱地址 +validation.interacETransfer.invalidQuestion=必须只包含字母、数字、空格和/或符号“_ , . ? -” +validation.interacETransfer.invalidAnswer=必须是一个单词,只包含字母、数字和/或符号- +validation.inputTooLarge=输入不能大于 {0} +validation.inputTooSmall=输入必须大于 {0} +validation.inputToBeAtLeast=输入必须至少为 {0} +validation.amountBelowDust=不允许低于 {0} 聪的零头限制。 +validation.length=长度必须在 {0} 和 {1} 之间 +validation.fixedLength=Length must be {0} +validation.pattern=输入格式必须为:{0} +validation.noHexString=输入不是十六进制格式。 +validation.advancedCash.invalidFormat=必须是有效的电子邮箱或钱包 ID 的格式为:X000000000000 +validation.invalidUrl=输入的不是有效 URL 链接。 +validation.mustBeDifferent=您输入的值必须与当前值不同 +validation.cannotBeChanged=参数不能更改 +validation.numberFormatException=数字格式异常 {0} +validation.mustNotBeNegative=不能输入负值 +validation.phone.missingCountryCode=需要两个字母的国家或地区代码来验证电话号码 +validation.phone.invalidCharacters=电话号码 {0} 包含无效字符 +validation.phone.insufficientDigits={0} 中没有足够的数字作为有效的电话号码 +validation.phone.tooManyDigits={0} 中的数字太多,不是有效的电话号码 +validation.phone.invalidDialingCode=数字 {0} 中的国际拨号代码对于 {1} 无效。正确的拨号号码是 {2} 。 +validation.invalidAddressList=使用逗号分隔有效地址列表 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties new file mode 100644 index 0000000000..c8e89d15eb --- /dev/null +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -0,0 +1,2967 @@ +# Keep display strings organized by domain +# Naming convention: We use camelCase and dot separated name spaces. +# Use as many sub spaces as required to make the structure clear, but as little as possible. +# E.g.: [main-view].[component].[description] +# In some cases we use enum values or constants to map to display strings + +# A annoying issue with property files is that we need to use 2 single quotes in display string +# containing variables (e.g. {0}), otherwise the variable will not be resolved. +# In display string which do not use a variable a single quote is ok. +# E.g. Don''t .... {1} + +# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces +# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! +# To make longer strings with better readable you can make a line break with \ which does not result in a line break +# in the display but only in the editor. + +# Please use in all language files the exact same order of the entries, that way a comparison is easier. + +# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or +# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. + + +#################################################################### +# Shared +#################################################################### + +shared.readMore=閲讀更多 +shared.openHelp=打開幫助 +shared.warning=警吿 +shared.close=關閉 +shared.cancel=取消 +shared.ok=好的 +shared.yes=是 +shared.no=否 +shared.iUnderstand=我瞭解 +shared.na=N/A +shared.shutDown=完全關閉 +shared.reportBug=在 Github 報吿錯誤 +shared.buyBitcoin=買入比特幣 +shared.sellBitcoin=賣出比特幣 +shared.buyCurrency=買入 {0} +shared.sellCurrency=賣出 {0} +shared.buyingBTCWith=用 {0} 買入 BTC +shared.sellingBTCFor=賣出 BTC 為 {0} +shared.buyingCurrency=買入 {0}(賣出 BTC) +shared.sellingCurrency=賣出 {0}(買入 BTC) +shared.buy=買 +shared.sell=賣 +shared.buying=買入 +shared.selling=賣出 +shared.P2P=P2P +shared.oneOffer=報價 +shared.multipleOffers=報價 +shared.Offer=報價 +shared.offerVolumeCode={0} 報價量 +shared.openOffers=可用報價 +shared.trade=交易 +shared.trades=交易 +shared.openTrades=進行中的交易 +shared.dateTime=日期/時間 +shared.price=價格 +shared.priceWithCur={0} 價格 +shared.priceInCurForCur=1 {1} 的 {0} 價格 +shared.fixedPriceInCurForCur=1 {1} 的 {0} 的固定價格 +shared.amount=數量 +shared.txFee=交易手續費 +shared.tradeFee=交易手續費 +shared.buyerSecurityDeposit=買家保證金 +shared.sellerSecurityDeposit=賣家保證金 +shared.amountWithCur={0} 數量 +shared.volumeWithCur={0} 總量 +shared.currency=貨幣類型 +shared.market=交易項目 +shared.deviation=Deviation +shared.paymentMethod=付款方式 +shared.tradeCurrency=交易貨幣 +shared.offerType=報價類型 +shared.details=詳情 +shared.address=地址 +shared.balanceWithCur={0} 餘額 +shared.utxo=Unspent transaction output +shared.txId=交易記錄 ID +shared.confirmations=審核 +shared.revert=還原 Tx +shared.select=選擇 +shared.usage=使用狀況 +shared.state=狀態 +shared.tradeId=交易 ID +shared.offerId=報價 ID +shared.bankName=銀行名稱 +shared.acceptedBanks=接受的銀行 +shared.amountMinMax=總額(最小 - 最大) +shared.amountHelp=如果報價包含最小和最大限制,那麼您可以在這個範圍內的任意數量進行交易。 +shared.remove=移除 +shared.goTo=前往 {0} +shared.BTCMinMax=BTC(最小 - 最大) +shared.removeOffer=移除報價 +shared.dontRemoveOffer=不要移除報價 +shared.editOffer=編輯報價 +shared.openLargeQRWindow=打開放大二維碼窗口 +shared.tradingAccount=交易賬户 +shared.faq=訪問 FAQ 頁面 +shared.yesCancel=是的,取消 +shared.nextStep=下一步 +shared.selectTradingAccount=選擇交易賬户 +shared.fundFromSavingsWalletButton=從 Bisq 錢包資金劃轉 +shared.fundFromExternalWalletButton=從您的外部錢包充值 +shared.openDefaultWalletFailed=打開默認的比特幣錢包應用程序失敗了。您確定您安裝了嗎? +shared.belowInPercent=低於市場價格 % +shared.aboveInPercent=高於市場價格 % +shared.enterPercentageValue=輸入 % 值 +shared.OR=或者 +shared.notEnoughFunds=您的 Bisq 錢包中沒有足夠的資金去支付這一交易 需要{0} 您可用餘額為 {1}。\n\n請從外部比特幣錢包注入資金或在“資金/存款”充值到您的 Bisq 錢包。 +shared.waitingForFunds=等待資金充值... +shared.depositTransactionId=存款交易 ID +shared.TheBTCBuyer=BTC 買家 +shared.You=您 +shared.sendingConfirmation=發送確認... +shared.sendingConfirmationAgain=請再次發送確認 +shared.exportCSV=導出保存為 .csv +shared.exportJSON=導出保存至 JSON +shared.summary=Show summary +shared.noDateAvailable=沒有可用數據 +shared.noDetailsAvailable=沒有可用詳細 +shared.notUsedYet=尚未使用 +shared.date=日期 +shared.sendFundsDetailsWithFee=發送:{0}\n來自:{1}\n接收地址:{2}\n要求的最低交易費:{3}({4} 聰/byte)\n交易大小:{5} Kb\n\n收款方將收到:{6}\n\n您確定您想要提現嗎? +# suppress inspection "TrailingSpacesInProperty" +shared.sendFundsDetailsDust=Bisq 檢測到,該交易將產生一個低於最低零頭閾值的輸出(不被比特幣共識規則所允許)。相反,這些零頭({0}satoshi{1})將被添加到挖礦手續費中。 +shared.copyToClipboard=複製到剪貼板 +shared.language=語言 +shared.country=國家或地區 +shared.applyAndShutDown=同意並關閉 +shared.selectPaymentMethod=選擇付款方式 +shared.accountNameAlreadyUsed=這個賬户名稱已經被已保存的賬户佔用。\n請使用另外一個名稱。 +shared.askConfirmDeleteAccount=您確定想要刪除被選定的賬號嗎? +shared.cannotDeleteAccount=您不能刪除這個賬户,因為它正在被使用於報價或交易中。 +shared.noAccountsSetupYet=還沒有建立帳户。 +shared.manageAccounts=管理賬户 +shared.addNewAccount=添加新的賬户 +shared.ExportAccounts=導出賬户 +shared.importAccounts=導入賬户 +shared.createNewAccount=創建新的賬户 +shared.saveNewAccount=保存新的賬户 +shared.selectedAccount=選中的賬户 +shared.deleteAccount=刪除賬户 +shared.errorMessageInline=\n錯誤信息:{0} +shared.errorMessage=錯誤信息 +shared.information=資料 +shared.name=名稱 +shared.id=ID +shared.dashboard=儀表盤 +shared.accept=接受 +shared.balance=餘額 +shared.save=保存 +shared.onionAddress=匿名地址 +shared.supportTicket=幫助話題 +shared.dispute=糾紛 +shared.mediationCase=調解事件 +shared.seller=賣家 +shared.buyer=買家 +shared.allEuroCountries=所有歐元國家 +shared.acceptedTakerCountries=接受的買家國家 +shared.tradePrice=交易價格 +shared.tradeAmount=交易金額 +shared.tradeVolume=交易總量 +shared.invalidKey=您輸入的密碼不正確。 +shared.enterPrivKey=輸入私鑰解鎖 +shared.makerFeeTxId=掛單費交易 ID +shared.takerFeeTxId=買單費交易 ID +shared.payoutTxId=支出交易 ID +shared.contractAsJson=JSON 格式的合同 +shared.viewContractAsJson=查看 JSON 格式的合同 +shared.contract.title=交易 ID:{0} 的合同 +shared.paymentDetails=BTC {0} 支付詳情 +shared.securityDeposit=保證金 +shared.yourSecurityDeposit=你的保證金 +shared.contract=合同 +shared.messageArrived=消息送達。 +shared.messageStoredInMailbox=消息保存在郵箱中。 +shared.messageSendingFailed=消息發送失敗。錯誤:{0} +shared.unlock=解鎖 +shared.toReceive=接收 +shared.toSpend=花費 +shared.btcAmount=BTC 總額 +shared.yourLanguage=你的語言 +shared.addLanguage=添加語言 +shared.total=合計 +shared.totalsNeeded=需要資金 +shared.tradeWalletAddress=交易錢包地址 +shared.tradeWalletBalance=交易錢包餘額 +shared.makerTxFee=賣家:{0} +shared.takerTxFee=買家:{0} +shared.iConfirm=我確認 +shared.tradingFeeInBsqInfo=≈ {0} +shared.openURL=打開 {0} +shared.fiat=法定貨幣 +shared.crypto=加密 +shared.all=全部 +shared.edit=編輯 +shared.advancedOptions=高級選項 +shared.interval=取消 +shared.actions=操作 +shared.buyerUpperCase=買家 +shared.sellerUpperCase=買家 +shared.new=新 +shared.blindVoteTxId=匿名投票交易 ID +shared.proposal=建議 +shared.votes=投票 +shared.learnMore=瞭解更多 +shared.dismiss=忽略 +shared.selectedArbitrator=選中的仲裁者 +shared.selectedMediator=選擇調解員 +shared.selectedRefundAgent=選中的仲裁者 +shared.mediator=調解員 +shared.arbitrator=仲裁員 +shared.refundAgent=仲裁員 +shared.refundAgentForSupportStaff=退款助理 +shared.delayedPayoutTxId=延遲支付交易 ID +shared.delayedPayoutTxReceiverAddress=延遲交易交易已發送至 +shared.unconfirmedTransactionsLimitReached=你現在有過多的未確認交易。請稍後嘗試 +shared.numItemsLabel=Number of entries: {0} +shared.filter=Filter +shared.enabled=啟用 + + +#################################################################### +# UI views +#################################################################### + +#################################################################### +# MainView +#################################################################### + +mainView.menu.market=交易項目 +mainView.menu.buyBtc=買入 BTC +mainView.menu.sellBtc=賣出 BTC +mainView.menu.portfolio=業務 +mainView.menu.funds=資金 +mainView.menu.support=幫助 +mainView.menu.settings=設置 +mainView.menu.account=賬户 +mainView.menu.dao=DAO + +mainView.marketPriceWithProvider.label=交易所價格提供商:{0} +mainView.marketPrice.bisqInternalPrice=最新 Bisq 交易的價格 +mainView.marketPrice.tooltip.bisqInternalPrice=外部交易所供應商沒有可用的市場價格。\n顯示的價格是該貨幣的最新 Bisq 交易價格。 +mainView.marketPrice.tooltip=交易所價格提供者 {0}{1}\n最後更新:{2}\n提供者節點 URL:{3} +mainView.balance.available=可用餘額 +mainView.balance.reserved=保證金 +mainView.balance.locked=凍結餘額 +mainView.balance.reserved.short=保證 +mainView.balance.locked.short=凍結 + +mainView.footer.usingTor=(via Tor) +mainView.footer.localhostBitcoinNode=(本地主機) +mainView.footer.btcInfo={0} {1} +mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB +mainView.footer.btcInfo.initializing=連接至比特幣網絡 +mainView.footer.bsqInfo.synchronizing=正在同步 DAO +mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} +mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1} +mainView.footer.btcInfo.connectingTo=連接至 +mainView.footer.btcInfo.connectionFailed=連接失敗: +mainView.footer.p2pInfo=比特幣網絡節點:{0} / Bisq 網絡節點:{1} +mainView.footer.daoFullNode=DAO 全節點 + +mainView.bootstrapState.connectionToTorNetwork=(1/4) 連接至 Tor 網絡... +mainView.bootstrapState.torNodeCreated=(2/4) Tor 節點已創建 +mainView.bootstrapState.hiddenServicePublished=(3/4) 隱藏的服務已發佈 +mainView.bootstrapState.initialDataReceived=(4/4) 初始數據已接收 + +mainView.bootstrapWarning.noSeedNodesAvailable=沒有可用的種子節點 +mainView.bootstrapWarning.noNodesAvailable=沒有可用的種子節點和節點 +mainView.bootstrapWarning.bootstrappingToP2PFailed=啟動 Bisq 網絡失敗 + +mainView.p2pNetworkWarnMsg.noNodesAvailable=沒有可用種子節點或永久節點可請求數據。\n請檢查您的互聯網連接或嘗試重啟應用程序。 +mainView.p2pNetworkWarnMsg.connectionToP2PFailed=連接至 Bisq 網絡失敗(錯誤報吿:{0})。\n請檢查您的互聯網連接或嘗試重啟應用程序。 + +mainView.walletServiceErrorMsg.timeout=比特幣網絡連接超時。 +mainView.walletServiceErrorMsg.connectionError=錯誤:{0} 比特幣網絡連接失敗。 + +mainView.walletServiceErrorMsg.rejectedTxException=交易被網絡拒絕。\n\n{0} + +mainView.networkWarning.allConnectionsLost=您失去了所有與 {0} 網絡節點的連接。\n您失去了互聯網連接或您的計算機處於待機狀態。 +mainView.networkWarning.localhostBitcoinLost=您丟失了與本地主機比特幣節點的連接。\n請重啟 Bisq 應用程序連接到其他比特幣節點或重新啟動主機比特幣節點。 +mainView.version.update=(有更新可用) + + +#################################################################### +# MarketView +#################################################################### + +market.tabs.offerBook=報價列表 +market.tabs.spreadCurrency=Offers by Currency +market.tabs.spreadPayment=Offers by Payment Method +market.tabs.trades=行情圖 + +# OfferBookChartView +market.offerBook.buyAltcoin=我想要買入 {0}(賣出 {1}) +market.offerBook.sellAltcoin=我想要賣出 {0}(買入 {1}) +market.offerBook.buyWithFiat=購買 {0} +market.offerBook.sellWithFiat=出售 {0} +market.offerBook.sellOffersHeaderLabel=出售 {0} 到 +market.offerBook.buyOffersHeaderLabel=購買 {0} 以 +market.offerBook.buy=我想要買入比特幣 +market.offerBook.sell=我想要賣出比特幣 + +# SpreadView +market.spread.numberOfOffersColumn=所有報價({0}) +market.spread.numberOfBuyOffersColumn=買入 BTC({0}) +market.spread.numberOfSellOffersColumn=賣出 BTC({0}) +market.spread.totalAmountColumn=總共 BTC({0}) +market.spread.spreadColumn=差價 +market.spread.expanded=Expanded view + +# TradesChartsView +market.trades.nrOfTrades=交易:{0} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} +market.trades.tooltip.candle.open=打開: +market.trades.tooltip.candle.close=關閉: +market.trades.tooltip.candle.high=高: +market.trades.tooltip.candle.low=低: +market.trades.tooltip.candle.average=平均: +market.trades.tooltip.candle.median=調解員: +market.trades.tooltip.candle.date=日期: +market.trades.showVolumeInUSD=Show volume in USD + +#################################################################### +# OfferView +#################################################################### + +offerbook.createOffer=創建報價 +offerbook.takeOffer=接受報價 +offerbook.takeOfferToBuy=接受報價來收購 {0} +offerbook.takeOfferToSell=接受報價來出售 {0} +offerbook.trader=商人 +offerbook.offerersBankId=賣家的銀行 ID(BIC/SWIFT):{0} +offerbook.offerersBankName=賣家的銀行名稱:{0} +offerbook.offerersBankSeat=賣家的銀行所在國家或地區:{0} +offerbook.offerersAcceptedBankSeatsEuro=接受的銀行所在國家(買家):所有歐元國家 +offerbook.offerersAcceptedBankSeats=接受的銀行所在國家(買家):\n {0} +offerbook.availableOffers=可用報價 +offerbook.filterByCurrency=以貨幣篩選 +offerbook.filterByPaymentMethod=以支付方式篩選 +offerbook.matchingOffers=Offers matching my accounts +offerbook.timeSinceSigning=賬户信息 +offerbook.timeSinceSigning.info=此賬户已驗證,{0} +offerbook.timeSinceSigning.info.arbitrator=由仲裁員驗證,並可以驗證夥伴賬户 +offerbook.timeSinceSigning.info.peer=由對方驗證,等待%d天限制被解除 +offerbook.timeSinceSigning.info.peerLimitLifted=由對方驗證,限制被取消 +offerbook.timeSinceSigning.info.signer=由對方驗證,並可驗證對方賬户(限制已取消) +offerbook.timeSinceSigning.info.banned=賬户已被封禁 +offerbook.timeSinceSigning.daysSinceSigning={0} 天 +offerbook.timeSinceSigning.daysSinceSigning.long=自驗證{0} +offerbook.xmrAutoConf=是否開啟自動確認 + +offerbook.timeSinceSigning.help=當您成功地完成與擁有已驗證付款帳户的夥伴交易時,您的付款帳户已驗證。\n{0} 天后,最初的 {1} 的限制解除以及你的賬户可以驗證其他人的付款賬户。 +offerbook.timeSinceSigning.notSigned=尚未驗證 +offerbook.timeSinceSigning.notSigned.ageDays={0} 天 +offerbook.timeSinceSigning.notSigned.noNeed=N/A +shared.notSigned=此賬户還沒有被驗證以及在{0}前創建 +shared.notSigned.noNeed=此賬户類型不適用驗證 +shared.notSigned.noNeedDays=此賬户類型不適用驗證且在{0}天創建 +shared.notSigned.noNeedAlts=數字貨幣不適用賬齡與簽名 + +offerbook.nrOffers=報價數量:{0} +offerbook.volume={0}(最小 - 最大) +offerbook.deposit=BTC 保證金(%) +offerbook.deposit.help=交易雙方均已支付保證金確保這個交易正常進行。這會在交易完成時退還。 + +offerbook.createOfferToBuy=創建新的報價來買入 {0} +offerbook.createOfferToSell=創建新的報價來賣出 {0} +offerbook.createOfferToBuy.withFiat=創建新的報價用 {1} 購買 {0} +offerbook.createOfferToSell.forFiat=創建新的報價以 {1} 出售 {0} +offerbook.createOfferToBuy.withCrypto=創建新的賣出報價 {0} (買入 {1}) +offerbook.createOfferToSell.forCrypto=創建新的買入報價 {0}(賣出 {1}) + +offerbook.takeOfferButton.tooltip=下單買入 {0} +offerbook.yesCreateOffer=是的,創建報價 +offerbook.setupNewAccount=設置新的交易賬户 +offerbook.removeOffer.success=撤銷報價成功。 +offerbook.removeOffer.failed=撤銷報價失敗:\n{0} +offerbook.deactivateOffer.failed=報價停用失敗:\n{0} +offerbook.activateOffer.failed=報價發佈失敗:\n{0} +offerbook.withdrawFundsHint=您可以從 {0} 中撤回您支付的資金。 + +offerbook.warning.noTradingAccountForCurrency.headline=選擇的貨幣沒有支付賬户 +offerbook.warning.noTradingAccountForCurrency.msg=您選擇的貨幣還沒有建立支付賬户。\n\n你想要用其他貨幣創建一個報價嗎? +offerbook.warning.noMatchingAccount.headline=沒有匹配的支付賬户。 +offerbook.warning.noMatchingAccount.msg=這個報價使用了您未創建過的支付方式。\n\n你現在想要創建一個新的支付賬户嗎? + +offerbook.warning.counterpartyTradeRestrictions=由於交易夥伴的交易限制,這個報價不能接受 + +offerbook.warning.newVersionAnnouncement=使用這個版本的軟件,交易夥伴可以驗證和驗證彼此的支付帳户,以創建一個可信的支付帳户網絡。\n\n交易成功後,您的支付帳户將被驗證以及交易限制將在一定時間後解除(此時間基於驗證方法)。\n\n有關驗證帳户的更多信息,請參見文檔 https://docs.bisq.network/payment-methods#account-signing + +popup.warning.tradeLimitDueAccountAgeRestriction.seller=基於以下標準的安全限制,允許的交易金額限制為 {0}:\n- 買方的帳目沒有由仲裁員或夥伴驗證\n- 買方帳户自驗證之日起不足30天\n- 本報價的付款方式被認為存在銀行退款的風險\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=基於以下標準的安全限制,允許的交易金額限制為{0}:\n- 你的買家帳户沒有由仲裁員或夥伴驗證\n- 自驗證你的帳户以來的時間少於30天\n- 本報價的付款方式被認為存在銀行退款的風險\n\n{1} + +offerbook.warning.wrongTradeProtocol=該報價要求的軟件版本與您現在運行的版本不一致。\n\n請檢查您是否運行最新版本,或者是該報價用户在使用一箇舊的版本。\n用户不能與不兼容的交易協議版本進行交易。 +offerbook.warning.userIgnored=您已添加該用户的匿名地址在您的忽略列表裏。 +offerbook.warning.offerBlocked=該報價被 Bisq 開發人員限制。\n接受該報價時,可能有一個未處理的漏洞導致了問題。 +offerbook.warning.currencyBanned=該報價中使用的貨幣被 Bisq 開發人員阻止。\n請訪問 Bisq 論壇瞭解更多信息。 +offerbook.warning.paymentMethodBanned=該報價中使用的付款方式被 Bisq 開發人員阻止。\n請訪問 Bisq 論壇瞭解更多信息。 +offerbook.warning.nodeBlocked=該交易者的匿名地址被 Bisq 開發人員限制。\n當獲取來自該交易者的報價,可能有一個未處理的漏洞導致了問題。 +offerbook.warning.requireUpdateToNewVersion=您的 Bisq 版本不再兼容交易。\n請通過 https://bisq.network/downloads 更新到最新的 Bisq 版本。 +offerbook.warning.offerWasAlreadyUsedInTrade=您不能吃單因為您已經完成了該操作。可能是你之前的吃單嘗試導致了交易失敗。 + +offerbook.info.sellAtMarketPrice=您會以市場價格進行出售(每分鐘更新) +offerbook.info.buyAtMarketPrice=您將以市場價格進行購買(每分鐘更新)。 +offerbook.info.sellBelowMarketPrice=您將以低於市場價 {0} 的價格進行出售(每分鐘更新) +offerbook.info.buyAboveMarketPrice=您將以高於市場價 {0} 的價格進行支付(每分鐘更新) +offerbook.info.sellAboveMarketPrice=您將以高於市場價 {0} 的價格進行出售(每分鐘更新) +offerbook.info.buyBelowMarketPrice=您將以低於市場價 {0} 的價格進行支付(每分鐘更新) +offerbook.info.buyAtFixedPrice=您會以這個固定價格購買。 +offerbook.info.sellAtFixedPrice=您會以這個固定價格出售。 +offerbook.info.noArbitrationInUserLanguage=如有任何爭議,請注意此報價的仲裁將在 {0} 內處理。語言目前設置為{1}。 +offerbook.info.roundedFiatVolume=金額四捨五入是為了增加您的交易隱私。 + +#################################################################### +# Offerbook / Create offer +#################################################################### + +createOffer.amount.prompt=輸入 BTC 數量 +createOffer.price.prompt=輸入價格 +createOffer.volume.prompt=輸入 {0} 金額 +createOffer.amountPriceBox.amountDescription=比特幣數量 {0} +createOffer.amountPriceBox.buy.volumeDescription=花費 {0} 數量 +createOffer.amountPriceBox.sell.volumeDescription=接收 {0} 數量 +createOffer.amountPriceBox.minAmountDescription=最小 BTC 數量 +createOffer.securityDeposit.prompt=保證金 +createOffer.fundsBox.title=為您的報價充值 +createOffer.fundsBox.offerFee=掛單費 +createOffer.fundsBox.networkFee=礦工手續費 +createOffer.fundsBox.placeOfferSpinnerInfo=正在發佈報價中... +createOffer.fundsBox.paymentLabel=Bisq 交易 ID {0} +createOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) +createOffer.fundsBox.fundsStructure.BSQ=({0} 保證金,{1} 採礦費)+ {2} 交易費 +createOffer.success.headline=你的報價已經發布 +createOffer.success.info=你可以在“業務/未完成報價”頁面內管理您的未完成報價。 +createOffer.info.sellAtMarketPrice=由於您的價格是持續更新的,因此您將始終以市場價格進行出售。 +createOffer.info.buyAtMarketPrice=由於您的價格是持續更新的,因此您將始終以市場價格進行購買。 +createOffer.info.sellAboveMarketPrice=由於您的價格是持續更新的,因此您將始終按照高於市場價 {0}% 的價格出售。 +createOffer.info.buyBelowMarketPrice=由於您的價格是持續更新的,因此您將始終支付低於市場價 {0}% 的價格。 +createOffer.warning.sellBelowMarketPrice=由於您的價格是持續更新的,因此您將始終按照低於市場價 {0}% 的價格出售。 +createOffer.warning.buyAboveMarketPrice=由於您的價格是持續更新的,因此您將始終支付高於市場價 {0}% 的價格。 +createOffer.tradeFee.descriptionBTCOnly=掛單費 +createOffer.tradeFee.descriptionBSQEnabled=選擇手續費幣種 + +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which 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} + +# new entries +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- 使用您的 Bisq 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 + +# 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請檢查您的互聯網連接或嘗試重啟應用程序。 +createOffer.setAmountPrice=設置數量和價格 +createOffer.warnCancelOffer=您已經為該報價充值了。\n如果您想立即取消,您的資金將劃轉到您的本地 Bisq 錢包並在“資金/提現”界面可以提現。\n您確定要取消嗎? +createOffer.timeoutAtPublishing=發佈報價時產生了一個錯誤。 +createOffer.errorInfo=\n\n掛單費已經支付,在最壞的情況下,你會失去了這筆費用。 我們很抱歉,但請記住,這是一個很小的數量。\n請嘗試重新啟動應用程序並檢查您的網絡連接,看看是否可以解決問題。 +createOffer.tooLowSecDeposit.warning=您設置的保證金低於推薦默認值 {0}。\n您確定要使用較低的保證金嗎? +createOffer.tooLowSecDeposit.makerIsSeller=在交易對手不遵循交易協議時,這給予您較少的保護。 +createOffer.tooLowSecDeposit.makerIsBuyer=對於遵守交易協議的交易對象,您的保護金額較低,因為風險存款較小。 其他用户可能更喜歡採取其他報價,而不是您的。 +createOffer.resetToDefault=不,恢復默認值 +createOffer.useLowerValue=是的,使用我較低的值 +createOffer.priceOutSideOfDeviation=您輸入的價格超過了市場價差價的最大值。\n最大值為 {0},您可以在偏好中進行調整。 +createOffer.changePrice=改變價格 +createOffer.tac=發佈該報價,我同意與滿足該條件的任何交易者進行交易。 +createOffer.currencyForFee=掛單費 +createOffer.setDeposit=設置買家的保證金(%) +createOffer.setDepositAsBuyer=設置自己作為買家的保證金(%) +createOffer.setDepositForBothTraders=設置雙方的保證金比例(%) +createOffer.securityDepositInfo=您的買家的保證金將會是 {0} +createOffer.securityDepositInfoAsBuyer=您作為買家的保證金將會是 {0} +createOffer.minSecurityDepositUsed=已使用最低買家保證金 + + +#################################################################### +# Offerbook / Take offer +#################################################################### + +takeOffer.amount.prompt=輸入 BTC 數量 +takeOffer.amountPriceBox.buy.amountDescription=賣出比特幣數量 +takeOffer.amountPriceBox.sell.amountDescription=買入比特幣數量 +takeOffer.amountPriceBox.priceDescription=每個比特幣的 {0} 價格 +takeOffer.amountPriceBox.amountRangeDescription=可用數量範圍 +takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=你輸入的數量超過允許的小數位數。\n數量已被調整為4位小數。 +takeOffer.validation.amountSmallerThanMinAmount=數量不能比報價內設置的最小數量小。 +takeOffer.validation.amountLargerThanOfferAmount=數量不能比報價提供的總量大。 +takeOffer.validation.amountLargerThanOfferAmountMinusFee=該輸入數量可能會給賣家造成比特幣碎片。 +takeOffer.fundsBox.title=為交易充值 +takeOffer.fundsBox.isOfferAvailable=檢查報價是否有效... +takeOffer.fundsBox.tradeAmount=賣出數量 +takeOffer.fundsBox.offerFee=掛單費 +takeOffer.fundsBox.networkFee=總共挖礦手續費 +takeOffer.fundsBox.takeOfferSpinnerInfo=正在下單... +takeOffer.fundsBox.paymentLabel=Bisq 交易 ID {0} +takeOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) +takeOffer.success.headline=你已成功下單一個報價。 +takeOffer.success.info=你可以在“業務/未完成交易”頁面內查看您的未完成交易。 +takeOffer.error.message=下單時發生了一個錯誤。\n\n{0} + +# new entries +takeOffer.takeOfferButton=複審:報價下單 {0} 比特幣 +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- 礦工手續費:{4}\n\n您有兩種選項可以充值您的交易:\n- 使用您的 Bisq 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 +takeOffer.alreadyPaidInFunds=如果你已經支付,你可以在“資金/提現”提現它。 +takeOffer.paymentInfo=付款信息 +takeOffer.setAmountPrice=設置數量 +takeOffer.alreadyFunded.askCancel=您已經為該報價充值了。\n如果您想立即取消,您的資金將劃轉到您的本地 Bisq 錢包並在“資金/提現”界面可以提現。\n您確定要取消嗎? +takeOffer.failed.offerNotAvailable=請求失敗,由於報價不再可用。 也許有交易者在此期間已經下單。 +takeOffer.failed.offerTaken=您不能對該報價下單,因為該報價已經被其他交易者下單。 +takeOffer.failed.offerRemoved=您不能對該報價下單,因為該報價已經在此期間被刪除。 +takeOffer.failed.offererNotOnline=下單失敗,因為賣家已經不在線。 +takeOffer.failed.offererOffline=您不能下單,因為賣家已經下線。 +takeOffer.warning.connectionToPeerLost=您與賣家失去連接。\n因為太多連接,他或許已經下線或者關掉了與您的連接。\n\n如果您還是能在報價列表中看到他的報價,您可以再次嘗試下單。 + +takeOffer.error.noFundsLost=\n\n你的錢包裏還沒有錢。 \n請嘗試重啟您的應用程序或者檢查您的網絡連接。 +# suppress inspection "TrailingSpacesInProperty" +takeOffer.error.feePaid=\n!\n +takeOffer.error.depositPublished=\n\n您的保證金轉賬已經發布。\n請嘗試重啟您的應用程序或者檢查您的網絡連接。\n如果始終存在問題,請到幫助界面聯繫開發者。 +takeOffer.error.payoutPublished=\n\n您的支付轉賬已經發布。\n請嘗試重啟您的應用程序或者檢查您的網絡連接。\n如果始終存在問題,請到幫助界面聯繫開發者。 +takeOffer.tac=接受該報價,意味着我同意這交易界面中的條件。 + + +#################################################################### +# Offerbook / Edit offer +#################################################################### + +openOffer.header.triggerPrice=觸發價格 +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price + +editOffer.setPrice=設定價格 +editOffer.confirmEdit=確認:編輯報價 +editOffer.publishOffer=發佈您的報價。 +editOffer.failed=報價編輯失敗:\n{0} +editOffer.success=您的報價已成功編輯。 +editOffer.invalidDeposit=買方保證金不符合 Bisq DAO 規定,不能再次編輯。 + +#################################################################### +# Portfolio +#################################################################### + +portfolio.tab.openOffers=我的未完成報價 +portfolio.tab.pendingTrades=未完成交易 +portfolio.tab.history=歷史記錄 +portfolio.tab.failed=失敗 +portfolio.tab.editOpenOffer=編輯報價 + +portfolio.closedTrades.deviation.help=Percentage price deviation from market + +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the fiat or altcoin payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} + +portfolio.pending.step1.waitForConf=等待區塊鏈確認 +portfolio.pending.step2_buyer.startPayment=開始付款 +portfolio.pending.step2_seller.waitPaymentStarted=等待直到付款 +portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到達 +portfolio.pending.step3_seller.confirmPaymentReceived=確定收到付款 +portfolio.pending.step5.completed=完成 + +portfolio.pending.step3_seller.autoConf.status.label=自動確認狀態。 +portfolio.pending.autoConf=自動確認 +portfolio.pending.autoConf.blocks=XMR 確認數:{0} / 需求量:{2} +portfolio.pending.autoConf.state.xmr.txKeyReused=交易密鑰已重複使用。請發起糾紛處理。 +portfolio.pending.autoConf.state.confirmations=XMR 確認:{0}/{1} +portfolio.pending.autoConf.state.txNotFound=交易並未在內存池中檢索。 +portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=無有效交易 ID / 交易密鑰 +portfolio.pending.autoConf.state.filterDisabledFeature=由開發者禁用 + +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FEATURE_DISABLED=自動確認功能已禁用。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=交易金額超過自動確認金額限制。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.INVALID_DATA=對等點提供不可用數據。{0} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支付交易已經發布 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.DISPUTE_OPENED=已發起糾紛。該交易的自動確認已被禁用 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=交易證明申請已經開始 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.PENDING=成功結果:{0}/{1} ;{2} +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.COMPLETED=所有服務都已被證明。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.ERROR=您請求的服務發生了錯誤。沒有自動確認。 +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.FAILED=服務返回失敗。沒有自動確認。 + +portfolio.pending.step1.info=存款交易已經發布。\n開始付款之前,{0} 需要等待至少一個區塊鏈確認。 +portfolio.pending.step1.warn=保證金交易仍未得到確認。這種情況可能會發生在外部錢包轉賬時使用的交易手續費用較低造成的。 +portfolio.pending.step1.openForDispute=保證金交易仍未得到確認。請聯繫調解員協助。 + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n + +portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'bitcoin', 'BTC', or 'Bisq'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. +# suppress inspection "TrailingSpacesInProperty" +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.altcoin=請從您的外部 {0} 錢包劃轉\n{1} 到 BTC 賣家。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cash=請到銀行並支付 {0} 給 BTC 賣家。\n\n +portfolio.pending.step2_buyer.cash.extra=重要要求:\n完成付款後在紙質收據上寫下:不退款。\n然後將其撕成2份,拍照片併發送給 BTC 賣家的電子郵件地址。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=請使用 MoneyGram 向 BTC 賣家支付 {0}。\n\n +portfolio.pending.step2_buyer.moneyGram.extra=重要要求:\n完成支付後,請通過電郵發送授權編號和照片給 BTC 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.westernUnion=請使用 Western Union 向 BTC 賣家支付 {0}。\n\n +portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付後,請通過電郵發送 MTCN(追蹤號碼)和照片給 BTC 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。 + +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} 給 BTC 賣家。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.f2f=請通過提供的聯繫人與 BTC 賣家聯繫,並安排會議支付 {0}。\n\n +portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 開始付款 +portfolio.pending.step2_buyer.recipientsAccountData=接受 {0} +portfolio.pending.step2_buyer.amountToTransfer=劃轉數量 +portfolio.pending.step2_buyer.sellersAddress=賣家的 {0} 地址 +portfolio.pending.step2_buyer.buyerAccount=您的付款帳户將被使用 +portfolio.pending.step2_buyer.paymentStarted=付款開始 +portfolio.pending.step2_buyer.fillInBsqWallet=Pay from BSQ wallet +portfolio.pending.step2_buyer.warn=你還沒有完成你的 {0} 付款!\n請注意,交易必須在 {1} 之前完成。 +portfolio.pending.step2_buyer.openForDispute=您還沒有完成您的付款!\n最大交易期限已過。請聯繫調解員尋求幫助。 +portfolio.pending.step2_buyer.paperReceipt.headline=您是否將紙質收據發送給 BTC 賣家? +portfolio.pending.step2_buyer.paperReceipt.msg=請牢記:\n完成付款後在紙質收據上寫下:不退款。\n然後將其撕成2份,拍照片併發送給 BTC 賣家的電子郵件地址。 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=發送授權編號和收據 +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=請通過電郵發送授權編號和照片給 BTC 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。\n\n您把授權編號和合同發給賣方了嗎? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=發送 MTCN 和收據 +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=請通過電郵發送 MTCN(追蹤號碼)和照片給 BTC 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。\n\n您把 MTCN 和合同發給賣方了嗎? +portfolio.pending.step2_buyer.halCashInfo.headline=請發送 HalCash 代碼 +portfolio.pending.step2_buyer.halCashInfo.msg=您需要向 BTC 賣家發送帶有 HalCash 代碼和交易 ID({0})的文本消息。\n\n賣方的手機號碼是 {1} 。\n\n您是否已經將代碼發送至賣家? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=有些銀行可能會要求接收方的姓名。在較舊的 Bisq 客户端創建的快速支付帳户沒有提供收款人的姓名,所以請使用交易聊天來獲得收款人姓名(如果需要)。 +portfolio.pending.step2_buyer.confirmStart.headline=確定您已經付款 +portfolio.pending.step2_buyer.confirmStart.msg=您是否向您的交易夥伴發起 {0} 付款? +portfolio.pending.step2_buyer.confirmStart.yes=是的,我已經開始付款 +portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=你沒有提供任何付款證明 +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=您還沒有輸入交易 ID 以及交易密鑰\n\n如果不提供此數據您的交易夥伴無法在收到 XMR 後使用自動確認功能以快速釋放 BTC。\n另外,Bisq 要求 XMR 發送者在發生糾紛的時候能夠向調解員和仲裁員提供這些信息。\n更多細節在 Bisq Wiki:https://bisq.wiki/Trading_Monero#Auto-confirming_trades +portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=輸入並不是一個 32 字節的哈希值 +portfolio.pending.step2_buyer.confirmStart.warningButton=忽略並繼續 +portfolio.pending.step2_seller.waitPayment.headline=等待付款 +portfolio.pending.step2_seller.f2fInfo.headline=買家的合同信息 +portfolio.pending.step2_seller.waitPayment.msg=存款交易至少有一個區塊鏈確認。\n您需要等到 BTC 買家開始 {0} 付款。 +portfolio.pending.step2_seller.warn=BTC 買家仍然沒有完成 {0} 付款。\n你需要等到他開始付款。\n如果 {1} 交易尚未完成,仲裁員將進行調查。 +portfolio.pending.step2_seller.openForDispute=BTC 買家尚未開始付款!\n允許的最長交易期限已經過去了。你可以繼續等待給予交易雙方更多時間,或聯繫仲裁員以爭取解決糾紛。 +tradeChat.chatWindowTitle=使用 ID “{0}” 進行交易的聊天窗口 +tradeChat.openChat=打開聊天窗口 +tradeChat.rules=您可以與您的夥伴溝通,以解決該交易的潛在問題。\n在聊天中不強制回覆。\n如果交易員違反了下面的任何規則,打開糾紛並向調解員或仲裁員報吿。\n聊天規則:\n\n\t●不要發送任何鏈接(有惡意軟件的風險)。您可以發送交易 ID 和區塊資源管理器的名稱。\n\t●不要發送還原密鑰、私鑰、密碼或其他敏感信息!\n\t●不鼓勵 Bisq 以外的交易(無安全保障)。\n\t●不要參與任何形式的危害社會安全的計劃。\n\t●如果對方沒有迴應,也不願意通過聊天進行溝通,那就尊重對方的決定。\n\t●將談話範圍限制在行業內。這個聊天不是一個社交軟件替代品或troll-box。\n\t●保持友好和尊重的交談。 + +# suppress inspection "UnusedProperty" +message.state.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +message.state.SENT=發出信息 +# suppress inspection "UnusedProperty" +message.state.ARRIVED=消息已抵達 +# suppress inspection "UnusedProperty" +message.state.STORED_IN_MAILBOX=已發送但尚未被對方接收的付款信息 +# suppress inspection "UnusedProperty" +message.state.ACKNOWLEDGED=對方確認消息回執 +# suppress inspection "UnusedProperty" +message.state.FAILED=發送消息失敗 + +portfolio.pending.step3_buyer.wait.headline=等待 BTC 賣家付款確定 +portfolio.pending.step3_buyer.wait.info=等待 BTC 賣家確認收到 {0} 付款。 +portfolio.pending.step3_buyer.wait.msgStateInfo.label=支付開始消息狀態 +portfolio.pending.step3_buyer.warn.part1a=在 {0} 區塊鏈 +portfolio.pending.step3_buyer.warn.part1b=在您的支付供應商(例如:銀行) +portfolio.pending.step3_buyer.warn.part2=BTC 賣家仍然沒有確認您的付款。如果付款發送成功,請檢查 {0}。 +portfolio.pending.step3_buyer.openForDispute=BTC 賣家還沒有確認你的付款!最大交易期限已過。您可以等待更長時間,並給交易夥伴更多時間或請求調解員的幫助。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.part=您的交易夥伴已經確認他們已經發起了 {0} 付款。\n\n +portfolio.pending.step3_seller.altcoin.explorer=在您最喜歡的 {0} 區塊鏈瀏覽器 +portfolio.pending.step3_seller.altcoin.wallet=在您的 {0} 錢包 +portfolio.pending.step3_seller.altcoin={0} 請檢查 {1} 是否交易已經到您的接收地址\n{2}\n已經有足夠的區塊鏈確認了\n支付金額必須為 {3}\n\n關閉該彈出窗口後,您可以從主界面複製並粘貼 {4} 地址。 +portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.cashByMail={0}Please check if you have received {1} with \"Cash by Mail\" from the BTC buyer. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the BTC buyer. +portfolio.pending.step3_seller.cash=因為付款是通過現金存款完成的,BTC 買家必須在紙質收據上寫“不退款”,將其撕成2份,並通過電子郵件向您發送照片。\n\n為避免退款風險,請僅確認您是否收到電子郵件,如果您確定收據有效。\n如果您不確定,{0} +portfolio.pending.step3_seller.moneyGram=買方必須發送授權編碼和一張收據的照片。\n收據必須清楚地顯示您的全名、城市、國家或地區、數量。如果您收到授權編碼,請查收郵件。\n\n關閉彈窗後,您將看到 BTC 買家的姓名和在 MoneyGram 的收款地址。\n\n只有在您成功收到錢之後,再確認收據! +portfolio.pending.step3_seller.westernUnion=買方必須發送 MTCN(跟蹤號碼)和一張收據的照片。\n收據必須清楚地顯示您的全名、城市、國家或地區、數量。如果您收到 MTCN,請查收郵件。\n\n關閉彈窗後,您將看到 BTC 買家的姓名和在 Western Union 的收款地址。\n\n只有在您成功收到錢之後,再確認收據! +portfolio.pending.step3_seller.halCash=買方必須將 HalCash代碼 用短信發送給您。除此之外,您將收到來自 HalCash 的消息,其中包含從支持 HalCash 的 ATM 中提取歐元所需的信息\n從 ATM 取款後,請在此確認付款收據! +portfolio.pending.step3_seller.amazonGiftCard=BTC 買家已經發送了一張亞馬遜電子禮品卡到您的郵箱或手機短信。請現在立即兑換亞馬遜電子禮品卡到您的亞馬遜賬户中以及確認交易信息。 + +portfolio.pending.step3_seller.bankCheck=\n\n還請確認您的銀行對帳單中的發件人姓名與委託合同中的發件人姓名相符:\n發件人姓名:{0}\n\n如果名稱與此處顯示的名稱不同,則 {1} +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.openDispute=請不要確認,而是通過鍵盤組合鍵“alt + o”或“option + o”來打開糾紛。 +portfolio.pending.step3_seller.confirmPaymentReceipt=確定付款收據 +portfolio.pending.step3_seller.amountToReceive=接收數量: +portfolio.pending.step3_seller.yourAddress=您的 {0} 地址 +portfolio.pending.step3_seller.buyersAddress=賣家的 {0} 地址 +portfolio.pending.step3_seller.yourAccount=您的交易賬户 +portfolio.pending.step3_seller.xmrTxHash=交易記錄 ID +portfolio.pending.step3_seller.xmrTxKey=交易密鑰 +portfolio.pending.step3_seller.buyersAccount=買方賬號數據 +portfolio.pending.step3_seller.confirmReceipt=確定付款收據 +portfolio.pending.step3_seller.buyerStartedPayment=BTC 買家已經開始 {0} 的付款。\n{1} +portfolio.pending.step3_seller.buyerStartedPayment.altcoin=檢查您的數字貨幣錢包或塊瀏覽器的區塊鏈確認,並確認付款時,您有足夠的塊鏈確認。 +portfolio.pending.step3_seller.buyerStartedPayment.fiat=檢查您的交易賬户(例如銀行帳户),並確認您何時收到付款。 +portfolio.pending.step3_seller.warn.part1a=在 {0} 區塊鏈 +portfolio.pending.step3_seller.warn.part1b=在您的支付供應商(例如:銀行) +portfolio.pending.step3_seller.warn.part2=你還沒有確認收到款項。如果您已經收到款項,請檢查 {0}。 +portfolio.pending.step3_seller.openForDispute=您尚未確認付款的收據!\n最大交易期已過\n請確認或請求調解員的協助。 +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.part1=您是否收到了您交易夥伴的 {0} 付款?\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.name=還請確認您的銀行對帳單中的發件人姓名與委託合同中的發件人姓名相符:\n每個交易合約的發送者姓名:{0}\n\n如果名稱與此處顯示的名稱不一致,請不要通過確認付款,而是通過“alt + o”或“option + o”打開糾紛。\n\n +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step3_seller.onPaymentReceived.note=請注意,一旦您確認收到,凍結交易金額將被髮放給 BTC 買家,保證金將被退還。 +portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=確定您已經收到付款 +portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=是的,我已經收到付款。 +portfolio.pending.step3_seller.onPaymentReceived.signer=重要提示:通過確認收到付款,你也驗證了對方的賬户,並獲得驗證。因為對方的賬户還沒有驗證,所以你應該儘可能的延遲付款的確認,以減少退款的風險。 + +portfolio.pending.step5_buyer.groupTitle=完成交易的概要 +portfolio.pending.step5_buyer.tradeFee=掛單費 +portfolio.pending.step5_buyer.makersMiningFee=礦工手續費 +portfolio.pending.step5_buyer.takersMiningFee=總共挖礦手續費 +portfolio.pending.step5_buyer.refunded=退還保證金 +portfolio.pending.step5_buyer.withdrawBTC=提現您的比特幣 +portfolio.pending.step5_buyer.amount=提現數量 +portfolio.pending.step5_buyer.withdrawToAddress=提現地址 +portfolio.pending.step5_buyer.moveToBisqWallet=在 Bisq 錢包中保留資金 +portfolio.pending.step5_buyer.withdrawExternal=提現到外部錢包 +portfolio.pending.step5_buyer.alreadyWithdrawn=您的資金已經提現。\n請查看交易歷史記錄。 +portfolio.pending.step5_buyer.confirmWithdrawal=確定提現請求 +portfolio.pending.step5_buyer.amountTooLow=轉讓金額低於交易費用和最低可能的tx值(零頭)。 +portfolio.pending.step5_buyer.withdrawalCompleted.headline=提現完成 +portfolio.pending.step5_buyer.withdrawalCompleted.msg=您完成的交易存儲在“業務/歷史記錄”下。\n您可以查看“資金/交易”下的所有比特幣交易 +portfolio.pending.step5_buyer.bought=您已經買入 +portfolio.pending.step5_buyer.paid=您已經支付 + +portfolio.pending.step5_seller.sold=您已經賣出 +portfolio.pending.step5_seller.received=您已經收到 + +tradeFeedbackWindow.title=恭喜您完成交易 +tradeFeedbackWindow.msg.part1=我們很想聽聽您的體驗如何。這將幫助我們改進軟件,優化體驗不好的地方。如欲提供意見,請填寫這份簡短的問卷(無需註冊),網址: +tradeFeedbackWindow.msg.part2=如果您有任何疑問或遇到任何問題,請通過 Bisq 論壇與其他用户和貢獻者聯繫: +tradeFeedbackWindow.msg.part3=感謝使用 Bisq + +portfolio.pending.role=我的角色 +portfolio.pending.tradeInformation=交易信息 +portfolio.pending.remainingTime=剩餘時間 +portfolio.pending.remainingTimeDetail={0}(直到 {1} ) +portfolio.pending.tradePeriodInfo=在第一次區塊鏈確認之後,交易週期開始。根據所使用的付款方法,採用不同的最大允許交易週期。 +portfolio.pending.tradePeriodWarning=如果超過了這個週期,雙方均可以提出糾紛。 +portfolio.pending.tradeNotCompleted=交易不會及時完成(直到 {0} ) +portfolio.pending.tradeProcess=交易流程 +portfolio.pending.openAgainDispute.msg=如果您不確定發送給調解員或仲裁員的消息是否已送達(例如,如果您在1天后沒有收到回覆),請放心使用 Cmd/Ctrl+o 再次打開糾紛。你也可以在 Bisq 論壇上尋求額外的幫助,網址是 https://bisq.community。 +portfolio.pending.openAgainDispute.button=再次出現糾紛 +portfolio.pending.openSupportTicket.headline=創建幫助話題 +portfolio.pending.openSupportTicket.msg=請僅在緊急情況下使用此功能,如果您沒有看到“提交支持”或“提交糾紛”按鈕。\n\n當您發出工單時,交易將被中斷並由調解員或仲裁員進行處理。 + +portfolio.pending.timeLockNotOver=你必須等到≈{0}(還需等待{1}個區塊)才能提交糾紛。 +portfolio.pending.error.depositTxNull=保證金交易無效。沒有有效的保證金交易,你使用創建糾紛。請到“設置/網絡信息”進行 SPV 重新同步。\n \n如需更多幫助,請聯繫 Bisq Keybase 團隊的 Support 頻道。 +portfolio.pending.mediationResult.error.depositTxNull=保證金交易為空。你可以移動該交易至失敗的交易。 +portfolio.pending.mediationResult.error.delayedPayoutTxNull=延遲支付交易為空。你可以移動該交易至失敗的交易。 +portfolio.pending.error.depositTxNotConfirmed=保證金交易未確認。未經確認的存款交易不能發起糾紛或仲裁請求。請耐心等待,直到它被確認或進入“設置/網絡信息”進行 SPV 重新同步。\n\n如需更多幫助,請聯繫 Bisq Keybase 團隊的 Support 頻道。 + +portfolio.pending.support.headline.getHelp=需要幫助? +portfolio.pending.support.text.getHelp=如果您有任何問題,您可以嘗試在交易聊天中聯繫交易夥伴,或在 https://bisq.community 詢問 Bisq 社區。如果您的問題仍然沒有解決,您可以向調解員取得更多的幫助。 +portfolio.pending.support.button.getHelp=開啟交易聊天 +portfolio.pending.support.headline.halfPeriodOver=確認付款 +portfolio.pending.support.headline.periodOver=交易期結束 + +portfolio.pending.mediationRequested=已請求調解員協助 +portfolio.pending.refundRequested=已請求退款 +portfolio.pending.openSupport=創建幫助話題 +portfolio.pending.supportTicketOpened=幫助話題已經創建 +portfolio.pending.communicateWithArbitrator=請在“幫助”界面上與仲裁員聯繫。 +portfolio.pending.communicateWithMediator=請在“支持”頁面中與調解員進行聯繫。 +portfolio.pending.disputeOpenedMyUser=您創建了一個糾紛。\n{0} +portfolio.pending.disputeOpenedByPeer=您的交易對象創建了一個糾紛。\n{0} +portfolio.pending.noReceiverAddressDefined=沒有定義接收地址 + +portfolio.pending.mediationResult.headline=調解費用的支出 +portfolio.pending.mediationResult.info.noneAccepted=通過接受調解員關於交易的建議的支出來完成交易。 +portfolio.pending.mediationResult.info.selfAccepted=你已經接受了調解員的建議。等待夥伴接受。 +portfolio.pending.mediationResult.info.peerAccepted=你的夥伴已經接受了調解員的建議。你也接受嗎? +portfolio.pending.mediationResult.button=查看建議的解決方案 +portfolio.pending.mediationResult.popup.headline=調解員在交易 ID:{0}上的建議 +portfolio.pending.mediationResult.popup.headline.peerAccepted=你的夥伴已經接受了調解員的建議 +portfolio.pending.mediationResult.popup.info=調解員建議的支出如下:\n你將支付:{0}\n你的交易夥伴將支付:{1}\n\n你可以接受或拒絕這筆調解費支出。\n\n通過接受,你驗證了合約的支付交易。如果你的交易夥伴也接受和驗證,支付將完成,交易將關閉。\n\n如果你們其中一人或雙方都拒絕該建議,你將必須等到(2)({3}區塊)與仲裁員展開第二輪糾紛討論,仲裁員將再次調查該案件,並根據他們的調查結果進行支付。\n\n仲裁員可以收取少量費用(費用上限:交易的保證金)作為其工作的補償。兩個交易者都同意調解員的建議是愉快的路徑請求仲裁是針對特殊情況的,比如如果一個交易者確信調解員沒有提出公平的賠償建議(或者如果另一個同伴沒有迴應)。\n\n關於新的仲裁模型的更多細節:https://docs.bisq.network/trading-rules.html#arbitration +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=您已經接受了調解員的建議支付但是似乎您的交易對手並沒有接受。\n\n一旦鎖定時間到{0}(區塊{1})您可以打開第二輪糾紛讓仲裁員重新研究該案件並重新作出支出決定。\n\n您可以找到更多關於仲裁模型的信息在:\nhttps://docs.bisq.network/trading-rules.html#arbitration +portfolio.pending.mediationResult.popup.openArbitration=拒絕並請求仲裁 +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這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Bisq 調解員處得到建議。\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.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您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 +portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=將交易移至失敗交易 +portfolio.pending.failedTrade.warningIcon.tooltip=點擊打開該交易的問題細節 +portfolio.failed.revertToPending.popup=您想要將該交易移至未完成交易嗎 +portfolio.failed.revertToPending=將交易移至未完成交易 + +portfolio.closed.completed=完成 +portfolio.closed.ticketClosed=已仲裁 +portfolio.closed.mediationTicketClosed=已調解 +portfolio.closed.canceled=已取消 +portfolio.failed.Failed=失敗 +portfolio.failed.unfail=再繼續之前,請保證你有一份根目錄的備份!\n您想要將此交易移至未完成的交易嗎?\n這是一個解鎖卡在失敗交易的資金的方法 +portfolio.failed.cantUnfail=目前該交易暫無法移至未完成的交易。\n請在完成交易後重試{0} +portfolio.failed.depositTxNull=交易無法恢復至未完成交易。保證金交易為空。 +portfolio.failed.delayedPayoutTxNull=交易無法恢復至未完成交易。延遲支付交易為空。 + + +#################################################################### +# Funds +#################################################################### + +funds.tab.deposit=存款 +funds.tab.withdrawal=提現 +funds.tab.reserved=保證金 +funds.tab.locked=凍結資金 +funds.tab.transactions=交易記錄 + +funds.deposit.unused=尚未使用 +funds.deposit.usedInTx=用在 {0} 交易 +funds.deposit.fundBisqWallet=充值 Bisq 錢包 +funds.deposit.noAddresses=尚未生成存款地址 +funds.deposit.fundWallet=充值您的錢包 +funds.deposit.withdrawFromWallet=從錢包轉出資金 +funds.deposit.amount=BTC 數量(可選) +funds.deposit.generateAddress=生成新的地址 +funds.deposit.generateAddressSegwit=原生 segwit 格式(Bech32) +funds.deposit.selectUnused=請從上表中選擇一個未使用的地址,而不是生成一個新地址。 + +funds.withdrawal.arbitrationFee=仲裁費用 +funds.withdrawal.inputs=充值選擇 +funds.withdrawal.useAllInputs=使用所有可用的充值地址 +funds.withdrawal.useCustomInputs=使用自定義充值地址 +funds.withdrawal.receiverAmount=接收者的數量 +funds.withdrawal.senderAmount=發送者的數量 +funds.withdrawal.feeExcluded=不含挖礦費的金額 +funds.withdrawal.feeIncluded=包含挖礦費的金額 +funds.withdrawal.fromLabel=從源地址提現 +funds.withdrawal.toLabel=提現地址 +funds.withdrawal.memoLabel=提現備註 +funds.withdrawal.memo=可選備註 +funds.withdrawal.withdrawButton=選定提現 +funds.withdrawal.noFundsAvailable=沒有可用資金提現 +funds.withdrawal.confirmWithdrawalRequest=確定提現請求 +funds.withdrawal.withdrawMultipleAddresses=從多個地址提現({0}) +funds.withdrawal.withdrawMultipleAddresses.tooltip=從多個地址提現:\n{0} +funds.withdrawal.notEnoughFunds=您錢包裏沒有足夠的資金。 +funds.withdrawal.selectAddress=從列表中選一個源地址 +funds.withdrawal.setAmount=設置提現數量 +funds.withdrawal.fillDestAddress=輸入您的目標地址 +funds.withdrawal.warn.noSourceAddressSelected=您需要從上面列表中選一個源地址。 +funds.withdrawal.warn.amountExceeds=您的金額超過所選地址的可用金額。\n請考慮在上表中選擇多個地址或調整手續費設置,來支付手續費。 + +funds.reserved.noFunds=未完成報價中沒有已用資金 +funds.reserved.reserved=報價 ID:{0} 接收在本地錢包中 + +funds.locked.noFunds=交易中沒有凍結資金 +funds.locked.locked=多重驗證凍結交易 ID:{0} + +funds.tx.direction.sentTo=發送至: +funds.tx.direction.receivedWith=接收到: +funds.tx.direction.genesisTx=從初始 tx: +funds.tx.txFeePaymentForBsqTx=BSQ tx 的礦工手續費支付 +funds.tx.createOfferFee=掛單和tx費用:{0} +funds.tx.takeOfferFee=下單和tx費用:{0} +funds.tx.multiSigDeposit=多重驗證保證金:{0} +funds.tx.multiSigPayout=多重驗證花費:{0} +funds.tx.disputePayout=糾紛花費:{0} +funds.tx.disputeLost=失敗的糾紛案件:{0} +funds.tx.collateralForRefund=押金退款:{0} +funds.tx.timeLockedPayoutTx=距鎖定鎖定支付 tx 的時間: {0} +funds.tx.refund=仲裁退款:{0} +funds.tx.unknown=未知原因:{0} +funds.tx.noFundsFromDispute=沒有退款的糾紛 +funds.tx.receivedFunds=收到的資金: +funds.tx.withdrawnFromWallet=從錢包提現 +funds.tx.withdrawnFromBSQWallet=BTC 已從 BSQ 錢包中取出 +funds.tx.memo=備註 +funds.tx.noTxAvailable=沒有可用交易 +funds.tx.revert=還原 +funds.tx.txSent=交易成功發送到本地 Bisq 錢包中的新地址。 +funds.tx.direction.self=內部錢包交易 +funds.tx.daoTxFee=BSQ tx 的礦工手續費支付 +funds.tx.reimbursementRequestTxFee=退還申請 +funds.tx.compensationRequestTxFee=報償申請 +funds.tx.dustAttackTx=接受零頭 +funds.tx.dustAttackTx.popup=這筆交易是發送一個非常小的比特幣金額到您的錢包,可能是區塊鏈分析公司嘗試監控您的交易。\n\n如果您在交易中使用該交易輸出,他們將瞭解到您很可能也是其他地址的所有者(資金歸集)。\n\n為了保護您的隱私,Bisq 錢包忽略了這種零頭的消費和餘額顯示。可以在設置中將輸出視為零頭時設置閾值量。 + +#################################################################### +# Support +#################################################################### + +support.tab.mediation.support=調解 +support.tab.arbitration.support=仲裁 +support.tab.legacyArbitration.support=歷史仲裁 +support.tab.ArbitratorsSupportTickets={0} 的工單 +support.filter=查找糾紛 +support.filter.prompt=輸入 交易 ID、日期、洋葱地址或賬户信息 + +support.sigCheck.button=Check signature +support.sigCheck.popup.info=如果向在 DAO 發送賠償請求,您需要在 Github 上粘貼您的賠償請求中的調解和仲裁過程的摘要消息。要使此聲明可驗證,任何用户都可以使用此工具檢查調解或仲裁人員的簽名是否與摘要消息匹配。 +support.sigCheck.popup.header=確認糾紛結果簽名 +support.sigCheck.popup.msg.label=總結消息 +support.sigCheck.popup.msg.prompt=複製粘貼糾紛總結消息 +support.sigCheck.popup.result=驗證結果 +support.sigCheck.popup.success=簽名有效 +support.sigCheck.popup.failed=簽名驗證失敗 +support.sigCheck.popup.invalidFormat=消息並不是正確的格式。請複製粘貼糾紛總結消息。 + +support.reOpenByTrader.prompt=您確定想要重新開啟糾紛? +support.reOpenButton.label=重新打開 +support.sendNotificationButton.label=私人通知 +support.reportButton.label=報吿 +support.fullReportButton.label=所有糾紛 +support.noTickets=沒有創建的話題 +support.sendingMessage=發送消息... +support.receiverNotOnline=收件人未在線。消息被保存到他們的郵箱。 +support.sendMessageError=發送消息失敗。錯誤:{0} +support.receiverNotKnown=Receiver not known +support.wrongVersion=糾紛中的訂單創建於一箇舊版本的 Bisq。\n您不能在當前版本關閉這個糾紛。\n\n請您使用舊版本/協議版本: {0} +support.openFile=打開附件文件(文件最大大小:{0} kb) +support.attachmentTooLarge=您的附件的總大小為 {0} kb,並超過最大值。 允許消息大小為 {1} kB。 +support.maxSize=文件允許的最大大小 {0} kB。 +support.attachment=附件 +support.tooManyAttachments=您不能在一個消息裏發送超過3個附件。 +support.save=保存文件到磁盤 +support.messages=消息 +support.input.prompt=輸入消息... +support.send=發送 +support.addAttachments=添加附件 +support.closeTicket=關閉話題 +support.attachments=附件: +support.savedInMailbox=消息保存在收件人的信箱中 +support.arrived=消息抵達收件人 +support.acknowledged=收件人已確認接收消息 +support.error=收件人無法處理消息。錯誤:{0} +support.buyerAddress=BTC 買家地址 +support.sellerAddress=BTC 賣家地址 +support.role=角色 +support.agent=Support agent +support.state=狀態 +support.chat=Chat +support.closed=關閉 +support.open=打開 +support.process=Process +support.buyerOfferer=BTC 買家/掛單者 +support.sellerOfferer=BTC 賣家/掛單者 +support.buyerTaker=BTC 買家/買單者 +support.sellerTaker=BTC 賣家/買單者 + +support.backgroundInfo=Bisq 不是一家公司,所以它處理糾紛的方式不同。\n\n交易雙方可以在應用程序中通過未完成交易頁面上的安全聊天進行通信,以嘗試自行解決爭端。如果這還不夠,調解員可以介入幫助。調解員將對情況進行評估,並對交易資金的支出提出建議。如果兩個交易者都接受這個建議,那麼支付交易就完成了,交易也結束了。如果一方或雙方不同意調解員的建議,他們可以要求仲裁。仲裁員將重新評估情況,如果有必要,將親自向交易員付款,並要求 Bisq DAO 對這筆付款進行補償。 +support.initialInfo=請在下面的文本框中輸入您的問題描述。添加儘可能多的信息,以加快解決糾紛的時間。\n\n以下是你應提供的資料核對表:\n\t●如果您是 BTC 買家:您是否使用法定貨幣或其他加密貨幣轉賬?如果是,您是否點擊了應用程序中的“支付開始”按鈕?\n\t●如果您是 BTC 賣家:您是否收到法定貨幣或其他加密貨幣的付款了?如果是,你是否點擊了應用程序中的“已收到付款”按鈕?\n\t●您使用的是哪個版本的 Bisq?\n\t●您使用的是哪種操作系統?\n\t●如果遇到操作執行失敗的問題,請考慮切換到新的數據目錄。\n\t有時數據目錄會損壞,並導致奇怪的錯誤。\n詳見:https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\n請熟悉糾紛處理的基本規則:\n\t●您需要在2天內答覆 {0} 的請求。\n\t●調解員會在2天之內答覆,仲裁員會在5天之內答覆。\n\t●糾紛的最長期限為14天。\n\t●你需要與仲裁員合作,提供他們為你的案件所要求的信息。\n\t●當您第一次啟動應用程序時,您接受了用户協議中爭議文檔中列出的規則。\n\n您可以通過 {2} 瞭解有關糾紛處理的更多信息 +support.systemMsg=系統消息:{0} +support.youOpenedTicket=您創建了幫助請求。\n\n{0}\n\nBisq 版本:{1} +support.youOpenedDispute=您創建了一個糾紛請求。\n\n{0}\n\nBisq 版本:{1} +support.youOpenedDisputeForMediation=您創建了一個調解請求。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedTicket=對方因技術問題請求獲取幫助。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedDispute=對方創建了一個糾紛請求。\n\n{0}\n\nBisq 版本:{1} +support.peerOpenedDisputeForMediation=對方創建了一個調解請求。\n\n{0}\n\nBisq 版本:{1} +support.mediatorsDisputeSummary=系統消息:\n調解糾紛總結:\n{0} +support.mediatorsAddress=仲裁員的節點地址:{0} +support.warning.disputesWithInvalidDonationAddress=延遲支付交易已經被用於一個不可用接受者地址。它與有效捐贈地址的任何 DAO 中參數值均不匹配。\n\n這可能是一個騙局。請將該事件通知開發者,在問題解決之前不要關閉該案件!\n\n糾紛所用的地址:{0}\n\n所有 DAO 參數中捐贈地址:{1}\n\n交易:{2}{3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\n您確定一定要關閉糾紛嗎? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n您不能進行支付。 +support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. +support.info.disputeReOpened=Dispute ticket has been re-opened. + +#################################################################### +# Settings +#################################################################### +settings.tab.preferences=偏好 +settings.tab.network=網絡信息 +settings.tab.about=關於我們 + +setting.preferences.general=通用偏好 +setting.preferences.explorer=比特幣區塊瀏覽器 +setting.preferences.explorer.bsq=Bisq 區塊瀏覽器 +setting.preferences.deviation=與市場價格最大差價 +setting.preferences.bsqAverageTrimThreshold=BSQ 率已超過閾值 +setting.preferences.avoidStandbyMode=避免待機模式 +setting.preferences.autoConfirmXMR=XMR 自動確認 +setting.preferences.autoConfirmEnabled=啟用 +setting.preferences.autoConfirmRequiredConfirmations=已要求確認 +setting.preferences.autoConfirmMaxTradeSize=最大交易量(BTC) +setting.preferences.autoConfirmServiceAddresses=Monero Explorer 鏈接(使用Tor,但本地主機,LAN IP地址和 *.local 主機名除外) +setting.preferences.deviationToLarge=值不允許大於30% +setting.preferences.txFee=提現交易手續費(聰/字節) +setting.preferences.useCustomValue=使用自定義值 +setting.preferences.txFeeMin=交易手續費必須至少為{0} 聰/字節 +setting.preferences.txFeeTooLarge=您輸入的數額超過可接受值(>5000 聰/字節)。交易手續費一般在 50-400 聰/字節、 +setting.preferences.ignorePeers=忽略節點 [洋葱地址:端口] +setting.preferences.ignoreDustThreshold=最小無零頭輸出值 +setting.preferences.currenciesInList=市場價的貨幣列表 +setting.preferences.prefCurrency=首選貨幣 +setting.preferences.displayFiat=顯示國家貨幣 +setting.preferences.noFiat=沒有選定國家貨幣 +setting.preferences.cannotRemovePrefCurrency=您不能刪除您選定的首選貨幣 +setting.preferences.displayAltcoins=顯示數字貨幣 +setting.preferences.noAltcoins=沒有選定數字貨幣 +setting.preferences.addFiat=添加法定貨幣 +setting.preferences.addAltcoin=添加數字貨幣 +setting.preferences.displayOptions=顯示選項 +setting.preferences.showOwnOffers=在報價列表中顯示我的報價 +setting.preferences.useAnimations=使用動畫 +setting.preferences.useDarkMode=使用夜間模式 +setting.preferences.sortWithNumOffers=使用“報價ID/交易ID”篩選列表 +setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API +setting.preferences.notifyOnPreRelease=Receive pre-release notifications +setting.preferences.resetAllFlags=重置所有“不再提示”的提示 +settings.preferences.languageChange=同意重啟請求以更換語言 +settings.preferences.supportLanguageWarning=如有任何爭議,請注意調解在 {0} 處理,仲裁在 {1} 處理。 +setting.preferences.daoOptions=DAO 選項 +setting.preferences.dao.resyncFromGenesis.label=從初始 tx 重構 DAO 狀態 +setting.preferences.dao.resyncFromResources.label=從指定資源重新構建 DAO 狀態 +setting.preferences.dao.resyncFromResources.popup=應用程序重新啟動後,Bisq 網絡治理數據將從種子節點重新加載,而 BSQ 同步狀態將從創始交易中重新構建。 +setting.preferences.dao.resyncFromGenesis.popup=從創始交易中出現同步會消耗大量時間以及 CPU 資源。您確定要重新同步嗎?通常,從最新資源文件進行重新同步就足夠了,而且速度更快。\n\n應用程序重新啟動後,Bisq 網絡治理數據將從種子節點重新加載,而 BSQ 同步狀態將從初始交易中重新構建。 +setting.preferences.dao.resyncFromGenesis.resync=從創始區塊重新同步並關閉 +setting.preferences.dao.isDaoFullNode=以 DAO 全節點運行 Bisq +setting.preferences.dao.rpcUser=RPC 用户名 +setting.preferences.dao.rpcPw=PRC 密碼 +setting.preferences.dao.blockNotifyPort=區塊通知端口 +setting.preferences.dao.fullNodeInfo=如果要將 Bisq 以 DAO 全節點運行,您需要在本地運行比特幣核心並啟用 RPC 。所有的需求都記錄在“ {0} ”中。 +setting.preferences.dao.fullNodeInfo.ok=打開文檔頁面 +setting.preferences.dao.fullNodeInfo.cancel=不,我堅持使用輕節點模式 +settings.preferences.editCustomExplorer.headline=瀏覽設置。 +settings.preferences.editCustomExplorer.description=從左側列表中選擇一個系統默認瀏覽器,或使用您偏好的自定義設置。 +settings.preferences.editCustomExplorer.available=可用瀏覽器 +settings.preferences.editCustomExplorer.chosen=已選擇的瀏覽器設置 +settings.preferences.editCustomExplorer.name=名稱 +settings.preferences.editCustomExplorer.txUrl=交易 URL +settings.preferences.editCustomExplorer.addressUrl=地址 URL + +settings.net.btcHeader=比特幣網絡 +settings.net.p2pHeader=Bisq 網絡 +settings.net.onionAddressLabel=我的匿名地址 +settings.net.btcNodesLabel=使用自定義比特幣主節點 +settings.net.bitcoinPeersLabel=已連接節點 +settings.net.useTorForBtcJLabel=使用 Tor 連接比特幣網絡 +settings.net.bitcoinNodesLabel=需要連接比特幣核心 +settings.net.useProvidedNodesRadio=使用公共比特幣核心節點 +settings.net.usePublicNodesRadio=使用公共比特幣網絡 +settings.net.useCustomNodesRadio=使用自定義比特幣主節點 +settings.net.warn.usePublicNodes=如果你使用公共比特幣網絡,你就會面臨嚴重的隱私問題,這是由損壞的 bloom filter 設計和實現造成的,它適用於像 BitcoinJ 這樣的 SPV 錢包(在 Bisq 中使用)。您所連接的任何完整節點都可以發現您的所有錢包地址都屬於一個實體。\n\n詳情請瀏覽: https://bisq.network/blog/privacy-in-bitsquare 。\n\n您確定要使用公共節點嗎? +settings.net.warn.usePublicNodes.useProvided=不,使用給定的節點 +settings.net.warn.usePublicNodes.usePublic=使用公共網絡 +settings.net.warn.useCustomNodes.B2XWarning=請確保您的比特幣節點是一個可信的比特幣核心節點!\n\n連接到不遵循比特幣核心共識規則的節點可能會損壞您的錢包,並在交易過程中造成問題。\n\n連接到違反共識規則的節點的用户應對任何由此造成的損害負責。任何由此產生的糾紛都將有利於另一方。對於忽略此警吿和保護機制的用户,不提供任何技術支持! +settings.net.warn.invalidBtcConfig=由於您的配置無效,無法連接至比特幣網絡。\n\n您的配置已經被重置為默認比特幣節點。你需要重啟 Bisq。 +settings.net.localhostBtcNodeInfo=背景信息:Bisq 在啟動時會在本地查找比特幣節點。如果有,Bisq 將只通過它與比特幣網絡進行通信。 +settings.net.p2PPeersLabel=已連接節點 +settings.net.onionAddressColumn=匿名地址 +settings.net.creationDateColumn=已建立連接 +settings.net.connectionTypeColumn=入/出 +settings.net.sentDataLabel=統計數據已發送 +settings.net.receivedDataLabel=統計數據已接收 +settings.net.chainHeightLabel=最新 BTC 區塊高度 +settings.net.roundTripTimeColumn=延遲 +settings.net.sentBytesColumn=發送 +settings.net.receivedBytesColumn=接收 +settings.net.peerTypeColumn=節點類型 +settings.net.openTorSettingsButton=打開 Tor 設置 + +settings.net.versionColumn=版本 +settings.net.subVersionColumn=子版本 +settings.net.heightColumn=高度 + +settings.net.needRestart=您需要重啟應用程序以同意這次變更。\n您需要現在重啟嗎? +settings.net.notKnownYet=至今未知... +settings.net.sentData=已發送數據 {0},{1} 條消息,{2} 條消息/秒 +settings.net.receivedData=已接收數據 {0},{1} 條消息,{2} 條消息/秒 +settings.net.chainHeight=Bisq DAO chain height: {0} | Bitcoin Peers chain height: {1} +settings.net.ips=添加逗號分隔的 IP 地址及端口,如使用8333端口可不填寫。 +settings.net.seedNode=種子節點 +settings.net.directPeer=節點(直連) +settings.net.initialDataExchange={0} [Bootstrapping] +settings.net.peer=節點 +settings.net.inbound=接收數據包 +settings.net.outbound=發送數據包 +settings.net.reSyncSPVChainLabel=重新同步 SPV 鏈 +settings.net.reSyncSPVChainButton=刪除 SPV 鏈文件並重新同步 +settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\nAfter the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\nDepending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. Do not interrupt the process otherwise you have to repeat it. +settings.net.reSyncSPVAfterRestart=SPV 鏈文件已被刪除。請耐心等待,與網絡重新同步可能需要一段時間。 +settings.net.reSyncSPVAfterRestartCompleted=重新同步剛剛完成,請重啟應用程序。 +settings.net.reSyncSPVFailed=無法刪除 SPV 鏈文件。\n錯誤:{0} +setting.about.aboutBisq=關於 Bisq +setting.about.about=Bisq 是一款開源軟件,它通過分散的對等網絡促進了比特幣與各國貨幣(以及其他加密貨幣)的交易,嚴格保護了用户隱私的方式。請到我們項目的網站閲讀更多關於 Bisq 的信息。 +setting.about.web=Bisq 網站 +setting.about.code=源代碼 +setting.about.agpl=AGPL 協議 +setting.about.support=支持 Bisq +setting.about.def=Bisq 不是一個公司,而是一個社區項目,開放參與。如果您想參與或支持 Bisq,請點擊下面連接。 +setting.about.contribute=貢獻 +setting.about.providers=數據提供商 +setting.about.apisWithFee=Bisq 使用 Bisq 價格指數來表示法幣與虛擬貨幣的市場價格,並使用 Bisq 內存池節點來估算採礦費。 +setting.about.apis=Bisq 使用 Bisq 價格指數來表示法幣與數字貨幣的市場價格。 +setting.about.pricesProvided=交易所價格提供商 +setting.about.feeEstimation.label=礦工手續費估算提供商 +setting.about.versionDetails=版本詳情 +setting.about.version=應用程序版本 +setting.about.subsystems.label=子系統版本 +setting.about.subsystems.val=網絡版本:{0};P2P 消息版本:{1};本地數據庫版本:{2};交易協議版本:{3} + +setting.about.shortcuts=快捷鍵 +setting.about.shortcuts.ctrlOrAltOrCmd=“Ctrl + {0}”或“alt + {0}”或“cmd + {0}” + +setting.about.shortcuts.menuNav=主頁面 +setting.about.shortcuts.menuNav.value=使用“Ctrl”或“Alt”或“cmd” + 數字鍵“1-9”來切換不同的主頁面 + +setting.about.shortcuts.close=關閉 Bisq +setting.about.shortcuts.close.value=“Ctrl + {0}”或“cmd + {0}”或“Ctrl + {1}”或“cmd + {1}” + +setting.about.shortcuts.closePopup=關閉彈窗以及對話框 +setting.about.shortcuts.closePopup.value=‘釋放’ 鍵 + +setting.about.shortcuts.chatSendMsg=發送信息到交易夥伴 +setting.about.shortcuts.chatSendMsg.value=“Ctrl + ENTER”或“alt + ENTER”或“cmd + ENTER” + +setting.about.shortcuts.openDispute=創建糾紛 +setting.about.shortcuts.openDispute.value=選擇未完成交易並點擊:{0} + +setting.about.shortcuts.walletDetails=打開錢包詳情窗口 + +setting.about.shortcuts.openEmergencyBtcWalletTool=打開應急 BTC 錢包工具 + +setting.about.shortcuts.openEmergencyBsqWalletTool=打開應急 BSQ 錢包工具 + +setting.about.shortcuts.showTorLogs=在 DEBUG 與 WARN 之間切換 Tor 日誌等級 + +setting.about.shortcuts.manualPayoutTxWindow=打開窗口手動支付雙重驗證存款交易 + +setting.about.shortcuts.reRepublishAllGovernanceData=重新推送 DAO 眾議廳數據(包括提案以及投票) + +setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again +setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} + +setting.about.shortcuts.registerArbitrator=註冊仲裁員(僅限調解員/仲裁員) +setting.about.shortcuts.registerArbitrator.value=切換至賬户頁面並按下:{0} + +setting.about.shortcuts.registerMediator=註冊調解員(僅限調解員/仲裁員) +setting.about.shortcuts.registerMediator.value=切換至賬户頁面並按下:{0} + +setting.about.shortcuts.openSignPaymentAccountsWindow=打開賬齡驗證窗口(僅限仲裁員) +setting.about.shortcuts.openSignPaymentAccountsWindow.value=切換至仲裁頁面並按下:{0} + +setting.about.shortcuts.sendAlertMsg=發送警報或更新消息(需要權限) + +setting.about.shortcuts.sendFilter=設置過濾器(需要權限) + +setting.about.shortcuts.sendPrivateNotification=發送私人通知到對等點(需要權限) +setting.about.shortcuts.sendPrivateNotification.value=點擊交易夥伴頭像並按下:{0} 以顯示更多信息 + +setting.info.headline=新 XMR 自動確認功能 +setting.info.msg=當你完成 BTC/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Bisq 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Bisq 使用由 Bisq 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置''中將每筆交易的最大 BTC 數量設置為自動確認以及所需確認的數量。\n\n在 Bisq Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://bisq.wiki/Trading_Monero#Auto-confirming_trades +#################################################################### +# Account +#################################################################### + +account.tab.mediatorRegistration=調解員註冊 +account.tab.refundAgentRegistration=退款助理註冊 +account.tab.signing=驗證中 +account.info.headline=歡迎來到 Bisq 賬户 +account.info.msg=在這裏你可以設置交易賬户的法定貨幣及數字貨幣,選擇仲裁員和備份你的錢包及賬户數據。\n\n當你開始運行 Bisq 就已經創建了一個空的比特幣錢包。\n\n我們建議你在充值之前寫下你比特幣錢包的還原密鑰(在左邊的列表)和考慮添加密碼。在“資金”選項中管理比特幣存入和提現。\n\n隱私 & 安全:\nBisq 是一個去中心化的交易所 – 意味着您的所有數據都保存在您的電腦上,沒有服務器,我們無法訪問您的個人信息,您的資金,甚至您的 IP 地址。如銀行賬號、數字貨幣、比特幣地址等數據只分享給與您交易的人,以實現您發起的交易(如果有爭議,仲裁員將會看到您的交易數據)。 + +account.menu.paymentAccount=法定貨幣賬户 +account.menu.altCoinsAccountView=數字貨幣賬户 +account.menu.password=錢包密碼 +account.menu.seedWords=錢包密鑰 +account.menu.walletInfo=Wallet info +account.menu.backup=備份 +account.menu.notifications=通知 + +account.menu.walletInfo.balance.headLine=Wallet balances +account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. +account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) +account.menu.walletInfo.walletSelector={0} {1} wallet +account.menu.walletInfo.path.headLine=HD keychain paths +account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\nKeep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. + +account.menu.walletInfo.openDetails=Show raw wallet details and private keys + +## TODO should we rename the following to a gereric name? +account.arbitratorRegistration.pubKey=公鑰 + +account.arbitratorRegistration.register=註冊 +account.arbitratorRegistration.registration={0} 註冊 +account.arbitratorRegistration.revoke=撤銷 +account.arbitratorRegistration.info.msg=請注意,撤銷後需要保留15天,因為可能有交易正在以你作為 {0}。最大允許的交易期限為8天,糾紛過程最多可能需要7天。 +account.arbitratorRegistration.warn.min1Language=您需要設置至少1種語言。\n我們已經為您添加了默認語言。 +account.arbitratorRegistration.removedSuccess=您已從 Bisq 網絡成功刪除仲裁員註冊信息。 +account.arbitratorRegistration.removedFailed=無法刪除仲裁員。{0} +account.arbitratorRegistration.registerSuccess=您已從 Bisq 網絡成功註冊您的仲裁員。 +account.arbitratorRegistration.registerFailed=無法註冊仲裁員。{0} + +account.altcoin.yourAltcoinAccounts=您的數字貨幣賬户 +account.altcoin.popup.wallet.msg=請確保您按照 {1} 網頁上所述使用 {0} 錢包的要求。\n使用集中式交易所的錢包,您無法控制密鑰或使用不兼容的錢包軟件,可能會導致交易資金的流失!\n調解員或仲裁員不是 {2} 專家,在這種情況下不能幫助。 +account.altcoin.popup.wallet.confirm=我瞭解並確定我知道我需要哪種錢包。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.upx.msg=在 Bisq 上交易 UPX 需要您瞭解並滿足以下要求:\n\n要發送 UPX ,您需要使用官方的 UPXmA GUI 錢包或啟用 store-tx-info 標誌的 UPXmA CLI 錢包(在新版本中是默認的)。請確保您可以訪問Tx密鑰,因為在糾紛狀態時需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\n在普通的區塊鏈瀏覽器中,這種交易是不可驗證的。\n\n如有糾紛,你須向仲裁員提供下列資料:\n \n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,UPX 發送方負責向仲裁員提供 UPX 轉賬的驗證。\n\n不需要支付 ID,只需要普通的公共地址。\n \n如果您對該流程不確定,請訪問 UPXmA Discord 頻道(https://discord.gg/vhdNSrV)或 Telegram 交流羣(https://t.me/uplexaOfficial)瞭解更多信息。\n\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.arq.msg=在 Bisq 上交易 ARQ 需要您瞭解並滿足以下要求:\n\n要發送 ARQ ,您需要使用官方的 ArQmA GUI 錢包或啟用 store-tx-info 標誌的 ArQmA CLI 錢包(在新版本中是默認的)。請確保您可以訪問Tx密鑰,因為在糾紛狀態時需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\n在普通的區塊鏈瀏覽器中,這種交易是不可驗證的。\n\n如有糾紛,你須向調解員或仲裁員提供下列資料:\n\n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,ARQ 發送方負責向調解員或仲裁員提供 ARQ 轉賬的驗證。\n\n不需要交易 ID,只需要普通的公共地址。\n\n如果您對該流程不確定,請訪問 ArQmA Discord 頻道(https://discord.gg/s9BQpJT)或 ArQmA 論壇(https://labs.arqma.com)瞭解更多信息。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.xmr.msg=在 Bisq 上交易 XMR 需要你理解並滿足以下要求。\n\n如果您出售 XMR,當您在糾紛中您必須要提供下列信息給調解員或仲裁員:\n- 交易密鑰(Tx 公鑰,Tx密鑰,Tx私鑰)\n- 交易 ID(Tx ID 或 Tx 哈希)\n- 交易目標地址(接收者地址)\n\n在 wiki 中查看更多關於 Monero 錢包的信息:\nhttps://bisq.wiki/Trading_Monero#Proving_payments\n\n如未能提供要求的交易數據將在糾紛中直接判負\n\n還要注意,Bisq 現在提供了自動確認 XMR 交易的功能,以使交易更快,但是您需要在設置中啟用它。\n\n有關自動確認功能的更多信息,請參見 Wiki:\nhttps://bisq.wiki/Trading_Monero#Auto-confirming_trades +# suppress inspection "UnusedProperty" +account.altcoin.popup.msr.msg=區塊鏈瀏覽器在 Bisq 上交易 XMR 需要您瞭解並滿足以下要求:\n\n發送MSR時,您需要使用官方的 Masari GUI 錢包、啟用store-tx-info標記的Masari CLI錢包(默認啟用)或Masari 網頁錢包(https://wallet.getmasari.org)。請確保您可以訪問的 tx 密鑰,因為如果發生糾紛這是需要的。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\nMasari 網頁錢包(前往 帳户->交易歷史和查看您發送的交易細節)\n\n驗證可以在錢包中完成。\nmonero-wallet-cli:使用命令(check_tx_key)。\nmonero-wallet-gui:在高級>證明/檢查頁面\n驗證可以在區塊瀏覽器中完成\n打開區塊瀏覽器(https://explorer.getmasari.org),使用搜索欄查找您的事務哈希。\n一旦找到交易,滾動到底部的“證明發送”區域,並填寫所需的詳細信息。\n如有糾紛,你須向調解員或仲裁員提供下列資料:\n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n不需要交易 ID,只需要正常的公共地址。\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,XMR 發送方負責向調解員或仲裁員提供 XMR 轉賬的驗證。\n\n如果您對該流程不確定,請訪問官方的 Masari Discord(https://discord.gg/sMCwMqs)上尋求幫助。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.blur.msg=在 Bisq 上交易 BLUR 需要你瞭解並滿足以下要求:\n\n要發送匿名信息你必須使用匿名網絡 CLI 或 GUI 錢包。\n如果您正在使用 CLI 錢包,在傳輸發送後將顯示交易哈希(tx ID)。您必須保存此信息。在發送傳輸之後,您必須立即使用“get_tx_key”命令來檢索交易私鑰。如果未能執行此步驟,以後可能無法檢索密鑰。\n\n如果您使用 Blur Network GUI 錢包,可以在“歷史”選項卡中方便地找到交易私鑰和交易 ID。發送後立即定位感興趣的交易。單擊包含交易的框的右下角的“?”符號。您必須保存此信息。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1.)交易ID,2.)交易私鑰,3.)收件人地址。調解或仲裁程序將使用 BLUR 事務查看器(https://blur.cash/#tx-viewer)驗證 BLUR 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有爭議的情況下,匿名發送方承擔100%的責任來向調解員或仲裁員核實交易。\n\n如果你不瞭解這些要求,不要在 Bisq 上交易。首先,在 Blur Network Discord 中尋求幫助(https://discord.gg/dMWaqVW)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.solo.msg=在 Bisq 上交易 Solo 需要您瞭解並滿足以下要求:\n\n要發送 Solo,您必須使用 Solo CLI 網絡錢包版本 5.1.3 或更高。\n\n如果您使用的是CLI錢包,則在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,您必須立即使用'get_tx_key'命令來檢索交易密鑰。如果未能執行此步驟,則以後可能無法檢索密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1)交易 ID,、2)交易密鑰,3)收件人的地址。調解員或仲裁員將使用 Solo 區塊資源管理器(https://explorer.Solo.org)搜索交易然後使用“發送證明”功能(https://explorer.minesolo.com/)\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,QWC 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Bisq 上交易。首先,在 Solo Discord 中尋求幫助(https://discord.minesolo.com/)。\n\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.cash2.msg=在 Bisq 上交易 CASH2 需要您瞭解並滿足以下要求:\n\n要發送 CASH2,您必須使用 CASH2 錢包版本 3 或更高。\n\n在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,必須立即在 simplewallet 中使用命令“getTxKey”來檢索交易密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1)交易 ID,2)交易密鑰,3)收件人的 CASH2 地址。調解員或仲裁員將使用 CASH2 區塊資源管理器(https://blocks.cash2.org)驗證 CASH2 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,CASH2 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Bisq 上交易。首先,在 Cash2 Discord 中尋求幫助(https://discord.gg/FGfXAYN)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.qwertycoin.msg=在 Bisq 上交易 Qwertycoin 需要您瞭解並滿足以下要求:\n\n要發送 Qwertycoin,您必須使用 Qwertycoin 錢包版本 5.1.3 或更高。\n\n在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,必須立即在 simplewallet 中使用命令“get_Tx_Key”來檢索交易密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息::1)交易 ID,、2)交易密鑰,3)收件人的 QWC 地址。調解員或仲裁員將使用 QWC 區塊資源管理器(https://explorer.qwertycoin.org)驗證 QWC 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,QWC 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Bisq 上交易。首先,在 QWC Discord 中尋求幫助(https://discord.gg/rUkfnpC)。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.drgl.msg=在 Bisq 上交易 Dragonglass 需要您瞭解並滿足以下要求:\n\n由於 Dragonglass 提供了隱私保護,所以交易不能在公共區塊鏈上驗證。如果需要,您可以通過使用您的 TXN-Private-Key 來證明您的付款。\nTXN-Private 密匙是自動生成的一次性密匙,用於只能從 DRGL 錢包中訪問的每個交易。\n要麼通過 DRGL-wallet GUI(內部交易細節對話框),要麼通過 Dragonglass CLI simplewallet(使用命令“get_tx_key”)。\n\n兩者都需要 DRGL 版本的“Oathkeeper”或更高版本。\n\n如有爭議,你必須向調解員或仲裁員提供下列資料:\n\n- txn-Privite-ket\n- 交易哈希 \n- 接收者的公開地址\n\n付款驗證可以使用上面的數據作為輸入(http://drgl.info/#check_txn)。\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。Dragonglass 發送方負責在發生爭議時向調解員或仲裁員提供 DRGL 轉賬的驗證。不需要使用付款 ID。\n\n如果您對這個過程的任何部分都不確定,請訪問(http://discord.drgl.info)上的 Dragonglass 尋求幫助。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.ZEC.msg=當使用 Zcash 時,您只能使用透明地址(以 t 開頭),而不能使用 z 地址(私有),因為調解員或仲裁員無法使用 z 地址驗證交易。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.XZC.msg=在使用 Zcoin 時,您只能使用透明的(可跟蹤的)地址,而不能使用不可跟蹤的地址,因為調解員或仲裁員無法在區塊資源管理器中使用不可跟蹤的地址驗證交易。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.grin.msg=GRIN 需要發送方和接收方之間的交互過程來創建交易。請確保遵循 GRIN 項目網頁中的説明,以可靠地發送和接收 GRIN(接收方需要在線,或至少在一定時間內在線)。\n \nBisq 只支持 Grinbox(Wallet713)錢包 URL 格式。\n\nGRIN 發送者需要提供他們已成功發送 GRIN 的證明。如果錢包不能提供證明,一個潛在的糾紛將被解決,有利於露齒微笑的接受者。請確保您使用了最新的支持交易證明的 Grinbox 軟件,並且您瞭解傳輸和接收 GRIN 的過程以及如何創建證明。\n請參閲 https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only,以獲得關於 Grinbox 證明工具的更多信息。\n +# suppress inspection "UnusedProperty" +account.altcoin.popup.beam.msg=BEAM 需要發送方和接收方之間的交互過程來創建交易。\n\n\n確保遵循 BEAM 項目網頁的指示可靠地發送和接收 BEAM(接收方需要在線,或者至少在一定的時間範圍內在線)。\n\nBEAM 發送者需要提供他們成功發送 BEAM 的證明。一定要使用錢包軟件,可以產生這樣的證明。如果錢包不能提供證據,一個潛在的糾紛將得到解決,有利於 BEAM 接收者。 +# suppress inspection "UnusedProperty" +account.altcoin.popup.pars.msg=在 Bisq 上交易 ParsiCoin 需要您瞭解並滿足以下要求:\n\n要發送 PARS ,您必須使用官方 ParsiCoin Wallet 版本 3.0.0 或更高。\n\n您可以在 GUI 錢包(ParsiPay)的交易部分檢查您的交易哈希和交易鍵,您需要右鍵單擊“交易”,然後單擊“顯示詳情”。\n\n如果仲裁是 100% 必要的,您必須向調解員或仲裁員提供以下內容:1)交易哈希,2)交易密鑰,以及3)接收方的 PARS 地址。調解員或仲裁員將使用 ParsiCoin 區塊鏈瀏覽器 (http://explorer.parsicoin.net/#check_payment)驗證 PARS 傳輸。\n\n如果你不瞭解這些要求,不要在 Bisq 上交易。首先,在 ParsiCoin Discord 尋求幫助(https://discord.gg/c7qmFNh)。 + +# suppress inspection "UnusedProperty" +account.altcoin.popup.blk-burnt.msg=要交易燒燬的貨幣,你需要知道以下幾點:\n\n燒燬的貨幣是不能花的。要在 Bisq 上交易它們,輸出腳本需要採用以下形式:OP_RETURN OP_PUSHDATA,後跟相關的數據字節,這些字節經過十六進制編碼後構成地址。例如,地址為666f6f(在UTF-8中的"foo")的燒燬的貨幣將有以下腳本:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\n要創建燒燬的貨幣,您可以使用“燒燬”RPC命令,它在一些錢包可用。\n\n對於可能的情況,可以查看 https://ibo.laboratorium.ee\n\n因為燒燬的貨幣是不能用的,所以不能重新出售。“出售”燒燬的貨幣意味着焚燒初始的貨幣(與目的地地址相關聯的數據)。\n\n如果發生爭議,BLK 賣方需要提供交易哈希。 + +# suppress inspection "UnusedProperty" +account.altcoin.popup.liquidbitcoin.msg=在 Bisq 上交易 L-BTC 你必須理解下述條款:\n\n當你在 Bisq 上接受 L-BTC 交易時,你不能使用手機 Blockstream Green Wallet 或者是一個託管/交易錢包。你必須只接收 L-BTC 到 Liquid Elements Core 錢包,或另一個 L-BTC 錢包且允許你獲得匿名的 L-BTC 地址以及密鑰。\n\n在需要進行調解的情況下,或者如果發生了交易糾紛,您必須將接收 L-BTC地址的安全密鑰披露給 Bisq 調解員或退款代理,以便他們能夠在他們自己的 Elements Core 全節點上驗證您的匿名交易的細節。\n\n如果你不瞭解或瞭解這些要求,不要在 Bisq 上交易 L-BTC。 + +account.fiat.yourFiatAccounts=您的法定貨幣賬户 + +account.backup.title=備份錢包 +account.backup.location=備份路徑 +account.backup.selectLocation=選擇備份路徑 +account.backup.backupNow=立即備份(備份沒有被加密!) +account.backup.appDir=應用程序數據目錄 +account.backup.openDirectory=打開目錄 +account.backup.openLogFile=打開日誌文件 +account.backup.success=備份成功保存在:\n{0} +account.backup.directoryNotAccessible=您沒有訪問選擇的目錄的權限。 {0} + +account.password.removePw.button=移除密碼 +account.password.removePw.headline=移除錢包的密碼保護 +account.password.setPw.button=設置密碼 +account.password.setPw.headline=設置錢包的密碼保護 +account.password.info=使用密碼保護,您需要在將比特幣從錢包中取出時輸入密碼,或者要從還原密鑰和應用程序啟動時查看或恢復錢包時輸入密碼。 + +account.seed.backup.title=備份您的錢包還原密鑰 +account.seed.info=請寫下錢包還原密鑰和時間!\n您可以通過還原密鑰和時間在任何時候恢復您的錢包。\n還原密鑰用於 BTC 和 BSQ 錢包。\n\n您應該在一張紙上寫下還原密鑰並且不要保存它們在您的電腦上。\n請注意還原密鑰並不能代替備份。\n您需要備份完整的應用程序目錄在”賬户/備份“界面去恢復有效的應用程序狀態和數據。 +account.seed.backup.warning=請注意種子詞不能替代備份。您需要為整個應用目錄(在“賬户/備份”選項卡中)以恢復應用狀態以及數據。\n導入種子詞僅在緊急情況時才推薦使用。 如果沒有正確備份數據庫文件和密鑰,該應用程序將無法運行!\n\n在 Bisq wiki 中查看更多信息: https://bisq.wiki/Backing_up_application_data +account.seed.warn.noPw.msg=您還沒有設置一個可以保護還原密鑰顯示的錢包密碼。\n\n要顯示還原密鑰嗎? +account.seed.warn.noPw.yes=是的,不要再問我 +account.seed.enterPw=輸入密碼查看還原密鑰 +account.seed.restore.info=請在應用還原密鑰還原之前進行備份。請注意,錢包還原僅用於緊急情況,可能會導致內部錢包數據庫出現問題。\n這不是應用備份的方法!請使用應用程序數據目錄中的備份來恢復以前的應用程序狀態。\n恢復後,應用程序將自動關閉。重新啟動應用程序後,它將重新與比特幣網絡同步。這可能需要一段時間,並且會消耗大量CPU,特別是在錢包較舊且有很多交易的情況下。請避免中斷該進程,否則可能需要再次刪除 SPV 鏈文件或重複還原過程。 +account.seed.restore.ok=好的,立即執行回覆並且關閉 Bisq + + +#################################################################### +# Mobile notifications +#################################################################### + +account.notifications.setup.title=安裝 +account.notifications.download.label=下載手機應用 +account.notifications.waitingForWebCam=等待網絡攝像頭... +account.notifications.webCamWindow.headline=用手機掃描二維碼 +account.notifications.webcam.label=使用網絡攝像頭 +account.notifications.webcam.button=掃描二維碼 +account.notifications.noWebcam.button=我沒有網絡攝像頭 +account.notifications.erase.label=在手機上清除通知 +account.notifications.erase.title=清除通知 +account.notifications.email.label=驗證碼 +account.notifications.email.prompt=輸入您通過電子郵件收到的驗證碼 +account.notifications.settings.title=設置 +account.notifications.useSound.label=在手機上播放提示聲音 +account.notifications.trade.label=接收交易信息 +account.notifications.market.label=接收報價提醒 +account.notifications.price.label=接收價格提醒 +account.notifications.priceAlert.title=價格提醒 +account.notifications.priceAlert.high.label=提醒條件:當 BTC 價格高於 +account.notifications.priceAlert.low.label=提醒條件:當 BTC 價格低於 +account.notifications.priceAlert.setButton=設置價格提醒 +account.notifications.priceAlert.removeButton=取消價格提醒 +account.notifications.trade.message.title=交易狀態已變更 +account.notifications.trade.message.msg.conf=ID 為 {0} 的交易的存款交易已被確認。請打開您的 Bisq 應用程序並開始付款。 +account.notifications.trade.message.msg.started=BTC 買家已經開始支付 ID 為 {0} 的交易。 +account.notifications.trade.message.msg.completed=ID 為 {0} 的交易已完成。 +account.notifications.offer.message.title=您的報價已被接受 +account.notifications.offer.message.msg=您的 ID 為 {0} 的報價已被接受 +account.notifications.dispute.message.title=新的糾紛消息 +account.notifications.dispute.message.msg=您收到了一個 ID 為 {0} 的交易糾紛消息 + +account.notifications.marketAlert.title=報價提醒 +account.notifications.marketAlert.selectPaymentAccount=提供匹配的付款帳户 +account.notifications.marketAlert.offerType.label=我感興趣的報價類型 +account.notifications.marketAlert.offerType.buy=買入報價(我想要出售 BTC ) +account.notifications.marketAlert.offerType.sell=賣出報價(我想要購買 BTC ) +account.notifications.marketAlert.trigger=報價距離(%) +account.notifications.marketAlert.trigger.info=設置價格區間後,只有當滿足(或超過)您的需求的報價發佈時,您才會收到提醒。您想賣 BTC ,但你只能以當前市價的 2% 溢價出售。將此字段設置為 2% 將確保您只收到高於當前市場價格 2%(或更多)的報價的提醒。 +account.notifications.marketAlert.trigger.prompt=與市場價格的百分比距離(例如 2.50%, -0.50% 等) +account.notifications.marketAlert.addButton=添加報價提醒 +account.notifications.marketAlert.manageAlertsButton=管理報價提醒 +account.notifications.marketAlert.manageAlerts.title=管理報價提醒 +account.notifications.marketAlert.manageAlerts.header.paymentAccount=支付賬户 +account.notifications.marketAlert.manageAlerts.header.trigger=觸發價格 +account.notifications.marketAlert.manageAlerts.header.offerType=報價類型 +account.notifications.marketAlert.message.title=報價提醒 +account.notifications.marketAlert.message.msg.below=低於 +account.notifications.marketAlert.message.msg.above=高於 +account.notifications.marketAlert.message.msg=價格為 {2}({3} {4}市場價)和支付方式為 {5} 的報價 {0} {1} 已發佈到 Bisq 報價列表。\n報價ID: {6}。 +account.notifications.priceAlert.message.title=價格提醒 {0} +account.notifications.priceAlert.message.msg=您的價格提醒已被觸發。當前 {0} 的價格為 {1} {2} +account.notifications.noWebCamFound.warning=未找到網絡攝像頭。\n\n請使用電子郵件選項將代碼和加密密鑰從您的手機發送到 Bisq 應用程序。 +account.notifications.priceAlert.warning.highPriceTooLow=較高的價格必須大於較低的價格。 +account.notifications.priceAlert.warning.lowerPriceTooHigh=較低的價格必須低於較高的價格。 + + + + +#################################################################### +# DAO +#################################################################### + +dao.tab.factsAndFigures=確切消息 +dao.tab.bsqWallet=BSQ 錢包 +dao.tab.proposals=管理 +dao.tab.bonding=關係 +dao.tab.proofOfBurn=資產清單掛牌費/燒燬證明 +dao.tab.monitor=網絡監視器 +dao.tab.news=新聞 + +dao.paidWithBsq=已用 BSQ 支付 +dao.availableBsqBalance=可用於支出(已驗證的+未確認的變更輸出) +dao.verifiedBsqBalance=所有已驗證的 UTXO 餘額 +dao.unconfirmedChangeBalance=已驗證的+未確認的變更輸出 +dao.unverifiedBsqBalance=所有未驗證交易的餘額(等待區塊確認) +dao.lockedForVoteBalance=用於投票 +dao.lockedInBonds=凍結餘額 +dao.availableNonBsqBalance=可用的非 BSQ 餘額(BTC) +dao.reputationBalance=聲望值(不會花費) + +dao.tx.published.success=你的交易已經成功發佈 +dao.proposal.menuItem.make=創建要求 +dao.proposal.menuItem.browse=瀏覽開啟的報償申請 +dao.proposal.menuItem.vote=為報償申請投票 +dao.proposal.menuItem.result=投票結果 +dao.cycle.headline=投票週期 +dao.cycle.overview.headline=投票週期總覽 +dao.cycle.currentPhase=現階段 +dao.cycle.currentBlockHeight=當前區塊高度: +dao.cycle.proposal=提議階段 +dao.cycle.proposal.next=下一個提議階段 +dao.cycle.blindVote=匿名投票階段 +dao.cycle.voteReveal=投票公示階段 +dao.cycle.voteResult=投票結果 +dao.cycle.phaseDuration=區塊 {0} (≈{1});區塊 {2} - {3})(≈{4} - ≈{5}) +dao.cycle.phaseDurationWithoutBlocks=區塊 {0} - {1} (≈{2} - ≈{3} ) + +dao.voteReveal.txPublished.headLine=投票公示交易發佈 +dao.voteReveal.txPublished=你的交易 ID 為 {0} 投票公示交易已經成功發佈。\n\n如果您已經參與了 DAO 投票,那麼這將由軟件自動完成。 + +dao.results.cycles.header=週期 +dao.results.cycles.table.header.cycle=週期 +dao.results.cycles.table.header.numProposals=請求 +dao.results.cycles.table.header.voteWeight=投票權重 +dao.results.cycles.table.header.issuance=發行 + +dao.results.results.table.item.cycle=週期 {0} 開始於:{1} + +dao.results.proposals.header=選定週期的請求 +dao.results.proposals.table.header.nameLink=名稱/鏈接 +dao.results.proposals.table.header.details=詳情 +dao.results.proposals.table.header.myVote=我的投票 +dao.results.proposals.table.header.result=投票結果 +dao.results.proposals.table.header.threshold=閾值 +dao.results.proposals.table.header.quorum=法定人數 + +dao.results.proposals.voting.detail.header=選定提案的投票結果 + +dao.results.exceptions=投票結果異常 + +# suppress inspection "UnusedProperty" +dao.param.UNDEFINED=未定義 + +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BSQ=BSQ 掛單費 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BSQ=BSQ 買單費 +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BSQ=最小 BSQ 掛單費 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BSQ=最小 BSQ 買單費 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_MAKER_FEE_BTC=BTC 掛單費 +# suppress inspection "UnusedProperty" +dao.param.DEFAULT_TAKER_FEE_BTC=BTC 買單費 +# suppress inspection "UnusedProperty" +# suppress inspection "UnusedProperty" +dao.param.MIN_MAKER_FEE_BTC=最小 BTC 掛單費 +# suppress inspection "UnusedProperty" +dao.param.MIN_TAKER_FEE_BTC=最小 BTC 買單費 +# suppress inspection "UnusedProperty" + +# suppress inspection "UnusedProperty" +dao.param.PROPOSAL_FEE=BSQ 提案手續費 +# suppress inspection "UnusedProperty" +dao.param.BLIND_VOTE_FEE=BSQ 投票手續費 + +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MIN_AMOUNT=最低 BSQ 報償申請數量 +# suppress inspection "UnusedProperty" +dao.param.COMPENSATION_REQUEST_MAX_AMOUNT=最高 BSQ 報償申請數量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MIN_AMOUNT=最低 BSQ 退還申請數量 +# suppress inspection "UnusedProperty" +dao.param.REIMBURSEMENT_MAX_AMOUNT=最高 BSQ 退還申請數量 + +# suppress inspection "UnusedProperty" +dao.param.QUORUM_GENERIC=BSQ 要求的一般提案的仲裁人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_COMP_REQUEST=BSQ 要求的報償申請的仲裁人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REIMBURSEMENT=BSQ 要求的退還申請的仲裁人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CHANGE_PARAM=BSQ 要求的改變參數的仲裁人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_REMOVE_ASSET=BSQ 要求的移除資產要求的人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_CONFISCATION=BSQ 要求的沒收申請的仲裁人數 +# suppress inspection "UnusedProperty" +dao.param.QUORUM_ROLE=BSQ 要求的綁定角色的仲裁人數 + +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_GENERIC=普通提案的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_COMP_REQUEST=報償申請的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REIMBURSEMENT=退還申請的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CHANGE_PARAM=改變參數的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_REMOVE_ASSET=移除資產的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_CONFISCATION=沒收申請的要求百分比 +# suppress inspection "UnusedProperty" +dao.param.THRESHOLD_ROLE=擔保角色的要求百分比 + +# suppress inspection "UnusedProperty" +dao.param.RECIPIENT_BTC_ADDRESS=接收方 BTC 地址 + +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE_PER_DAY=資產清單掛牌費每日支付 +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=最小資產交易量 + +# suppress inspection "UnusedProperty" +dao.param.LOCK_TIME_TRADE_PAYOUT=其他交易支出tx的鎖定時間 +# suppress inspection "UnusedProperty" +dao.param.ARBITRATOR_FEE=BTC 仲裁費 + +# suppress inspection "UnusedProperty" +dao.param.MAX_TRADE_LIMIT=最高 BTC 交易限額 + +# suppress inspection "UnusedProperty" +dao.param.BONDED_ROLE_FACTOR=擔保角色對 BSQ 的影響 +# suppress inspection "UnusedProperty" +dao.param.ISSUANCE_LIMIT=每個週期的 BSQ 發行限額 + +dao.param.currentValue=當前值:{0} +dao.param.currentAndPastValue=當前餘額:{0}(提案時的餘額:{1}) +dao.param.blocks={0} 區塊 + +dao.results.invalidVotes=在那個投票週期中,我們有無效的投票。如果投票沒有在 Bisq 網絡中很好地分佈,就會發生這種情況。\n{0} + +# suppress inspection "UnusedProperty" +dao.phase.PHASE_UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_PROPOSAL=提議階段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK1=休息1 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BLIND_VOTE=匿名投票階段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK2=休息2 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_VOTE_REVEAL=投票公示階段 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_BREAK3=休息3 +# suppress inspection "UnusedProperty" +dao.phase.PHASE_RESULT=結果階段 + +dao.results.votes.table.header.stakeAndMerit=投票權重 +dao.results.votes.table.header.stake=份額 +dao.results.votes.table.header.merit=獲得的 +dao.results.votes.table.header.vote=投票 + +dao.bond.menuItem.bondedRoles=擔保角色 +dao.bond.menuItem.reputation=擔保的名譽 +dao.bond.menuItem.bonds=擔保 + +dao.bond.dashboard.bondsHeadline=擔保式 BSQ +dao.bond.dashboard.lockupAmount=鎖定資金 +dao.bond.dashboard.unlockingAmount=正在鎖定資金(等到鎖定時間過後) + + +dao.bond.reputation.header=為名譽鎖定擔保 +dao.bond.reputation.table.header=我的名譽擔保 +dao.bond.reputation.amount=鎖定的 BSQ 數量 +dao.bond.reputation.time=在區塊中的解鎖時間 +dao.bond.reputation.salt=鹽 +dao.bond.reputation.hash=哈希 +dao.bond.reputation.lockupButton=鎖定 +dao.bond.reputation.lockup.headline=確認鎖定交易 +dao.bond.reputation.lockup.details=鎖定數量:{0}\n解鎖時間:{1} 區塊(≈{2})\n\n礦工手續費:{3}({4} 聰/字節)\n交易大小:{5} 字節\n\n你確定想要繼續? +dao.bond.reputation.unlock.headline=確認解鎖交易 +dao.bond.reputation.unlock.details=解鎖金額:{0}\n解鎖時間:{1} 區塊(≈{2})\n\n挖礦手續費:{3}({4} 聰/Byte)\n交易大小:{5} Kb\n\n你想繼續這個操作嗎? + +dao.bond.allBonds.header=所有擔保 + +dao.bond.bondedReputation=擔保的名譽 +dao.bond.bondedRoles=擔保角色 + +dao.bond.details.header=交易方詳情 +dao.bond.details.role=角色 +dao.bond.details.requiredBond=需要的 BSQ 擔保 +dao.bond.details.unlockTime=在區塊中的解鎖時間 +dao.bond.details.link=鏈接到交易方描述 +dao.bond.details.isSingleton=是否可由多個交易方擔任 +dao.bond.details.blocks={0} 區塊 + +dao.bond.table.column.name=名稱 +dao.bond.table.column.link=綁定 +dao.bond.table.column.bondType=連接類型 +dao.bond.table.column.details=詳情 +dao.bond.table.column.lockupTxId=鎖定 Tx ID +dao.bond.table.column.bondState=連接狀態 +dao.bond.table.column.lockTime=解鎖時間 +dao.bond.table.column.lockupDate=鎖定日期 + +dao.bond.table.button.lockup=鎖定 +dao.bond.table.button.unlock=解鎖 +dao.bond.table.button.revoke=撤銷 + +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.bondState.READY_FOR_LOCKUP=尚未擔保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_PENDING=等待鎖定 +# suppress inspection "UnusedProperty" +dao.bond.bondState.LOCKUP_TX_CONFIRMED=鎖定的擔保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_PENDING=等待解鎖 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCK_TX_CONFIRMED=解鎖 Tx 確認 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKING=正在解鎖擔保 +# suppress inspection "UnusedProperty" +dao.bond.bondState.UNLOCKED=擔保已解鎖 +# suppress inspection "UnusedProperty" +dao.bond.bondState.CONFISCATED=沒收的擔保 + +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.BONDED_ROLE=擔保角色 +# suppress inspection "UnusedProperty" +dao.bond.lockupReason.REPUTATION=擔保名譽 + +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.GITHUB_ADMIN=Github 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_ADMIN=論壇管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ROCKET_CHAT_ADMIN=Keybase 管理員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=YouTube 管理 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq 運維人員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BITCOINJ_MAINTAINER=BitcoinJ-fork 運維人員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.NETLAYER_MAINTAINER=Netlayer 運維人員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=網站運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=論壇運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SEED_NODE_OPERATOR=種子節點運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=價格節點運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=比特幣節點運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=交易所運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=瀏覽器運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=移動通知中繼運營者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=域名持有者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS 管理者 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=調解員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=仲裁員 +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC 贊助地址所有者 + +dao.burnBsq.assetFee=資產清單 +dao.burnBsq.menuItem.assetFee=資產清單掛牌費 +dao.burnBsq.menuItem.proofOfBurn=燒燬證明 +dao.burnBsq.header=資產清單掛牌費 +dao.burnBsq.selectAsset=選擇資產 +dao.burnBsq.fee=手續費 +dao.burnBsq.trialPeriod=試用期 +dao.burnBsq.payFee=支付手續費 +dao.burnBsq.allAssets=所有資產 +dao.burnBsq.assets.nameAndCode=資產名稱 +dao.burnBsq.assets.state=狀態 +dao.burnBsq.assets.tradeVolume=交易量 +dao.burnBsq.assets.lookBackPeriod=確認期 +dao.burnBsq.assets.trialFee=試用期手續費 +dao.burnBsq.assets.totalFee=總共已支付費用 +dao.burnBsq.assets.days={0} 天 +dao.burnBsq.assets.toFewDays=資產清單掛牌費過低。在試用期中最低數量為 {0}。 + +# suppress inspection "UnusedProperty" +dao.assetState.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=在試用期 +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=活躍的交易 +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED=因不活躍而被取消 +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=移除投票 + +dao.proofOfBurn.header=燒燬證明 +dao.proofOfBurn.amount=數量 +dao.proofOfBurn.preImage=預覽 +dao.proofOfBurn.burn=燒燬 +dao.proofOfBurn.allTxs=所有燒燬證明交易 +dao.proofOfBurn.myItems=我的燒燬證明交易 +dao.proofOfBurn.date=日期 +dao.proofOfBurn.hash=哈希 +dao.proofOfBurn.txs=交易記錄 +dao.proofOfBurn.pubKey=公鑰 +dao.proofOfBurn.signature.window.title=使用燒燬證明交易中的密鑰驗證消息 +dao.proofOfBurn.verify.window.title=使用燒燬證明交易中的密鑰確認消息 +dao.proofOfBurn.copySig=將驗證複製到剪貼板 +dao.proofOfBurn.sign=驗證 +dao.proofOfBurn.message=消息 +dao.proofOfBurn.sig=驗證 +dao.proofOfBurn.verify=確認 +dao.proofOfBurn.verificationResult.ok=確認成功 +dao.proofOfBurn.verificationResult.failed=確認失敗 + +# suppress inspection "UnusedProperty" +dao.phase.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.phase.PROPOSAL=提議階段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK1=匿名投票前的休息階段 +# suppress inspection "UnusedProperty" +dao.phase.BLIND_VOTE=匿名投票階段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK2=投票公示前的休息階段 +# suppress inspection "UnusedProperty" +dao.phase.VOTE_REVEAL=投票公示階段 +# suppress inspection "UnusedProperty" +dao.phase.BREAK3=公佈結果前的休息階段 +# suppress inspection "UnusedProperty" +dao.phase.RESULT=投票結果階段 + +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.PROPOSAL=提議階段 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.BLIND_VOTE=匿名投票 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.VOTE_REVEAL=投票公示 +# suppress inspection "UnusedProperty" +dao.phase.separatedPhaseBar.RESULT=投票結果 + +# suppress inspection "UnusedProperty" +dao.proposal.type.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.proposal.type.COMPENSATION_REQUEST=報償申請 +# suppress inspection "UnusedProperty" +dao.proposal.type.REIMBURSEMENT_REQUEST=退還申請 +# suppress inspection "UnusedProperty" +dao.proposal.type.BONDED_ROLE=擔保角色的提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.REMOVE_ASSET=移除資產提案 +# suppress inspection "UnusedProperty" +dao.proposal.type.CHANGE_PARAM=修改參數的提議 +# suppress inspection "UnusedProperty" +dao.proposal.type.GENERIC=一般提議 +# suppress inspection "UnusedProperty" +dao.proposal.type.CONFISCATE_BOND=沒收擔保請求 + +# suppress inspection "UnusedProperty" +dao.proposal.type.short.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.COMPENSATION_REQUEST=報償申請 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REIMBURSEMENT_REQUEST=退還申請 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.BONDED_ROLE=擔保角色 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.REMOVE_ASSET=移除一個數字貨幣 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CHANGE_PARAM=修改參數 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.GENERIC=一般提議 +# suppress inspection "UnusedProperty" +dao.proposal.type.short.CONFISCATE_BOND=沒收擔保 + +dao.proposal.details=提議細節 +dao.proposal.selectedProposal=選定賠償要求 +dao.proposal.active.header=當前週期的提案 +dao.proposal.active.remove.confirm=您確定要移除該提案嗎?\n如果您刪除該提案,提案費將會丟失。 +dao.proposal.active.remove.doRemove=是,移除我的提案 +dao.proposal.active.remove.failed=不能移除提案。 +dao.proposal.myVote.title=投票 +dao.proposal.myVote.accept=接受提議 +dao.proposal.myVote.reject=拒絕提案 +dao.proposal.myVote.removeMyVote=忽略提案 +dao.proposal.myVote.merit=所得的 BSQ 的投票權重 +dao.proposal.myVote.stake=Stake 的投票權重 +dao.proposal.myVote.revealTxId=投票公示的交易 ID +dao.proposal.myVote.stake.prompt=在投票中最大可用份額:{0} +dao.proposal.votes.header=設置投票的份額,併發布您的投票 +dao.proposal.myVote.button=發佈投票 +dao.proposal.myVote.setStake.description=在對所有提案進行投票後,您必須鎖定BSQ來設置投票的份額。您鎖定的 BSQ 越多,你的投票權重就越大。\n\n投票會鎖定 BSQ 將在投票顯示階段再次解鎖。 +dao.proposal.create.selectProposalType=選擇提案類型 +dao.proposal.create.phase.inactive=請等到下一個提案階段 +dao.proposal.create.proposalType=提議類型 +dao.proposal.create.new=創建新的賠償要求 +dao.proposal.create.button=創建賠償要求 +dao.proposal.create.publish=發佈提案 +dao.proposal.create.publishing=正在發佈提案中... +dao.proposal=提案 +dao.proposal.display.type=提議類型 +dao.proposal.display.name=確切的 GitHub 的用户名 +dao.proposal.display.link=詳情的鏈接 +dao.proposal.display.link.prompt=提案的鏈接 +dao.proposal.display.requestedBsq=申請的 BSQ 數量 +dao.proposal.display.txId=提案交易 ID +dao.proposal.display.proposalFee=提案手續費 +dao.proposal.display.myVote=我的投票 +dao.proposal.display.voteResult=投票結果總結 +dao.proposal.display.bondedRoleComboBox.label=擔保角色類型 +dao.proposal.display.requiredBondForRole.label=角色需要的擔保 +dao.proposal.display.option=選項 + +dao.proposal.table.header.proposalType=提議類型 +dao.proposal.table.header.link=綁定 +dao.proposal.table.header.myVote=我的投票 +# suppress inspection "UnusedProperty" +dao.proposal.table.header.remove=移除 +dao.proposal.table.icon.tooltip.removeProposal=移除我的提案 +dao.proposal.table.icon.tooltip.changeVote=當前投票:“{0}”。更改投票至:“{1}” + +dao.proposal.display.myVote.accepted=已接受 +dao.proposal.display.myVote.rejected=已拒絕 +dao.proposal.display.myVote.ignored=已忽略 +dao.proposal.display.myVote.unCounted=投票結果不包括在內 +dao.proposal.myVote.summary=已投票:{0};投票權重:{1}(獲得的:{2} + 獎金:{3})({4}) +dao.proposal.myVote.invalid=投票無效 + +dao.proposal.voteResult.success=已接受 +dao.proposal.voteResult.failed=已拒絕 +dao.proposal.voteResult.summary=結果:{0};閾值:{1}(要求> {2});仲裁人數:{3}(要求> {4}) + +dao.proposal.display.paramComboBox.label=選擇需要改變的參數 +dao.proposal.display.paramValue=參數值 + +dao.proposal.display.confiscateBondComboBox.label=選擇擔保 +dao.proposal.display.assetComboBox.label=需要移除的資產 + +dao.blindVote=匿名投票 + +dao.blindVote.startPublishing=發佈匿名投票交易中 +dao.blindVote.success=我們的匿名投票交易已經成功發佈。\n\n請注意,您必須在線在投票公示階段,以便您的 Bisq 應用程序可以發佈投票公示交易。沒有投票公示交易,您的投票將無效! + +dao.wallet.menuItem.send=發送 +dao.wallet.menuItem.receive=接收 +dao.wallet.menuItem.transactions=交易記錄 + +dao.wallet.dashboard.myBalance=我的錢包餘額 + +dao.wallet.receive.fundYourWallet=你的 BSQ 接收地址 +dao.wallet.receive.bsqAddress=BSQ 錢包地址(刷新未使用地址) + +dao.wallet.send.sendFunds=提現 +dao.wallet.send.sendBtcFunds=發送非 BSQ 資金(BTC) +dao.wallet.send.amount=BSQ 數量 +dao.wallet.send.btcAmount=BTC 數量(無 BSQ 資金) +dao.wallet.send.setAmount=設置提現數量(最小量 {0}) +dao.wallet.send.receiverAddress=接收者的 BSQ 地址 +dao.wallet.send.receiverBtcAddress=接收者的 BTC 地址 +dao.wallet.send.setDestinationAddress=輸入您的目標地址 +dao.wallet.send.send=發送 BSQ 資金 +dao.wallet.send.inputControl=Select inputs +dao.wallet.send.sendBtc=發送 BTC 資金 +dao.wallet.send.sendFunds.headline=確定提現申請 +dao.wallet.send.sendFunds.details=發送:{0}\n來自:{1}\n要求的礦工手續費:{2}({3}比特/節)\n交易大小:{4}字節\n\n接收方會收到:{5}\n\n您確定您想要提現這些數量嗎? +dao.wallet.chainHeightSynced=最新確認區塊:{0} +dao.wallet.chainHeightSyncing=等待區塊... 已確認{0}/{1}區塊 +dao.wallet.tx.type=類型 + +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED=未定義 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNDEFINED_TX_TYPE=不被認可 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNVERIFIED=未驗證的 BSQ 交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.INVALID=無效的 BSQ 交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.GENESIS=初始交易 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.TRANSFER_BSQ=劃轉 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.received.TRANSFER_BSQ=接收 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.sent.TRANSFER_BSQ=發送 BSQ +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PAY_TRADE_FEE=付交易費記錄 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.COMPENSATION_REQUEST=報償申請記錄 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.REIMBURSEMENT_REQUEST=退還申請的手續費 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROPOSAL=提案手續費 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.BLIND_VOTE=投票記錄 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.VOTE_REVEAL=投票公示 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.LOCKUP=鎖定擔保 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.UNLOCK=解鎖擔保 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=資產清單掛牌費 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=燒燬證明 +# suppress inspection "UnusedProperty" +dao.tx.type.enum.IRREGULAR=不正常 + +dao.tx.withdrawnFromWallet=BTC 已從錢包中取出 +dao.tx.issuanceFromCompReq=報償申請/發放 +dao.tx.issuanceFromCompReq.tooltip=導致新 BSQ 發行的報償請求。\n發行日期:{0} +dao.tx.issuanceFromReimbursement=退還申請/發放 +dao.tx.issuanceFromReimbursement.tooltip=導致新 BSQ 發放的退還申請。\n發放日期: {0} +dao.proposal.create.missingBsqFunds=您沒有足夠的 BSQ 資金來創建提案。如果您有一個未經確認的 BSQ 交易,您需要等待一個區塊鏈確認,因為 BSQ 只有在包含在一個區塊中時才會被驗證。\n缺失:{0} + +dao.proposal.create.missingBsqFundsForBond=你沒有足夠的 BSQ 資金來承擔這個角色。您仍然可以發佈這個提案,但如果它被接受,您將需要這個角色所需的全部 BSQ 金額。\n缺少:{0} + +dao.proposal.create.missingMinerFeeFunds=您沒有足夠的BTC資金來支付該提案交易。所有的 BSQ交易需要用 BTC 支付挖礦手續費。\n缺少:{0} + +dao.proposal.create.missingIssuanceFunds=您沒有足夠的BTC資金來支付該提案交易。所有的 BSQ交易需要用 BTC 支付挖礦手續費以及發起交易也需要用 BTC 支付所需的 BSQ 數量({0} 聰/BSQ)\n缺少:{1} + +dao.feeTx.confirm=確認 {0} 交易 +dao.feeTx.confirm.details={0}手續費:{1}\n礦工手續費:{2}({3} 聰/byte)\n交易大小:{4} Kb\n\n您確定您要發送這個 {5} 交易嗎? + +dao.feeTx.issuanceProposal.confirm.details={0}手續費:{1}\n為 BSQ 提案所需要的BTC:{2}({3}聰 / BSQ)\n挖礦手續費:{4}({5}聰 /字節)\n交易大小:{6}Kb\n\n如果你的要求被批准,你將收到你要求數量的 2 個 BSQ 提議的費用。\n\n你確定你想要發佈{7}交易? + +dao.news.bisqDAO.title=Bisq DAO +dao.news.bisqDAO.description=正如 Bisq交易是分散的,並且不受審查,它的治理模型也是如此—— Bisq DAO 和 BSQ 是使其成為可能的工具。 +dao.news.bisqDAO.readMoreLink=瞭解有關 Bisq DAO 的更多信息 + +dao.news.pastContribution.title=過去有所貢獻?申請 BSQ +dao.news.pastContribution.description=如果您對 Bisq 有貢獻,請使用下面的 BSQ 地址,並申請參與 BSQ 初始分發。 +dao.news.pastContribution.yourAddress=你的 BSQ 錢包地址 +dao.news.pastContribution.requestNow=現在申請 + +dao.news.DAOOnTestnet.title=在我們的測試網絡上運行 BISQ DAO +dao.news.DAOOnTestnet.description=核心網絡 Bisq DAO 還沒有啟動,但是您可以通過在我們的測試網絡上運行它來了解 Bisq DAO 。 +dao.news.DAOOnTestnet.firstSection.title=1.切換至 DAO 測試網絡模式 +dao.news.DAOOnTestnet.firstSection.content=從設置頁面切換到 DAO 測試網絡。 +dao.news.DAOOnTestnet.secondSection.title=2.獲得一些 BSQ +dao.news.DAOOnTestnet.secondSection.content=在 Slack 上申請 BSQ 或在 Bisq 上購買 BSQ 。 +dao.news.DAOOnTestnet.thirdSection.title=3.參與投票週期 +dao.news.DAOOnTestnet.thirdSection.content=就修改 Bisq 的各個方面提出建議並進行表決。 +dao.news.DAOOnTestnet.fourthSection.title=4.探索 BSQ 區塊鏈瀏覽器 +dao.news.DAOOnTestnet.fourthSection.content=由於 BSQ 只是比特幣,你可以看到 BSQ 交易在我們的比特幣區塊瀏覽器。 +dao.news.DAOOnTestnet.readMoreLink=閲讀完整的文檔 + +dao.monitor.daoState=DAO 狀態 +dao.monitor.proposals=提案狀態 +dao.monitor.blindVotes=匿名投票狀態 + +dao.monitor.table.peers=節點 +dao.monitor.table.conflicts=矛盾 +dao.monitor.state=狀態 +dao.monitor.requestAlHashes=要求所有哈希 +dao.monitor.resync=重新同步 DAO 狀態 +dao.monitor.table.header.cycleBlockHeight=週期/區塊高度 +dao.monitor.table.cycleBlockHeight=週期 {0} /區塊 {1} +dao.monitor.table.seedPeers=種子節點:{0} + +dao.monitor.daoState.headline=DAO 狀態 +dao.monitor.daoState.table.headline=DAO 狀態的哈希鏈 +dao.monitor.daoState.table.blockHeight=區塊高度 +dao.monitor.daoState.table.hash=DAO 狀態的哈希 +dao.monitor.daoState.table.prev=以前的哈希 +dao.monitor.daoState.conflictTable.headline=來自不同實體的 DAO 狀態哈希 +dao.monitor.daoState.utxoConflicts=UTXO 衝突 +dao.monitor.daoState.utxoConflicts.blockHeight=區塊高度:{0} +dao.monitor.daoState.utxoConflicts.sumUtxo=所有 UTXO 的總和:{0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=所有 BSQ 的總和:{0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO 狀態與網絡不同步。重啟之後,DAO 狀態將重新同步。 + +dao.monitor.proposal.headline=提案狀態 +dao.monitor.proposal.table.headline=提案狀態的哈希鏈 +dao.monitor.proposal.conflictTable.headline=來自不同實體的提案狀態哈希 + +dao.monitor.proposal.table.hash=提案狀態的哈希 +dao.monitor.proposal.table.prev=以前的哈希 +dao.monitor.proposal.table.numProposals=提案編號 + +dao.monitor.isInConflictWithSeedNode=您的本地數據與至少一個種子節點不一致。請重新同步 DAO 狀態。 +dao.monitor.isInConflictWithNonSeedNode=您的一個對等節點與網絡不一致,但您的節點與種子節點同步。 +dao.monitor.daoStateInSync=您的本地節點與網絡一致 + +dao.monitor.blindVote.headline=匿名投票狀態 +dao.monitor.blindVote.table.headline=匿名投票狀態的哈希鏈 +dao.monitor.blindVote.conflictTable.headline=來自不同實體的匿名投票狀態哈希 +dao.monitor.blindVote.table.hash=匿名投票狀態的哈希 +dao.monitor.blindVote.table.prev=以前的哈希 +dao.monitor.blindVote.table.numBlindVotes=匿名投票編號 + +dao.factsAndFigures.menuItem.supply=BSQ 供給 +dao.factsAndFigures.menuItem.transactions=BSQ 交易 + +dao.factsAndFigures.dashboard.avgPrice90=90天平均 BSQ/BTC 交易價格 +dao.factsAndFigures.dashboard.avgPrice30=30天平均 BSQ/BTC 交易價格 +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) +dao.factsAndFigures.dashboard.availableAmount=總共可用的 BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart + +dao.factsAndFigures.supply.issuedVsBurnt=已發放的 BSQ 已銷燬的 BSQ + +dao.factsAndFigures.supply.issued=已發放的 BSQ +dao.factsAndFigures.supply.compReq=Compensation requests +dao.factsAndFigures.supply.reimbursement=Reimbursement requests +dao.factsAndFigures.supply.genesisIssueAmount=在初始交易中心有問題的 BSQ +dao.factsAndFigures.supply.compRequestIssueAmount=報償申請發放的 BSQ +dao.factsAndFigures.supply.reimbursementAmount=退還申請發放的 BSQ +dao.factsAndFigures.supply.totalIssued=Total issued BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} +dao.factsAndFigures.supply.burnt=BSQ 燒燬總量 + +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=交易總量 +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + +dao.factsAndFigures.supply.locked=BSQ 全局鎖定狀態 +dao.factsAndFigures.supply.totalLockedUpAmount=擔保的鎖定 +dao.factsAndFigures.supply.totalUnlockingAmount=正在從擔保解鎖 BSQ +dao.factsAndFigures.supply.totalUnlockedAmount=已從擔保解鎖 BSQ +dao.factsAndFigures.supply.totalConfiscatedAmount=已從擔保沒收 BSQ +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees + +dao.factsAndFigures.transactions.genesis=創始交易 +dao.factsAndFigures.transactions.genesisBlockHeight=初始區塊高度 +dao.factsAndFigures.transactions.genesisTxId=初始交易 ID +dao.factsAndFigures.transactions.txDetails=BSQ 交易統計 +dao.factsAndFigures.transactions.allTx=所有 BSQ 交易記錄 +dao.factsAndFigures.transactions.utxo=所有未用交易的量 +dao.factsAndFigures.transactions.compensationIssuanceTx=所有報償請求問題的交易記錄 +dao.factsAndFigures.transactions.reimbursementIssuanceTx=所有退回申請問題的交易記錄 +dao.factsAndFigures.transactions.burntTx=所有費用支付記錄 +dao.factsAndFigures.transactions.invalidTx=所有無效交易記錄 +dao.factsAndFigures.transactions.irregularTx=所有不正常的交易記錄: + + + +#################################################################### +# Windows +#################################################################### + +inputControlWindow.headline=Select inputs for transaction +inputControlWindow.balanceLabel=可用餘額 + +contractWindow.title=糾紛詳情 +contractWindow.dates=報價時間/交易時間 +contractWindow.btcAddresses=BTC 買家/BTC 賣家的比特幣地址 +contractWindow.onions=BTC 買家/BTC 賣家的網絡地址 +contractWindow.accountAge=BTC 買家/BTC 賣家的賬齡 +contractWindow.numDisputes=BTC 買家/BTC 賣家的糾紛編號 +contractWindow.contractHash=合同哈希 + +displayAlertMessageWindow.headline=重要資料! +displayAlertMessageWindow.update.headline=重要更新資料! +displayAlertMessageWindow.update.download=下載: +displayUpdateDownloadWindow.downloadedFiles=下載完成的文件: +displayUpdateDownloadWindow.downloadingFile=正在下載:{0} +displayUpdateDownloadWindow.verifiedSigs=驗證驗證: +displayUpdateDownloadWindow.status.downloading=下載文件... +displayUpdateDownloadWindow.status.verifying=驗證驗證中... +displayUpdateDownloadWindow.button.label=下載安裝程序並驗證驗證 +displayUpdateDownloadWindow.button.downloadLater=稍後下載 +displayUpdateDownloadWindow.button.ignoreDownload=忽略這個版本 +displayUpdateDownloadWindow.headline=Bisq 有新的更新! +displayUpdateDownloadWindow.download.failed.headline=下載失敗 +displayUpdateDownloadWindow.download.failed=下載失敗。\n請到 https://bisq.io/downloads 下載並驗證。 +displayUpdateDownloadWindow.installer.failed=無法確定正確的安裝程序。請通過 https://bisq.network/downloads 手動下載和驗證。 +displayUpdateDownloadWindow.verify.failed=驗證失敗。\n請到 https://bisq.io/downloads 手動下載和驗證。 +displayUpdateDownloadWindow.success=新版本成功下載並驗證驗證 。\n\n請打開下載目錄,關閉應用程序並安裝最新版本。 +displayUpdateDownloadWindow.download.openDir=打開下載目錄 + +disputeSummaryWindow.title=概要 +disputeSummaryWindow.openDate=工單創建時間 +disputeSummaryWindow.role=交易者的角色 +disputeSummaryWindow.payout=交易金額支付 +disputeSummaryWindow.payout.getsTradeAmount=BTC {0} 獲得交易金額支付 +disputeSummaryWindow.payout.getsAll=最大 BTC 支付數 {0} +disputeSummaryWindow.payout.custom=自定義支付 +disputeSummaryWindow.payoutAmount.buyer=買家支付金額 +disputeSummaryWindow.payoutAmount.seller=賣家支付金額 +disputeSummaryWindow.payoutAmount.invert=使用失敗者作為發佈者 +disputeSummaryWindow.reason=糾紛的原因 +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=可用性 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=違反協議 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=不回覆 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=詐騙 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=其他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=可選交易 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=錯誤的發送者賬號 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=交易夥伴已超時 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=交易已穩定 + +disputeSummaryWindow.summaryNotes=總結説明 +disputeSummaryWindow.addSummaryNotes=添加總結説明 +disputeSummaryWindow.close.button=關閉話題 + +# Do no change any line break or order of tokens as the structure is used for signature verification +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.msg=工單已關閉{0}\n{1} 節點地址:{12}\n\n總結:\n交易 ID:{3}\n貨幣:{4}\n交易金額:{5}\nBTC 買家支付金額:{6}\nBTC 賣家支付金額:{7}\n\n糾紛原因:{8}\n\n總結:\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=\n\n下一個步驟:\n打開未完成交易,接受或拒絕建議的調解員的建議 +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一個步驟:\n不需要您採取進一步的行動。如果仲裁員做出了對你有利的裁決,你將在 資金/交易 頁中看到“仲裁退款”交易 +disputeSummaryWindow.close.closePeer=你也需要關閉交易對象的話題! +disputeSummaryWindow.close.txDetails.headline=發佈交易退款 +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.buyer=買方收到{0}在地址:{1} +# suppress inspection "TrailingSpacesInProperty" +disputeSummaryWindow.close.txDetails.seller=賣方收到{0}在地址:{1} +disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}({4}satoshis/byte)\n事務大小:{5} Kb\n\n您確定要發佈此交易嗎? + +disputeSummaryWindow.close.noPayout.headline=未支付關閉 +disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情況下關閉嗎? + +emptyWalletWindow.headline={0} 錢包急救工具 +emptyWalletWindow.info=請在緊急情況下使用,如果您無法從 UI 中訪問您的資金。\n\n請注意,使用此工具時,所有未結報價將自動關閉。\n\n在使用此工具之前,請備份您的數據目錄。您可以在“帳户/備份”中執行此操作。\n\n請報吿我們您的問題,並在 Github 或 Bisq 論壇上提交錯誤報吿,以便我們可以調查導致問題的原因。 +emptyWalletWindow.balance=您的可用錢包餘額 +emptyWalletWindow.bsq.btcBalance=非 BSQ 聰餘額 + +emptyWalletWindow.address=輸入您的目標地址 +emptyWalletWindow.button=發送全部資金 +emptyWalletWindow.openOffers.warn=您有已發佈的報價,如果您清空錢包將被刪除。\n你確定要清空你的錢包嗎? +emptyWalletWindow.openOffers.yes=是的,我確定 +emptyWalletWindow.sent.success=您的錢包的餘額已成功轉移。 + +enterPrivKeyWindow.headline=輸入密鑰進行註冊 + +filterWindow.headline=編輯篩選列表 +filterWindow.offers=篩選報價(用逗號“,”隔開) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) +filterWindow.accounts=篩選交易賬户數據:\n格式:逗號分割的 [付款方式ID|數據字段|值] +filterWindow.bannedCurrencies=篩選貨幣代碼(用逗號“,”隔開) +filterWindow.bannedPaymentMethods=篩選支付方式 ID(用逗號“,”隔開) +filterWindow.bannedAccountWitnessSignerPubKeys=已過濾的帳户證人簽名者公鑰(逗號分隔十六進制公鑰) +filterWindow.bannedPrivilegedDevPubKeys=已過濾的特權開發者公鑰(逗號分隔十六進制公鑰) +filterWindow.arbitrators=篩選後的仲裁人(用逗號“,”隔開的洋葱地址) +filterWindow.mediators=篩選後的調解員(用逗號“,”隔開的洋葱地址) +filterWindow.refundAgents=篩選後的退款助理(用逗號“,”隔開的洋葱地址) +filterWindow.seedNode=篩選後的種子節點(用逗號“,”隔開的洋葱地址) +filterWindow.priceRelayNode=篩選後的價格中繼節點(用逗號“,”隔開的洋葱地址) +filterWindow.btcNode=篩選後的比特幣節點(用逗號“,”隔開的地址+端口) +filterWindow.preventPublicBtcNetwork=禁止使用公共比特幣網絡 +filterWindow.disableDao=禁用 DAO +filterWindow.disableAutoConf=禁用自動確認 +filterWindow.autoConfExplorers=已過濾自動確認瀏覽器(逗號分隔地址) +filterWindow.disableDaoBelowVersion=DAO 最低所需要的版本 +filterWindow.disableTradeBelowVersion=交易最低所需要的版本 +filterWindow.add=添加篩選 +filterWindow.remove=移除篩選 +filterWindow.btcFeeReceiverAddresses=比特幣手續費接收地址 +filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation + +offerDetailsWindow.minBtcAmount=最小 BTC 數量 +offerDetailsWindow.min=(最小 {0}) +offerDetailsWindow.distance=(與市場價格的差距:{0}) +offerDetailsWindow.myTradingAccount=我的交易賬户 +offerDetailsWindow.offererBankId=(賣家的銀行 ID/BIC/SWIFT) +offerDetailsWindow.offerersBankName=(賣家的銀行名稱): +offerDetailsWindow.bankId=銀行 ID(例如 BIC 或者 SWIFT ): +offerDetailsWindow.countryBank=賣家銀行所在國家或地區 +offerDetailsWindow.commitment=承諾 +offerDetailsWindow.agree=我同意 +offerDetailsWindow.tac=條款和條件 +offerDetailsWindow.confirm.maker=確定:發佈報價 {0} 比特幣 +offerDetailsWindow.confirm.taker=確定:下單買入 {0} 比特幣 +offerDetailsWindow.creationDate=創建時間 +offerDetailsWindow.makersOnion=賣家的匿名地址 + +qRCodeWindow.headline=二維碼 +qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Bisq 錢包 +qRCodeWindow.request=付款請求:\n{0} + +selectDepositTxWindow.headline=選擇糾紛的存款交易 +selectDepositTxWindow.msg=存款交易未存儲在交易中。\n請從您的錢包中選擇一個現有的多重驗證交易,這是在失敗的交易中使用的存款交易。\n\n您可以通過打開交易詳細信息窗口(點擊列表中的交易 ID)並按照交易費用支付交易輸出到您看到多重驗證存款交易的下一個交易(地址從3開始),找到正確的交易。 該交易 ID 應在此處列出的列表中顯示。 一旦您找到正確的交易,請在此處選擇該交易並繼續\n\n抱歉給您帶來不便,但是錯誤的情況應該非常罕見,將來我們會嘗試找到更好的解決方法。 +selectDepositTxWindow.select=選擇存款交易 + +sendAlertMessageWindow.headline=發送全球通知 +sendAlertMessageWindow.alertMsg=提醒消息 +sendAlertMessageWindow.enterMsg=輸入消息: +sendAlertMessageWindow.isSoftwareUpdate=Software download notification +sendAlertMessageWindow.isUpdate=Is full release +sendAlertMessageWindow.isPreRelease=Is pre-release +sendAlertMessageWindow.version=新版本號 +sendAlertMessageWindow.send=發送通知 +sendAlertMessageWindow.remove=移除通知 + +sendPrivateNotificationWindow.headline=發送私信 +sendPrivateNotificationWindow.privateNotification=私人通知 +sendPrivateNotificationWindow.enterNotification=輸入通知 +sendPrivateNotificationWindow.send=發送私人通知 + +showWalletDataWindow.walletData=錢包數據 +showWalletDataWindow.includePrivKeys=包含私鑰 + +setXMRTxKeyWindow.headline=證明已發送 XMR +setXMRTxKeyWindow.note=在下面添加 tx 信息可以更快的自動確認交易。更多信息::https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=交易 ID (可選) +setXMRTxKeyWindow.txKey=交易密鑰 (可選) + +# We do not translate the tac because of the legal nature. We would need translations checked by lawyers +# in each language which is too expensive atm. +tacWindow.headline=用户協議 +tacWindow.agree=我同意 +tacWindow.disagree=我不同意並退出 +tacWindow.arbitrationSystem=糾紛解決方案 + +tradeDetailsWindow.headline=交易 +tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID: +tradeDetailsWindow.tradeDate=交易時間 +tradeDetailsWindow.txFee=礦工手續費 +tradeDetailsWindow.tradingPeersOnion=交易夥伴匿名地址 +tradeDetailsWindow.tradingPeersPubKeyHash=交易夥伴公鑰哈希值 +tradeDetailsWindow.tradeState=交易狀態 +tradeDetailsWindow.agentAddresses=仲裁員/調解員 +tradeDetailsWindow.detailData=Detail data + +txDetailsWindow.headline=Transaction Details +txDetailsWindow.btc.note=You have sent BTC. +txDetailsWindow.bsq.note=You have sent BSQ funds. BSQ is colored bitcoin, so the transaction will not show in a BSQ explorer until it has been confirmed in a bitcoin block. +txDetailsWindow.sentTo=Sent to +txDetailsWindow.txId=TxId + +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + +walletPasswordWindow.headline=輸入密碼解鎖 + +torNetworkSettingWindow.header=Tor 網絡設置 +torNetworkSettingWindow.noBridges=不使用網橋 +torNetworkSettingWindow.providedBridges=連接到提供的網橋 +torNetworkSettingWindow.customBridges=輸入自定義網橋 +torNetworkSettingWindow.transportType=傳輸類型 +torNetworkSettingWindow.obfs3=obfs3 +torNetworkSettingWindow.obfs4=obfs4(推薦) +torNetworkSettingWindow.meekAmazon=meek-amazon +torNetworkSettingWindow.meekAzure=meek-azure +torNetworkSettingWindow.enterBridge=輸入一個或多個網橋中繼節點(每行一個) +torNetworkSettingWindow.enterBridgePrompt=輸入地址:端口 +torNetworkSettingWindow.restartInfo=您需要重新啟動以應用更改 +torNetworkSettingWindow.openTorWebPage=打開 Tor Project 網頁 +torNetworkSettingWindow.deleteFiles.header=連接問題? +torNetworkSettingWindow.deleteFiles.info=如果您在啟動時有重複的連接問題,刪除過期的 Tor 文件可能會有所幫助。如果要嘗試修復,請單擊下面的按鈕,然後重新啟動。 +torNetworkSettingWindow.deleteFiles.button=刪除過期的 Tor 文件並關閉 +torNetworkSettingWindow.deleteFiles.progress=關閉正在運行中的 Tor +torNetworkSettingWindow.deleteFiles.success=過期的 Tor 文件被成功刪除。請重新啟動。 +torNetworkSettingWindow.bridges.header=Tor 網絡被屏蔽? +torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的國家或地區屏蔽,您可以嘗試使用 Tor 網橋。\n \n訪問 Tor 網頁:https://bridges.torproject.org/bridges,瞭解關於網橋和可插拔傳輸的更多信息。 + +feeOptionWindow.headline=選擇貨幣支付交易手續費 +feeOptionWindow.info=您可以選擇用 BSQ 或 BTC 支付交易費用。如果您選擇 BSQ ,您會感謝這些交易手續費折扣。 +feeOptionWindow.optionsLabel=選擇貨幣支付交易手續費 +feeOptionWindow.useBTC=使用 BTC +feeOptionWindow.fee={0}(≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) + + +#################################################################### +# Popups +#################################################################### + +popup.headline.notification=通知 +popup.headline.instruction=請注意: +popup.headline.attention=注意 +popup.headline.backgroundInfo=背景資料 +popup.headline.feedback=完成 +popup.headline.confirmation=確定 +popup.headline.information=資料 +popup.headline.warning=警吿 +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 報吿中,如果包含 bisq.log 文件,那麼調試就會變得更容易。 + +popup.error.tryRestart=請嘗試重啟您的應用程序或者檢查您的網絡連接。 +popup.error.takeOfferRequestFailed=當有人試圖接受你的報價時發生了一個錯誤:\n{0} + +error.spvFileCorrupted=讀取 SPV 鏈文件時發生錯誤。\n可能是 SPV 鏈文件被破壞了。\n\n錯誤消息:{0}\n\n要刪除它並開始重新同步嗎? +error.deleteAddressEntryListFailed=無法刪除 AddressEntryList 文件。\n \n錯誤:{0} +error.closedTradeWithUnconfirmedDepositTx=交易 ID 為 {0} 的已關閉交易的保證金交易仍未確認。\n \n請在“設置/網絡信息”進行 SPV 重新同步,以查看交易是否有效。 +error.closedTradeWithNoDepositTx=交易 ID 為 {0} 的保證金交易已被確認。\n\n請重新啟動應用程序來清理已關閉的交易列表。 + +popup.warning.walletNotInitialized=錢包至今未初始化 +popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Bisq 使用Java)會在 MacOS 中引發彈出警吿(``Bisq 希望從任何應用程序接收擊鍵'').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Bisq”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Bisq將升級到新的 Java 版本,以避免該問題。 +popup.warning.wrongVersion=您這台電腦上可能有錯誤的 Bisq 版本。\n您的電腦的架構是:{0}\n您安裝的 Bisq 二進制文件是:{1}\n請關閉並重新安裝正確的版本({2})。 +popup.warning.incompatibleDB=我們檢測到不兼容的數據庫文件!\n\n那些數據庫文件與我們當前的代碼庫不兼容:\n{0}\n\n我們對損壞的文件進行了備份,並將默認值應用於新的數據庫版本。\n\n備份位於:\n{1}/db/backup_of_corrupted_data。\n\n請檢查您是否安裝了最新版本的 Bisq\n您可以下載:\nhttps://bisq.network/downloads\n\n請重新啟動應用程序。 +popup.warning.startupFailed.twoInstances=Bisq 已經在運行。 您不能運行兩個 Bisq 實例。 +popup.warning.tradePeriod.halfReached=您與 ID {0} 的交易已達到最長交易期的一半,且仍未完成。\n\n交易期結束於 {1}\n\n請查看“業務/未完成交易”的交易狀態,以獲取更多信息。 +popup.warning.tradePeriod.ended=您與 ID {0} 的已達到最長交易期,且未完成。\n\n交易期結束於 {1}\n\n請查看“業務/未完成交易”的交易狀態,以從調解員獲取更多信息。 +popup.warning.noTradingAccountSetup.headline=您還沒有設置交易賬户 +popup.warning.noTradingAccountSetup.msg=您需要設置法定貨幣或數字貨幣賬户才能創建報價。\n您要設置帳户嗎? +popup.warning.noArbitratorsAvailable=沒有仲裁員可用。 +popup.warning.noMediatorsAvailable=沒有調解員可用。 +popup.warning.notFullyConnected=您需要等到您完全連接到網絡\n在啟動時可能需要2分鐘。 +popup.warning.notSufficientConnectionsToBtcNetwork=你需要等待至少有{0}個與比特幣網絡的連接點。 +popup.warning.downloadNotComplete=您需要等待,直到丟失的比特幣區塊被下載完畢。 +popup.warning.chainNotSynced=Bisq 錢包區塊鏈高度沒有正確地同步。如果最近才打開應用,請等待一個新發布的比特幣區塊。\n\n你可以檢查區塊鏈高度在設置/網絡信息。如果經過了一個區塊但問題還是沒有解決,你應該及時的完成 SPV 鏈重新同步。https://bisq.wiki/Resyncing_SPV_file +popup.warning.removeOffer=您確定要移除該報價嗎?\n如果您刪除該報價,{0} 的掛單費將會丟失。 +popup.warning.tooLargePercentageValue=您不能設置100%或更大的百分比。 +popup.warning.examplePercentageValue=請輸入百分比數字,如 5.4% 是“5.4” +popup.warning.noPriceFeedAvailable=該貨幣沒有可用的價格。 你不能使用基於百分比的價格。\n請選擇固定價格。 +popup.warning.sendMsgFailed=向您的交易對象發送消息失敗。\n請重試,如果繼續失敗報吿錯誤。 +popup.warning.insufficientBtcFundsForBsqTx=你沒有足夠的 BTC 資金支付這筆交易的挖礦手續費。\n請充值您的 BTC 錢包。\n缺少的資金:{0} +popup.warning.bsqChangeBelowDustException=該交易產生的 BSQ 變化輸出低於零頭限制(5.46 BSQ),將被比特幣網絡拒絕。\n\n您需要發送更高的金額以避免更改輸出(例如,通過在您的發送金額中添加零頭),或者向您的錢包中添加更多的 BSQ 資金,以避免生成零頭輸出。\n\n零頭輸出為 {0}。 +popup.warning.btcChangeBelowDustException=該交易創建的更改輸出低於零頭限制(546 聰),將被比特幣網絡拒絕。\n\n您需要將零頭添加到發送量中,以避免生成零頭輸出。\n\n零頭輸出為{0}。 + +popup.warning.insufficientBsqFundsForBtcFeePayment=您需要更多的 BSQ 去完成這筆交易 - 錢包中最後剩餘 5.46 BSQ 將無法用於支付交易手續費因為 BTC 協議中的零頭限制。\n\n你可以購買更多的 BSQ 或用 BTC支付交易手續費\n\n缺少 BSQ 資金:{0} +popup.warning.noBsqFundsForBtcFeePayment=您的 BSQ 錢包沒有足夠的資金支付 BSQ 的交易費用。 +popup.warning.messageTooLong=您的信息超過最大允許的大小。請將其分成多個部分發送,或將其上傳到 https://pastebin.com 之類的服務器。 +popup.warning.lockedUpFunds=你已經從一個失敗的交易中凍結了資金。\n凍結餘額:{0}\n存款tx地址:{1}\n交易單號:{2}\n\n請通過選擇待處理交易界面中的交易並點擊“alt + o”或“option+ o”打開幫助話題。 + +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=忽略並繼續 + +# suppress inspection "UnusedProperty" +popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 +# suppress inspection "UnusedProperty" +popup.warning.priceRelay=價格傳遞 +popup.warning.seed=種子 +popup.warning.mandatoryUpdate.trading=請更新到最新的 Bisq 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Bisq 論壇。 +popup.warning.mandatoryUpdate.dao=請更新到最新的 Bisq 版本。強制更新禁止了舊版本舊版本的 Bisq DAO 和 BSQ 。更多信息請訪問 Bisq 論壇。 +popup.warning.disable.dao=Bisq DAO 和 BSQ 被臨時禁用的。更多信息請訪問 Bisq 論壇。 +popup.warning.noFilter=We did not receive a filter object from the seed nodes. This is a not expected situation. Please inform the Bisq developers. +popup.warning.burnBTC=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 BTC 來轉賬。 + +popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Bisq Keybase 團隊的 Support 頻道 + +popup.warning.trade.txRejected.tradeFee=交易手續費 +popup.warning.trade.txRejected.deposit=押金 +popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Bisq support channel at the Bisq Keybase team. + +popup.warning.openOfferWithInvalidMakerFeeTx=交易 ID 為 {0} 的掛單費交易無效。\n交易 ID = {1}。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Bisq Keybase 團隊的 Support 頻道 + +popup.info.securityDepositInfo=為了確保雙方都遵守交易協議,雙方都需要支付保證金。\n\n這筆存款一直保存在您的交易錢包裏,直到您的交易成功完成,然後再退還給您。\n\n請注意:如果您正在創建一個新的報價,Bisq 需要運行另一個交易員接受它。為了讓您的報價在線,保持 Bisq 運行,並確保這台計算機也在線(即,確保它沒有切換到待機模式…顯示器可以待機)。 + +popup.info.cashDepositInfo=請確保您在您的地區有一個銀行分行,以便能夠進行現金存款。\n賣方銀行的銀行 ID(BIC/SWIFT)為:{0}。 +popup.info.cashDepositInfo.confirm=我確認我可以支付保證金 +popup.info.shutDownWithOpenOffers=Bisq 正在被關閉,但仍有公開的報價。\n\n當 Bisq 關閉時,這些提供將不能在 P2P 網絡上使用,但是它們將在您下次啟動 Bisq 時重新發布到 P2P 網絡上。\n\n為了讓您的報價在線,保持 Bisq 運行,並確保這台計算機也在線(即,確保它不會進入待機模式…顯示器待機不是問題)。 +popup.info.qubesOSSetupInfo=你似乎好像在 Qubes OS 上運行 Bisq。\n\n請確保您的 Bisq qube 是參考設置指南的説明設置的 https://bisq.wiki/Running_Bisq_on_Qubes +popup.warn.downGradePrevention=不支持從 {0} 版本降級到 {1} 版本。請使用最新的 Bisq 版本。 +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. + +popup.privateNotification.headline=重要私人通知! + +popup.securityRecommendation.headline=重要安全建議 +popup.securityRecommendation.msg=如果您還沒有啟用,我們想提醒您考慮為您的錢包使用密碼保護。\n\n強烈建議你寫下錢包還原密鑰。 那些還原密鑰就是恢復你的比特幣錢包的主密碼。\n在“錢包密鑰”部分,您可以找到更多信息\n\n此外,您應該在“備份”界面備份完整的應用程序數據文件夾。 + +popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\nPlease ensure:\n- the node is fully synced before starting Bisq\n- pruning is disabled ('prune=0' in bitcoin.conf)\n- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf) + +popup.shutDownInProgress.headline=正在關閉 +popup.shutDownInProgress.msg=關閉應用可能會花一點時間。\n請不要打斷關閉過程。 + +popup.attention.forTradeWithId=交易 ID {0} 需要注意 +popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. + +popup.info.multiplePaymentAccounts.headline=多個支付賬户可用 +popup.info.multiplePaymentAccounts.msg=您有多個支付帳户在這個報價中可用。請確你做了正確的選擇。 + +popup.accountSigning.selectAccounts.headline=選擇付款賬户 +popup.accountSigning.selectAccounts.description=根據付款方式和時間點,所有與支付給買方的付款發生的爭議有關的付款帳户將被選擇讓您驗證。 +popup.accountSigning.selectAccounts.signAll=驗證所有付款方式 +popup.accountSigning.selectAccounts.datePicker=選擇要驗證的帳户的時間點 + +popup.accountSigning.confirmSelectedAccounts.headline=確認選定的付款帳户 +popup.accountSigning.confirmSelectedAccounts.description=根據您的輸入,將選擇 {0} 支付帳户。 +popup.accountSigning.confirmSelectedAccounts.button=確認付款賬户 +popup.accountSigning.signAccounts.headline=確認驗證付款賬户 +popup.accountSigning.signAccounts.description=根據您的選擇,{0} 付款帳户將被驗證。 +popup.accountSigning.signAccounts.button=驗證付款賬户 +popup.accountSigning.signAccounts.ECKey=輸入仲裁員密鑰 +popup.accountSigning.signAccounts.ECKey.error=不正確的仲裁員 ECKey + +popup.accountSigning.success.headline=恭喜 +popup.accountSigning.success.description=所有 {0} 支付賬户已成功驗證! +popup.accountSigning.generalInformation=您將在帳户頁面找到所有賬户的驗證狀態。\n\n更多信息,請訪問https://docs.bisq.network/payment-methods#account-signing. +popup.accountSigning.signedByArbitrator=您的一個付款帳户已被認證以及被仲裁員驗證。交易成功後,使用此帳户將自動驗證您的交易夥伴的帳户。\n\n{0} +popup.accountSigning.signedByPeer=您的一個付款帳户已經被交易夥伴驗證和驗證。您的初始交易限額將被取消,您將能夠在{0}天后驗證其他帳户。 +popup.accountSigning.peerLimitLifted=您其中一個帳户的初始限額已被取消。\n\n{0} +popup.accountSigning.peerSigner=您的一個帳户已足夠成熟,可以驗證其他付款帳户,您的一個帳户的初始限額已被取消。\n\n{0} + +popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness +popup.accountSigning.confirmSingleAccount.headline=確認所選賬齡證據 +popup.accountSigning.confirmSingleAccount.selectedHash=已選擇證據哈希值 +popup.accountSigning.confirmSingleAccount.button=驗證賬齡證據 +popup.accountSigning.successSingleAccount.description=證據 {0} 已被驗證 +popup.accountSigning.successSingleAccount.success.headline=成功 + +popup.accountSigning.unsignedPubKeys.headline=未驗證公鑰 +popup.accountSigning.unsignedPubKeys.sign=驗證公鑰 +popup.accountSigning.unsignedPubKeys.signed=公鑰已被驗證 +popup.accountSigning.unsignedPubKeys.result.signed=已驗證公鑰 +popup.accountSigning.unsignedPubKeys.result.failed=未能驗證公鑰 + +#################################################################### +# Notifications +#################################################################### + +notification.trade.headline=交易 ID {0} 的通知 +notification.ticket.headline=交易 ID {0} 的幫助話題 +notification.trade.completed=交易現在完成,您可以提取資金。 +notification.trade.accepted=您 BTC {0} 的報價被接受。 +notification.trade.confirmed=您的交易至少有一個區塊鏈確認。\n您現在可以開始付款。 +notification.trade.paymentStarted=BTC 買家已經開始付款。 +notification.trade.selectTrade=選擇交易 +notification.trade.peerOpenedDispute=您的交易對象創建了一個 {0}。 +notification.trade.disputeClosed=這個 {0} 被關閉。 +notification.walletUpdate.headline=交易錢包更新 +notification.walletUpdate.msg=您的交易錢包充值成功。\n金額:{0} +notification.takeOffer.walletUpdate.msg=您的交易錢包已經從早期的下單嘗試中得到足夠的資金支持。\n金額:{0} +notification.tradeCompleted.headline=交易完成 +notification.tradeCompleted.msg=您現在可以提現您的資金到您的外部比特幣錢包或者劃轉它到 Bisq 錢包。 + + +#################################################################### +# System Tray +#################################################################### + +systemTray.show=顯示應用程序窗口 +systemTray.hide=隱藏應用程序窗口 +systemTray.info=關於 Bisq 信息 +systemTray.exit=退出 +systemTray.tooltip=Bisq:去中心化比特幣交易網絡 + + +#################################################################### +# GUI Util +#################################################################### + +guiUtil.miningFeeInfo=請確保您的外部錢包使用的礦工手續費費用足夠高至少為 {0} 聰/字節,以便礦工接受資金交易。\n否則交易所交易無法確認,交易最終將會出現糾紛。 + +guiUtil.accountExport.savedToPath=交易賬户保存在路徑:\n{0} +guiUtil.accountExport.noAccountSetup=您沒有交易賬户設置導出。 +guiUtil.accountExport.selectPath=選擇路徑 {0} +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountExport.tradingAccount=交易賬户 ID {0}\n +# suppress inspection "TrailingSpacesInProperty" +guiUtil.accountImport.noImport=我們沒有導入 ID {0} 的交易賬户,因為它已經存在。\n +guiUtil.accountExport.exportFailed=導出 .CSV 失敗,因為發生了錯誤。\n錯誤 = {0} +guiUtil.accountExport.selectExportPath=選擇導出路徑 +guiUtil.accountImport.imported=交易賬户導入路徑:\n{0}\n\n導入賬户:\n{1} +guiUtil.accountImport.noAccountsFound=在路徑 {0} 找不到導出的交易賬户。\n文件名為 {1}。 +guiUtil.openWebBrowser.warning=您將在系統網絡瀏覽器中打開一個網頁。\n你現在要打開網頁嗎?\n\n如果您沒有使用“Tor 瀏覽器”作為默認的系統網絡瀏覽器,則將以默認連接到網頁。\n\n網址:“{0}” +guiUtil.openWebBrowser.doOpen=打開網頁並且不要再詢問我 +guiUtil.openWebBrowser.copyUrl=複製 URL 並取消 +guiUtil.ofTradeAmount=的交易數量 +guiUtil.requiredMinimum=(最低需求量) + +#################################################################### +# Component specific +#################################################################### + +list.currency.select=選擇幣種 +list.currency.showAll=顯示全部 +list.currency.editList=編輯幣種列表 + +table.placeholder.noItems=最近沒有可用的 {0} +table.placeholder.noData=最近沒有可用數據 +table.placeholder.processingData=處理數據… + + +peerInfoIcon.tooltip.tradePeer=交易夥伴 +peerInfoIcon.tooltip.maker=製造者 +peerInfoIcon.tooltip.trade.traded={0} 匿名地址:{1}\n您已經與他交易過 {2} 次了\n{3} +peerInfoIcon.tooltip.trade.notTraded={0} 匿名地址:{1}\n你還沒有與他交易過。\n{2} +peerInfoIcon.tooltip.age=支付賬户在 {0} 前創建。 +peerInfoIcon.tooltip.unknownAge=支付賬户賬齡未知。 + +tooltip.openPopupForDetails=打開彈出窗口的詳細信息 +tooltip.invalidTradeState.warning=這個交易處於不可用狀態。打開詳情窗口以發現更多細節。 +tooltip.openBlockchainForAddress=使用外部區塊鏈瀏覽器打開地址:{0} +tooltip.openBlockchainForTx=使用外部區塊鏈瀏覽器打開交易:{0} + +confidence.unknown=未知交易狀態 +confidence.seen=被 {0} 人查看 / 0 確定 +confidence.confirmed=在 {0} 區塊中確認 +confidence.invalid=交易無效 + +peerInfo.title=對象資料 +peerInfo.nrOfTrades=已完成交易數量 +peerInfo.notTradedYet=你還沒有與他交易過。 +peerInfo.setTag=設置該對象的標籤 +peerInfo.age.noRisk=支付賬户賬齡 +peerInfo.age.chargeBackRisk=自驗證 +peerInfo.unknownAge=賬齡未知 + +addressTextField.openWallet=打開您的默認比特幣錢包 +addressTextField.copyToClipboard=複製地址到剪貼板 +addressTextField.addressCopiedToClipboard=地址已被複制到剪貼板 +addressTextField.openWallet.failed=打開默認的比特幣錢包應用程序失敗了。或許您沒有安裝? + +peerInfoIcon.tooltip={0}\n標識:{1} + +txIdTextField.copyIcon.tooltip=複製交易 ID 到剪貼板 +txIdTextField.blockExplorerIcon.tooltip=使用外部區塊鏈瀏覽器打開這個交易 ID +txIdTextField.missingTx.warning.tooltip=所需的交易缺失 + + +#################################################################### +# Navigation +#################################################################### + +navigation.account=“賬户” +navigation.account.walletSeed=“賬户/錢包密鑰” +navigation.funds.availableForWithdrawal=“資金/提現” +navigation.portfolio.myOpenOffers=“資料/未完成報價” +navigation.portfolio.pending=“業務/未完成交易” +navigation.portfolio.closedTrades=“資料/歷史” +navigation.funds.depositFunds=“資金/收到資金” +navigation.settings.preferences=“設置/偏好” +# suppress inspection "UnusedProperty" +navigation.funds.transactions=“資金/交易記錄” +navigation.support=“幫助” +navigation.dao.wallet.receive=“DAO/BSQ 錢包/接收” + + +#################################################################### +# Formatter +#################################################################### + +formatter.formatVolumeLabel={0} 數量 {1} +formatter.makerTaker=賣家 {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.fiat=您創建新的報價 {0} {1} +formatter.youAreCreatingAnOffer.altcoin=您正創建報價 {0} {1}({2} {3}) +formatter.asMaker={0} {1} 是賣家 +formatter.asTaker={0} {1} 是買家 + + +#################################################################### +# Domain specific +#################################################################### + +# we use enum values here +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +BTC_MAINNET=比特幣主幹網絡 +# suppress inspection "UnusedProperty" +BTC_TESTNET=比特幣測試網絡 +# suppress inspection "UnusedProperty" +BTC_REGTEST=比特幣迴歸測試 +# suppress inspection "UnusedProperty" +BTC_DAO_TESTNET=比特幣 DAO 測試網絡(棄用) +# suppress inspection "UnusedProperty" +BTC_DAO_BETANET=Bisq DAO 測試網絡(比特幣主要網絡) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=比特幣 DAO 迴歸測試 + +time.year=年線 +time.month=月線 +time.week=周線 +time.day=日線 +time.hour=小時 +time.minute10=10分鐘 +time.hours=小時 +time.days=天 +time.1hour=1小時 +time.1day=1天 +time.minute=分鐘 +time.second=秒 +time.minutes=分鐘 +time.seconds=秒 + + +password.enterPassword=輸入密碼 +password.confirmPassword=確認密碼 +password.tooLong=你輸入的密碼太長,不要超過 500 個字符。 +password.deriveKey=從密碼中提取密鑰 +password.walletDecrypted=錢包成功解密並移除密碼保護 +password.wrongPw=你輸入了錯誤的密碼。\n\n請再次嘗試輸入密碼,仔細檢查拼寫錯誤。 +password.walletEncrypted=錢包成功加密並開啟密碼保護。 +password.walletEncryptionFailed=無法設置錢包密碼。您可能導入了與錢包數據庫不匹配的還原密鑰。請在 Keybase 上聯繫開發者(https://keybase.io/team/bisq]) +password.passwordsDoNotMatch=這2個密碼您輸入的不相同 +password.forgotPassword=忘記密碼? +password.backupReminder=請注意,設置錢包密碼時,所有未加密的錢包的自動創建的備份將被刪除。\n\n強烈建議您備份應用程序的目錄,並在設置密碼之前記下您的還原密鑰! +password.backupWasDone=我已備份 +password.setPassword=Set Password (I already made a backup) +password.makeBackup=Make Backup + +seed.seedWords=錢包密鑰 +seed.enterSeedWords=輸入錢包密鑰 +seed.date=錢包時間 +seed.restore.title=使用還原密鑰恢復錢包 +seed.restore=恢復錢包 +seed.creationDate=創建時間 +seed.warn.walletNotEmpty.msg=你的比特幣錢包不是空的。\n\n在嘗試恢復較舊的錢包之前,您必須清空此錢包,因為將錢包混在一起會導致無效的備份。\n\n請完成您的交易,關閉所有您的未完成報價,並轉到資金界面撤回您的比特幣。\n如果您無法訪問您的比特幣,您可以使用緊急工具清空錢包。\n要打開該應急工具,請按“alt + e”或“Cmd/Ctrl + e” 。 +seed.warn.walletNotEmpty.restore=無論如何我要恢復 +seed.warn.walletNotEmpty.emptyWallet=我先清空我的錢包 +seed.warn.notEncryptedAnymore=你的錢包被加密了。\n\n恢復後,錢包將不再加密,您必須設置新的密碼。\n\n你要繼續嗎? +seed.warn.walletDateEmpty=由於您尚未指定錢包日期,因此 Bisq 將必須掃描 2013.10.09(BIP39創始日期)之後的區塊鏈。\n\nBIP39 錢包於 Bisq 於 2017.06.28 首次發佈(版本 v0.5)。因此,您可以使用該日期來節省時間。\n\n理想情況下,您應指定創建錢包種子的日期。\n\n\n您確定要繼續而不指定錢包日期嗎? +seed.restore.success=新的還原密鑰成功地恢復了錢包。\n\n您需要關閉並重新啟動應用程序。 +seed.restore.error=使用還原密鑰恢復錢包時出現錯誤。{0} +seed.restore.openOffers.warn=您有公開報價,如果您從種子詞恢復,則這些報價將被刪除。\n您確定要繼續嗎? + + +#################################################################### +# Payment methods +#################################################################### + +payment.account=賬户 +payment.account.no=賬户編號 +payment.account.name=賬户名稱 +payment.account.userName=用户暱稱 +payment.account.phoneNr=電話號碼 +payment.account.owner=賬户擁有者姓名: +payment.account.fullName=全稱(名,中間名,姓) +payment.account.state=州/省/地區 +payment.account.city=城市 +payment.bank.country=銀行所在國家或地區 +payment.account.name.email=賬户擁有者姓名/電子郵箱 +payment.account.name.emailAndHolderId=賬户擁有者姓名/電子郵箱 / {0} +payment.bank.name=銀行名稱 +payment.select.account=選擇賬户類型 +payment.select.region=選擇地區 +payment.select.country=選擇國家或地區 +payment.select.bank.country=選擇銀行所在國家或地區 +payment.foreign.currency=你確定想選擇一個與此國家或地區默認幣種不同的貨幣? +payment.restore.default=不,恢復默認值 +payment.email=電子郵箱 +payment.country=國家或地區 +payment.extras=額外要求 +payment.email.mobile=電子郵箱或手機號碼 +payment.altcoin.address=數字貨幣地址 +payment.altcoin.tradeInstantCheckbox=使用數字貨幣進行即時交易( 1 小時內) +payment.altcoin.tradeInstant.popup=對於即時交易,要求交易雙方都在線,能夠在不到1小時內完成交易。\n \n如果你已經有未完成的報價以及你不能即時完成,請在資料頁面禁用這些報價。 +payment.altcoin=數字貨幣 +payment.select.altcoin=選擇或搜索數字貨幣 +payment.secret=密保問題 +payment.answer=答案 +payment.wallet=錢包 ID +payment.amazon.site=Buy giftcard at +payment.ask=Ask in Trader Chat +payment.uphold.accountId=用户名或電子郵箱或電話號碼 +payment.moneyBeam.accountId=電子郵箱或者電話號碼 +payment.venmo.venmoUserName=Venmo 用户名: +payment.popmoney.accountId=電子郵箱或者電話號碼 +payment.promptPay.promptPayId=公民身份證/税號或電話號碼 +payment.supportedCurrencies=支持的貨幣 +payment.supportedCurrenciesForReceiver=Currencies for receiving funds +payment.limitations=限制條件 +payment.salt=帳户年齡驗證鹽值 +payment.error.noHexSalt=鹽值需要十六進制的。\n如果您想要從舊帳户轉移鹽值以保留帳齡,只建議編輯鹽值字段。帳齡通過帳户鹽值和識別帳户數據(例如 IBAN )來驗證。 +payment.accept.euro=接受來自這些歐元國家的交易 +payment.accept.nonEuro=接受來自這些非歐元國家的交易 +payment.accepted.countries=接受的國家 +payment.accepted.banks=接受的銀行(ID) +payment.mobile=手機號碼 +payment.postal.address=郵寄地址 +payment.national.account.id.AR=CBU 號碼 +shared.accountSigningState=賬户驗證狀態 + +#new +payment.altcoin.address.dyn={0} 地址 +payment.altcoin.receiver.address=接收者的數字貨幣地址 +payment.accountNr=賬號: +payment.emailOrMobile=電子郵箱或手機號碼 +payment.useCustomAccountName=使用自定義名稱 +payment.maxPeriod=最大允許交易時限 +payment.maxPeriodAndLimit=最大交易時間:{0}/ 最大買入:{1}/ 最大出售:{2}/賬齡:{3} +payment.maxPeriodAndLimitCrypto=最大交易期限:{0}/最大交易限額:{1} +payment.currencyWithSymbol=貨幣:{0} +payment.nameOfAcceptedBank=接受的銀行名稱 +payment.addAcceptedBank=添加接受的銀行 +payment.clearAcceptedBanks=清除接受的銀行 +payment.bank.nameOptional=銀行名稱(可選) +payment.bankCode=銀行代碼 +payment.bankId=銀行 ID (BIC/SWIFT): +payment.bankIdOptional=銀行 ID(BIC/SWIFT)(可選) +payment.branchNr=分行編碼 +payment.branchNrOptional=分行編碼(可選) +payment.accountNrLabel=賬號(IBAN) +payment.accountType=賬户類型 +payment.checking=檢查 +payment.savings=保存 +payment.personalId=個人 ID +payment.makeOfferToUnsignedAccount.warning=With the recent rise in BTC price, beware that selling 0.01 BTC or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >0.01 BTC, so you only deal with signed/trusted buyers\n- keep any offers to sell <0.01 BTC to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nBisq 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 BTC price, beware that selling 0.01 BTC 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\nBisq 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.clearXchange.info=Zelle是一項轉賬服務,轉賬到其他銀行做的很好。\n\n1.檢查此頁面以查看您的銀行是否(以及如何)與 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特別注意您的轉賬限額-匯款限額因銀行而異,銀行通常分別指定每日,每週和每月的限額。\n\n3.如果您的銀行不能使用 Zelle,您仍然可以通過 Zelle 移動應用程序使用它,但是您的轉賬限額會低得多。\n\n4.您的 Bisq 帳户上指定的名稱必須與 Zelle/銀行帳户上的名稱匹配。 \n\n如果您無法按照貿易合同中的規定完成 Zelle 交易,則可能會損失部分(或全部)保證金。\n\n由於 Zelle 的拒付風險較高,因此建議賣家通過電子郵件或 SMS 與未簽名的買家聯繫,以確認買家確實擁有 Bisq 中指定的 Zelle 帳户。 +payment.fasterPayments.newRequirements.info=有些銀行已經開始核實快捷支付收款人的全名。您當前的快捷支付帳户沒有填寫全名。\n\n請考慮在 Bisq 中重新創建您的快捷支付帳户,為將來的 {0} 買家提供一個完整的姓名。\n\n重新創建帳户時,請確保將銀行區號、帳户編號和帳齡驗證鹽值從舊帳户複製到新帳户。這將確保您現有的帳齡和簽名狀態得到保留。 +payment.moneyGram.info=使用 MoneyGram 時,BTC 買方必須將授權號碼和收據的照片通過電子郵件發送給 BTC 賣方。收據必須清楚地顯示賣方的全名、國家或地區、州和金額。買方將在交易過程中顯示賣方的電子郵件。 +payment.westernUnion.info=使用 Western Union 時,BTC 買方必須通過電子郵件將 MTCN(運單號)和收據照片發送給 BTC 賣方。收據上必須清楚地顯示賣方的全名、城市、國家或地區和金額。買方將在交易過程中顯示賣方的電子郵件。 +payment.halCash.info=使用 HalCash 時,BTC 買方需要通過手機短信向 BTC 賣方發送 HalCash 代碼。\n\n請確保不要超過銀行允許您用半現金匯款的最高金額。每次取款的最低金額是 10 歐元,最高金額是 10 歐元。金額是 600 歐元。對於重複取款,每天每個接收者 3000 歐元,每月每個接收者 6000 歐元。請與您的銀行核對這些限額,以確保它們使用與此處所述相同的限額。\n\n提現金額必須是 10 歐元的倍數,因為您不能從 ATM 機提取其他金額。 創建報價和下單屏幕中的 UI 將調整 BTC 金額,使 EUR 金額正確。你不能使用基於市場的價格,因為歐元的數量會隨着價格的變化而變化。\n +# suppress inspection "UnusedMessageFormatParameter" +payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Bisq 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Bisq Wiki 查看更多信息[HYPERLINK:https://bisq.wiki/Account_limits]。 +# suppress inspection "UnusedProperty" +payment.limits.info.withSigning=為了降低這一風險,Bisq 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Bisq Wiki 上查看更多:\nhttps://bisq.wiki/Account_limits + +payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 + +payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不是像以往的電話號碼或電子郵件。 +payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名''以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 +payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 + +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=在 Bisq 上交易 US Postal Money Orders (USPMO)您必須理解下述條款:\n\n- BTC 買方必須在發送方和收款人字段中都寫上 BTC 賣方的名稱,並在發送之前對 USPMO 和信封進行高分辨率照片拍照,並帶有跟蹤證明。\n- BTC 買方必須將 USPMO 連同交貨確認書一起發送給 BTC 賣方。\n\n如果需要調解,或有交易糾紛,您將需要將照片連同 USPMO 編號,郵局編號和交易金額一起發送給 Bisq 調解員或退款代理,以便他們進行驗證美國郵局網站上的詳細信息。\n\n如未能提供要求的交易數據將在糾紛中直接判負\n\n在所有爭議案件中,USPMO 發送方在向調解人或仲裁員提供證據/證明時承擔 100% 的責任。\n\n如果您不理解這些要求,請不要在 Bisq 上使用 USPMO 進行交易。 + +payment.cashByMail.info=Trading using cash-by-mail (CBM) on Bisq requires that you understand the following:\n\n● BTC buyer should package cash in a tamper-evident cash bag.\n● BTC buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n● BTC buyer should send the cash package to the BTC seller with Delivery Confirmation and appropriate Insurance.\n● BTC seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\nCBM trades put the onus to act honestly squarely on both peers.\n\n● CBM trades have less verifiable actions than other fiat trades. This makes handling dispute much harder.\n● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any CBM dispute.\n● Mediators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n● If a mediator is engaged, and if either peer rejects the mediator's suggestion, both peers' funds will be sent to a Bisq 'donation' address [HYPERLINK:https://bisq.wiki/Arbitration#Time-Locked_Payout_Transaction], and the trade will effectively be completed.\n● If a trader rejects a mediation suggestion and opens arbitration, it could lead to a loss of both the trading and the deposit funds.\n● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute. For Cash by Mail trades the Arbitrators decision is final.\n● Reimbursement requests any lost funds resulting from Cash By Mail trades to the Bisq DAO will NOT be considered.\n\nTo be sure you fully understand the requirements of cash-by-mail trades, please see: [HYPERLINK:https://bisq.wiki/Cash_by_Mail]\n\nIf you do not understand these requirements, do not trade using CBM on Bisq. + +payment.cashByMail.contact=聯繫方式 +payment.cashByMail.contact.prompt=Name or nym envelope should be addressed to +payment.f2f.contact=聯繫方式 +payment.f2f.contact.prompt=您希望如何與交易夥伴聯繫?(電子郵箱、電話號碼、…) +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=與網上交易相比,“面對面”交易有不同的規則,也有不同的風險。\n\n主要區別是:\n●交易夥伴需要使用他們提供的聯繫方式交換關於會面地點和時間的信息。\n●交易雙方需要攜帶筆記本電腦,在會面地點確認“已發送付款”和“已收到付款”。\n●如果交易方有特殊的“條款和條件”,他們必須在賬户的“附加信息”文本框中聲明這些條款和條件。\n●在發生爭議時,調解員或仲裁員不能提供太多幫助,因為通常很難獲得有關會面上所發生情況的篡改證據。在這種情況下,BTC 資金可能會被無限期鎖定,或者直到交易雙方達成協議。\n\n為確保您完全理解“面對面”交易的不同之處,請閲讀以下説明和建議:“https://docs.bisq.network/trading-rules.html#f2f-trading” +payment.f2f.info.openURL=打開網頁 +payment.f2f.offerbook.tooltip.countryAndCity=國家或地區及城市:{0} / {1} +payment.f2f.offerbook.tooltip.extra=附加信息:{0} + +payment.japan.bank=銀行 +payment.japan.branch=分行 +payment.japan.account=賬户 +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 BTC seller via your Amazon account. \n\nBisq will show the BTC 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://bisq.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) + + +# We use constants from the code so we do not use our normal naming convention +# dynamic values are not recognized by IntelliJ + +# Only translate general terms +NATIONAL_BANK=國內銀行轉賬 +SAME_BANK=同銀行轉賬 +SPECIFIC_BANKS=轉到指定銀行 +US_POSTAL_MONEY_ORDER=美國郵政匯票 +CASH_DEPOSIT=現金/ATM 存款 +CASH_BY_MAIL=Cash By Mail +MONEY_GRAM=MoneyGram +WESTERN_UNION=西聯匯款 +F2F=面對面(當面交易) +JAPAN_BANK=日本銀行匯款 +AUSTRALIA_PAYID=澳大利亞 PayID + +# suppress inspection "UnusedProperty" +NATIONAL_BANK_SHORT=國內銀行 +# suppress inspection "UnusedProperty" +SAME_BANK_SHORT=相同銀行 +# suppress inspection "UnusedProperty" +SPECIFIC_BANKS_SHORT=指定銀行 +# suppress inspection "UnusedProperty" +US_POSTAL_MONEY_ORDER_SHORT=美國匯票 +# suppress inspection "UnusedProperty" +CASH_DEPOSIT_SHORT=現金/ATM 存款 +# suppress inspection "UnusedProperty" +CASH_BY_MAIL_SHORT=CashByMail +# suppress inspection "UnusedProperty" +MONEY_GRAM_SHORT=MoneyGram +# suppress inspection "UnusedProperty" +WESTERN_UNION_SHORT=西聯匯款 +# suppress inspection "UnusedProperty" +F2F_SHORT=F2F +# suppress inspection "UnusedProperty" +JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=支付 ID + +# Do not translate brand names +# suppress inspection "UnusedProperty" +UPHOLD=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM=MoneyBeam(N26) +# suppress inspection "UnusedProperty" +POPMONEY=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY=支付寶 +# suppress inspection "UnusedProperty" +WECHAT_PAY=微信支付 +# suppress inspection "UnusedProperty" +SEPA=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT=SEPA 即時支付 +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS=更快的支付方式 +# suppress inspection "UnusedProperty" +SWISH=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE=Zelle(ClearXchange) +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS=數字貨幣 +# suppress inspection "UnusedProperty" +PROMPT_PAY=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD=亞馬遜電子禮品卡 +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT=Altcoins Instant + +# 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" +UPHOLD_SHORT=Uphold +# suppress inspection "UnusedProperty" +MONEY_BEAM_SHORT=MoneyBeam(N26) +# suppress inspection "UnusedProperty" +POPMONEY_SHORT=Popmoney +# suppress inspection "UnusedProperty" +REVOLUT_SHORT=Revolut +# suppress inspection "UnusedProperty" +PERFECT_MONEY_SHORT=Perfect Money +# suppress inspection "UnusedProperty" +ALI_PAY_SHORT=支付寶 +# suppress inspection "UnusedProperty" +WECHAT_PAY_SHORT=微信支付 +# suppress inspection "UnusedProperty" +SEPA_SHORT=SEPA +# suppress inspection "UnusedProperty" +SEPA_INSTANT_SHORT=SEPA Instant +# suppress inspection "UnusedProperty" +FASTER_PAYMENTS_SHORT=更快的支付方式 +# suppress inspection "UnusedProperty" +SWISH_SHORT=Swish +# suppress inspection "UnusedProperty" +CLEAR_X_CHANGE_SHORT=Zelle +# suppress inspection "UnusedProperty" +CHASE_QUICK_PAY_SHORT=Chase QuickPay +# suppress inspection "UnusedProperty" +INTERAC_E_TRANSFER_SHORT=Interac e-Transfer +# suppress inspection "UnusedProperty" +HAL_CASH_SHORT=HalCash +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_SHORT=數字貨幣 +# suppress inspection "UnusedProperty" +PROMPT_PAY_SHORT=PromptPay +# suppress inspection "UnusedProperty" +ADVANCED_CASH_SHORT=Advanced Cash +# suppress inspection "UnusedProperty" +TRANSFERWISE_SHORT=TransferWise +# suppress inspection "UnusedProperty" +AMAZON_GIFT_CARD_SHORT=亞馬遜電子禮品卡 +# suppress inspection "UnusedProperty" +BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant + +# Deprecated: Cannot be deleted as it would break old trade history entries +# suppress inspection "UnusedProperty" +OK_PAY_SHORT=OKPay +# suppress inspection "UnusedProperty" +CASH_APP_SHORT=Cash App +# suppress inspection "UnusedProperty" +VENMO_SHORT=Venmo + + +#################################################################### +# Validation +#################################################################### + +validation.empty=不允許留空。 +validation.NaN=輸入的不是有效數字。 +validation.notAnInteger=輸入的不是整數。 +validation.zero=不允許輸入0。 +validation.negative=不允許輸入負值。 +validation.fiat.toSmall=不允許輸入比最小可能值還小的數值。 +validation.fiat.toLarge=不允許輸入比最大可能值還大的數值。 +validation.btc.fraction=此充值將會產生小於 1 聰的比特幣數量。 +validation.btc.toLarge=不允許充值大於{0} +validation.btc.toSmall=不允許充值小於{0} +validation.passwordTooShort=你輸入的密碼太短。最少 8 個字符。 +validation.passwordTooLong=你輸入的密碼太長。最長不要超過50個字符。 +validation.sortCodeNumber={0} 必須由 {1} 個數字構成。 +validation.sortCodeChars={0} 必須由 {1} 個字符構成。 +validation.bankIdNumber={0} 必須由 {1} 個數字構成。 +validation.accountNr=賬號必須由 {0} 個數字構成。 +validation.accountNrChars=賬户必須由 {0} 個字符構成。 +validation.btc.invalidAddress=地址不正確,請檢查地址格式。 +validation.integerOnly=請輸入整數。 +validation.inputError=您的輸入引起了錯誤:\n{0} +validation.bsq.insufficientBalance=您的可用錢包餘額為 {0}。 +validation.btc.exceedsMaxTradeLimit=您的交易限額為 {0}。 +validation.bsq.amountBelowMinAmount=最小金額為 {0} +validation.nationalAccountId={0} 必須由{1}個數字組成。 + +#new +validation.invalidInput=輸入無效:{0} +validation.accountNrFormat=帳號必須是格式:{0} +# suppress inspection "UnusedProperty" +validation.altcoin.wrongStructure=地址驗證失敗,因為它與 {0} 地址的結構不匹配。 +# suppress inspection "UnusedProperty" +validation.altcoin.ltz.zAddressesNotSupported=LTZ 地址需要以 L 開頭。 不支持以 Z 開頭的地址。 +# suppress inspection "UnusedProperty" +validation.altcoin.zAddressesNotSupported=LTZ 地址需要以 L 開頭。 不支持以 Z 開頭的地址。 +# suppress inspection "UnusedProperty" +validation.altcoin.invalidAddress=這個地址不是有效的{0}地址!{1} +# suppress inspection "UnusedProperty" +validation.altcoin.liquidBitcoin.invalidAddress=不支持本地 segwit 地址(以“lq”開頭的地址)。 +validation.bic.invalidLength=輸入長度既不是 8 也不是 11 +validation.bic.letters=必須輸入銀行和國家或地區代碼 +validation.bic.invalidLocationCode=BIC 包含無效的地址代碼 +validation.bic.invalidBranchCode=BIC 包含無效的分行代碼 +validation.bic.sepaRevolutBic=不支持 Revolut Sepa 賬户 +validation.btc.invalidFormat=無效格式的比特幣地址 +validation.bsq.invalidFormat=無效格式的 BSQ 地址 +validation.email.invalidAddress=無效地址 +validation.iban.invalidCountryCode=國家或地區代碼無效 +validation.iban.checkSumNotNumeric=校驗必須是數字 +validation.iban.nonNumericChars=檢測到非字母數字字符 +validation.iban.checkSumInvalid=IBAN 校驗無效 +validation.iban.invalidLength=數字的長度必須為15到34個字符。 +validation.interacETransfer.invalidAreaCode=非加拿大區號 +validation.interacETransfer.invalidPhone=請輸入可用的 11 為電話號碼(例如 1-123-456-7890)或郵箱地址 +validation.interacETransfer.invalidQuestion=必須只包含字母、數字、空格和/或符號“_ , . ? -” +validation.interacETransfer.invalidAnswer=必須是一個單詞,只包含字母、數字和/或符號- +validation.inputTooLarge=輸入不能大於 {0} +validation.inputTooSmall=輸入必須大於 {0} +validation.inputToBeAtLeast=輸入必須至少為 {0} +validation.amountBelowDust=不允許低於 {0} 聰的零頭限制。 +validation.length=長度必須在 {0} 和 {1} 之間 +validation.fixedLength=Length must be {0} +validation.pattern=輸入格式必須為:{0} +validation.noHexString=輸入不是十六進制格式。 +validation.advancedCash.invalidFormat=必須是有效的電子郵箱或錢包 ID 的格式為:X000000000000 +validation.invalidUrl=輸入的不是有效 URL 鏈接。 +validation.mustBeDifferent=您輸入的值必須與當前值不同 +validation.cannotBeChanged=參數不能更改 +validation.numberFormatException=數字格式異常 {0} +validation.mustNotBeNegative=不能輸入負值 +validation.phone.missingCountryCode=需要兩個字母的國家或地區代碼來驗證電話號碼 +validation.phone.invalidCharacters=電話號碼 {0} 包含無效字符 +validation.phone.insufficientDigits={0} 中沒有足夠的數字作為有效的電話號碼 +validation.phone.tooManyDigits={0} 中的數字太多,不是有效的電話號碼 +validation.phone.invalidDialingCode=數字 {0} 中的國際撥號代碼對於 {1} 無效。正確的撥號號碼是 {2} 。 +validation.invalidAddressList=使用逗號分隔有效地址列表 diff --git a/core/src/main/resources/prevent-app-nap-silent-sound.aiff b/core/src/main/resources/prevent-app-nap-silent-sound.aiff new file mode 100644 index 0000000000..032de6803e Binary files /dev/null and b/core/src/main/resources/prevent-app-nap-silent-sound.aiff differ diff --git a/core/src/main/resources/wallet/checkpoints.testnet.txt b/core/src/main/resources/wallet/checkpoints.testnet.txt new file mode 100644 index 0000000000..39816c5943 --- /dev/null +++ b/core/src/main/resources/wallet/checkpoints.testnet.txt @@ -0,0 +1,755 @@ +TXT CHECKPOINTS 1 +0 +752 +AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO +AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+ +AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh +AAAAAAAAqyWrJaslAAAfgAEAAADVvohqq6/37HpI1ny+8ocighkonisERvJ5nJwKAAAAABRFuGqIOs3bebDFZqd1DKPx/yZF4hv7t75rH8mL6OU4SgXDT8D/PxwRM21b +AAAAAAAAyp/Kn8qfAAAnYAEAAAAJ32RQJkRW3NnJauV8zVdv1GyjywgAeyThAnA6AAAAAHgqS/OxxffyRWqPFV9a6kVP6TLL/BdPF/InquOuDahAmgbDT///AB0b6TAK +AAAAAAAA0oLSgtKCAAAvQAEAAADIqR9HFGtw9hGv0+7AjLdBuE7qquf2/yroAR4GAAAAACEToV2EvK5Bsqy40yb6dolkX0wznLv0ZJH/QM+caUy46wfDT8D/Pxxm44G6 +AAAAAAAA8g7yDvIOAAA3IAEAAAAt8TjFJtObUiEUbrI/cLpprIFFTeRZJK4R4BE5AAAAAPVSM5kPkEOEjDCOCZ07cr+ubMXqMwCJXzKST6Su6v0EOwnDT/D/DxyNS/k3 +AAAAAAABDyAp3m5wAAA/AAEAAABgihej4EwDON/uSI4q/PU1KTneQP6WM8fWhFkKAAAAAKDeflgTtHdLOGbA+QnPM79mkIfcDH7lnxGfTQqC/oJS/6XdT64NFxwDDaN+ +AAAAAAABXpJsk6OrAABG4AEAAABDwpe+kkSg7Pr/FmVn1S5zi9WRp8BU+kLGvYYHAAAAAKCXvBsx1I7hN2Eo98PxXy4Tw3zyHgWgs1UfLx729Ul8CFIXULUISByHDMLy +AAAAAAABdVSVRkswAABOwAEAAABIf2vMbyVCfVVHzCqD8ZYxG72vae7zGJafxFkTAAAAADcEEJlk7N1JRcIoIsmZI6+5jlywPgT4G4OqXQo2cJFL+hcrUFYXTRwAc7t9 +AAAAAAABikGNM6lgAABWoAIAAAD0utrWc3XGCMjFnVLY2ocg0K713zWvgtWY3g0nAAAAACziDeW1Nka5RMLLqnGkaHjFheB4KHHAQv1lBTrKCNy/9YlEUEhFahwRXHfc +AAAAAAABnHQVLa7RAABegAIAAABEBuul85YGMli8USBHHC0ad5j5rcQVwggJFlxCAAAAALfYH6058oBlLDpYNfoMTsKkUZIYJjh+nclBp7AmapcIjfdLUK/EKhxRuD+j +AAAAAAABxq+GQnGZAABmYAIAAADJF3gZfRY/yDiBRX+7IoiNFEpdgHdjmHFkl6UGAAAAAFOP3WV/zYiLQek74P7IM02aYogSMqU6XAaIAAx9absfg4xVUFslFhwAUAiq +AAAAAAACDe0/RIMbAABuQAIAAAAUqP9xpNSMbK9Xqf19PZvHzDyfNu84cKFOORgDAAAAABHAgY55zJGNd9F/9bMiktFUpKnrLGjd8WcNnTCshkHr/z9lULzWEhxPZ+tZ +AAAAAAACeRlhb4h5AAB2IAIAAAAmSSj8WnlKDsW+jhD7Qrz3SciMR3Fo6LjQ+dQKAAAAAFFkvzXQFwVJc3tMjEtoeq0OObsdnoZXSj7et4/OCdd0GmlmUK+1BBwDNPEF +AAAAAAAEImRyPMVrAAB+AAIAAAD24cxQ35v7QgFi42X9JteDWBNnwKSn8mg+5gcCAAAAAA5lzaiXTzmJyur8qkatZl/9B/5VjLY/P2Of7ihNuDqkQ2xrUABFARzsKyX7 +AAAAAAAGnm8xQk8/AACF4AIAAABid2hDHtNAIiJbaFjh1QXMTdvusmbuEixtQjsAAAAAACUTe1hKaqMB8LjZH/CVamDgQeQJ57gWwQ5D7AvypaH9eQSUULrKAhz9YvoV +AAAAAAAJX9qHy4QOAACNwAIAAACD3Xy438Vxeih981r64SvX3nip+YPU44bu9nwBAAAAANv/I4We9HtL6dLAt8froYA+bCoCbWx2a1kqAsCtJGLLD42dUPlwARxHGmk6 +AAAAAAANmIMsxov9AACVoAIAAABKWinKi+JAhvuv0gxGPorJlQP6LMfAb+GfHUsAAAAAAKprbcs2tZ18bf2k9i7KRhoMz4Xe+Grztn2r/RSfgcOqstuwUGeBARzyVgx8 +AAAAAAAP53aE8+woAACdgAIAAACInn2w4Nu95pUfv680B8PW/q+/+OQmd5+wkdFGAAAAAFSOydwgMjtZe5QNWxZyBxEWbTwRUCkjWkq+HP/7XjZHfp3SUP//AB1TL/Br +AAAAAAAP71mM1vQLAAClYAIAAABTldzbKHg+pR5pQHF+62BF9xuZwlWgng4gyM6CAAAAADvWQX2N47E47zfwfiwZGqgd2yrn7AflxMv9a2Dd9kmA8PfVUMD/Pxz7UFiz +AAAAAAAQDnmr9xMrAACtQAIAAAAj3T7olHw3zDxcpEa7PM/D2eKvBEgtNmttuXU3AAAAAGCSpfHhYowdqbF6cg+1NmJx/O0mhu5FveZkzDJff3bYiGzaUPD/Dxz0zSq9 +AAAAAAAQiFGmHDTZAAC1IAIAAAACHkE/Bzu71qowrV5ok0+dpGiH8bKsBCSpNXgDAAAAACJFJf5JMxqAfvSq33kwgVTbPD7Yl2+VBHiru7QP/D5ihczjUGYgCBzA/Yp5 +AAAAAAAQ/SjJqv6TAAC9AAIAAADyH3i2pgjYtyCJiuP9rF+65ghrLvbRBnfRdc8GAAAAAD5y7GWBfuXxYOvK/QLSSrtVBlFFj8GSG3Vi4uYDreC9hdIEUTeKDhxz43cq +AAAAAAARXr7F2OwgAADE4AIAAAD2pkIZ+whGiyUE/PVwCdofqQzNP3BDrs1X8t8DAAAAAJOCt/LzpROOqBISholWN+TxfXWC1qS1TJLRLnBhjpbds4oYUSuHDxxjbJGs +AAAAAAAR2yMkdgZfAADMwAIAAAC8MUFtYvg740chOFJWUyf2iccIjmVniNGXLVMAAAAAAMNbskcDnn3YxI1xTFKBeJ4pdEJ7jSPXnBxx9TjeqhhjlEAdUVv2Axwa1Z8a +AAAAAAATwMsdJwm3AADUoAIAAACzWdH7wqJxzWkSc6uzOoYFhXSu0O0dsZu332YCAAAAAF9TfrClFIYl9TKr8JHJ2vUYWrm8hILpEYcPyXOSwtsYyRsnUY8dAhznXP6S +AAAAAAAV2XqP1tBPAADcgAIAAADhQryu3KQfuEhhQYhqEtYc9Iut/9gNXJ0R8J80AAAAAMIx1x+pIcYaDk9potb6rnDxzzh6F85FJAUw6VW8CCFM0pw/Uf//AB2GLNE2 +AAAAAAAV4V2XudgyAADkYAIAAAAQSNHw4NwZ1yZRYHEQm94wjSE1Q+rumUebk2E8AAAAAIEY6ZkhEXAdgMOOxuqm0moqh0VBL6RI/B4g2l79CsOFozpAUcD/Pxzv6bRY +AAAAAAAV/ykF5AF4AADsQAIAAABzz5AEEeoTmE5c8Lyai6xGQNIqsdJI1g5XhJI9AAAAANlVnBg+I1TzV/Oi6Z4G+uM8uasH4jZYE+1hBYuWqzUE8RxJUYrKHhwKupBR +AAAAAAAWNsMetAeEAAD0IAIAAAArJRwoXb5YGsfVw1WuIec0YDV1vc8M3OZmCzYBAAAAAAc8ccHdUfKc2YgGiCQoFZk5v2xwO8b3CbRKpRqQFFigOwFWUfWAFRwGmb+r +AAAAAAAWjHa3L3oeAAD8AAIAAAAmPcIuMPXhz8EmQvf3sweD7Sbusr8WSVBCTxEMAAAAAOYDL/aUzuKwKf7pnLa53Wlf2TOCnoWsHg6OjWo6BkeNW4BfUWcQCxwNpbY8 +AAAAAAAXLEQGoi48AAED4AIAAACNT+2/IgcKzm9zotpKNgS3YxKCgcq0l88/rKwHAAAAAEbX7trLeerRGyAeLnQGaVdJAZVH6r92TuoB8kbN8evvtZ9pUTQRBhxB9Xol +AAAAAAAYFg+3P/t1AAELwAIAAACAQlCtXvKP8CUGaFbL1xZujOQrnH/SMXfvaJcFAAAAAKfJgoikQxRLdzvNHUinrzNFdiDHD0+c1f0XB0gX10sfYXN8UVEwBhzNNUbI +AAAAAAAZXCQ+cL42AAEToAIAAAD8/LokZtsHJpq2zPujbIkbJb3kjFFSH4+QyFEDAAAAABHu71n4aZX4iKT8Nh+hKxG8GZn76aeyyGe0aJIzzlLC+86AURSMARzcbzh+ +AAAAAAAdfmQ/2mWRAAEbgAIAAABScFkVSUtlv0/fkEQ7WE+UTcsuni4hjgSuzNdoAAAAADRI8nT3Vajs4Z1eh/ljERDyXtefjOXDAr92jjFXvAYOHyiRUbziAB3S6uqP +AAAAAAAdh0vZ0BvAAAEjYAIAAAB3/SAZbclg2lizZKG31TtHJVP1PyOHcUE2qioOAAAAAMnHLQ1zohIDZEz3JGBWZvpvTSkwcxgW1mrbsExUCeNDdm+RUQCvOBwLjyRj +AAAAAAAdqupBpwQ8AAErQAIAAAAIA805vQ3P7RcPabnCWW6PIY16yHtk4Nv2HrEeAAAAAMc+4EMTUg+FGfsyNXBedo55Ulpi2+agdtcp6NJcAVwYYh2SUcArDhw6rtle +AAAAAAAeOWPhArXtAAEzIAIAAACcrQqkCA86M4JYpP+oNCQkk+B89v/DTOJVeC4CAAAAAB7lUvx3mkkzmy3QBZ381FXVTBmQ8vmnzgbY5paxUJ0Y+yGUUfCKAxxtajMA +AAAAAAAgbX6kn+AbAAE7AAIAAABjvwfpVGl7TCtDxfWW4cFjhJrAWGIY4/QPExACAAAAAExA5aaCfoBi0xuQ9pqbr+ifW+u09lZyt9jlCliSSRWI8e6ZUUgcARyBNUkV +AAAAAAAm/x5h6qyeAAFC4AIAAADvscg/JoBhoewy+I391G2k2F7uiL8i4+1yE+AAAAAAADj4fIul26iU3SqhOc8r7KtWBeUg3O6cfWtbAoy7PUSQC4CkUZiiAByuVupX +AAAAAAAtLLR0DET8AAFKwAIAAABPPD2AXBzPvMTtkiz1Ci5RZ7I0SIhdbeKWGHCgAAAAALJbQGNxmuVuU3bWlGlB2z01Q2jvapgIsqd7fvsrnNR/ItC9Uf//AB0nE5ZP +AAAAAAAtNJd770zfAAFSoAIAAAD+tsbp4BBAebcAOAgkmD4lYP295fOIFazZyJ+GAAAAAAc26pXqkH5G5HRjR6xnHRvyfaYaq9JdSIU68+lODpqJ8wy+UcD/PxwAgLqZ +AAAAAAAtVCObe2xrAAFagAIAAADGDivZ8jjsI9th+IYmCQtgYQeP8jYHw14Ws0MdAAAAAKEe8C9urGnH6zBVV9pv5epgmquOUipYL5Ggl/BisABV56O+UfD/Dxxkw5Ci +AAAAAAAt0lQZq+qbAAFiYAIAAAAuhphvsvuTs6lB2TpMGTAVDt4zWW4FwqgtCfQOAAAAACSJgh3AuV/1DtPw/bVYneSZ469yzHp5ics9WGMFHvUu8NO/Ufz/Axyyolv1 +AAAAAAAvyJCMFff9AAFqQAIAAABkTz8Sk9rXGV1O6cg7sEahvmL5CTkCcJ/T/uMBAAAAAFjPYZcyLJo7nYJJac+4YoVjMu6UQ0HlskSnFfJEMbi4NcTEUYMQARzRe0fc +AAAAAAA04A2xONe+AAFyIAIAAAAuqhar8psbYvTPnpl4u5BShEl26c3ju3fIuLQAAAAAAOtJIYrfxrD6zyMThSuvIcxzNIfvSl0KkjNUqR5pkBZhcW/YUVgiARwFc1Jg +AAAAAAA4z5MyT2OOAAF6AAIAAACC2qfe/ut36oe5ZftfaP2jJ8QOOohCRzu6lWAAAAAAAHx9tYVSpVi/ftDcfcjdUK6FxEnq350/LP6+O6fWhsCimX/vUcBqARyeNkQw +AAAAAAA9nEKi94FpAAGB4AIAAADhKHMyTCqM8ihgkDumSv86VULAT6NoYGqn8GgBAAAAAJN9PuowPKZ3uf2+Lc/wpJPvWvtFxgnHGQFMsN50/xtgdsX7USfxABxGBYMV +AAAAAABD2vqhzoFqAAGJwAIAAAAZOekiaS1n6doMUSCCs8quuvBPrIlJmwfzEK8AAAAAACLE/Y3QULBLrGheJKDQ1tIRAa1gW8nv/tKmVAFukDg2smQMUgfZABzain+3 +AAAAAABLrAOUOshlAAGRoAIAAAA1LIZ7gSOZtIoKl/UGzVfNofLqxaT2oK2RJzEAAAAAACdNB0Q9SagWg2oNg3lqzdLn1vC9evzoPdI01SlEQi6CHCoaUuehAByhdGWM +AAAAAABSG+FjB2HDAAGZgAIAAABaZVhjYb1h0M806cho68+yDCOcnRlNXPTU81UAAAAAAER003E/MTWg5v6Q7/6efO9RYF7tEv5YJWw6UrQmKxReW8MyUqzXABwVYsFP +AAAAAABZ0GqNUFV7AAGhYAIAAAAh6PGWUKM7nUba6cp9L5NLy3Xdv4antZsStjYAAAAAANL8QK94/FjvOX3yWTsjEUTgyboyp40kAPzdQyQ+U+WpDBw+UoqEAByE4P3r +AAAAAABoK0K4lPNQAAGpQAIAAABZm57wDw45xHPuEsweldfsUTJnhuQTHCfPvC8AAAAAADXgYcvy8F2up2S/7ow00167lQ2MT+yG5KFResc99AU+cP5FUkl3OBsqUpTK +AAAAAACL7M+sgJttAAGxIAIAAABSZLr9oFo4gufkXqQ+iNiRsw7ELwC/gXopTjEAAAAAACuO+53UAeXseVlGv0vTR/xerDxrP+qBzEE8GAyFAN8eNVhGUtIdDhuMDv+L +AAAAAADuQmc14/dDAAG5AAIAAAAfxp5X/HykRvLIe0AudyLfVbX3aMKzw56Bc8TTAAAAAH1WS1YLNe3Ty392krSabe9mLneGsPIISh5jt79lEsSYTENUUgvBAB1gzOlk +AAAAAADuTNyqjbrwAAHA4AIAAAC4HjW56jRX/nCP3dntaI5uKgo9JpGzvab9OKR1AAAAAEUQ8nC6D/Lk9ywX1f4Fh2e91KDG+2nfhcRPlDK9z2a6KnJUUsBCMBxEhOVi +AAAAAADudmlQAhE5AAHIwAIAAAA2F//Qh6Crdo4i/9nLmJeDqE5KiwBQ7CsKrusXAAAAAL2e55cd47RWbahFr5kjcsrP/anqEu0VvLp/ufU3ZaNBag5YUrAQDBzr3A7Q +AAAAAADvGls6Oj3lAAHQoAIAAACc0hc4CW9d4kXgjXgn7QVt8SWxErwLHKNrRgAIAAAAAOrCjkpak4eNPNGucFxlWeYHS7NXLQ2AvmKwgD3ByweOd7NaUiwEAxw4WgcM +AAAAAADxiQSIacWSAAHYgAIAAADfTsbtceXn5dhc6T4KbLq5rhHmAvJkIh/EYyEAAAAAALf6dN29NHyAmPbiAfUMcQ0sZrlFCgcNvJ5wrB8DOW4E5wlgUkzfABwyNx98 +AAAAAAD6k7ZE84v9AAHgYAIAAAAxTkFe/u2tS0102gz0F636XHWMzm94F9SCmN4AAAAAAFy3gvHvgx6pY+uhZbp3t4WJmkADgwsm2gZ6xmb98BjMMVRgUgDTNxtGeOiV +AAAAAAEevn03Gq2LAAHoQAIAAAC071p5e4e6BufOlwV8luT3OwO/Xgr2Qc9S8yoAAAAAABJ89xnW66dxP++b77YFdssFqZP5D9skWesOol5hUXBNhcZgUsD0DRsu2lUO +AAAAAAGvaZj/t0tiAAHwIAIAAAC/3pXDbRMbGngge8h40tTyKMacsNAdDwOgnAAAAAAAANbKP1TFCpifOprBEY9A5up2H5A1lGO3YIo9JtyuKuYTTi9iUjB9Axu6gc2H +AAAAAAJGxHHsrscSAAH4AAIAAABCVlIuquzF0nNNWM/iNX70BTgO2qAGbadwc9L/AAAAAKX0Bqo1Ruyt/R76Tq6kCj6Fi20/F2WK4P2ys/MnO5ZqAsB/Uv//AB0B9Xy9 +AAAAAAJGzFT0kc71AAH/4AIAAAA6wgl2WyJSYfbfnMEdfrONyyCguplQpwqRpg1yAAAAACJB6he61NwAKXjkw2dMS61Novi4pf73qDyrL/agGCrZt85/UsD/PxwSzAYF +AAAAAAJG6+EUHe6BAAIHwAIAAABieWmbu6Hmhs1OYlnGMHSsG+l8UujlvATHLFwKAAAAAGRmJvkqq6jq9N5DuA3b0Jo8IYeuzcCJmJMleHO0xxi9xfR/UvD/DxwEZ1RB +AAAAAAJHahGSTmyxAAIPoAIAAAC8cal0DDF/lrFrBSgGKwyw+siDZKDpNFFUc0IIAAAAAM+68NvsIrg95XL4zTFvOOfGeBfbRNtUpdYH1Q8KzUNvGmOAUvz/Axwhb6E1 +AAAAAAJJYtOLEGVxAAIXgAIAAAAhP3d8ACLZu07N6QXbe9GJHz4ce2QEKKVNb/YCAAAAADcRuOJZ5Ul0GaUvq7efr8SUjcRKTtGVl0VLm8IgA4nzUFeBUv//ABx9Sq4v +AAAAAAJRPuJnH0F4AAIfYAIAAAC9GK5oSrM6UdjW3jsKZ1J+lII/SzV6ptDJRmHEAAAAAD+sz2k1WmRBNkdReWYNHj8Y3rzbAIkVWuX399BMuw4sMjKFUsD/PxxjCsDi +AAAAAAJRXm6Gq2EEAAInQAIAAAC6HgDWohCHEwZodphJV/5uCJZX573n29cpxGEvAAAAAHcSWev2NiF4H87tlnAgG4eJevqIUhWFgCuZD3IwDRYy5LeFUvD/DxxaK0E6 +AAAAAAJR3J8E2980AAIvIAIAAADCML077IuBXOclWVEQPQkU3S0tLN9Xiufj89oBAAAAAKkVj6a9TamRZgOjqgH7rqvIpr6DruWQ63bcvC0ulnao2+GGUvz/AxyjCura +AAAAAAJTyNPxEMtnAAI3AAIAAADgbUMq9fjNElWkEUHBB3WyaansbKyaekKnM+ECAAAAAMT9MJ4nx1jCP//clpaJSEyus2B8dcBQ8hdSo5fskqJKXqaKUv//ABzDhR3+ +AAAAAAJbhAOsQIaPAAI+4AIAAAB8SIGHcDiOw9fOCaIpUsXo0OENdGaGRU03xPcAAAAAAI/Q8maW+Ca+UWoTC/j7v7OW12WVr9Rq8Vx4cS0tAkgoB82MUsD/Pxsm9fDJ +AAAAAAJ0bL9eXcHRAAJGwAIAAADgQybiIZ+Hm3+5A+cvfPWmTus+uDC6KaOeuzUAAAAAAD+J3Lp9nPE9O2rC2oDAM+VJ0Rn//sLJfDt/2iIaADNLrV+XUjWoJBt8qsmz +AAAAAAKfzIPKFev8AAJOoAIAAAAJhv6KHK3RcknykQB76ZzgXai8ZuKuCj5QIBIAAAAAAEIy2wRXv0nyKdEbmA0WgWTc4BXDVs7Wwe9DrTORsMqVfA6iUnM3FRuWDRVx +AAAAAAL3gLY0dDe3AAJWgAIAAACcPHftqN36iCglEyH1N9AmuCuu0vhXBMYUphAAAAAAAPncAh1bTcPAW2cGka4nHPyHrvhegr9txsvxgovEza5InganUkOxBRs2LLSU +AAAAAAMzm65V6HGnAAJeYAIAAAAoRbvwNz5H7YG9aaacGe/E6v40hKr4zy6Ii1pYAAAAABzszkPCMRLo483xtIA3phv2NIzWyxF+VE9wnnQ4GhqLbG7IUv//AB1leSzC +AAAAAAMzo5Fdy3mKAAJmQAIAAACC8rfZnzvSIg7jR1IYvKh1XvsYgtgq6F/b/wSVAAAAAJISWLGv5+9MDpbpWIIGmBiKP4k5hqC4xYnW7l7eM4c+sZPIUsD/PxyOTdhw +AAAAAAMzwx19V5kWAAJuIAIAAABolZsowzHAqajNvAbgRlfPNC7YaDFlzTvXx94NAAAAAOChQr+AWSR3b3SAItlopgrxFpt/hF7eB1cJ4MB5aNWlsefIUvD/Dxwc0t5p +AAAAAAM0QS/7ahcoAAJ2AAIAAABzx7zz/qSzilODLq73YVkeJVFNa4vL6OQOYrYMAAAAAMbCQMlQphASXCOhpjP8Uy3E/9PD7+9ngqVcP3p8tuaokijKUvz/AxygWypq +AAAAAAM2ObLz7Q+pAAJ94AIAAACCq79emKNEX6eKlepmMrcSp+3BE4VzQQhKIjkCAAAAANi83P2DDyR0ZXV/Sr7RqkoTeD/FpL2wvGmsEH+QpIexZmnMUv//ABxnfIw1 +AAAAAAM91wCROqzvAAKFwAIAAAC8MVxa+IGZit/POyncVDxAAJMHGcxvWMPHMCYAAAAAABh7r0VdkWAB/2/3FMFNlwl5owl3tKB39EJzcBPktePsTn/QUsD/PxvE+g/W +AAAAAANdYyAdWjjvAAKNoAIAAACcY73RxW7ojNDmZmU/mEoJAVQf1x/lRE/D2CgAAAAAAIWWPpLRCvZ07NN/awNOE2G/ZEI7Zs+o16FpGPjXu2aCnPPQUvD/Dxsg2Sgl +AAAAAAOflgdGKBC4AAKVgAIAAAB9L7ymtyKDNaQUZlqzrmBfYrQ3PNyKgvGVyg0AAAAAAGzjxSd6mt4hONV4yNJCXNtI5kGaAOarl8pQ6CnvIpcVj8PkUoYsERs8uJqz +AAAAAAQPYo5U0WsbAAKdYAIAAAALGsD4CoHENlFplB07oaMxGk1H3J2VGNpPyC7XAAAAANCubRokr4vqFaZA49lVS0UnJgO6fmZXkkcwFcnUZNeK9+TnUsD/PxwbvVZh +AAAAAAQPgcZ0CYpTAAKlQAIAAAA1D075NGOVeietHIpA5J6WYIEgCn/rlklgKXwyAAAAAA/bhIlBb3RCfgl0lWQ3tDenZyueEEd8k5nR+DLUwb0AbjDoUvD/DxzisGSm +AAAAAAQP//byOgiDAAKtIAIAAAC3G/CSEGzDKmN6C0uwrcEcv+f+/BAZnAAd1BsOAAAAAH/xcDEgUt0ZUR/wezsoyB5yPwuGNDhKldTzy9j4WJjn6fLoUvz/AxxuLtSU +AAAAAAQR267N8eQ5AAK1AAIAAAC232RyyJcwvmdnyYvvgpieIkm5ldC5SO2vpQECAAAAAJt8Wb09aJPi2M7TTDg/0lrzSzqri8ccE0KEiCNh+vSOSd7qUv//ABxTkpHg +AAAAAAQYDshR405IAAK84AIAAAAzaso61E/wDlVYJMEBdbWjCrtpEgvI+fPoKD8AAAAAAEICoFVs8Esv9oEIdo0xZDR5vn2s/B6NlzpXH0jlbyur3FPwUt+lSxvCoJsA +AAAAAAQlxiFA/bcHAALEwAIAAACJyvQlJWhVgJl2fa9BPzhnjDwQoKY4TsGf4CkAAAAAAKMqF5Otltu9ej7KxOoySYqJQYIjSnDBL8K4Y0THn7ePt+D8UuZsMxtp0Evm +AAAAAAQ8XQh8vEl8AALMoAIAAACrt/Js013iZjVkkmqKYT1YLdOlK1iVKWCX4lwoAAAAACXrWgg+lG8DO4y0O/EHnDj9YSz78HDppy8ynl/KjOqd98MEU9ehbRyJxzIo +AAAAAAQ8b1mmHJjgAALUgAIAAABimaPWrdZLn2djukCJQUy9hNyAhCvF+lT6RXBlAAAAAOWLtPPYtaOOMVFDqpKPJJU0jNP3XyAGvR3anFN63mxi2vQEU3VoGxw7WrRg +AAAAAAQ8uFSfJyeuAALcYAIAAABF9EwyNczLmT/Paz2BULd90urE93+kBb5Yu7cXAAAAAMUu+SYK1S5/81jSvHeYuH9ewSuzyjDLsYAhyEgFie6oNy4FUx3aBhwNZDzp +AAAAAAQ90KSmHAPSAALkQAIAAAAlm6yb07S4GvqbOZMVXE9rSVB08RfftjkOMToAAAAAAIxhSSEVSpFItTUY2xKDl0Xo3meZ3Nuwbbkpah+97OFQNcoFU4e2ARz84fR0 +AAAAAARBcxirmBH/AALsIAIAAAA8NrxvZxdjJiF4V0K6o867bmOpSDUATynF+wsAAAAAALPlaH5zoQtWK+KoTgmXVfdm3YtKiEpJDHaY4V09Qv6Jph0JU8ChbRsPLTKS +AAAAAARNmcfvhrwVAAL0AAIAAAB+xYMnzDu5TqoRRjwFtmWfwVrqLtokDNcdNSkjAAAAACpD204PPPKNB5+6Gqj6tbf76bFh2HVu4UNBWcH/R2tioAgQU84qYBwAO/8b +AAAAAARNrrlHQ9jYAAL74AIAAABX4CQMCN2BSOeK2FOwWun85MZY0aRvqgUQL+BMAAAAAOkIEY4Xp0ZRTvlBe1Bq+KRCI3G+gt7xPnxmStCDuM/1kzMQU7MKGBxFYMSf +AAAAAAROAF2obdnwAAMDwAIAAABlVVNU4Z++Ea0HAwzGRwfeRVK3z+it2kylqv0AAAAAAB7BxaCHtEeYjNYZxT5DVvE7fAItNQT2IBnWwn/bcznh47gQU6wCBhyg36s9 +AAAAAARPSuuEFRIiAAMLoAIAAACX8rYYl7or7XVsyjAFi8wcLfu07Q6WL0f3SdwDAAAAAGuAB5oe2oBxQk4pT6VoSTcOMxyP9+lQNFdsl4nI2w+m2lURU6uAARyb2sol +AAAAAART6SbQnr6HAAMTgAIAAADuaJ5NzcPH2sWRuY4eTcg6rgP/n7nUadcEpkwBAAAAAL//re0qZ4IetXKbNi1hN0fomNCNbIO1cEZGwmwTFG9MbekTU8AqYBs6gX+H +AAAAAARk2yG+E6sbAAMbYAIAAADlVcYD1TvPuFLZ1KSBLWdROFaDVT067+y4ChkAAAAAAJwh1bYS4WKpzk31tV/fI8/TuqWz2dGwc8UGpxlNM9wQd4UYU7AKGBvq0uGw +AAAAAASBPhQZaL9HAAMjQAIAAAAHv1+wty/Mc8x05uO3lJzB4RGrf5gHtmPSBBIAAAAAAGbJsimVKN/SuF81dtky3pUzl1PtJ2+lh5gcs3daRPzHM5ArU0u8GBvQVyRb +AAAAAATJaNTfth0+AAMrIAIAAAAuMzm5wEEfiPHW3SUMAeZc41iuqFnDen+NHhQAAAAAAA+zOgnWUEtNxae+LwGlsa2tBzi04GHdRWdN8CPG7kq2Bd4uUxIvBhuGrY2U +AAAAAAU6JGoNVaImAAMzAAIAAAB04jDDZN/c8eP0H5GvXUcM8nT7E91d7wbdRGOUAAAAAKMud0Kz1TjRtb5jqWuP8oDalIevoPCTwXnYZJZmMgWHJk9EU///AB0bSHSQ +AAAAAAU6LE0VOKoJAAM64AIAAAAlFW+bWhmmO55+fXST/NcWTUCvy1u4SC6Jp3fsAAAAAIjuPZOIuIjGdIDgdfVSIojtcdLshGVRLtVM7QBiuXkpeJhEU8D/PxyxcUpU +AAAAAAU6S6M0jslfAANCwAIAAACXYq2wcjgOOtJJr8UOLodplc4j5JOO6k3ABaMYAAAAALQ7Z87Y7eoEDJ5qT4++BkrLpZkJfdz/0E8Mqb6891e8pwFFU/D/Dxy1uCp4 +AAAAAAU6vDulJzn3AANKoAIAAACFqDgxSaLO0BLa5gqB+btAZOsucZpsqPjFMl4FAAAAAFZAfhA/4soVk+NjlO+q9BS8G/bUUykfcR/CB2CnZy9NPVVGU/z/Axxhz8pw +AAAAAAU8tP2d6TK3AANSgAIAAABLfZsDq/W1FJjW7GeoHlrjhk4j5Lyte+bLP5EBAAAAAJd0EH539WuDsZsC+yfl2aQFRuMt0w0zB32kjCQI3SmNzV5GU///AByEBpDB +AAAAAAVEhhdvAwPJAANaYAIAAABAqiNitFvS+4t6ycn4+SH96gxyBOeuA0T+cSwAAAAAAIy1zLRKERjOPqwoTxduSoD1/EFfPd6jE/JDQfa183ZU2JVGU8D/PxushcdT +AAAAAAVdG+4E2ZmHAANiQAIAAACo+dgJEfvcfw5vgc36kSUnXHjQNhiBDNpCOjQAAAAAABOj0Vd55g0iHOuJhByB8OCeMhGWn7E2pMXyJiZ0YjH55wNLU/D/DxvSrxup +AAAAAAXWXLZFodnWAANqIAIAAADhMuNHOd48oM2po6B5PC7X0K9/6j6LYIKxCQMAAAAAAFI49ps8oC/6+O3v+e5G7A+Q874q2ylioLLQ/X9t6x6g28pLU/z/AxtzF/xY +AAAAAAdFQX4kL1SMAANyAAIAAAAnOW9airS1KtSS50xHSgK812SKlcMh5ATMBAIAAAAAAB9IHpYBO00wEWj7eOTuzM/eB85ihD8vie2gkBbJOdaVGipTU4yWARvco2FI +AAAAAAiHrpQcgHpsAAN54AIAAACNNBf5eMXbxBeeiE2ndFBXgAew4De9VXIOAapeAAAAAKJI4yvBNczk0Ru8RZOoQCGahbBEdSX0Y0hTh0/UzBL4WglmU///AB0DCOEg +AAAAAAiHtnckY4JPAAOBwAIAAAAOPtZb5JfTcADkfQKd7V1jxm0oacCWkS32JdSgAAAAALMBQS3hzNPm0h//fYMMRfccTXijEueIFGeLs9i+CXc0Sw9mU8D/PxyC4WPT +AAAAAAiH1gND76HbAAOJoAIAAAAfp8RsPgqFAEof8oe2ue5k+U6p0tXijAY3/VUzAAAAAD+nayQIsNM4J/nGt9h+FvFJWeRbJj26SS+S/9/FHwMdrhlmU/D/DxwAsP8X +AAAAAAiIVDPCICALAAORgAIAAAAMZC5OdlYY6aNwWbJqgw3JYGjJ2ExC6auNFKkMAAAAAK56AIhzD+8IQHMXmxI6F2UQ4fFh+doWPtilxXGDvhEUoy1mU/z/AxxD3dCC +AAAAAAiKSQW28hTbAAOZYAIAAADM4zRT7wCBOiz9Idp4iUu+NlTz3o7enYI/4r0AAAAAAMG3WB2q51jbjY+lg2jCvudbP+Cs50gqNN4LlIgYDJdcRGxmU///ABxgW9FE +AAAAAAiR9EViMcATAAOhQAIAAAAS7i2cfA8P5BMHIxie5alVXebqq79/QvEmuygAAAAAAPsN/QqV6Ud3Cq50fPHuH6Wjy2NmbIbYZqb4yMwhs04A/w5nU8D/Pxsqcf8l +AAAAAAiuvRMq/4jEAAOpIAIAAAAZaXbHINdCZEiH+OKz6YsPXmvul+VU0iJUwCoAAAAAAKNN8ybmD4qpYvq5kzLPMrBKTUhYhjV+ieVSl807M+b313FoU/D/DxsIlYOq +AAAAAAkGf9xLhGNmAAOxAAIAAABiJOPFxznQ/4Gg9uDDh9nc0bEBlt2H8bbQNg4AAAAAAIGEq3nlIkxB0q5A8dK2AVhXXrYQETarjKFCqZFVFre1pZduUw1UBRvl6z8U +AAAAAAnoiOjGVpp6AAO44AIAAAC6OPRj9KDlMSlIPrB6Bur+XEqj/REqKh97jQQAAAAAAKeIVEj7NGWqOTHttymDRIDoNkzQRZM45StoXB+Fiw4TLpN2Ux1LAhtZUamv +AAAAAAsPUAuUBFoKAAPAwAIAAABiNKup6G2SKX1q++Okh7D7SadzdST0pMiIq4D3AAAAAODPlUAeDw72MkzVTZfxvlT8e137Nu0Wp4i1VzgaGUbs0vGHU+rwAB0Av13+ +AAAAAAsPWG0B0t2YAAPIoAIAAADSdPV84Irt87+7NdGqux6qkaALxKjmQKnmuxMDAAAAALwQTJCjKl+wJ5+2ZqlOl7xj+mnC1OoKD9+fdpRd2di3ovaHU4A6PBwCBTKg +AAAAAAsPefK5DQNtAAPQgAIAAADoBGt1ZPvaxTEytx7ccQ0KZ2IlSCInqiGXskodAAAAAOFKF1Iguqr8NomT5MLNeC5c0V4w2FgogLwcYkdt4ncdS/6HU6AODxxDq2Bw +AAAAAAsP//mVeTcoAAPYYAIAAABtuc0Y0IdnzHkzD7nxy1IDz5YrlhmNw97Li1gGAAAAAGbDnJxB0GfmTIE0PA1FL8r0Imrq/DmFp0LRLPgKNMR6gA+IU6jDAxzZ+Ytz +AAAAAAsSFjz5dzC5AAPgQAIAAABrheoTDcLvpYhfEqO0K9+9LKqdT8IaojmV5yIAAAAAANFQVuSLbPXHvpo2Dv1JPVlYt2TRU28yJcX/gKShQYsIxz2IU+rwABwqURwT +AAAAAAsaWxT0g+RvAAPoIAIAAAD3zUZdrMAjy7puv+bv6Fe+D6F4xUxY7n9LKN0AAAAAAMw2xq3lqe0A3e57r5ykyoxEINwIjLAmoYwIS02BqmTgi5WIU4A6PBtuAxaL +AAAAAAs6Hqk0RXMSAAPwAAIAAACiau54wKWk1y9vlpe1P1wGBlU9yRH0hzcUURQAAAAAAEJDLYAo+R9Y1QL3Yfv8tt096O3na4jGpX/m11OjzBAndJyJU6AODxst0B06 +AAAAAAubF6NSjmDbAAP34AIAAADR+v2hWQ+g0xFuIvcHoO+DnR/cFOTxSjl3qQwAAAAAADUAlhbXvAB4uoWBCktmv8oUdjNjKBkyVWIdWe1o1UnvLq6NU6jDAxseTCwY +AAAAAAx413LatcNPAAP/wAIAAACOhrprXP/GA470G3NgTrjlvGt5eg5YgKqMHwEAAAAAAELhYxIgo7cDYokiSgguhot3DmlvacmYwD01SmwD8DNNWM+YU+ZEAhtTJ5xz +AAAAAA3adfdwdRs6AAQHoAIAAADJOH+79rpfDLbT5/u+9s56Q4vwGq92E+tbcAEAAAAAAC0RzoE9M4sEu05T8AbrcFTxpvzpPeVxWgcDe4qFjfDRd1GmU8ioARu1zBOB +AAAAAA/66w++gvSKAAQPgAIAAACiR/IrS+KeQ6jvI3SjaIODtak82SJU9PE4yAAAAAAAAEZtaSIeGForZrROXUJXlvzV5mD89jksTXAK3kU9Pyn5IZezU2QxARt9fxVm +AAAAABLhWxFHjeq+AAQXYAIAAABic9Epm0Tvz6y9GCxS8nrRMovcf9AxfYWtEQEAAAAAAAC12sBZX9AS0b+oNhKWvwY5NB9dUM5rFoYbA6Q9aJD+Bru+U064ABtKB73S +AAAAABdm+ybJWukUAAQfQAIAAACYwxz2Ag/1Q4xKGuxxbwb2288Oa2H8GHcqMQAAAAAAAMUrI8P+eiKGEuR79w0jA3GSCRDSwD7alPiwmSKxScc0IvjLU+aCABuxiGYd +AAAAABsReUXYg09RAAQnIAIAAAAegCR6uZe/FiwMRqUgVxwJZ+8EuFl6ISsjfQAAAAAAAF4cE87hE69H7vopdpJlydQXDMXU0Q7eG1YverWUUmxwSW/hUzaYABup6IQ5 +AAAAACHanik9zNs7AAQvAAIAAADrADlQFy7QqizndusAxbSFVljtrJU2wzVyFwAAAAAAALtk9vYGNJ2w0PcwqcKxABpxkhvtcUFOBPR102HPaVJa0WrrU+UyUhrzJ17p +AAAAADEGznIzZueRAAQ24AIAAAA3FsihN8ZAMcSKZNvXmmaw0qziqKcNicX1NwAAAAAAADU4nTOdDXFBl2XBYK9THmtjRqqBtZfC0puiP64blhWg8/TzU3bvJRqQV4cO +AAAAAEHZgaSykw7UAAQ+wAIAAAAhAw+Be4N+kMF0C5pPakJcEzW8I/JjQJAjIQAAAAAAAL5eJ/LZWdH3mWlPkrYSiWXRnaiP1bqzRYkxoO6pCjBbEJwDVM79HxqLG1Wg +AAAAAFEKfRWP+FoNAARGoAIAAACWP88FvTCHbYkkn++vDxdBkxrZV6OdgNthCQAAAAAAAIRt5mcgaMqAk/rRCmVnxpdQlGDeWY4FfxZl63cH57l7vUYWVAJUIBqIR6XI +AAAAAFwNmsR/XcyJAAROgAIAAABgt8o+UYcEC/XmUEmX9jnipsPPmE6H6wXNjy4NAAAAAEKTOzLKltjQZb4DcVo7Cf/PuJfWEfh4Jmaay2ljumSx4R4tVP//AB262uKK +AAAAAFwNoqeHQNRsAARWYAIAAAA/c/ZrBhjT1Y/kE9vZCNeuHkPg+iPbyAglNn4BAAAAAJ6/u2MY2WPdZpQuA3HNaaWJq7LbW1Maz6F8H8IntbubPz8tVMD/PxyQ1nJm +AAAAAFwNwMKlW/KHAAReQAIAAADjxyOAFHEMt4XC/Qsl23T5Ylq6t5Bv40+8shEAAAAAAEyPJRcM9Bluj3F39zPHnDgEyrUXe64iVnwk0qZqE0GzRXktVPD/Dxx4idjj +AAAAAFwOPUAh2W8EAARmIAIAAACZzjz6DZDof3W2E5kiXEmqsy7d1Y03C1covgEAAAAAAL3aiXGnkNLIexu0sF6xM8MfOYxbdnbnS0fxMkEQ4rhK668tVPz/AxxwNCXY +AAAAAFwQJEoI41YMAARuAAIAAAC5UI5HV0UIWaC0s6t9aYNaSwtOMqXQFrvQKyQAAAAAADM/4MJG9Dc0Si6ffLuIV2Bf2a1iGb6ke1hLGx36WQbQ0fstVP//ABxXCwd8 +AAAAAFwXc+VYfqWgAAR14AIAAAD27fIqLWuVX1SQC5eN1FprPVAu1S6jjdFpztgAAAAAALLHdOFzgKn/sCRCuICZMlKoDlYc0E3k6yxgauJVYvD43GcuVMD/Pxv4v1D7 +AAAAAFwzkN11dsJ8AAR9wAIAAAAA+++XJWTjqXcbRFtr0tsKHoDXDN2zDL9VZjYAAAAAAK77pd6gozke4iD9e2K/QIP90cbeE9ARyPpZYHmfSHq9QvouVPD/DxumhKa5 +AAAAAFyvQYEmGnKkAASFoAIAAAC4LY28OfxF2w5iSwgcfxjRQE03qHxg1ACqxw0AAAAAAL2DVSmnUi33pyc9odsWayrMM8AJxGc/SgVzCC8TomxNo4ovVPz/AxtpLQy2 +AAAAAF5DhKdpQLQ3AASNgAIAAACL9u0JrUeVjhiRkVAkoWZ/Uc9lp3BunA32ugMAAAAAABR84a893LWPjGPMHNStnfKB87KlYXLnTjZkHd9UuEGLXmUyVP//ABtihmtQ +AAAAAGNU6lJEafOQAASVYAIAAAAZnzRWyRwY/sHbc95fB1NThX7TtI87iXb04AAAAAAAAFpJIayG0G90ujdMFcA4FrZKCSLV+kzpQu621X2N8yru0eE3VLsLTBqUqBtJ +AAAAAGy9hX1JM0/mAASdQAIAAACliVpV4SkfxXXyHxB637JPSt+6inXetxbtMgAAAAAAAMbNZzKgTFHwiyr57TJ33fg/XLl89ukLMN2ibxqi9XUkX1xEVF5gMxoCQgP7 +AAAAAHyrfZsQ4xOcAASlIAIAAADNCjv/f81BdoEtcx+TjQNf+/Yg/vLwA01WCgAAAAAAAOZu4ZsZ7P60DJIPRH2IOWgMAfT6x3t/tebcFBDa48ObvgxMVJNmFRo8Jzh+ +AAAAAJCwtKkNkcFwAAStAAIAAADFVehLRPgXvWorLjKOVF3NhcrUqM790KQaAQAAAAAAAM8u/NqgwqB6uKVX3AinlcvK7INqnMeXeppoL4+VzqJ+R81YVLzIDhpOJmeS +AAAAAKjKu0oWNsugAAS04AIAAAD8XCCWPc2n/CEPr8jURxBTRLUOeiZAhVgQBgAAAAAAAAsl1sD/b4kDFdTy8VVvbNUhdlfEKxPBMQIEzyMkvPlPFdNmVF8yCxp/bc6f +AAAAANHpwni4PXSsAAS8wAIAAABk5eKmFIrt0j7tWITcuHaP30kxhBReqw13AgAAAAAAANpTkxX/lbCpZlobCsLhzMIgfcYcMWjWCAxnSS2xDqSS/MpzVOfdBxr0tUdi +AAAAAQoGRUpkYd18AATEoAIAAADGvrgrVFmQN9LCw4ZLvSxgZvQyvXWfvA63BgAAAAAAAJFRRJ6nDN2iJW2h/It/J7MAdMRLODkxHgWXrzskFlkBntWAVLaNBRoftJ5g +AAAAAUq00Zj6SihbAATMgAIAAACWuCFO2QrKxRiyqD8GcSOK+XR2+onzmlfvAAAAAAAAAHKEJpkywLjNuxg8LMNZMYycixxDvGAbndmwFo4m4zf32BGQVFSTBBoYIfJW +AAAAAXaWnlDdjVfvAATUYAIAAADRpMjU3QJ5qJoQAZCrAZU/OFORj5jcCGEbAgAAAAAAAErlc3tVrXxC0qHbmPXyThFive+P8XSUNRdXw1CC2FeDfM6nVFXaBRpAZhTo +AAAAAa4jBOu1HXDKAATcQAIAAADIGJKZv08ec5TEeVinFBoOHyD1Crwi/wIsAgAAAAAAABqi+SPFsq48SMt3ACkb3Y2SfU+6HUS0oyAs6+iwFk4HlGW4VNY9BRrf5R4O +AAAAAe4567vRTvHvAATkIAIAAABdDWQLDKxYqSReE7jLiLiCjtDMwe2IyVEmAwAAAAAAAKmZjKj1alojHDP+I1sJUwyuq6+GiFz6kSi0YD2tJBZEqUzMVO+mBRqhGzwF +AAAAAo0G5MMJ4HJlAATsAAIAAADzjYslceey9XQGrHrAWMM4bDLtC3mg3+9aAQAAAAAAAL1LirEEiXYWJ0sZ+Wad+etnWmdCXttc6sKq3bo6jSqWDkLYVNiiAxoebsrU +AAAAArXodqvl0kNXAATz4AIAAADXVF8AZA3Sky2x8gyiP322Efn+asg0FS6CAAAAAAAAADkIN5w788qhvrSx5RgOp6qPgryHi6qSqaLOqvIBJzFvcsP0VOCcBRpV9wBG +AAAAAzIMe7BLy42yAAT7wAIAAAB/3mEuU34owlTQCxXWP2AcO5KMdnYSh4szAAAAAAAAAAoLdqMeHBpTGSPJd/P4Nq1wdAQVNj8rabuiE4dh1iXxoir/VDopAxpP2FWX +AAAAA5xktnVqaxx0AAUDoAIAAABBJ8CEpRO9ClDXK856E2Frg7+U6MrAUq/QAgAAAAAAAF+JS2fhUcM7pEa7CYeWRFFzAUEvl4L8TkJumLRTiMnVsSkPVem5Aho/ZXkA +AAAAA8qci5mj2FLmAAULgAIAAAAMzmjURBhUf3nPsdvsoP2iBxyCMYeN81ikQ4RvAAAAAEcO5r47sSCmQveUGs0dAEKF6Y/xNx+ekd/LiEcZt0pl0bYrVf//AB1EQgzk +AAAAA8qck3yru1rJAAUTYAMAAAAEZzgmqVxYThXcKo2RC4sdpnlJUfUn4rTY+7YAAAAAABlOV4Lp9Qbe0hXeC312MEIE+bOaHFwdQzlx8JORyQg5uNcrVcD/Pxyxsba5 +AAAAA8qcswLLQXpPAAUbQAMAAABnhxgADB6DBqI4cSVKnS+5Fe6dfeJ4ArFJXk8PAAAAAEN1d50UxPVas1Gapdh9ZbDgB0AQXQl/SvtKFtSamhgxcfkrVfD/DxwqOaWP +AAAAA8qdMTNJcfh/AAUjIAMAAABcqRUsr3BNbri1tRrDtTfpglb8Bpmu2Jt26ccCAAAAAERQFZkZ31sb137etPVvo1/bR1LnZ51n7U9PUoc/CcBMuhosVfz/Axx/9MCn +AAAAA8qfKfVCM/E/AAUrAAMAAADoWCDUvRNjOLswouQe/klR9JMvCn44zbgdivYAAAAAAAnZ6dzLtRxzZoHuTg6thYfx4FNSS+IHVC1XVGiWUOPd/0gsVf//ABwaJBt8 +AAAAA8qnDP0lO9Q/AAUy4AMAAAAla3d3+b79GtZ1Edx1ZvFCEa1TmA+PDDZc9vAAAAAAAGhvGoDONSeuoOWT+jV/udY+9ZsDG6x/UAqF8pgY5y+pIWUsVcD/PxuBPXRM +AAAAA8rGmRyxW2A/AAU6wAMAAAC37NdyJVbCpopI5xfzvf7+fe/LEWUVNOY09wQAAAAAAIOtO83VLVjH1dYxXIiHLLW84ajPa74lgP97mMd3uF9IFIEsVfD/DxuReASh +AAAAA8tEyZrh2ZA/AAVCoAMAAAC4XE2laxjgWwPCJGOBJq5nxX0x6IdnoadGKgAAAAAAAI/TrSShj1sBsdaYq/XZWg/9FMkR0dxDK+UAAI6pCsx/eKEsVfz/AxsKs9/i +AAAAA809S5Rj0xBBAAVKgAMAAADgKHxgwvd9Vbt5J0WIjVNjASd+t3UY8OaTpQEAAAAAALLDnvUznciqus7SK4byku6jN1E8/ffU3GcoQMJ27hIHxeAsVf//ABuz2K1r +AAAAA9NMU3drthgkAAVSYAMAAADEVPZCFcfHUR4WpnflS/Y1JUUK1Quhr88kTwAAAAAAAIYAQD8ypxI80xzPUfpsMpDZ0ZuF2V2cSnIAAkL7zcixpDAuVcD/PxrPSVzb +AAAAA9uoYZzS0i4dAAVaQAMAAADVDVyr1rv6CI6MLXUhdl4fsLn8jPBjS+hzznxZAAAAAImdkl73FBR1fyA2a8pRWme/ajRAjsoMKOW6Q0OYn/Ftr8s7Vfq9AB1ZnGey +AAAAA9uobD1+9/L8AAViIAMAAAD26cXvE6tFL2AYYrihrp/JG0f/H3dHEvCdN618AAAAAML/DoA6Q4esDmifCL11dpd1fGSoJpGONGlnwFy4iQS0mNA7VYB+Lxyhcp70 +AAAAA9uolsAvjwZ6AAVqAAMAAAAE6OEEsUaqL87dw/2+STkKbJ3ESb6SWLKqhWANAAAAACGiSXsw1jxsMa/8YfnQQ5jMtnqzPuJ3vgDpgoSg9ZcAvtc7VaDfCxzVG/4R +AAAAA9upQMrx62QwAAVx4AMAAAAg9KFN6AEeJupUGGuWqa6WOAkpotZCa3iWWb4CAAAAAA46zm4VPNEEBiTlj+tjFgaRpeczU6eyZpTkoXxoRTNJOOc7Vej3AhxqftB3 +AAAAA9ur50vFWBCmAAV5wAMAAADgtdnsaiKHUjUSdKyO9rISC9gVsXDiViC3hCYAAAAAANWpxpPQXyg3nUdMTgcGqFcvaYPARp7fqrsgIgmjJe2DYQQ8Vfq9ABxfFYul +AAAAA9u17XOADDaJAAWBoAMAAADKy1/BL1xKFtMJndXLb8Vx46lWeMhmQ7LvJ+p5AAAAAHMOBQwv6cl0r2hrk5+iP5N5Hqcd486Cfbz/RBzs0GVaOVI8VcD/PxwLpY3q +AAAAA9u2DOSffVX6AAWJgAMAAABdghg0aI0g8Dgcpe/7oU1Uh0CljFSk2yUFEpQWAAAAABb9uduV2QJrYmob1qHNfrL0Hh8KOJ33iHhBEx4B69J/s188VfD/DxxWQnqR +AAAAA9u2ip0dNdOyAAWRYAMAAAACpcmdc98Bx5ElGCJpmgqdq1ExVfM4KTcN6JMGAAAAAA+NDjnyBRVh2lBgX3abQJy8pQWgCsfD4AsdCNdm6ILo9W88Vfz/AxwKE2jK +AAAAA9u4bbcAT7bKAAWZQAMAAACLhIabAp7jwv3mSNRzL3ELkRF4kxGFPLKcDZUCAAAAAJpsw8N6b78pnAgUnc84elj8RnThgM+GLgBzVuPggdlW6Yg8Vf//ABylinZu +AAAAA9u+XLLvS6XAAAWhIAMAAADuTBZGQUleqlPmKvYBnPhKdhfbM4+kxlYAxJEAAAAAABp0iXcSFYXh+XB2u0I84GwG5dZ55ErTqFFmYPIXCtF1l9g8VcD/PxuWkr6F +AAAAA9vTz09h6BhHAAWpAAMAAABxCQdkFXjCINVrncQeQYiMzevh9Jqj4Uc8Nx8AAAAAALAe/Af0EDCquR8zaht7qe4ASwXOC21SZmh4UPBEVD0finU9VfD/DxsWZWFo +AAAAA9xJYE7y56jRAAWw4AMAAAD+QAm9nzVV7qKALZexlXCGm3BYecVwIUvKCQgAAAAAAPWcLTpUDLRUMMH5rRRhc+7Svupz+dmqmMH/q4ecS1llK5Q9Vfz/AxshoQ/k +AAAAA92WZEr246uBAAW4wAMAAACyDzb/QcbbK1F8Qhxm0L0NN7FHhDDDb9Z7jgMAAAAAAM6hONKVcDjMfczY5XgOU3AXQO9sunJr8ODJkZcAo4I+jZFAVf//ABuzJr6M +AAAAA+J7bC3+xrNkAAXAoAMAAAAd9Z9tZtmgRdea2kGK+kHywgyXwR/e4nbfJTOaAAAAACCihnYjzn0maeY7guGILaYftQPMbqrB5RxCRgblbBphJmlDVcD/PxwBwqRF +AAAAA+J7i6geQNLeAAXIgAMAAABKSiXgYrm0/4b1dWQWGzIaXJmKpEO/N/gv6XYxAAAAAK2uXbg1TjSb+oK+xkY6mg2/Fot83/+AaxaJYkxftUw+H3BDVfD/DxwJrw4h +AAAAA+J8CdiccVEOAAXQYAMAAABe0G5xDBaF0O+W9Nlcgv9bzbx1m4WjHOiSMq4BAAAAAPa1LBreFLnOgnqWokBfjCBZLXcRGwdL5cyreZ4RmFYsqHFDVfz/AxwQsjKm +AAAAA+J+AKKTO0fWAAXYQAMAAADynJY8/3kPoVkXYRtNpcq1i6L7Erlr0/DE2jsDAAAAAHXR1P7vnV3JEQhpYgkedbrijJPFWjoUvRgi2+AGTRFe6YNDVf//ABzi3Nlm +AAAAA+KF46p2QyrWAAXgIAMAAABrQTsQOmJsxKURtSdYjBLrvzppCJ8sDvq3bUwAAAAAAN0UPHTez7gaHDvqEOpqmA1vf22pr0WrgAhdjvmBuuvDrpRDVcD/Pxvc465I +AAAAA+KkOBbKr38kAAXoAAMAAAApELD16XMW3en8An3Hk09fDA12MZReuYv0hz0AAAAAAAQNAupIZDfmuA2EfQUZLca5phhrCl515Wb2Bj0tJLpHMK1DVfD/DxuwuqbZ +AAAAA+MVeVcL77/zAAXv4AMAAACwcu7OiITeM0xYWuL4KcpI0nXrcXW52MNPIQ4AAAAAAJ9pD7e9Y3WOIBi0QcVrtv4DMwZBy05luSe5wL8X0TUZXANEVfz/AxuG38Wi +AAAAA+S3vFNO7AFOAAX3wAMAAAD8DY5dybvmZtHzkb2MCnfeAg1wKLhZcZsmMwEAAAAAAA0ACjN92ARePUYgSiYdhH+2xqnLT0MiHhsHQzHKTlHezh1FVf//ABvIvQR4 +AAAAA+kQxDZWzwkxAAX/oAMAAABZwBeChYc1WGb7gnliUUT4h/33CDqT3eKSzAAAAAAAAJECgzDSZ50z1Uex9v1z80afq4cHqUTmwLQrkI6Ne7dBIQdIVcD/PxpBcRHa +AAAAA/SW4FTjgjnEAAYHgAMAAAB99C9N9myyM+0dVVzLswVAMRaCTAzCNoFwMAAAAAAAANlKNgxLv3QKuejwXH6Fq9oZ1jc3/9abx9GMR2hwHGj08UhNVfs5EhqiaRFf +AAAABA2psNd9zfdzAAYPYAMAAADwIWHs9DgLZ5JwmgQ1wEjkmbYxL9CeRx00oXLEAAAAANyjWGt+8HJYzBJ8tKfqZKsbHhhGLd5lK63K7EK4cmnKXWdXVRuNAB0c0i9F +AAAABA2pvwXiaRXwAAYXQAMAAACKVBXtUnXqzRQUESg4pas4o2FxmxvU7tY0iDUKAAAAAObmWP7j/BIimT/GM5t7SnpmFVxxOYIRokQ5RC/BJcqumHxXVcBGIxwngVoh +AAAABA2p975WQclNAAYfIAMAAACJEOGb7UBRTsGZWIMR7VdukViDXHfq2nJz5xMOAAAAACHCmabaaGOlmvDmP8JzA0nwlPiivZMkMN5tlltZ9OUNorpXVbDRCBxY2X+x +AAAABA2q3FmmCetSAAYnAAMAAAB8zxI9zzSwxWJ5acbuv+STT71lJEv5MQnNUoIAAAAAAGXuz7nLYbNz1+jaGfon66sJw6NnNpUGdj8XOl5A+LZAYt1XVWw0AhwhjweM +AAAABA2uUfa+zpLWAAYu4AMAAABby3oyi5s0YQGoJL4YgeZxvjQSg0M63mzNEpgBAAAAAOKOEvCmud3vJgK7S9CpXb46LXcvzmKmGxQG/psfpZEMQ31YVRuNABx4foFS +AAAABA279sEQG0HhAAY2wAMAAAAm9o1ixn25Tsogsjo7TD50gIOftd8aMJ9JZVgAAAAAACk8K6MPl+D21uEneZ6g2nZhat4usoilMAAplr3iEKgcnSBZVcBGIxvriREQ +AAAABA3mH/dWEBkTAAY+oAMAAACop3yF16QUoLCQ7/Bb+gG0jjekYJUyYR5+9xUAAAAAAMDXwp4DxClySS4srmtYTbSWJFEfP+nisrQcCi2ZDfgItN5aVbDRCBvFJQo8 +AAAABA5mfwzrRuIiAAZGgAMAAADoktnTykx4ODPp/ziVuka+FH+OumOk6CW2y8vpAAAAAGaQQHDedNzCdZNqJ0AIb58RKGXd+SYrPTHOMuw+yflfvNBdVcD/PxwqLl/g +AAAABA5mno0KxwGiAAZOYAMAAABCfN++Qi0xmn6t5eHFHLnrI8Kqz3MozO9pcbIPAAAAAGO1hAlcn07rmKQoWdESq/WdAepWJGP0rMywfLnVYVZD0tRdVfD/DxxywpWT +AAAABA5nHL2I93/SAAZWQAMAAABK8kD6JYF+4d6EsAoUddFGHT8ApD+/vy7WlpwMAAAAALm34EBHsjwyEAbRVLe3XJQQluOxehDgFKidlatgg63HtOBdVfz/AxxqAnMz +AAAABA5pEc5+CHThAAZeIAMAAADBHMdmsf8IaBKV1Za0HlZKNIq2ZCCrsWan4xEDAAAAADMahE0Kl6HKZoRNYiZ5X9VHtub4/y5aVI6fg40av5i0rAVeVf//AByFj/Pk +AAAABA5wiEL0fOtOAAZmAAMAAACRa9AZx70/+pDUWhQ1C/u9CfmjTDqsYiLIaHVnAAAAAIeOXgRU59TOVyLq7f2G5DHTdWzX0gwe9THsH/wpwU9SpHJeVcD/PxxlkBhV +AAAABA5wp8wUBgrXAAZt4AMAAABGR9CXnJKPGJgeNNkxZ7TJ9UCR/VF+Zaupe+cOAAAAAEK/20MfaYQBJbYdnGFmMqFzP3gRNoHaGgpk/g4n0WVVhHleVfD/DxxQoS+8 +AAAABA5xJfySNokHAAZ1wAMAAABIEaFuG02SMd0LoC6U+nBSOD+PFnRtGsm5fqYIAAAAAJZI8dx8K+YWH8sC5PQ0oFuhS1nLUx1+Z+xKO+N08tRYvoReVfz/AxxnTPMQ +AAAABA5zGo+GyX2YAAZ9oAMAAADFZkTPZLFauOkADH9VqLSDjOF51ZBuAuDJk38BAAAAAJyGOJ7gUtJiNfVEKoATedK++5AMOKUyo0RlxRYuSaFWqaxeVf//ABxKi+KD +AAAABA56vtYrECHXAAaFgAMAAAACBkw3AhywbOn9tSjZlZiy5qgRN8A1kjf6TpkAAAAAAEuyZWMpeWeHJAwnJjGa/S3cCsLDST7UgO1whTxcUmDHVwVfVcD/PxsQGKrV +AAAABA6ZayzXZs4PAAaNYAMAAADyeUNeqJaXPsj2Eqd3LWfd9r19fB3XQ8kjn76zAAAAALmqkYv9l8FKuvIH4GB2EFXdo2K1sM1mHfyqVDdftdeM5yFfVcD/Pxxma4ae +AAAABA6Ziov2xe1uAAaVQAMAAAAUE3+KlxMHCT7H8SL4lpMMdCtGutwrC5sI4RMHAAAAABJxW2ssZVpiPEd1SpwkClZyZqPSxsIKfxpFhUTt6L+4LipfVfD/DxwEkkTO +AAAABA6aCLx09mueAAadIAMAAADtkiKrYF51LiRAjsC6PH+x3E3OyXoaQFjTRPwCAAAAAD/txqcKYjzJ0HEUYpuNWrGmphSIkJlHE128xug7y1uphDdfVfz/Axxk13AY +AAAABA6cAX5tuGReAAalAAMAAABevOF7RcG58NV9RmrJOjWouXOfBOcL00/A8h4CAAAAANVfvrNlsrhkKH2fPYzj2+ptMFI6jPAnLFb/zVYalXA2xkNfVf//AByOMuB2 +AAAABA6j5IZQwEdeAAas4AMAAABu0yVJakNaf70RYV3RXGfPckGRy73LDERbuSMAAAAAADQwABsBJp72x/kVpLbYmFC3fUIFs0oG+vLUhbUWa6+xIVZfVcD/PxsnlJQo +AAAABA7DAMFs+2N6AAa0wAMAAADsmwOavy462L37eWX0o/1dS20qdBv9vx1SgxkAAAAAAI/oevffCAXl3Ww6fWzyG4UzkNCEc+kyNky86SD8iz5fK3JfVfD/Dxu88X3D +AAAABA85gbLt7OP1AAa8oAMAAADBZ2pvyO69bSET9kaJFMINqY/mMiQx6ttTTQQAAAAAAO0lPbbkXS9C3uS9/gacHHfioum+YEwXJKUQLtHjGdJuNtNfVfz/AxsAA7mD +AAAABBEKxCIwXCSUAAbEgAMAAACNWkeu+1vDNi6Hlo+Czh3/clqlIS3xknZmBwEAAAAAAFafGkRd8tuifxwroUoEMpf2Ih8/MEOdvZqWzFJRD++Xq85iVf//ABuASMtG +AAAABBaczAU4Pyx3AAbMYAMAAADVW6iotnBQKZUcWOJXfO8B+FrCTnQBDB0tJQAAAAAAAJC+/LEhmAOZQB6KwQ2nZSZqjNnoLUtH+ZRRUtThNYCc1RhlVcD/PxrpTn+Q +AAAABCWA3xBf2IY7AAbUQAMAAABRwvxzNsNKQ4Ps+pvzv3SeTQJ3hhUG3BZOpR0cAAAAAMi4kNGMy2OcLLBLKfIo06em6n21a4C4/LpR1u4iiwX6+il2VfjsAB3kLvD+ +AAAABCWA55OYfLHRAAbcIAMAAAABI5Ksi50O44Vrg2NtO2s+/AXCpHvfv4auxtC0AAAAAHfhx75j2et63gMy+hPqlY+tCkgS6PbHjakwgZFZYDQ2xTt2VQA+OxyXfJX9 +AAAABCWBCagwZ+WJAAbkAAMAAADnPrMemHR4DL9K8A2NkMFv3UfYi2e/IrCcFxYrAAAAAHIF/XpDGtlhE/VgTznxFBaENZkpgpkiTpPuQuBrHO2pE0R2VYDPDhyAsJss +AAAABCWBkfqQFLRqAAbr4AMAAABBVqIz+eYH0e92BvRhoE/chNEkZIVBrkpVg2cOAAAAABvBUGUNyayAcxoLIrXu5n3ZNLsz1qxILHA7fDYDp4PnXk92VeCzAxzw5Rwi +AAAABCWDs0QOx/fNAAbzwAMAAACw8cC4cy9kTgRLnhw4nqCXPVJLW4kZtbqs5FoAAAAAAH/YtlG8olQks3x5r0ZK2AS3h+T7w6vCZ7Fcfvb3rRhFWV12VfjsABz5TaiB +AAAABCWMOGoJlQVZAAb7oAMAAACBYd/StyqEo3XQjYYhq3VLgGRwlF1ZyVkrNxAAAAAAAD3YlYyYbKn50Km7PURbc+eg/wLU0T8e/uo22Kg+w/8+WHF2VQA+Oxvzrx2w +AAAABCWuTQH0yTuKAAcDgAMAAABpm5Ir0YJPipJonC/JwCoU00ngOwkEo35Yny8AAAAAAFgZXOYm3X0wGsMhLZXkh/kbEpK5MvMUIz9oT4Pdw/dVUZZ2VYDPDht2r9eA +AAAABCYxfgUR8okwAAcLYAMAAADrNGwUR9HT0cTAKPLEZcLQvmE83HY8G+XK1AUAAAAAAPwDlTFL3HjWAcwGWl/gOw7y9KnE+YALnlRYdfroDD+bnvZ2VeCzAxsv8Xah +AAAABCgn1uOKcwCtAAcTQAMAAADle7BbVFGbA1ZmWSKb38xSy5CkhbSg4MC2pQEAAAAAAP7hM34OePYUVxHvdF0s0qz4zx+bpDVAiSx1F621kVhcpe14VfjsABvptEQX +AAAABC24x9T1mjn8AAcbIAMAAAD16Kbc8FWIaLs8KNZdmavtCF317Thjl9Aj1wAAAAAAAFvqp0RhleYvoikusysyUYaZbVVEDQVirxI0y0acqAokkUV7VQA+OxqXUigx +AAAABD4YZ3WL0YYsAAcjAAMAAAC7EdY4bGVw42KRTx6/TWdhD2ygfyyoCALp+uu/AAAAAJjCpEdzy6uRRB6xaeG7XZ0/+0fzwyx+YKwjmvjKzS7LAFqAVcS1Rhy3QEYk +AAAABD4Yg+EcCTHPAAcq4AMAAABQKNaqWoypgmryp6R2Hcxwf8gkUKuSKksUzZEMAAAAAMgJNML3Y8/eHRIJ07z4eMfoANxGuv/gaR9ZC+jvBPUgoWWAVXGtERyVYNpG +AAAABD4Y9hefwCu/AAcywAMAAABTaYG0DduVqh3dYEMzcAydaUySw2n7XOsUgXwHAAAAAOEeRCZIbKIbWI4j4xbkZPhYMYIBz0PpFqwPxMYa2HG92W+AVVxrBBz40XPo +AAAABD4avvHIZxAcAAc6oAMAAABD3upPSyyQRyI55ePG1/oKoYiO9rgdCT5EpkgDAAAAABTNOU3Ah9v+847h+8XrqV4q9LXBm4YnbEVrER5Bc4sEs3qAVdcaARxCt2Y3 +AAAABD4h4lprAqlyAAdCgAMAAAAKsCVstPWhfutQDRs2G1I2Y6XYmoIFHjVwO9YAAAAAAD1VaxNDLxNcNQasmsgYQYlWZVzkgoupYfnhFC+3gMVBEYuAVcC1RhuVb399 +AAAABD49s+3h+gwyAAdKYAMAAAB5kP7Z/9JUroKiOjGNzr8bChJy5e3wYzFgWEYAAAAAAIPpTbppVkgifUeSw16DgZYfQSC1RhDQIDQ6vjWfcDF6JKKAVXCtERswINIP +AAAABD6ldBQIOO0CAAdSQAMAAADIeDN3cqV/mfVyzqAZ+6wjb4U025PHELRjgAQAAAAAADMganCl4PXIixh2OEnOZJQMLlQZq9xdEEY27HRKaT9stuGAVVxrBBuiuqvl +AAAABEAoZBITjwkwAAdaIAMAAABK0DOvEh9bukMv/Iu6SwZkR4v/k6V/WG77KAIAAAAAAEDWTPsyzaaDgQZadY4JdAmWOAmZSMVb2m0ZIDYz6vwY4IGBVdcaARvfOOgz +AAAABEYj1cLsFcb9AAdiAAMAAABi3z/LfNntDLv5RgPhbFWxIompog5ct93jggAAAAAAALvsXrzzEWUnXgymdKU6OxjCmdj1yNjzxiag28O57z6udrqEVcC1RhpgU/UI +AAAABFYBrBSWgX4SAAdp4AMAAADiWmv/ruUwNMLXTFX6vt8hHLhEa4jUVBtXBQAAAAAAAIZ02QsYBZKPEBu0esUuDEIwxE/FBIwyvruabEuN37MiI1iOVaDWJBolx3ab +AAAABGDIjw2LQqjQAAdxwAMAAACnwq18ptIbPTfDY1Fh8xOfOhCO4GHZoKE8YGowAAAAALF5L+tVdSsR6EiQUUZmgPEFtUnF9bhWVTbQlZJuTDYu+rWaVYarAB1iWi+7 +AAAABGDImtLqmxw7AAd5oAMAAADYMKXjsOmy8h518Xg839rrht4itjKY4Xq6k10ZAAAAAD761TxnLQ2hgky6aNd4srGQlo53MAAPMu2i4l1oGuYnn9KaVYDhKhw6q8w8 +AAAABGDIyehn/PHJAAeBgAMAAABpnsRp4XNsjJsCHiqwCObwk0gLsovGywANbUcAAAAAAEsuNz5kyi6H176knXdUjUEy5m+M7udOim8+uRykf/RUld2aVWC4Chxhx1ba +AAAABGDJhj5dhF+hAAeJYAMAAAAVArzIoViZ/V8UbDs7zTxMJAyWBfwQvESANFEAAAAAADs4ousZHJtV1ZfZS1AeN8pzBR93lumkmbOVgCkpSkS43eyaVRiuAhxHrliF +AAAABGDMd5Yzoi6gAAeRQAMAAAAJfFPHP0aw4EwoLbGS1UDCKNyuqdy6A5I23+QBAAAAAL9DwW/AOaH4DhURtgLyTgxqQ9/6Wswm+EOIoAoudOFOzQabVYarABzPGhPF +AAAABGDYPPWMGXpbAAeZIAMAAACDvs5gK8q5uc93JPWW6k2oGFYYyQ6LRM0czJEAAAAAABeA9PYdS1ctVTIHb4UN49u1t+nvUDXiyKOPMXWynmxqvs6bVYDhKhsq7Lnn +AAAABGEDbhs58+BfAAehAAMAAADR1+HOT/0iPgGwocH1bGszZJzsbQzvwJkCpSQAAAAAAFGyt3m5bVUEEAn1K4Yk70OyBT61cPD4PGsSKuhD8JZGSJ6eVWC4Chssn37w +AAAABGGx9mLUtrOhAAeo4AMAAACWuw8v3A/KbwO5BfbnfNkMJ9GDN2zwHYwkSQMAAAAAALFSN4+bAopzPV8lViqdGK97iDFq15QZBeiEc2exj4e5Bg2iVRiuAhuCKHeJ +AAAABGOfnrEJaHe5AAewwAMAAABp6iow9xuZP1aS7S1/6yZth4DXoyd0PnnmrwEAAAAAAGGdlYCu8GBEOScGcV6Rd2bnHVpZSwKZ0IlJIJ9WPUxPiiWmVYarABuDHDEp +AAAABGnWpbbPe7MKAAe4oAMAAADJgAcvfEiD+RrNy6DJYXYwzPzZJJ4TbgBIcQAAAAAAAEV+Jmw8BOc1aVlymlXaRBItMMj9itfoboC1Oq4SxEeNSSKpVYDhKhrRMNCC +AAAABHsPgE7lvndtAAfAgAMAAAApvyHZKCFPmOQkFClk8vajd4FzhBk0KnI0JwAAAAAAABla/LgUe5sORevJOlxJ8zM+fYqKx+VBMLrvXR2b5WE0TmquVVFFDBrrNrHc +AAAABJOsB6e28aZQAAfIYAMAAADSjFP1Y56gT/WSDtSuLaRBenV3pEleqaossAkBAAAAABfcvdvNrRvXY10YvuVAQLJFpEC+ecRvVqlbN91/F8CTvA+9VafLAB11AV1q +AAAABJOsEX9wFdQKAAfQQAMAAAC5moPMnG9zlXHWW6sB9wZZwgRa//unj4v/xxGaAAAAAOJHkru4ml+02bhwklOQlB3a1eCJvI6boZ6g8S+mIsxTVjm9VcDpMhwfz2P6 +AAAABJOsOP8LlxovAAfYIAMAAAAYXqTB0EbF+6cVW2YFiR44be9iHK9gzeD4dx0rAAAAAHF6cLjPxIcfzegX3GD6rVTIxOE03vSrvoNzG/+3z37Mxzq9VXC6DBxlx1ud +AAAABJOs156Z/B2UAAfgAAMAAABArP5uVS0l+ZDIrj8bLwjF7afOIy+e3hewjFgCAAAAAIfH135xBIJpntJJPYbi13FGSE6yDaU6YvxjszNqDZFHFzy9VZwuAxyddGmb +AAAABJOvJbtcJxk5AAfn4AMAAADc9zqz1hE+SR2GugPJvM/b0mQaS0rCrXfxB4sAAAAAAF6Xn9iTpIaFIX+aCHJUU+J/BzCErYMigw9tm+QNUut9hYe9VafLABym+Nzo +AAAABJO4p7GV4jLsAAfvwAMAAAB+dXSDTG6/RjJTQ8jns45eJ2UhBY6qIFDbuLoAAAAAAF7Gghh2pprYA9iQ+sb7G7JkYCK7zqz9KP11U5wq6kW2J9a9VcDpMhulYhQ5 +AAAABJPeA747ow+zAAf3oAMAAABrtbllZojqwNloWNzxxXJsvW1+MvyLyv2IhBMAAAAAAJvvjZzpTha+DmZCPcMloqZccCrl2VXydEFv8wU7cupBvke+VXC6DBsYkP6D +AAAABJRptRQJLpFKAAf/gAMAAACnSiYw/GGlk0BdcegXuPH+Z+aL8PtOlfwhQAkAAAAAAJuYpLpSdzl1TTUDy14Xbg3t7dfndzYpo6kR1Bf1VqJqTRq/VZwuAxt1beuC +AAAABJYukPtFuG1XAAgHYAMAAAAFYLBQQN764xsAeEEMZZvAxt+aty0HZD4Y6AAAAAAAAB8g/iN6MlSmjzNfb23aIPCPioq5E6bAjqgLw5MNO+BeeFTBVafLABvkZIhY +AAAABJ1XWGk7mgULAAgPQAMAAABwuRJKusA50HBP2plp1o3GkdFMl3UcZuiTTAAAAAAAAG0NWn3wR+rUZ7EEF6pQuLofCAcZGNExFjbU0c8azyyzKOLEVcDpMhouVp7K +AAAABLbJ5BEDaO3mAAgXIAMAAAAzTYKHKYKm5UZprO7sno/m9Za5S0Dss3XGIQAAAAAAAOM3vtzQt0U+M4L8GZI4hmfOT/5g/wkZ+bRp9MkaiRducwbMVQuKExpDkCRW +AAAABOPnTm4kdRvkAAgfAAMAAAAtPP5ofDAHikE78/KdCY9jNOzRJQ/ThFbYCgAAAAAAAAsXTPTONoRUocLV2OCD6OGFqkQZWRJczFUHYfi7RZeiS0LYVWfyDBq+gaYK +AAAABVM0WSzkqQtmAAgm4AMAAABEWnheGDKJrOPUdYkbXdJ0zsRCGSrEatetxgkAAAAAADubmXa5tJzbcww1DyVC48atLa4EoW0Mh8Bm8H+XZHm4NUvmVajCAB2qsuPv +AAAABVM0Y4womGtHAAguwAMAAAAkSVfpzjtOjinOr5Fd7Mq1b+uDFogWRzWNgn0eAAAAACxXLEA4RY9sqND2XRPx+lZ8QN/t7EHYO3YdzzsoN90xrEzmVQCqMBw6HyxN +AAAABVM0jPxwPbCNAAg2oAMAAABkrZpOu/CdaWz/S3CfdRQvDxBWPv3YA7KKQw4KAAAAAM/VGETZVZ5WuQBaabEUpDfuh2qYJvTcm9nuRyb1JzDXYoDmVYAqDBwQyaHB +AAAABVM1MvCvNAU/AAg+gAMAAABIYhunRCxkGDv/XMtkNWYPHtP1/cGL35UoXH0LAAAAALD3q4u2xP0sfk2QlqGSWiKuH8xdWwSnu4rwswhyE559cormVaAKAxxIR1ip +AAAABVM3ysGrDV/nAAhGYAMAAABzoh2Sq9UVXFLtJaOUPUJGVJd1CuD8ks2eIJMBAAAAAJCWU/9udV3BMVyGpBzo7AfA5bt0VvGrvWIFmDXOjoNEQ5bmVajCABxJQmEQ +AAAABVNCKgWactJpAAhOQAMAAAAYZPQNRZd/OIZOwUuRhlHRhcfb72esMNxbgyAAAAAAAKiZXQK7N+xOBkDa4RTVf74kaCWnh8/6Hyw1rTLro1vwEKbmVQCqMBtD6RZu +AAAABVNrI6rKDjkoAAhWIAMAAABj61QKRkgeG6ub02TGHrBw4XVMITAAHzJ4kAwAAAAAAEe9iCwAbxsdjL6YR6yeom0Wo+ABiB0OzA+NfUh0Aig+Rb7mVYAqDBudH7+W +AAAABVQRAt/xWWRiAAheAAMAAAAfEuAwHOKfZDd1XtIwN+sIkdyQ4a2G1GOLxgoAAAAAAFJHa3cHqGFqYfDBFC/+EY5OW58AB6upn89otOtfLltBYBPnVaAKAxsg+y4H +AAAABVao09vKt4aXAAhl4AMAAAA/Pv4nIiJYht8DCJy520d2ehole2dLX/xNqQIAAAAAAMiBbKa+Ojl+aZaUvUs1//3k60ZK93NEknngSbP1ZmynzPHnVajCABti9R8D +AAAABWA9kFxQOsaqAAhtwAMAAADz906CqNwG39uVredx1bP/lRU7WVAO77t2sAAAAAAAAG23WO+F0yfpkhyy3bgH4CelvMfms5eGS3YCl3fLDUhcTW7qVQCqMBqGTpST +AAAABXkn/KxHvqo1AAh1oAMAAABBA//2tuEeMG/Ljw4cJ2jSXNIzQtK8yHVwFgAAAAAAAOzlVbnCnr1J6UYOPUTfetcNdqUuG0EZ5GbjslA3prtXRrXvVePpDRp6nMAP +AAAABZWLtemGhca2AAh9gAcAACC7VwLF2J6k5vdTUjzXJQP/MFNErBLPe1iqBzEBAAAAAA3uPvHAlwCOo073b6LvNbFDMnCSS8KStcXsjVGOQj8TWXb7VY+jAB3TXECT +AAAABZWLwjQTl+AsAAiFYAMAAAA99EHJG2sYFzte4qg4ILpcja8RFdX/yUk99Q8JAAAAAK9nPOfvkSApwjASoHfuPnwaxdtTlzZPnJwVEVxciFk8Con7VcDjKBwNzQCe +AAAABZWL85SJkbolAAiNQAMAAABE6NjFXvBV2tVbHXajcZUGVsyDiX/3B1ZNcJ4NAAAAAEnMRJ1zhjQGK9K5rS8+Y4M2Znr9QIGTsXTHYwpco5zDy5L7VfA4Chxnafjc +AAAABZWMuRZheTHKAAiVIAMAAACwQ79LwTe8qejlggrrhA6EYOgKDzm2D5XAtLICAAAAAKnsSrIpFyR9NPXEhmgZgAUJIoqUoMFACOP5XQJ/6wNrLJ37VTyOAhzTgiWC +AAAABZWPzx3BFyf9AAidAAMAAACnX+Yhn25fkOyAHIyHzmi3UHjZ4dw0JHoTEL4AAAAAAMTE+z+aVP0BE+/HGXxvO3iPjMl2ZeLrEPZyluXhKTeiR6z7VY+jABwLvPkc +AAAABZWcJzs/jxCIAAik4AMAAAD0awI8np2YHSb9jSlJ9hRcOuZIwNSDAfhm2VwAAAAAALyvOOnLwiuVhb/Gvlz8//RYkEu5vAiyV6iAYvK9b5uk5MT7VcDjKBtSVUoc +AAAABZXJ3SpNHNDFAAiswAMAAADTgehe/lIKmzqdka23cqOxxIAjCKQGUOUwjw4AAAAAAGJ0mwydkn1jPI5jKniIg8tCkAx2qPcZRvlV8e7LRmXKnvT7VfA4ChvW+l8x +AAAABZZ/0cpnd7B8AAi0oAMAAADEoWk210bHzJQTP6I8Xa7rGa03ott7wqRwjAEAAAAAAPRsnmLX9KWCe0ef1/qjhXUbvDT648cQHbOrU5tqDy2dGZL8VTyOAhu671EC +AAAABZkSxNXTLzWuAAi8gAMAAAC0JVv6r8gy0nYBr65gZXpveCcylYPIMoTMKgIAAAAAAP9dPyl1k4GLKoBdG9m6Ry58WfbVNR+B7B1wOUd1P7dlFnr+VY+jABsGzZQT +AAAABaU6s4IE01nzAAjEYAcAACAcOo91+7NVID+wH8/Ydoyw/izL77QoW+lUGAAAAAAAAB8I192MjDMBSQiIQuVN99GCbhGELZxrV0E/vsY2CHZ79s4GVgbSSRq3Io4S +AAAABb4UJY5vnj91AAjMQAMAAAC+I+8bVPE0zSFr0cA+Taxyvqg+Wp/DwoBwGAAAAAAAAB+nuXJ7fRbbHotJKl3vRZ5uafHKhF6aj4LW1ZS2Uz9T+BMUVjQLNRosMWxF +AAAABd54exbN/LXBAAjUIAMAAADUAFU4Vtu7zrvHfEbfkHbsWPYktWM8anaTFQAAAAAAALZyA/21/n5NOtI6UvjEOsHjCNKuDG2fvXOP3/73XHiyapojVq+dLBrt1AzV +AAAABgbYcaeZijEWAAjcAAMAAAD+zX6fO6hLFl5AGITsaSEZF8byQRrFbZbWEgAAAAAAAPWpASIs/PvesA2V0NgbxyHv6jNmyt1cArkP2/WL7TT3jycxVqrBIBqgxU+Y +AAAABjg27r4338hCAAjj4AQAAAAASfcYyrhqESVm2Wp1HrdIBLAn4QUmCn0MHQAAAAAAAC7auR8p3vDVo/rJ6LkIOMWkhGmSbrF69+CO75yDGfhFzP82VllfChp5t/GF +AAAABnd1iEhN/c9oAAjrwAQAAACSx4N3P0L/b8JswVYoYPfRWllX0oKSpJ5DywkAAAAAAKGfe5TYmsWxX5n+wp4/QyVfTHjN8jPBLYLa8NQzBs/9fohBVqKSAB0PCKM+ +AAAABnd1lgCC/iX4AAjzoAQAAAAo0Nt4bWH9JxJRS0jgIZJjCkLXlMk2g7mCkogAAAAAAJxQbGo3ec3MbT3NgsjrxEMPpDaVy9X9uvR9JBaDq5XLi5tBVoCoJBypyiz/ +AAAABnd1zRQOntUvAAj7gAQAAADZYGS2IZ4gO/2pDKQSmUzHORLSS4ZnZ62ipHEAAAAAADBMhXiWR6LvVoRdrDTm7JREa+ke0MCs7iikgnR1MBQpg6ZBViAqCRwu1j0L +AAAABnd2qWI9IZIMAAkDYAQAAAAPt6mOvuaSSDrSQ5LPqMvOWeiRcjp8mjws7fMAAAAAAKacgpHNEvTge0AHIMRgJygoi1d4sNx7XuPgbQYiutswk7FBVohKAhxVs7aD +AAAABnd6Gpr3LI1gAAkLQAQAAAAKYrf8Ap7KAfUceh3rxhjOhfb09rf43/9YQmoAAAAAAH1LzyaNIymekZtFEwLr25VV3DQD7bOH8Fw5mEWsYWgHe7xBVqKSAByrqt5Z +AAAABneH333fWIKRAAkTIAQAAAD5lPNMg8im7/BJ6g+zpMZpTcKpZxKdEBqP90oAAAAAALrX/Tzw1Uexv8MYEBSMo6LYD3Rtp5nGsYM2c9ripHOI98dBVoCoJBu/mSPh +AAAABne+8wmACGcTAAkbAAQAAACyZCrC1UJnZ1cQmTzTLBuocIQZR1cUu+/zOxkAAAAAAPUZxaRjSUCBzxdVrc6gJEfULrch0Cfcgxn09mL6jxUaAdtBViAqCRva4gsT +AAAABniX+1MJtFbqAAki4AQAAACKtFu5fqWczhha7v4HZ6ZU8Ns7jEp+jHk2tAYAAAAAAAyITWfR92W7/6STVluuu1Q1p/AdBXN0D2q6sz/NJQoRofxBVohKAhvQjJy+ +AAAABnu+IlLChfpuAAkqwAQAAAA0m+bZrTiGP7+oNPNY+UMzF0/qAjqqfAy1HQAAAAAAAKK5KLsI5DK+8UaGtIGp/sERiVSRke3WJK1WPtBiT8r0cHRCVqKSABsfOixB +AAAABoaUTumztn6mAAkyoAQAAADYAVCtGFFVRk3ec2Ys5NJhjZbFzgvMKRZiPAAAAAAAAGqpEogGM8xlwgl35y5LhBfTqp6gbE25ncTf2mOYNS9w8dVDVoCoJBqj0r3C +AAAABqQ0U93rvy9hAAk6gAQAAAB71V4MSV5P+1RARFBGOgE0Rd32ZYKR8r5iEgAAAAAAAD2yvDKZnU13TCi102Aq90BZwsKwr6ShW1QGYtIr5jef4dxGViAqCRroq5SG +AAAABtOJvqUDnYW1AAlCYAQAAAAr78t1vXPGdnkzx5dZZAYAQ4TMcC2X7s4gAQAAAAAAAJbJyF6JHCv1oGlZ8YXCmouIthhTwJ4ZVT6l/wsbixO/SitRVgweBRqKicv8 +AAAABvvLEIyJUPoNAAlKQAQAAACvP4LXgGqKigKqPW6/1sze1Xq5FqEYxMBNExa6AAAAAChrx2nADZIppMaisXpqUx/ZQPczFk51w2XUJu7R8gFhXVFlVv//AB0qK6ep +AAAABvvLGG+RNAHwAAlSIAQAAAC2hM5WBID9N01ZEZJYSHJ8WipAYw5m4G9tDaChAAAAALvjnWt3dk0AuGJdzZ8uwIae6c39x1FC1occdQ+sVJ8cHFZlVsD/Pxwll2jl +AAAABvvLN/uwwCF8AAlaAAQAAACVs7N8CF6k2yMjH07TOm62FaeqKSyWj8fOZkc5AAAAACqZt2/QUZqvMDAIMl484rL53OVXMAnkgbwtyfb8gADDv2BlVvD/DxwCBrjK +AAAABvvLtiwu8J+sAAlh4AQAAADMlTmerukQas+h7IVoZF/A7BBCCF5vZyAwWrEIAAAAAGyKpJZC1ErxPa9hT+P+MIyP4opWtwMNSqrgm+fPJfPmgYNlVvz/Axws8Fy0 +AAAABvvNru4nsphsAAlpwAQAAAArxMUtTDaaCf2r+qYokIuocGZRbk94voq5fs8CAAAAAKGUdpcQmA5XbfgqOo5lXVFtCm7QBDEfYzdsWeeZtWJ13hJmVv//ABzFIrvx +AAAABvvVi/wEwHVyAAlxoAQAAAA1Mc31II11CR+89THwkiIdUOo+5J56M+xfAVQAAAAAAHku7uE1j25+ZQiOsIK0qXLvtLiu4FOSfwTTdkc2sepnrdlnVsD/Pxtiiymp +AAAABvv1GBuQ4AFyAAl5gAQAAAD6I6k1cTOabEW8J5aTkC9aFUjYkhGfGtMkjQYAAAAAAH1BZGHNu5m3KylxAprolNIPy7xZckagbEmEtVyqkc90/uVnVvD/DxtgcpHl +AAAABvxzSJnBXjFyAAmBYAQAAAClpIiUqJ1rD53m1vm3Rrr5VcTcjiVYk9ArJgYAAAAAAHpMLwnJjauu577mycRpgrDAfPJRTc2HJnPfeWWO+k+6sAFoVvz/Axvs+0nN +AAAABv5sCpKDVvFzAAmJQAQAAAAmZoVffHKxWH2sJGyw2niKqT5GYXnmevd7MgIAAAAAAAjZyObBnHcLdyS6IBnGb0ZK2/LkqTddWvxAPe2C+C2/r3poVv//ABvRmJc/ +AAAABwUGfl3Xdwh8AAmRIAQAAABtXsIf645wh9a0A9JnshYbhtes+wYWXCNyZgAAAAAAAMhuKiyJYmFmVLNEbb5au8akWp7kZMDLbIHaEkL06kPSW3h1VhW0ABuTAK8L +AAAABw43te4WgZ7IAAmZAAQAAABHropK27GZG3Toi+rWRlwSX3GoE3OkOZoMrAAAAAAAAF6sraSWwkCwtb9L39Rs0EI/LUGgYWs4+RmIS73RP+s+ERaDVtCEABu1nT1y +AAAABxoYIkRGL33DAAmg4AQAAADmesNU8i+BHZ5NZhITHjT27OCqVniOV/aTfgAAAAAAAPTSPevbsinUXeGfXiOJJpeEWexsOSmS2XeU1dez1eVTheSKVlUrOBp13qgx +AAAABziw6HldBf6SAAmowAQAAABinzEWdyypP85FK5uFV0p9XocmOUSxzQqYKgAAAAAAAKpOea7r32sxwuEx+V3noNAkGCJRkBrLUBXILEDR5G1Vr3SWVvcpIxq8bOhJ +AAAAB0rOaL/o3r0uAAmwoAQAAAA9RlKvC1SZpLILBM1rmmmvDahYvBQ1xoL9O2gAAAAAAOemGWBvmuNs1KJ+b7UAnqvwlxfVaPgtQxQCgKTBD2mQFEydVukzXxzRV+Ms +AAAAB0rOfeQa9s4KAAm4gAQAAABq4HUXUJHvAFHIcgv5Ne9KKVuWrGx6jZJiYj43AAAAAA3x2/pKDIRtRglw/VHZM4JkUOG/c+w5hE9XLMDAh/paInedVvrMFxwGcFB6 +AAAAB0rO0rhza3BDAAnAYAQAAACkxyVqtuvDg/UwKt0n8Y0TR3dNLcZlGwZEvPcAAAAAAKVNVin3T86FBQsosbRMexsAzu7RdUhZTnSPfgpcsEEC5sGdVj7zBRxNwfxi +AAAAB0rQJgnx7MM9AAnIQAQAAAA7BAd2O6RR49v+OqJ8S52UbGTFtzrh0DgcpxICAAAAAEY/q2ITHUbsBeuxORNrUXY/tix1SDo3R6oIcAvNKaV5CCCeVs98ARwMNdG9 +AAAAB0rVc1GzQhLKAAnQIAQAAADtjOl2jXOAQgI7Ckb0Mi9XhOihzF3udo/oNqgAAAAAABns6GFcRwGeG9J8ETJJXpbS5KGKYRpzLEzVPGmgikYU1oOeVsAzXxt68K1h +AAAAB0rqqHC4l2icAAnYAAQAAAAOLqo//8QiNR9zoAtB8pAyuzYWlkGYHBtsP0EAAAAAALuUHHCzCVRLnCWCxtM871a6vLM/dEp7yV58+i7MAspzbByfVvDMFxuD8cU3 +AAAAB0s/fOzN7MfFAAnf4AQAAABIAjB6UCiIBOze1KpPbQFTKqsQYSISdBA2IwgAAAAAAO4E0nVJOqquxVkquYO/HXuYCW4+qPn2jZWV5QCvIToHUTugVjzzBRtdkUie +AAAAB0ySzt0jQlQoAAnnwAQAAAC/0wgYE2cUmRR3YdoU41HQHWIBP6I1P3oeqQAAAAAAACh/kAcT0YKwxUY59V3pWO64KWv6jDgTRDrsAsokPVmQldmgVs98ARs0j/7C +AAAAB1GlmoYOjeIrAAnvoAQAAAA1RrDBbYyH9Dq/0V5AYn+6wyeCI28PlXZlzQAAAAAAABVNHSz97NSH5OGIsNoEwGhGHwxaH7ta7WyI1Bdt/MC9BgyiVsAzXxp+r0si +AAAAB1rxZi24R2gLAAn3gAQAAADbKAg0cHS1YFamGXpgogIUU+kD2kQedOmZQGcAAAAAAG9rq6ZfZ08L37+LnRMvQupATSxiYOsLhpvvpka0aD7fd+umVobgQxw0cnc5 +AAAAB1rxg71QYycNAAn/YAQAAABxNtJRuCJb8vu4hnuSSURm0G605vmFrgFAdMQwAAAAACxftz+vxz1zfAJrRfhVPUReT06zhagCwAda7dM8AicQAw6nViH4EBxkds3g +AAAAB1rx+rgqyGFMAAoHQAQAAADLNvRIEQUQTmsbRmiDJufhjPjtnusSGq2VUC8AAAAAACjp0z2GC2Z+wTns+pZgjxu1YdHr5JJ50QTGMhNo1tFJ1SOnVgg+BByE1C+d +AAAAB1rz1qOwWu/rAAoPIAQAAAA2OWwCCR9tCl3dlGCpAqs5uDxhubitfqfi01cAAAAAABaC3HwT+pZVTSTSsVufNKNBUToHLpgZTqhsJMinZsfxQDqnVoIPARxEcp7L +AAAAB1r7RlHGpUIEAAoXAAQAAAAyYuncwvJRuoTlydanvcsCkc030G59iL4oCk4AAAAAANuTn3IXFbJ3GmdG3Tpq6HDmTfGxD08AcnHfwCNs2T0YhVCnVoDgQxsD1pGT +AAAAB1sZBQofzopqAAoe4AQAAABC8WIhrCV3NH4NXh9ZRc3EgSSnzn6fCdhGEyEAAAAAABOthKXl9edFLcPUZQjyoIcCNQ8ZkD8sOxvMt2wIXYt6B26nViD4EBux7Do2 +AAAAB1uF3cdktl7UAAomwAQAAACYIRxhHEbh761sJmz3hjsDtm127qzFBvzpmhkAAAAAAFfDbtt9fJx1aZ6nGS+36yvk4p60q4FeVy2JjIMG4yhrOsWnVsD/PxyLJIal +AAAAB1uF/UGEMH5OAAouoAQAAACMf5AeDzNEaP0tFVi543FBgrx1Wk+NVUhXnkwAAAAAAAmonKSCdIGOy5zCBrAhchUmGJyo7uH6sT/QRW5zSumjM9+nVvD/DxzvI/gA +AAAAB1uGe3ICYPx+AAo2gAQAAABW1UDCo/rrkhREuE7d5rl+AO7R2mETUvHdehMAAAAAALOhHTERVinHQTGIMWi0cyu/Q6IZ4d0bMKUBt07aE+cmx/anVvz/AxxWJ8vv +AAAAB1uIdDP7IvU+AAo+YAQAAAA4BzWDt97sxa42ow6Je9hgV8MnU06Wt3vH1FQAAAAAADpMoeqMZMX9tfz82QbRyW1xVkrkEUqw14c4D+Aj5e1paw6oVv//ABz1RbRh +AAAAB1uQVzveKtg+AApGQAQAAABP57vI9kreAs3ZiC+RcbRkYfZNeukYSNAGe2sAAAAAAF6ItKd/eC46I8nyFj65gP3F1fKTRNxwTaFa2IWreJuH2CSoVsD/Pxuxuo73 +AAAAB1uv41tqSmQ+AApOIAQAAACCB2dj7DPeUWiLHu5ronY3GgZcM5A+joWpDzgAAAAAADDfbuI5R6GkH2iAxuxpe0Z8JClANYTZEtm2M63dmuhf60KoVvD/Dxtk5syI +AAAAB1wi1IJbcVTyAApWAAQAAADVzGhz/9Nwj/zv8dwCGhj56YZzznn7A4tZQggAAAAAACHjR5BwNBJE4kZE8F6bODu5loa8hDlnGnPvuABnRXaFW5uoVvz/AxszS1fE +AAAAB12lV93ezNbMAApd4AAAADDsTduHG5L4jqlQE5+cLYgyJLjHP3PN3l5H0wMAAAAAAEQNJ86WFpNt8y5M9hzqQC6dUn0xouqMiqimIKczPmOTrCurVv//ABsmVZ36 +AAAAB2ToX8Dmr96vAAplwAAAADDOS55fvrmCgBsjDyGkx10KC8zbOHrVqDDjAAAAAAAAAAEnBI6dANrGVKXFZksSWbhNDOjljFcIbi3yLk9Dbbw8XoGsVsD/PxrCkjsr +AAAAB2+IEldtUhlfAAptoAQAAAAZbkID2Dmw3MSyDGDSeDogBP2i+wzVf/iPIQAAAAAAAFhFb8GKwJ2HoXHEL+DPcaUQQvobBaTKnLpQebSaAU3Y1dmyVj0AFhp1QCUX +AAAAB4DwoqLZPNOZAAp1gAQAAAAdcjcKQAPCjb/mKuzCfep5rEgDJkYx7ePmlnwAAAAAAN67euhYO8duiy8wiSfHc2xcOHjWsKe3g5sdj9HINN3hDT6+VkGeAB3UXO5K +AAAAB4Dwr1CCVdw1AAp9YAQAAAClBZaa44EGWK1Kc9PPtFUXL6OLwnAEJvcLobpaAAAAAPMlFK5Wrsm7TvgNyf1SUPH5YcyRZOJpEi+MfPZX9con3Fi+VkCQJxwlb0ga +AAAAB4Dw4lit9iKuAAqFQAQAAACwfeCzuTokah4QMArq8GheBVb6yvz/5ibCd14AAAAAADX4Bw+xTByZ8yBklQQ5HSqdrNckKY5FJmQiCXxyj8jY9m2+VhDkCRwODr1T +AAAAB4DxrnlcdzyUAAqNIAQAAADhmjo42bv7MotszDOQsJS811LowYlb+Cf+iekAAAAAAPEcW2KL1Cj+Y0k4hc3AInnSKhkElg975OvYifTJIMA+2YK+VgR5AhyXoxkg +AAAAB4D03vwWe7PsAAqVAAQAAACsN30y/tDkgV78X0RiaRU075NiFIOFQ2LpoZEAAAAAAMSAcU5mdQfT9ykUT/i+mD2TRZQ5ORdFj/RLOBBzSjyY1pa+VkGeABzWh+Pu +AAAAB4EBoQb+jaEMAAqc4AQAAAAitZMLW/2Wazm8+LTt35vpEH2B4hcykKZ4eUcAAAAAAM8v7Azlc33KKWU5cu8WPK3RnRD1XxbmpCW+2LKCz8YUU7G+VkCQJxv723rF +AAAAB4Ey3g+8FvTtAAqkwAQAAAA87cyPH6EcDxZJKK7xfJOKtehIPMCpE1+e/QsAAAAAADa82YLfPH7WW6zWE/Oma/vWWmsrIURC8w2Ca+UwIUevnuW+VhDkCRubM13G +AAAAB4HpDxj23ppqAAqsoAQAAAA5Bm/LV82PRVXOtwOMifI+NbSHmDe2MTzdWAEAAAAAAJ50jEFiFc93BI7oK04FrzpEcu+aeconSu4vbZmrOlnfso2/VgR5AhsAnH1W +AAAAB4R3z3RnK/h0AAq0gAQAAACKozatNCpSRciftamCa1cFYDY4nWu1ngDXKQAAAAAAAHrZahDvch9TFthh5XYWy1E+ZtEY5kR1zeu8BspYO3mQs8HAVkGeABsJdNxm +AAAAB4p4mPv58PHNAAq8YAQAAACX6b5cxtMVp4VY0vzM6bhLIFJjE9eKK6gH4uYAAAAAAP8Pqi4pH15B/zZ//hSYMyNuavXKshXhted6KRK9PyB69onEVsD/Pxy198is +AAAAB4p4uGoZXxE7AArEQAQAAACMRIN+X8LF1jRQ8j0xGuW1TazMNMUP9SlE3PoAAAAAAE/Btbsz2j71y0kUBVZ0yTLcqfNmRtpveLZcAjSEoWPyLajEVvD/DxydVqDf +AAAAB4p5NpqXj49rAArMIAQAAAC9/Bm+RL8TJkza/44RYV5ol5Srxrbaz++NxRkAAAAAAGzhYcyqXyISzGMwlNEbhr6MIsXAi/+2jSgfjGzkey8Esr7EVvz/AxwCXFXU +AAAAB4p7L1yQUYgrAArUAAQAAAA5TEfr0g9tR2HyBahsmMDdQl0ExZNghE1EzwYAAAAAAKmMWcwojkByjPiQ9lo6ySjlheZGNLvb8W0bHlcmym06I9fEVv//ABznVUNf +AAAAB4qDEmRzWWsrAArb4AQAAABoBUf+dIlZiRQ7C0HYmwLMiOPHGlseaj1qhdAAAAAAAOnqa0WTGFETmSC7Z+u1I5XQ1/cHgC1gO+DVzVc7BAQmje/EVsD/Pxtp0lip +AAAAB4qiDqdvnGdPAArjwAQAAADa0UwajTHV6yT9ECTiEWI4pSwIeCtT7WOumj0AAAAAAPbCIp3+LjzULjOJKF0gHtVlCZpI0E5irYTFM6eIP7HWER3FVvD/DxvnIOI/ +AAAAB4sf3ytAIDdVAArroAQAAAB3EWkl4rdHWRSMgUYygZP+49WmIWD4eTgARQMAAAAAAHOs6iyAJknWUDjsuilRul/+z9ebtFzgQ0AC3erwwn3yFPnFVvz/Axss0eOm +AAAAB40YoSQCGPdWAArzgAQAAADsUoyHHkqcn1oA7m5zroErr+ed5OPNMDc5dQEAAAAAAOHYDjOWwnggcFLFo+1xHSB3NmgJ5/4z/0cWuDdTrxdrysrIVv//ABsZ2iEv +AAAAB5T7qQcJ+/85AAr7YAQAAAAqaAhOVk26OJobY6rWMoYll6jCrnnIIcaohwAAAAAAAF0iMfxyV1OoMLdTDv1kswZIk31UeBzi7ywS3WBhP1tB3xvKVsD/Pxol2joz +AAAAB7OtnATiD6E8AAsDQAQAAAArEdPlbur2VAq5m5+sUXszr5y9NVmXskGmLgAAAAAAAEQ/QAKoTbEjOIE6YmQ7d+ZbvwJyxJiqD5ZmplGOhPSqIqPRVt8LGhpJ9/fe +AAAAB/x1QcsVFgAVAAsLIAQAAADh/wqxUEgjOVH3K2J1mJSvXNK2uoxqm09UDAAAAAAAAAEr3HwB/h3h8V4jEAcKbIQxpLQdRiAgdletLxbWXA81K9HcVqXFDxr8iPFc +AAAACGvw3FlNjtyTAAsTAAQAAACZiSbV3Q5XHYrVgKljOEmcOPMpKiM3TvxmDwAAAAAAANwEtv6sKp13rsvN79dhBom8PHZJwdLNzk0ozx2ADu8rUePqVp8EDBpYRE9u +AAAACKuYXf3Zgp4kAAsa4AQAAAAlX7OBapUHtRwVZLmd4IY1mBT8sayKuyv+ExIAAAAAABMSCj8JLavoSWgc0XQIHQDFr80V5gcEzRvRYBV3Hrs+DSr0VvSAAB1VDku7 +AAAACKuYbZ4fRayTAAsiwAQAAAAFDtAQQchnQwGOhPgKrTPBxDuRwd0CBhbCgroAAAAAAAYTubfQ81qfcbKpckUq7sEWiwqk3+yRFOAT/cWsEXc7qj30VgA9IBwRWFey +AAAACKuYrD69GM3PAAsqoAQAAABwCOwgtu/Yx1LA8KCzoccVqXUSUvkcSzaLqbcAAAAAANIa5ObvXkWA4xdHLbJ4mhMfZVnPTZAQHe9dLhp+aC1Ai0v0VkAPCBw1+Zs4 +AAAACKuZpsE0ZWJ9AAsygAQAAAAiR3j8RDKqWKGiI7btKoFUPuSutApUI9ti3EcAAAAAALkWeqgJgPYCT3B02+CiEWp0uiZDwFCTMHuAu+/eFwtxulr0VtADAhxa3yA+ +AAAACKudkMsRl7U3AAs6YAQAAADKZ1fQO775nmNeW0JabholgliFw1MOoS0AkVIAAAAAABe7zq6Jbq0swc5xbwJ1u19PnpN0xqS+qCPfAkqnUKqjhWr0VvSAABzND2A8 +AAAACKutOPKGYQ/fAAtCQAQAAAD1cbwMFToQxcm0gjUMbwoFeNxLWICz2cDL+18AAAAAAJaHNnOo6MBVF39b8C4SSmonM4M62jbG7Ca7llI281TCvX/0VgA9IBsxHks8 +AAAACKvmZKiDp4wQAAtKIAQAAADzX5tI9xzWMo3btsN+cild+qgni6awBsxpegoAAAAAABpqKLTpNLBQw7TX0qxYgLBL4stUitcN/ejmAhwylYdLecP0VkAPCBtT4Ofs +AAAACKzUACDsx+whAAtSAAQAAAAB0wp8Xx80vlVgeocr2phgqu+M2ViXxLguIQcAAAAAAE7aE51S+BuFi2dO2vCZcB9GzyjazOXRbvp3eb2a2p+IX1b1VtADAhvLMkuV +AAAACK9OyPuRh2JZAAtZ4AQAAAAc3uEgoFzGDGR13i7VsSUpTxZtP/TzjJ0J0WgAAAAAAKqPCYmDfGsA4+WiqcjqEYygnF9EWOXLuSCJNeEy+lXNo5j3VsD/PxwD+gRh +AAAACK9O6ISxEIHiAAthwAQAAACEqKYoGAmxQ6eFa1PsoOvChIHQS0GkaETv3dUIAAAAAOJBuhtOxhOO2ystwiiS2bBHSUzzrJL2Yyp0y0vmxa/sqq73VvD/DxxTVqCl +AAAACK9PZrUvQQASAAtpoAQAAACNANkXtOGN8BI8y1UpVZgL582wg5dYO7kN5MoKAAAAAG5eRh/0EaS//DFmYEL4u87/EdbSbtW9A6og3MPHFfXXnML3Vvz/AxwsiEhj +AAAACK9RX3coAvjSAAtxgAQAAADlkN4QNFN7eMMLDBh5Vz3aGYczbwNUOqoepWYAAAAAAFnsYnDMVAK6rJgOcupue0vKuMlWIxJvIIS5gSBbYfJk5df3Vv//AByv+7Ke +AAAACK9ZPYQGD9bXAAt5YAQAAACJmtDsIDi6C/F6G7DvUg5kaawKlZfopncWUosAAAAAAOngvGu2lBCVov5N+tj3fNjx6400HtApvGQW1M4wpgN64hH5VsD/PxszGwGM +AAAACK93qepydkMfAAuBQAQAAACH2gQg/bowILKZv+5PCT5AV88PqxwxmePwIxgAAAAAAOrco+kve80uB22jkyoCIHLhiFs5/QRis8JkCcm1GQkEucH7VvD/Dxu+Otxk +AAAACK/qGxjjpLPbAAuJIAQAAADc7zhT2l8UAVa+k7Ww9d05ZKkM0Lz1q8Ih+AsAAAAAAJeGEA6dSCjpt9x4IwABsCmlUafBL8SdEGfm8Uv7coENuEr8Vvz/AxtK2Kf2 +AAAACLGEHi3mubVXAAuRAAQAAACVEClKPbm6UPuobs5tzE4Bd5EoQQGvshn99AMAAAAAAKm8cak7jQ/KZ4nfmafEaxswv3S79OYU4Xi0ICsUJq6G9Xb9Vv//ABvcSPPg +AAAACLckJhDunL06AAuY4AQAAACYhNoOB/z+352Xoc7zyd3KlKVc5QIqiS2EEQAAAAAAAMjfIT2Fap+yj8xcKjjsOrXIj2APFO0Lgfa/5VWYce5uIjj/VsD/PxqoGbTF +AAAACMaYOYsCFtC0AAugwAQAAAA08Q5xLoOz+ZqnrpOK4ZWIew4c9SCdJaCKIAAAAAAAAF9T8MwaIZT1E9HMXLFkYsxAo1rmixxRepOZrT3g8jp8FvwCV/D/DxreFhIl +AAAACPEGNo1/Qb1QAAuooAQAAAB/KBPt0n4YaGRcdLbzNs9v+/fh3nKehI8hBwAAAAAAANLP3ZP9FS7vVobJIopxaGtP8DPjqi1vGqiSXhyBtEhEwm4JV9SWBRrroiWf +AAAACWYJS4k3IaxaAAuwgAEAACCzYQaUr8LEcQZVx8Bz1uzGVEW3wkHmylb2eQ8AAAAAAPGFt5+tv7IJkLJFLNfSbLYUZ+HiBnWNFD0YVCjm0nRrE7MRV0unchyAQEpc +AAAACWYJXSVEspESAAu4YAEAACCKznfJdiFSxDTHv8etLmOkyVQxHP0FjBdNmxkAAAAAACEFjGzom7jg7ARQnUpODZNXigWxrKuOjSauoPhvLzOOm74RV9KpHBwSGLlL +AAAACWYJo5V8z6NfAAvAQAAAACDGs6z5IrLwlGGoVZRYhJhNLt7mslxjgdSCBl0IAAAAALDH/xOnFRMLOWs9jIxQfv7bymHHdKPbkFrz8wRNvhbmzMsRV3QqBxxbIAUF +AAAACWYKvVZw4snyAAvIIAAAACDv2j20gm/S2B1ZVJh7v0ssWL8JsBRqeKalclkBAAAAAPL6s4/L+hEywOenVMRTkKbE8l5uoYOBAIY0RUzJ/X6tHc0RV53KARyxlDbs +AAAACWYPJFpBL3veAAvQAAAAACDkj0YZKGbmtCz5wJSq98rv0Ltt6cu/vbdyv2QAAAAAAEzRbMN6g8p+xgiL2IEpWrDbYnEInbGQa4vSiDX68YeIQ9ARV0CnchvdI6kD +AAAACWYgwGmCYlsrAAvX4AAAACDpnqj3lUtGrP5LP8ndLjhixGYi1kq7Ynlr8z0AAAAAAG136eohQv+Lge68NT44D9BQU2iLBvUUofAOQsQZvcGRwd8RV9CpHBvYLX5e +AAAACWZnMKaHLdhfAAvfwAAAACBjYpK9J0jSZYy3CJfi7lq437Udtk5CnzptUQYAAAAAACDoIHb3yAMNdXMGD8wlUq+PBKymOPeXFikCi7UGNf3zH/4RV3QqBxs3bQVi +AAAACWeAzeH6+JxBAAvnoAAAACD3KqlH7Hne7Pv2UFYViW412zgmcZSy4Z9wwQEAAAAAAKomOfH4vcp/slYt7WJWNqIUW0mbcu7W9j2RyMxyp8W0hFwSV53KARsNIYcl +AAAACWvn0bJHsHdlAAvvgAAAACAzoCFXF4K81xl1M8urldGMwhY8WUs0UTLC3wAAAAAAAMoYUfklDxD+TAiQe6e5Spboq+ik3UW/wBRoDxzASGy4FGYSV0CnchobfxHc +AAAACX2D4PN6j+vVAAv3YAAAACDAaEKufE2D8z6+rUycPfr8N9GBhQu/0i2uCgAAAAAAAEU8UzfRjnKWjKTF083b6hvVZcdEFDFX2IveEvaHa8DW9n4SV9CpHBptMaht +AAAACcP0HfhGDcV3AAv/QAAAACAPBvCD08dCs8O6cY5m/iqbaIeupdnzr1sqCQAAAAAAAOjKI/rLkZZ8lU11SBjFm27ilJYQ9ix7XcKgnspIFR/py6ASV3QqBxoaybnr +AAAAClTyf7EiS3p1AAwHIAAAACCjF6rahGnCOcCiLtMB2oyL2JhmEi59/+4GqQ8AAAAAALwKgFaIVAZOCu6vq6o7B+MmEP4dn4KBUhavRwDsvuTs8dchV4rTAB3gD+0T +AAAAClTyiTsDUh+IAAwPAAAAACB7xPWsoy7gsqJDCaVkBxMUuUWy/bzCveUVQy0AAAAAAPrNV7jNZJ/W1g1tIEvX6KfJYtEZuRnt3tALfhhxZxUyye4hV4DiNBz/dQCW +AAAAClTyr2hp37YGAAwW4AAAACCg8bwGI0AldwIgY++/lZJxZC2Svkzgg7vJK/wAAAAAAOSuKhwhakNG9hc3rgBgsnY3uNUtvcViibZOGAo9jN+VYvohV6A4DRwkHXKq +AAAAClTzSB4EFh+/AAwewAAAACDN6l9XG23YeYT6vVNhjiDb1kYnYZdLM3wOaUIBAAAAAKI9cjQVScSAi9OaQCJx+ZhEuI18/I0L1R39LjRMun44LQYiVyhOAxwAvbqM +AAAAClT1qvRs795BAAwmoAAAACAN+1NOs7lzykXHrSsVkskW/twY7bvN6CB4mmIAAAAAAIYM86jV2KF8gu/DJSeFBw43faVZHNqQgb7lgkAMPhnTHRIiV4rTABwnwauY +AAAAClT/Nk4QVuAqAAwugAAAACB3GHAMSU9qWj1XrIHcsj/s9ECOJAP+89vpwiMAAAAAAOI66i9yhvdWEwOtTuk/PmcEAtASUW8WXWoth1Lcm+vqCh8iV4DiNBt3qu30 +AAAAClUlY7Sd8veNAAw2YAAAACDbagFYiOn3qONtcYDfPi3jNvqVvsTRtkOKZRMAAAAAAAC5UrlB0xMPyyXpjTa7GgNvzskV8p9vSZ/ujrwFWp7w6zciV6A4DRuQbOl5 +AAAAClWuN+3MH2fKAAw+QAAAACDe1jWdK4j4ezhkfjCiw1dLptipbUuMXSlHyAcAAAAAAFZFBYlkys7FZJY2GB6vuAvwDGua3xZ0nLH21D01DOnDg3QiVyhOAxu7/mxd +AAAAClfQUJJCSgbPAAxGIAAAACBrUhWSnecvj8p9HAgfQY+hLr2ArzIzggv5KwMAAAAAACz7iGfsgWnovO+Sl7AE1YAH5yDQdPFqZbNb0O5MeGUzMBUjV4rTABtj8a98 +AAAACl+0G5gNuk6rAAxOAAAAACDINoYXT9lri93GoJdoABHoUmlM8qZdZcAbAAAAAAAAABmSIfxzGMOpj1JZ70FHgDh2eXPUM+bJxgyCUlZCIeh2p3EkV4DiNBoIjRSp +AAAACnNUyIxpCuAIAAxV4AAAACCg4+ZfFsis6/HCy3/biFTihvYMGZNTsQkdHgAAAAAAADtkVW7joxXh03DvCAjyzFO6vzVdOZAESTThqx+Wy9iWdsonV6A4DRrF4jMg +AAAACqiUQLJN63tlAAxdwAAAACA2q6dFKIMdf/hbcP3OV3AG9N5XQdu6ZuCaxwwAAAAAADraVYXyVnQMl8/LgZFitYJL/rwOZnuVCogZjkDXzN33sHotV1MoTxzrGJpJ +AAAACqiUWh/C2Fa9AAxloAAAACAZVxk6MxUZ0tHybd4ugHVItfbvtJRLJJiDQBQAAAAAABcKg2kHWMqqFFMVYDUXwVruuTD2nrwUb/nZQHKUv5zFf5AtVxTKExwwJj1g +AAAACqiUwCYHV8zQAAxtgAAAACBXcsgySOMX4ZtWuU1pm2T6YlHAEHF1vKE1QREAAAAAAGSlph9XS/aU7XibOH64lPCrn02HMoKwQL4HTX8hkUWWbpstV4XyBByrdQqs +AAAACqiWWD8Zf4sMAAx1YAAAACAfIcbaB7vAFOI+a9zN5GEHuolJkK/hziv7FZsAAAAAABGpAhLjc8bz2gKfgdjNYhNlvouMqXsXzycDNaHW4Hc9T6MtV6E8ARz7XP0R +AAAACqicuKSra3eLAAx9QAAAACAsBQs/5zsqIDbEvgp7RRq0vVXzrVQs3m1QvhIAAAAAAMredFd9Rdt8wWDftNVWEor7Xcw2AZ25Cb1/+RxGMonvoKQtV0AoTxvEQBPt +AAAACqi2OjrzGzFmAAyFIAAAACAIPa1HQtSmPj91nVzgBUhJDWVwVcZf6wx2YwkAAAAAAAsVNuhO7KZe2S34YbK5PGKww/sJ81KHZ66G16yaoDnMGr0tVxDKExuMNxFa +AAAACqkcQJQR2hjTAAyNAAAAACAlCKe2uviHefp415fmmIMIX/62aD9nd0nF/gsAAAAAALEyONuUQmEC5aGyfTzhzfraLHhaQF3YxulJ4573HMq7YeYtV4TyBBshQ9uk +AAAACqq0WfiM1b5pAAyU4AAAACAafs8WDFpKdD+pOxMmDa8xACwDFzxBuyj05wAAAAAAABJOMBPHz2tTDkZqd6tKFD9UPQoKauhHBruME5q1qqQdozkuV6E8ARvWuUmS +AAAACrEUv4p4xGxeAAycwAAAACBM6y89ybcMlgL2VZnYtlCziv/qxiODN4C8NgAAAAAAAER2b2MNzwO0lf/JqBo7lUWym5gHN/CIYv4iMqZGhdwBAS8vV0AoTxrGHGVs +AAAACsp8dnlp3ZsDAAykoAAAACAKwrgX4MJHd7cVHdmAmuRrNn+xPYK6rtxeDQ8AAAAAAMZs93uFRAyMOEWJznQhVkAY8wfWLSWTFQra7J+BxyaoDNQ0Vz5KThwKHPEh +AAAACsp8kENUKqH1AAysgAAAACApiEkBVlBVUXQ4vsPhEaKtUnRaq4KAeVf9+B8AAAAAAGBimX2njLYGnWEvfSDM15kryfiGaWbef9GrLBQd0ctX/+U0V4+SExwRRYcg +AAAACsp8917rqb9+AAy0YAIAACAzKRdYgFySze7wQhGMFU/WCoHlhcXqNbVg+SAAAAAAAKnAgog4fNoLrYSaPyy+J95FiN3d8QDavakEczHG//31rgI1V6PkBByTx0YD +AAAACsp+k/3avoNhAAy8QAAAACCFfY7+VAAh/h6w7nJZxOVgoQcVIo02CZTYewYAAAAAABY4PvNoMhCcjK5Z00Xgfq1tA4uIXNTkKa/eMlw+K1VmSxA1Vyg5ARxW45DU +AAAACsqFBn2I/u3dAAzEIAAAACAJO+lcds3UZ49FkLqD4LrGplyD1DIy6tXyITUAAAAAACRZQ7F2/UPxsMOE8ruPrnGrf8y5AwZ6WsBYxrfUH6g01yA1VwBKThspuomb +AAAACsqe0HxCAJ+uAAzMAAAAACAdDM7lMkIHgKR3P9knznj7cbX374TEoPM/qwUAAAAAAPQ6Xz9PhbVfKO77q9Txn90luKwnV2WM+SwdVd3SJCpB2jU1V4CSExvAb5lD +AAAACssF62O8rg4bAAzT4AAAACB8M9paVrpbd3x+HwdSgEkEbJSGuxzMp+zJSBAAAAAAAK78eXK2LczK9u840YOrWQyN5s0TZJUy+YjlJqzBouAiuEk1V6DkBBsj+jIM +AAAACsyii09MyWomAAzbwAAAACBhrMn+7gVWqPtCghQSiEd9TZJvYb5B3qzKVAEAAAAAADxS/avVrpFTzjq315y8Simh1bPyCAUiyEp7v2YBdmr0ZFI1Vyg5ARsQny8w +AAAACtMVCv2NNvHwAAzjoAAAACAQbWj+9G6K6FHtLcwhUI0aowYCjDNvvXVPwwAAAAAAAAJGlqbm4lE4UJ2CQilA8Vt4IDbxVcADfoNW5mTe7s8XpF01VwBKThparCuB +AAAACulSADKEnlLxAAzrgAAAACAfGmvgiXm1EJKwqwj5nmOIuhTKpkls2IEFHgAAAAAAAAcPiZeYVoVIsiBsPy/R1mvdXupA2KWxn1Hd5yftqxqE4KQ2V4CSExpZYJdg +AAAACwiJvSPzDmoRAAzzYAAAACDzOqdKLeaQU6WSL3dP7BYOQD7eC7Uw+Avc5tAAAAAAAFIUOhwpiR7VcGjaI0Q08Y61+mO6sBZAWx8g+tBGG6PsNKQ9V6BUYRxF2Ofu +AAAACwiJ0doyLIRTAAz7QAAAACAmq7lxKvcWVdBN4IpDdPYvYCr3msYs86IBJZwGAAAAAEWqykCERMc/hXcKmBy5yj+aEu0E57lgXE1TImKH3npiYbc9VyhVGBwm2xxG +AAAACwiKJNPJQBxRAA0DIAAAACA9gN9n+gxkPtvrXhPrwljwPzaNmwxVVImzJskAAAAAAEFr3B61lUkPSNo8yrhVFyA/p81nK99GWM4YW6oR4PgYRsE9V0oVBhx/f3y4 +AAAACwiLcLolxewRAA0LAAAAACCXgdxFqoOFdVP23xjp4VuvYjZcCeLpGJyt6REAAAAAAPj0f9Bu0VqhngnmtUE4a8QMN+DeSTq1cp8cY35zsvYB5Mw9V1KFARxZhbYA +AAAACwiQoFVLfCWIAA0S4AAAACAhrYUteje8uhYcTLEWi+wvDQ1AB9/c9E9iEfMAAAAAAL28qwO/GsbqNPnNh5jDjLYl6Iatm+dbVMZCTSEH3uMJcto9V4BUYRskrTAM +AAAACwilXsHiVRsiAA0awAAAACDFf5es1goMe5OAW/4usRfn341elajC/atcRS4AAAAAAEOj7r6rfZawmEmGObiDVV5srIdpYo1as0SA85mpU7mIb+w9VyBVGBuY4DRR +AAAACwj0m2hcUgM+AA0ioAAAACAo0lKKB3ibys/Dv5DnCHmsmfVAszkjoomI7Q8AAAAAAFl+dLXSJAHqsZ6yJWiM5IPzK8gOc0J69j7pCwsNUFILGgY+V0gVBhu/JeAn +AAAACwomiZpepJyDAA0qgAAAACBlsz10UHGMxpXNItRUS7FdDrBZ+LNWKUmkLAEAAAAAAMlbuzcrB27NUBsPwi6ai7An0HpHOoyI29FLuKZ5ykrZlGk+V1KFARvdFqfQ +AAAACw7vkTGcJJ1MAA0yYAAAACCBJLdPsYqFkAIcpQ2OTUp2NqC0nsW69+hYTAAAAAAAABuZtQf8cBhKGCMYFzZ+kBkNrD4L94dX7uoof9ZEod14o4Y/V4BUYRp+VfZC +AAAACyHUjbhtHeH4AA06QAQAAADvnEzXU2v9aPnold6L/nbBALi09T9WmbxgLwAAAAAAAInKh/Kit9lSL3SK2HRJ/r2cCpdnewVCuWuw4zqV1BRNibJDVyBVGBpSMm47 +AAAACzNgivvKJFXbAA1CIAAAACC1fZvk4wj6HcSk+Aed82Eli224tbA/CtH/CwAAAAAAAFF4VOZr3f3Be0of0zejnBiNm6v3lvOUAiiPFShN2eHFKf9WV2JxGRpQqjjg +AAAAC0d9N59AwaTRAA1KAAAAACAsA52Girsag3GL4Ns6hkbBRTFO/dicvpaiBgAAAAAAAKGxSjF2+e3aON9cYhx0e/oHNxbxbIRtXEonDYBVXW5TeNFhV9jqDhok1tLt +AAAAC2d+0CAKRv37AA1R4AAAACD5ke6bUV656OLmS2C9d+Ii/zkQiYJuSX9VDAAAAAAAAMqtHOMVi5WOF1wadcxZqTpr4txxcQXQViyFg7WoZFDGqZJuV/5OChqMrEIr +AAAAC6fobOt3YlKqAA1ZwAAAACBV30euQTM5kg0p/6e7NS9/HxwF80scEdIwmzkAAAAAAFRUhjO2BzX/TD4MUlWJsdZHhhKtqQrPauYIVfSnIEFZz7B1V7+4YhyzAz7J +AAAAC6fogV1w+/YdAA1hoAAAACAmLnNpH2dMW4TzFqp/A2LtSlB0xNPo5Dk0c4gfAAAAANvhlQYd9i2G32ypw2+ypBHbAh+bVADr5CSAsAPXv3rz4PN1Vy+uGBx1P5CG +AAAAC6fo0yu5QTWLAA1pgAAAACBLDWxPmJBk5G7Lzg4bSltWW7/x4qd2q9KTL6oYAAAAAGarKFL0xpqeabWoiR6pj//U8yjz/rIRCUz9XzWn3NlVb/d1V4srBhy41Q4H +AAAAC6fqGmUCWTbaAA1xYAAAACCt1qYUX39xOtd2H22yWiBXBWqJvudc9vx0JuIFAAAAAHQFCnOXvMvtaWZfSag4rTecLh/WTMz9Ni9B9MrFOshsefl1V+KKARy229eW +AAAAC6fvN0yh38yJAA15QAAAACAAAS+r4dhhnTUIWVPJFcePDyFzeQlMMmuGPSIAAAAAAD3wSVovvE8CnxfpSnZgB2SlGfmN1MOKEwMEiecBiHs3wf11V4C4YhtFk1xR +AAAAC6gDqusf+jMGAA2BIAAAACAKyVIqn1fcD05VhUTsUxl4er1JOLQqRdYfMisAAAAAADCXBftPyG7TDWGPy5SC+526LyZN40C2+iGiNA/8YRtPcQd2VyCuGBtHdZAB +AAAAC6hVeWUYY+SZAA2JAAAAACAquT5NFiWNPL9VhBG9JSMjQQNLdbVRjHQyMRcAAAAAAF9LChYZd+1GDaAm0iGqYsLTMTp9CdqPG8utrNytNz982xN2V4grBhu8rusA +AAAAC6mcs0z6CrqkAA2Q4AAAACC6n20QXE8+GeKh628OkHoavjtFuful3owVQwEAAAAAANp4z6iDsqBLj5ZfNGAHxgSSEDj8of4x7NmJS/VXOJJrejl2V+KKARux+xUE +AAAAC665muyAphqxAA2YwAAAACBWTKjG8JhkoDZN++qkIZk9WwIwVq9WFHV3RgEAAAAAAFwrx5Px427sgiRsqedZd4r5Pa8k1IlAwTdWIERGeiMdd0d2V4C4Yhoh0hZW +AAAAC8MtOWqbE6qmAA2goAQAAACSrvjG4XiPddKS2qT7BbjQH8vT7DAtz3Y5JAAAAAAAAFKRhxOtEuQLTG86Fxh+puIAezWHyEMUH1m/73bh/N1twJZ3VyCuGBpevjoT +AAAADBObB49KZpemAA2ogAAAACBp8lxnn9BultC+5ENLAJIl6bG8R/KzGtsjEwAAAAAAAIYKZNtpjJ+CqbXx4Cxn1smxLMLXdgfopuQpRNqaYsme6Gp6V4grBhqOGFwp +AAAADKG+eFoC/G5gAA2wYAAAAGCNeTilJobqdvLKRNYF8Mz1EPfKtsQiDEF6AQAAAAAAAIE6Yx7EeGGgM829XxFJ9eAIt3qX6MgNlhWTYi/ro2hT531+V+KKARqSfAtI +AAAADrk8D0lbn77YAA24QAAAACDRApmVyVfAnUH76eDhAuPF++Lc36DjdBMrCxcAAAAAAIBd2pRJ/jQVmFPCRVx8vLgfsqaWQg4ImrJd/u99S5LcRIGZV///AB0dKszD +AAAADrk8Fyxjgsa7AA3AIAAAADAJq9Rt70GCwIzFOKGtOi3aDGiHBGMp2dT6tBUAAAAAACpuBExz4ENprDpUdYuAsiSgzH1ZJQ4KilDgwZlTCbCgT7uZV8D/PxyhBEJk +AAAADrk8NriDDuZHAA3IAAAAADCZH856VE1BGiD12cdt4NmjE9RDCGLxabi6vgoAAAAAAPie4AXBNxLSCyYCCt7ErCHHsQkzlYXi8FSQ+zf6/LB1n7yZV/D/DxzPVkWm +AAAADrk8tOkBP2R3AA3P4AAAADBllXxbXovWrc28z6uOnfRyI70usegvXPDaCwsAAAAAAKzAOx7IjWpUiBEpyBDDI71RFAQr5NDEXzYN7YY0KDEbusaZV/z/AxyKY55a +AAAADrk+rar6AV03AA3XwAAAADCUck5TvvRlQEsLvs1fq53zlF2JyOIb7kKvoQwAAAAAAHlKt/5WfhW3WzVE9o9NnqFeVlelsCBGhKEP6zKQN3U7Wu2ZV///ABxRqiTQ +AAAADrlGkLLdCUA3AA3foAAAADCpnk5uDQgtq6xioQH4MlKwffjPhr9SBIzM0GkAAAAAADVlHBcb1hKkjIKZ9HUZSldpWevAv2cl2oHnYj87TGwiqu6ZV8D/PxsjfddK +AAAADrlmHNJpKMw3AA3ngAAAADBTOzBBlxMQtLKn/nBPP8t46bX7OtxbX2j3xxkAAAAAAOMjSxmLHM6CK5czL/ViX6maYLZmLMoSvHPSVaNEpGDX8AOaV/D/DxtcSU4U +AAAADrnkTVCZpvw3AA3vYAAAADCV1SlW+D9yPT/B18Emi67ZOojR9HjbIc50OA0AAAAAAHvQOMjeADF8AjKFuJe0zfOhYIQjppdn87ERjWfzO6I5LTeaV/z/AxtIT0LA +AAAADrvdD0lbn7w4AA33QAAAADCLn3AV5sh3vbyELB8ikNrYMIa1cv91UsKHPwMAAAAAAPnDFPWhae6UBSmU34MVQYYbIZl2ougp4uBVfKSBPXAc7+2aV///ABudVN1h +AAAADsPAFyxjgsQbAA3/IAAAADDRrs54sIAOMyYkS6KzIX3eN2frg2Suqq1y3AAAAAAAADoJf/ShL8NF6mzUBKdIt/SllfTgjfeiyO08fH3kZgRdyBCdV8D/PxqJ25ED +AAAADuNMNriDDuOnAA4HAAAAACApqFfwSeTlbhtcBC/ycVJWZMQqsI0iNxP7AgAAAAAAAG7KEirKHr1/SPn4ZLT+nb3UTrm98+oWY1ywVDLJ03bdi7+fV/D/DxpBqWaI +AAAAD2BQrXlgpguWAA4O4AAAACBO6cgvrBNqFQL2Fb2dgfCaiYgwDWT/diOQAQAAAAAAADCj6yID0P2rPA4JO4UVYlbh7il5VJrZ/Px3N0H1XnExI/ynV6sdBxo8UBxL +AAAAEEJvSeNfseUBAA4WwAAAACCUty2OZD135FtFfuM5+s6KO0ooyKFhl+R+AAAAAAAAAEDupwwP+ihkzyOZcl9j4QH/KCaet4NiNx7FWxasDxBKW7S5V1DUBhp2t+yS +AAAAEWZON940iQTDAA4eoAAAACCTqwQ/ZTyaMklDST2ZR79yvI0BV3+nldS0BQAAAAAAAKBkz9sK+MEQ3oYlxPKoio78AMMVj2r5BEiYRhjomIC5eFi/V+8TAhpBVQyD +AAAAFExt2wOanLhVAA4mgAAAADDOgW8QhETnVAMmuwp4WP3M9eIoiNbmOsudAAAAAAAAAEmC3eDrRbEPwI/cBAFMlbIc4XSc+5xfdaXNM+wqW611DzjRV/ACAhrie5OZ +AAAAFtE60M28lfB3AA4uYAAAACCaMvcGJ5duBEGiuSuFbxyV21K+OS20HwIYsc6jAAAAAHXoGMtj9AEm10IMjWbGHJcthL9C5749MXSNVnlDPWjN0GnnV///AB0ibsDK +AAAAFtE62LDEePhaAA42QAAAACCD2XeXqwYyOFut0/Bw4J230XxOe8dU+iccjk5XAAAAABLcCaK5nEDzeR+0VVqR5QmwWJecqHcCuJWlvLlCfstjnm7nV8D/PxwUcmvk +AAAAFtE6+DzkBRfmAA4+IAAAACD+p9SxJT1v/58zLCK66MMEfj7VITR0wUD2YtU5AAAAACLyMqj435DNbBe5x4yYNsEr7eOy2SLmfTmAfk1kL5Hg6njnV/D/DxwO0YHR +AAAAFtE7dm1iNZYWAA5GAAAAACAmXrHDvr9BclgR+3cPgCZcjeFxeGlc1VkVywEAAAAAABOVjKBC3EcVYA+8Zkh466kzt8Ro8LhhiPHUwqIT/Kc9j4vnV/z/Axwh7DoQ +AAAAFtE9by9a947WAA5N4AAAACBbXt5nncW8mu/CDOtfZe8mqHxsdRkDCZMa4AYAAAAAAM1mc/zfDzOVsR0tQ3NXfSI0MwRRiH7KsV8qg/aH+VxQl6fnV///ABwoxarL +AAAAFtFFUjc9/3HWAA5VwAAAACAZRKfTiu/adJ1EvNiq9noHimMyMAFMWq8aqAAAAAAAAPjzuuE7t/EiD4vOLHgs0kppqK83s16zaMul3CzSQFA4O8bnV8D/PxvJTn1J +AAAAFtFk3lbKHv3WAA5doAAAACBK4iv8mu+RYlZ0j+TlLY4pYSEvWMFjSVfQhwAAAAAAAFs66rCfYvUumiTXyqqlc2I0bX/lP7PcmYgDSEuL1vEEq+XnV/D/DxvKhjzi +AAAAFtHjDtT6nS3WAA5lgAAAACCNZUYcP+lBC2xmxqiC/o62X5Rj1nw0HOdRMgMAAAAAANKp6yqkAA42E9C0y2nq1COWx+VPQkmVCCG7F3DsQyX9JgXoV/z/AxsJZn2u +AAAAFtPb0M28le3XAA5tYAAAACAIeaBQ51vhvWNZGrLyld8/ubURRw5vfoWS9QAAAAAAAMZf7GAbrkzJOqd6goDTCvELuoveaD2tbuVtYo0tlKs5ZTnoV///ABtkjcWM +AAAAFtu+2LDEePW6AA51QAAAACDthu7A1465am24IoOs3TQpAaVCuRhuQshTQwAAAAAAAIGGSZjgvyvLeypwcHuQhRkL2EEO2bllTY/MBWdbUFR2yPXoV8D/PxqfQu01 +AAAAFvZm9JHgWhGbAA59IAAAACBCrF8KJWrOhnH5xSSV6TDFMA19LvSZtkV4Ak8AAAAAAE/F+F7/yAKr+bGyL/KByzwlIQG23GlHEE9iz8vPGu/SnGrtV8D/PxwPC+FQ +AAAAFvZnD/37xi0HAA6FAAAAACBk38W9b6I07UNqdOYxc4Y0KAiFB/kNBtx+QMYvAAAAAHj7r3rJqK8yQmHzLo1Anru+u2LgidBd3MQlwKFJ5R+zPnTtV/D/DxyU2LPj +AAAAFvZnjNV4naneAA6M4AAAACCts/sPg/7gvjr045kHsVMWTr1lNuqRfAAzhwQAAAAAAIekU6P/7aPz+z9neNBna5u6EIihC/4xSfTI6DfRjjMM+4TtV/z/AxyVbXQd +AAAAFvZphZdxX6KeAA6UwAAAACB///tI8UDgtFCtwc16eSADS4zHoOv8WhybWgIAAAAAAOmNdM2v7Z140jUnjL0TAfFt7WZcORVfh88q0JbcZZIdCpPtV///AByR477o +AAAAFvZxaJ9UZ4WeAA6coAAAACA35kkLMA8srR3Ly7phHOQNV8XB7Ydz0ibQpnwAAAAAAIHRkgLNQqTLL2rPrQganu7cXd8EZHOk+sMRRDjbDWk85KDtV8D/PxvIErHP +AAAAFvaOuUulE9YtAA6kgAAAACC7mkp5UCwhDmm0tP2IQbL6pILaCdV6p4P2WAIAAAAAAI070Qh7ITtTUH4mIvzHd7CM1mt10pBOwf/TFSkMoC/VBLHtV/D/DxspgEGE +AAAAFvcCWmhGMHbWAA6sYAAAACBAJHjxyGNNy1Uz5BMe4xROSDDJE6y257wRUQEAAAAAAAp5ACLSkVL6bfOLFZ3AzMnnUUKYSKrngn3hU6tnzCB/esztV/z/Axvld4Mp +AAAAFvjC3QnI0fe4AA60QAAAACC8glNQvr9l2H94E346yBCP8pWddMfK3k8HygEAAAAAAHgygNfgRBo/5rOQ3MdPcD1Th48lpOl3tTEWqqJJLs2gofXtV///ABvDepcW +AAAAFv+g5OzQtP+bAA68IAAAACAstVirUpNdahbB8xXA333A/QW0StRdUKYRHAAAAAAAAFk9boB2W8MhaBFMGlGwRrKBgFxeFZV2h1d6++Zduw3MIY/uV8D/Pxp2zSpc +AAAAFxYY/anpchhYAA7EAAAAACCsVo/dfrgBLlSrtl9VxGhsjaDvtMrh+PeDDAAAAAAAAPp4Ty1aaYXxrO4Bwug+gHTDUa6yWmHwHrPA1Q68QzrKYEjwV/D/Dxoa09c2 +AAAAF0opNlwiJFEKAA7L4AAAACDdlwmqgXh97cMuUST/SpkqPVeIU6HrDB1WCQAAAAAAABMtBumhPQQyqiLmeWH+PimZ1chF9K/t8UZQIkqgY//UfrP0V/z/Axp+wlqY +AAAAF8PpthapYv43AA7TwAAAACCe7tZ+py5b17SGVln9UhIKp3bA+7FNubvPx30AAAAAAKYRNzQJxAaX5+/PTNgqXtXU6IgGaN00LA4S7GbqX2id2Lz9V24lfhwW02WK +AAAAF8PpxgV5Pm7CAA7boAAAACCs9HWUfNPttzeHA5GhvZ6yrnMvhZCMirb+UQIAAAAAAMPA/zFACA5MO18V7ppOTJrUoASmIJSwoiwfXVvJ97/6uOH9V1uJHxztGdFh +AAAAF8PqBgrXJ3DXAA7jgAAAAGAGkbS6J3x4hjksG5GIEpUU5VR2BKleBJeaPwEAAAAAAOBqhLFcrTGyPd3ngDLuTx5YbzrWuF7ClO7wdwnQulpjJef9V1biBxwZcbO3 +AAAAF8PrBiBnPGVrAA7rYAAAADA66PNSRnC3EnCeNHIxoOu3TJ/GviIfxjV3w+gGAAAAAOqkKmorYOEV+txAyBHUKfYqQLoHKeYr4pXqXMw8ygAjtO39V5X4ARw5mS/l +AAAAF8PvBneq5dl+AA7zQAAAAGC5hJxX5UuQj2aiZkninwe6Uw7l2ENwZdWtQfMBAAAAAMXBRS7u22PKI29+WDFcg8nWB0UMGhF0nxNWPffCSZyqvfP9V0AlfhtfelJk +AAAAF8P/B9S5i6nLAA77IAAAACBWO9GezKda9BKZijDxwoPRS0I5edd31FwnpikAAAAAAL2miQQQxBlvJnA/wNvGJE3SI8h9XRuEB74ahGuj5IbQM/X9V1CJHxv2yCMG +AAAAF8Q3CIScgcYRAA8DAAAAACACsa5w6mo1Mqg0uqXJON/u0lvfrh9eniM2AwoAAAAAAMPDIu9T6pV42XROMrJ9yJEe/Io6jq8uBUvsqh/D9RC1//b9V1TiBxv6KC3q +AAAAF8UU4F7L3YA0AA8K4AAAACCaYKPHLjjZon6Oi+EmOmpfBSS2TqaTULKiwQMAAAAAANZu5dm8cf1duw1nlbaUkAtpQj4/FS6vZ+S8mhC1DhDONBb+V5X4ARvDPj1j +AAAAF8jEjQ58YN7MAA8SwAAAACApg+5CF+1Y5UemFhe9k4du+q0izkCHfr+JvQEAAAAAAC+EDBqL5htuN2/AWmqMgcIw+PZcy98L+WrFUaz3TluKRWj+V0AlfhqekWsN +AAAAF9JUBdIhwHQCAA8aoAAAACC9gPaJOxyR4sB3wh9YH2Ia4QBAybIbH0IgEwAAAAAAALQ+kaXBe64LG+7UgI1yXLDmSyzkpNWXsx16FumpHwyvtfIAWFCJHxocqACl +AAAAF+lhi3Gksa/UAA8igAAAACB86iJZcNX2IAbFy2yMZpnlrMRDLa71SamQzlQAAAAAALRl8xi1PiSa9WETIipKRa8+e4txik6arEL9thVB7WHTo9wGWM3MUhwG+C/P +AAAAF+lho4q/+yv3AA8qYAAAACDIco19HaOHr7zNGpdXGkKTxZwWc2t+haT+Fg0gAAAAAJogmWxqJA8zuYYV4fLaUydrKMqAlYck3cmS+amF4iwDJd4GWDOzFBzv+LiQ +AAAAF+lh+hxYrih7AA8yQAAAACBC6fvFDly3JV0wvnjYHineSRy5E/eIHBgCVucAAAAAAMa1lmT1LxSNqozTDJA/vPohJZ6BR8iQGFqE5/XtPQNikQQHWMwsBRwA4SU7 +AAAAF+ljgEGu9rWiAA86IAAAACDpxl2OioVVC+wy4iphmeQP3FED8mU2PJwW+IEAAAAAAOgfKGICFqGxzPrfN4k/JJn3a4RlrKbiSmJjVoTNzOpb+B4HWDNLARwvsUiR +AAAAF+lpmBIoIuaMAA9CAAAAACB5zCaM4cCKtVpws707EbhDUxJdQ/uTdS4jrdcAAAAAAOkLl8ZFyJBRakfvGUI9mfI5CsUL1O4siQfuhw74uZrbvUsHWMDMUhsB+ANy +AAAAF+mB+meMq8DhAA9J4AAAACCBGpsuJKZdyRJFdHAO/PLALcqTMeOH2BX2bQcAAAAAABg4VF0GRbSmkHNkdlQWBkKievJ8wSkN8SqGY4yUpFKyZXEHWDCzFBudWkbK +AAAAF+njg70ezzn1AA9RwAAAACBYn3dlFRyQsal+immbQoZmRXQ+hCkZPQliSggAAAAAANr+/lIrGBNucTK1kIatfBwvyH4YmaQULm1odwY0gc0DB4oHWMwsBRsbD0Fa +AAAAF+tpqRNnXS4DAA9ZoAAAACBLXmur35fmqFGvX4r3v2Q6yvPKn97pAdrRUQIAAAAAANvyrTNEtsDcU+N8qZDyCHI38kzlaWmOae6c+neSarvMAasHWDNLARvQ+xUX +AAAAF/GCPmyJlP48AA9hgAAAACDs0MIYI5o2DQ043OoopmBgtuAfH2uZmIeaYAAAAAAAAJFv4eW7pIEuLhpZ6b7+8X0AZ9G1CBf5u1L+TBQJMzOiFx0IWMDMUhpZFAuP +AAAAGAnkk9ESdEcCAA9pYAAAACB026HT6hrHIkBHtnXeboqIbegioESw6gp3FAAAAAAAAMgGwys8SvhXtPP9CfD+ml976MBcqHUpcU1qd0dn+31e80MJWDCzFBpbPkZe +AAAAGGthi2TFtzsyAA9xQAAAACC43QNyh32I6SXPjgM8oRqRS13eCsnrBB0QBAAAAAAAAArYC/EIVg1qGGitevzJVw8HBCInCYbG1Xd9QKaeTzRoi8IMWMwsBRoZplIi +AAAAGZkGN3lTQJ/lAA95IAAAACATtsQd2aTtsKLXSkjk5Hk6/MusGv+3HrYQBHB8AAAAAAmvcIZ7Q9FkGrD7PKh/zs0VzBPak6EXMoe1oBucTpbUpNQcWOreAB0XQD6e +AAAAGZkGQIf+mdUvAA+BAAAAACAkgJi3I/FvgEIFaZi+aSsKPP1C4D5BMaA49F8FAAAAAPzpD/LpbGdNo5mPhoZcNK83D6OjIreudArh9cMN/j1rwNkcWIC6NxwuYvrf +AAAAGZkGZMKr/roWAA+I4AAAACD+abxT5sqlUd7qM6FdyeTeL2ZQJW1lOyyfX8ouAAAAAIyGUUO9Dbd9W2GaIYeSIQjXqhW/j89Wad5c1vCtOD5n1vAcWKDuDRyOEX/T +AAAAGZkG9a1hklWUAA+QwAAAACA/7/8wlF+gxKK+7RFjRvro3l8JQ7nF8Mx2m2cMAAAAAChT0omCty82fYKc4K1fGvtCxoojkLgCE5ReJ3ZJyc1pHwIdWKh7AxzXf+OY +AAAAGZkJOVg34NsrAA+YoAAAACBwuTrewrflGxRKewrcD3eKAPpY3tnVMk5Jb3wAAAAAAPvbSIldgbB6ev2yiWTRSEY8obrnMdopntprNQ1dyOMFPxMdWOreABxcogTM +AAAAGZkSSAORGwFHAA+ggAAAACCq1MaMa4HznUaIgg354gEE55MSnNKKbKLV3NUAAAAAALEPsa6l7rUYga/8qt9KP3MVGN52bH5gpdg6GtZui2/2PCcdWIC6NxsDb3Dr +AAAAGZk2grD2A6l1AA+oYAAAACA6fUrFYsoXvsqZhFWxr3IGcSC2v6xs85RNdQ0AAAAAABKW/xfbjD8iVoooAhvZFnQM2pTxgOxd/s3OR/2V2H6H0kYdWKDuDRtswT5e +AAAAGZnHbWaJpkowAA+wQAAAACB/VoYrep9hhXpaxwMonpIffw8TS3G6j7drCQQAAAAAABpfFemaOXaii6UsgRCcy1sd71Nuds5ARo+dW4vfq8LiTHYdWKh7AxuP49Z1 +AAAAGZwLGDzYMOS6AA+4IAAAACCM9l7CkyNeaoIqqCN3G59f822JNUzUOGxoDgIAAAAAAJHVX5wYoU/IPJfYm5/I46Y1eqbCfVqb6u8QLrtowWODB2seWOreABsp/dna +AAAAGaUZw5YSW1bEAA/AAAAAACDaM5JbH3pV6fqObJVaIOoJQUi2DFyI9ppPUAAAAAAAADZzt7bOgVfTz8r0FbZ0CRjfdhCodp1wM0qpq9nJQbJediEhWIC6NxqFv5ZG +AAAAGcZ4RBWmZoIhAA/H4AAAACD7dSLpdOhvBPclOGqTozRk5hfsIEMu5UaHDgAAAAAAAEnUJI7uxqAaJUUUVFpVa73CPIGZEF9YG/HttxhVjsrmQ5gsWAWPIhp1jB4B +AAAAGfihfGG8+LnwAA/PwAQAAADiHEuHWW1ZklGivlmZYj9znwwnJqCpGNhIDwAAAAAAAJGmS2QaFRA3YvIsnxSmTfWV0M65rUAfZjllMm2qjGoRHOw4WEgUFxps1ZL0 +AAAAGkXX1sPIJ3OPAA/XoAAAACCek5gdfB/WFZhCZOq3nyDEsM+hKBgkW7BMIanqAAAAALOTZeH2dUvQl0uFTfxVlThOWIl5oDcNNKi9j4oytxmmbflDWEqZAB0fgqwl +AAAAGkXX4++biFOZAA/fgAAAACBiUROpJ3OT6A5Smhk6fzCSo8vV7eTMAmBakX8fAAAAABHvJGkRac3nayOOezSoK+oPIJrfbLcSfvhwaZa7Scpgkv9DWIBSJhziOhXX +AAAAGkXYGJ7pC9PBAA/nYAAAACCXcyqGu8QbOLMCKX6SYjQCjt/NvYfWBsbgTmcJAAAAAGRTv5BKOkCYA6o2JHJGlCFzZAzbjl8EgUxGvR/R1r2WKhdEWKCUCRwsyUnq +AAAAGkXY61wfGdRiAA/vQAAAACB3vHjem5CCgl3wVyup7LvYBC/rEUs6CIxS8/MBAAAAAPnIq0Kn2+JbzUDP3JPDEVtTyhhrFbhivSVoEE1rB5z2/ydEWChlAhw85rx4 +AAAAGkXcNlD3Ud7GAA/3IAAAACCIf8w4A2520c/Ar5ek7MpCu013kfuuWcR6kTEBAAAAAKhNF2miqDEMcHcAIRP+P3EGXcgcQ4ziDmwngs3+hBGgRTZEWEqZABzmBYOd +AAAAGkXpYiRYMhA2AA//AAAAACBcHHeDBijYwSpqMLsGpTQAtUw1iRIeCEREYycAAAAAADF+30o5iPYdA59MiKDAdTFmzJ/6UpywZz7fPv87M0oFi0xEWIBSJhvq1zOt +AAAAGkYeEXHbst3WABAG4AAAACDaineze2hPHvz26SGxuFvUKTNX7vt4/6xSqBcAAAAAAM5mDPWpxNCD980RIn6cUjsvDly2sH1AeZaLPzL28knM3HNEWKCUCRt0kLku +AAAAGkbwzqfpthw3ABAOwAAAACACWljzZESvBbDsoMRUsoD2Jyj7gqnsABqeeAYAAAAAAOhxrWsyJ2sTRneS9+7dPoiX6Iv6Bn6TrruT8dT9YeAUw+NEWChlAhtUqsUl +AAAAGko7w4AhwyV6ABAWoAAAACD64JqjLPJUGSp/3WxirmjOMmudQemxVJ8uXQEAAAAAAP2T5t3me5bEPS4e9Ex0g3JXfGfetEEPNfdATZJiGHx/PYpFWEqZABsGzgKp +AAAAGldR4QMQ7gzuABAegAAAACCXh/9oenRKyDzyAxEgtjS075/tZZEWvJN4LwAAAAAAAFA/u7P5DAkZxWL5C4dVaPEYh0QbQ4/q4nKQGSTxM8tubcdIWIBSJhoazXsk +AAAAGovYC29/k4SWABAmYAAAACACWUawi42K+bgsFrbwblASqkjdGGo1zxxHCAAAAAAAAG+BEDANOWuwW1OPmwysgU6mVTxZ3wC8p7GOxppdl72iSpZNWIv5CRrQy062 +AAAAG0P0b5VrzjRpABAuQAAAACAKwk7jlqnxejDFcRR48xbevBFBPLWdC/CWBQAAAAAAAIuQqPVVcNLJ3NgVijFSv+o6dKf5mRFVvTKRPZfNiWvVhS9aWN/OBhrRqMwd +AAAAHFnawu0xI3KAABA2IAAAACBRqbwADBQD5hXoUmWQpmPAM1hfT/YGJF17AAAAAAAAAPLqZbrZAW0hWccr4q1g4F3/7Wak0ixT1EKw1xEe6SLPqJJjWGZ2Axqk1BuZ +AAAAHc5rHSnTyie6ABA+AAAAACAek3iuiZNhVjEfUrvCd9cvCc4QX7KhQFeTXcqTAAAAAFyp+8UoL+J0bou7QwwQwl8jpj+a+CAFJIy6I1G2BMujVJ94WP//AB0NnXJx +AAAAHc5rJQzbrS+dABBF4AAAACBrKKX6DTkO+luuyus6eIoPCkByHzQnlVVIOi8pAAAAAMQRPLl36/0xICZhe2YY9yzRH4P9jMyG2xnzYZyxtvh+LqR4WMD/Pxwvr9zj +AAAAHc5rRJj7OU8pABBNwAAAACAOY8dNVj7EEstbuMHIQ29oTw/OH40Q8N3XLoUeAAAAAIyBrRurXlv7W04t2g/qqgl4/on56mjlOF0KuXoju+uMAq54WPD/Dxyho9XR +AAAAHc5rwsl5ac1ZABBVoAAAACCHn+hnN4WB4z3vfuzAiBP3s8bm86Hz57GP6ksMAAAAAJiG8/ij08IlLGG8jSDECQQDRfbW27+z9kJoHYgPjdEm/MR4WPz/Axz+f0l8 +AAAAHc5tu4tyK8YZABBdgAAAACB4xfKGUihIPzceCQa34Kw4+g4wxFUbcSEa854CAAAAACYd430PGThY4atX4YHb++NgjRobesFzrSG2knSgkrvOXN54WP//AByamRFe +AAAAHc51npNVM6kZABBlYAAAACDnzORywa2t8ymDsrF7Zih+kdOT8Qiwa3hgkJcAAAAAAC+ktVs8ZvUxKKORFhciz4QXTenOchzvIwz6dJfhPXIRR+l4WMD/PxtU7bs0 +AAAAHc6VKrLhUzUZABBtQAAAACDTHvolquu7E1hE71XgTrBFe1EnFjsBmOsMAxkAAAAAAJ1LG3XCfZtHFzU+xbYmaK5xcYMRtfltTFGiYGYgTv5ecPB4WPD/Dxu+bXS/ +AAAAHc8TWzER0WUZABB1IAAAACCoCYvhDpRgX+3m8UNxdtgsyKPJT3m7RyIg3woAAAAAANxonmhJUvxi37cUelXwYyCTp1b3/z08Dk8AtPSQAkHO2/F4WPz/Axs8zcOl +AAAAHdEMHSnTyiUaABB9AAAAACDzal9DKSej0nIQYFgNG4g9Oxjmix1I4mGQsQAAAAAAAGPII6IBXdPyM8UlYzw/FTZg6/Y8W/jzbNKnKl+Mctxifmt5WP//ABsEOFzt +AAAAHdjvJQzbrSz9ABCE4AAAACD8Gf5FSQZtRieF0awUNB7lU9rU6Cmb9TRtegAAAAAAAJz7hY1McEpRsNPsD5Kh4OUtWN/70PGZuiLS8IBsmGxMfVZ6WMD/PxqXFVKC +AAAAHffpsqbJ7WVpABCMwAAAACCZ0YrKD/kpvaCAOLj+Kd5i+rWePiw6IcpmLgAAAAAAADpmcHP0ry6tHXS9I+g1ERf0hFvNJ0mUDt8crZh9QkqBIXV/WBa9ERreS5MY +AAAAHmTB5It+3bGRABCUoAAAACDNthdKnuL/dBvbA2MptX3zKAmNcgChCqFMCwAAAAAAAEZEKNlVRoApTxmjU+yrmKp4po72oj25wLsdQtGT7yeEewCKWGwhChppfo+n +AAAAHti41lG+Zi1rABCcgAAAACBybByxX89GCyyTxo0f9nU3dFV5VhlrnzBDAQAAAAAAAEAeJTpXr85JyqupGsUF1DCIaCuC3YDWOBdUm5n7YBNAz4ChWOzlDBrYFSNo +AAAAH0DCOAJqJTzGABCkYAAAACDVq9KpdY9aY5Q1MIvVo7nAGMxgrLMJVE2wAAAAAAAAAGsPBLwFeIb0DAuM7pZGYZq1qr0z30rxAcOCVZZnIPbZ5Yi2WBOxDhppY+az +AAAAH8UGWcJNE6aAABCsQAAAACBxo+l2U4TYTD+CzsJIgmB4ogMnVSr2mSYKBAAAAAAAACGcdEEpnl88Q9EY/02H2vLpcyURxXKTYSfSGkeSViJ8BeG+WN2iBhoXHhrb +AAAAIKtMIeYpK0sdABC0IAAAACAlmUQ961o/SOEdm66LlU+ZY0k96LcNhcvPBQAAAAAAAO8sFj2o5/Al4HzxSNf+Y0aptZpbghNoqZukygbWhUBFEjLPWM/dBRrOI+Uw +AAAAIb9GJJ7Eh8OfABC8AAAAACD4PTHZ47A6GIyYVtgPyadxNhWBFAznIAL4kAEAAAAAAIR/JRPqFJjHwyoiezZX5hhqe4K6TPGODkyU/ft/v1JBV6XaWM+eAB09ZvnU +AAAAIb9GMVVnFAKlABDD4AAAACDPLPfONf4BCJ8xtZSgwlRl9nZn0EGGFwY6VnSTAAAAALE5+w7G8lL548+kMKvHqVX1OGhxsxlCAKF2OegO6+heQLPaWMCzJxwFQaXd +AAAAIb9GZC/xRQadABDLwAAAACC1fOy+QGcbEP+SKZ2k1ATR7lDe54lKjl5uTj4AAAAAAECT5eYsQV/y68n5g2tQ80CR0PBboX+qNDn+8mDNhuvvRMPaWPDsCRyRxp/n +AAAAIb9HL5oaCR5eABDToAAAACCNSLWKT2aBym4x2phPiSclub0cRKpK0XfTjGEAAAAAACv/LfaFKIuzcPDF+IFJ5jnZfPbb2+z7PxkZ3TvqpCrdvdPaWDx7AhwKz9dM +AAAAIb9KXUK9GY0hABDbgAAAACB7tyk0/iYDQOn7D8Ho/BKG9RLn/oV4ibJ6lw0AAAAAAFpYzaB9r8ra2xfoD8KhXZWT1hB+U8RD/tfOuzGKzuW9yeTaWM+eABxbT2AU +AAAAIb9XE+VJW1AMABDjYAAAACALEewDqb24gcmpo0zKZfpEx1uMkw/dh7R1NQwAAAAAAJnzKZIC4cj1Ow7xLdlSNNj+bMk5NklJXs6crz1bkP7GMfbaWMCzJxtWJv2G +AAAAIb+J7m96Ylu5ABDrQAAAACAWXDe75vVY5VVUDzzxWj7lv3gm1theWYnGeiQAAAAAACOY80B9IzcTuW3+MpXebBGfwGTabO+epQOTpqFgZcHDBw/bWPDsCRu0RaLk +AAAAIcBVWJg+fpJOABDzIAAAACCLpDRR+IKAjplttiT4SFozH1F2EWKqdlMXPQkAAAAAAGlgiZE2aUAyqkJQ+PdnxNsV/affmXQgJ/aQktiQNR2sczDbWDx7Ahtqji8C +AAAAIcODATtO73xjABD7AAAAACD44Vi+G+EjwnytJSxf05dilDfH8iLpj0TAwAAAAAAAAAmv9DHV2sI5LsyWSP1ykwYtvAcLj10szRkDvFkXuP6p3K/bWM+eABsFky/D +AAAAIdAkrxU3MLYDABEC4AAAACC18u2eZ8+XkDLz4x0777KJsItl2w3B5+SrFgAAAAAAANIubrNAIphXooj4B86Z8wP/KBZuRxsbNUYLSw56LXO68XzfWMCzJxqrjOCi +AAAAIgEvLEZXMYaOABEKwAAAACD0y9QLo99p/HQgSVeS68LYPbhSjLalzlcnJAAAAAAAANKO3hwsA1VpU2nHPpLsQm1Me7BxUAsXTS9Gy87n4YjSWaDoWE2KExp9RJAC +AAAAIl4yRQnMPS/jABESoAAAACAWs+1qrq5EXdPfVbFdgwExvp9QanLXPT+BBgAAAAAAANJvDKKJbnRSH0T8W57g5uO/9wz2OFao0lLn55LXrFRjBSD1WAE4DRprh+KX +AAAAIteysNc7nEzeABEagAAAACB68civgdnSZEHo3oWaKH7/Gu6MHP+c88Q8BQAAAAAAAPjQHcd2czgltmo0osB/PueJS6J0fv8NuFLXES4nYRSkxeUFWXUADBrQa/dG +AAAAI0UZ9ZK4KYikABEiYAAAACD6m6dcXg1Uf1BgpEcs9ayJ9n7T8VyAJJd4AAAAAAAAAEYvkKF77XgEuuauv/oi7Ag1VWWraiTScMj2LDuSIGBvsLMXWQ+XCxrTP3EO +AAAAI+r4GI9tbC9qABEqQAAAACBOXhuDCJ5BuUZiTeWlcAJUZX+IUom7GBbXAwAAAAAAAAWcd82WhOyn8yAseY/HM0f5mlSEX8W0I/KOVIeslu18K1YfWaHKBBpVYQLY +AAAAJOI87cDGNMqOABEyIAAAACDcBjCks0vM4Qr9HSk7bl8BfNb6rT6jE6VRAgAAAAAAAK5tVFqb5EEvt4R59IbJr2ZO5ezGuSi7VJ2Sai+aGU1hr5o2WS0KBhqWZD1p +AAAAJfiPkz00UafSABE6AAAAACDVl+Kq3oZJ1ML2mnpxB4MpaO99Hgfm1vjjCYCWAAAAAKvHSZfL2encbK8ygyREFo7htEj9LMyeGR8Lue2SO6wnaAlGWQvWAB3IAjXS +AAAAJfiPnKv4yyKWABFB4AAAACAwzcyYlI6v6ils+88OV/qNmhs++ruD3zS3rLxiAAAAAPEiwVglcd6yzhQxQzKQxVA35Kp0eMe8w6ZRKNngkWuHNS1GWcCCNRxQ418L +AAAAJfiPwmcKsRWGABFJwAAAACAU9G7Cszf2caZJtfMP80FTw5LlXsLTG5PAGEYdAAAAAGXDFTnTgaVbK6OoB7VhHVlbDQHyMJHjO6P5VyJWGzPsPHlGWbBgDRyexdBp +AAAAJfiQWVNSSOklABFRoAAAACCGWj9JAC1BHHEun18pyqcGrVK28QSglt8psxgEAAAAADkaWQECNJ/RJlwLGsDEfeYTRs8mfxr+bLdxzDkbB7dFTI1GWSxYAxwNdOFg +AAAAJfiStLjlC09RABFZgAAAACA2CIjnbXcDQagjeppsEJ6WSzwCfSMnkI64vwIDAAAAAP5MUgbB04l0q4EohUxDRbDfWbEqVkDXB6PAzS9NkOKjwpdGWQvWABwmxdKA +AAAAJficI31eiKDtABFhYAAAACCl/NdE1m77Aq9TVbu8a7L4On6LPNlPE9n3uWkAAAAAAFDzedInrn0IXofQRX6qyd3z7WNKqtnhKYkHwRCOfywHh5xGWcCCNRsidXuW +AAAAJfjB3o9Eff77ABFpQAAAACD6dEfoCSLjOOjjVOjegIvRenfike5AQkTsEQQAAAAAAOX0y5+W1C11WSBsQzBfnPTEVpaq47g3rSOdiYHLKLP0O6ZGWbBgDRvJcJYP +AAAAJflYkXEl6D9eABFxIAAAACBSUCZPRJayt/E5gRp+fCIFM0bl3qTrIvNzfAAAAAAAAF+qwK6aEbba1HCJeKM5ksBZAZ0p5cPO66meAfRSUNbkLNhGWSxYAxtJ/6D6 +AAAAJfu0Qo+FPj+7ABF5AAAAACCeorbURAPAWKfeTe5nXZOHnbvtKiTA+fCtKQEAAAAAALq691c4t4yjFJFAy/rQIMKJmpDpfMkbl+vfTsmYH/WrNypHWQvWABuApynS +AAAAJgUjBwkClkkRABGA4AAAACBQqNvTldL28auu4mat9plkSxtuixVx5pQ1NAAAAAAAANK4E9YdqK9PK5+2b2qz0LCX0kwWY5t+3QUJqlXpJw6vvxlJWcCCNRocSwSv +AAAAJiUktlkpz6ExABGIwAAAACAb2u7EyLFpNtW8NDAnAg56uYi1IroiXA0PMgAAAAAAAMjNDVO86E8YzofVZ9Xpm5kASNy8VE6bIE4L0VWrJBRAz/1TWReSHxoYzg1n +AAAAJmUACmqBJoJgABGQoAAAACAy4+Hcp7OXeuEIrblE5tu9vWBcGkeYLduMHgAAAAAAAEXsYB8budqg7vLn70pOh8hXNXYKG7AI2Cq+k7JlxXX2PipYWYXkBxqe1vuF +AAAAJzuE2uHgBEpiABGYgAAAACARZaUcm39ahb1U9xJo46JRb+gn0M1MmckmAAAAAAAAAC9f5JwkWDxQ9EsDVP6/IDZiAmt5sF+z0b2lHoijQSsGQp5oWccHBxp8iAue +AAAAKCNh63Gt+dTeABGgYAAAACBJSOgDgMi5ccn8aNS1xJMEjVuzCCwKgmp0AgAAAAAAAGdGivFyrmbHKM+jvTew4K43kzrBF0gPHc9X60yTF6S3cJV5WTl1BhrmKDrs +AAAAKQq6H988JY1iABGoQAAAACBcg+xuB+sT+qBi2cCq5ZPA6Tz3XWSSToSflAIAAAAAABPUlyG58Dp40s+srO6T204fGYgiUqFzB99aBbIxafCBnHCNWf//AB1xXdeL +AAAAKQq6J8JECJVFABGwIAAAACDo8E3IaE685EYCQnwiQYq5kzmG4opGZ9r7VgEAAAAAAARdDy/FITqcWuk3WiRBogLxQCJ9ksYI51e17gTmnM2lyYuNWcD/PxxHWHGH +AAAAKQq6R05jlLTRABG4AAAAACBN9xbUgqvBlxz0Rin3+8+g4hDdyCJxgGw8tAMAAAAAALzoCgaI1tr6VtC35HRFXE7Ss8bOb1e6hi8HDc1m0DF9n6qNWfD/DxyqI+kY +AAAAKQq6xX7hxTMBABG/4AAAACAM2vTrSTC1jZopRug9S0JdfgBJtrE4JFxj4AQAAAAAAIVIo498ZiPi6I29wB87PvS/Mb2LgALV0KR6racwFiC22MiNWfz/AxzEWqxl +AAAAKQq8vkDahyvBABHHwAAAACDZNlIPofCU5/JyFUn8d7o1K9G3Kso8nrfkWAIAAAAAAHu0zkNbOIuwf/lVrGYPuxe09c6tVI0KTWAB+A8dHWCd0eaNWf//ABxAW52O +AAAAKQrEoEm8kA3CABHPoAAAACC93qw1oijYSDyX6iP7uGcEm0leEM7+2XVogfUAAAAAAEp0sCVD5C4j352D5RT80udxPAotEg9dMzrIiIdPAaVZAQSOWcD/PxtwdqLr +AAAAKQrkLGlIr5nCABHXgAAAACD9tm3xnNXWnojAYSMHrmRJS8ek9wqB7Lo7NQEAAAAAAJHiBXPe8XkL8T0x2OMp+51rpsU79r2n5wNy5WTOxv/DnweOWfD/Dxs2MEmv +AAAAKQtiTOhpLrnDABHfYAAAACBB8QqBrZ3sNQ0nnwb63+PXvqYMZ3OUOIxNNQsAAAAAAKQn7PDBJXOPfkMe80rKIcRhylftkubG8bazibZF8RRKDBGOWfz/Axu56leF +AAAAKQ1bDuErJ3nEABHnQAAAACA2yjXoEwpAO+/RahQytaJbCAj6MIfN5zkf+gIAAAAAAF3rvg+D+er26Ah8g7Tj0OycU+3AD7XEx7KHbcGnr5c+6z2OWf//ABsCIBFL +AAAAKRU+FsQzCoGnABHvIAAAACDX0FjdPG5cbCDskkpHfvK/g5cqZVnuPpTH0gAAAAAAAI5iTjOj25WevLsOGpopFfCriUnc8ZSNiJM7NriSY6G0IaOOWcD/PxqD3fxk +AAAAKTTKNlBSlqEzABH3AAAAACCf7A6c/Htass+5YkLu+0kLSZdKqNm0gUEDNgAAAAAAAKLB4LyZizGXfUh0IuglaV70JsWTb2aLqqxBr61yt5gcKPePWfD/DxqvHYjk +AAAAKbDgZxMawdexABH+4AAAACAKfLMVOcg7Rn1+atkyJ5D8UvGS5syBk7XeBwAAAAAAAAm4pj/iZ4Y8LpUunrgf0dHD7d/FZ7sCfYCOrAxJ4KdXhdWXWRbKBhoZUVzs +AAAAKjYGgQfdfDB5ABIGwAAAACAyZtFWnJIxA1FOqlwG5mZHrgzeKvfAYjSDMn3qAAAAAFfHZunHo5xgovA+d3C87DQTT9a5ubLUPGDJ6avFPdV90DizWf//AB1j1VSQ +AAAAKjYGiOrlXzhcABIOoAAAACBp4jkCudMCYmZX/HU+hMd7mhL5yNRw093g/GgAAAAAAHuJWZec9EwCDvv2PoH5SLODHkh0T2ZNNwXH4THJ7rF77j6zWcD/PxwFF87R +AAAAKjYGqHcE61foABIWgAAAACBpoDx4iuUQdAHuqO3YtsoxU529+JOUnCwhorQmAAAAAI7yFn4GIMzzROCOzGsEItGSZTyQFeVWo69M8yi1ASooAE6zWfD/DxzwA9ax +AAAAKjYHJqeDG9YYABIeYAAAACD1OxTec0KEEAb8eKaweLuxHX/RHMODftyiXlMJAAAAAJy6d3QDs6YmtcFBJtapUq+Yy8zaYBhHhm7s2Pi3MHhVhGSzWfz/AxyyVHgj +AAAAKjYJH2l73c7YABImQAAAACC66L4ir3gV4qNEVzPOqmXpLri41ZHKutXLB4YAAAAAANXwlim+KbvjKRt0vygcm1dM/c1INHI8h5Ifc2pWA1aX45CzWf//ABw1v+G4 +AAAAKjYRAnFe5bHYABIuIAAAACDZgfNOFMOPArs1QU1s55xW+XbhxJheD67t3DAAAAAAAPNwWu5F17rexNksn07lVRbCaobOt9dSYFm5jA4ufqEQURy0WcD/PxsIk7wS +AAAAKjYwapnHDhnhABI2AAAAACC+rYxJTMczKqdHMlG/Vak0qSsiMVeoPw4/fCkAAAAAADKbBggkpSu3CRKJ/eA8bUk5pVvcgDWLR2T640Hihm/V71q2WfD/Dxtquem3 +AAAAKjat+yFXlanrABI94AAAACCUXnnyoYzcDi7xnZyRWIt1bkjbyyCBHkoNCgIAAAAAAC4EdKmqQrJuZ5yxcT338oj0ksG14jNf59G7h7kVl+F8CAu5Wfz/Axu4sXu1 +AAAAKjhQcdVbMWo6ABJFwAAAACCA2lQoBaWKbrzmcoYVYEpTtr+CMQLJr3OHBwAAAAAAAIORLErgHJnVFPXtP6KWBVuvvsnD4tt4AAM3rkOE7c6hFEDDWUg2AhugJ28G +AAAAKjvhLateuYglABJNoAAAACC3vp+x5b2G7Tmk+Lu5BXg1N+HZBwiAZ93sAgEAAAAAAFkae1nqzp8j7B1sz8DoOhxMhBvgytSdsUrUrH9mYeRhUbjDWZKNABtaVx2o +AAAAKkmq9YHCvStKABJVgAAAACBVOhzBX1LBAvkNPiPLOlNltHrGN+d+PC49XAAAAAAAANQzBJki0xt97YDLOAVHWix7KZ5pSLmP+6szuqawEpEeoz/GWYBkIxrWLB0r +AAAAKnEIW1VugctrABJdYAAAACCwR6uwtRWY3E1qupETRqTKh//Ou81QFnd3GgAAAAAAAMv47ZELz6VGx3UdxNphgp9gn/w1g81vmNPEkcyPJLC/QVvVWWf4HBr5isa1 +AAAAKra5gwlRQukGABJlQAAAACDDHeBKsEYm0iMHHZAyW6tSTqiij9K7kMMeBwAAAAAAAI27SRurNd6vKBq8nOrr0glxgWVIJiFef4+ueZCBoe7osX7VWRk+BxoOs6V3 +AAAAK81+Prvcy51HABJtIAAAACA6Q0aY80BEV/rdNcmc1TbnbvLGEPeUMezDBQAAAAAAABqkuj+SlYp9/G6arSBWsPyceMdZZv4lZg2U4HHZWS54cgLWWYbPARpZX8BE +AAAAMCiRxy4VT21yABJ1AAAAACDimf/XAcqiAoS2njtWrR7WaZKTkUWS9j3MAQAAAAAAAAQJEZtRQeK720o2oOguzwNz8pzhAZEx6Ix8z5NDojuvamTYWYDhcxlrAq+X +AAAAMbQCm3aOvLN6ABJ84AAAACD0UigcZTC6kv2G+1BXa+S/rFaoEzqvzlQ1i3eMAAAAAC3op89VEGywCWm1YReuPaLhojAcl79u3vof972zNBAr0Vr7Wf//AB2CV54K +AAAAMbQCo1mWn7tdABKEwAAAACDFZTuFDiTswyDswQYnCzMzPNo9B3bq+HUfo3wQAAAAAGaTeXP9xCz8HJG9LpI861n9vBZ7kKagee5V6k8iK8WivV77WcD/Pxz1LFF8 +AAAAMbQCwuW2K9rpABKMoAAAACBG2qIG1zDzlvpMe96RSEbXKj4Rzmkof46wmboGAAAAAI+g3C1yGdkn7zemaTWY/dtaxP8VPs6RwkwkkvxdhxHv+GP7WfD/DxwONuBK +AAAAMbQDQRY0XFkZABKUgAAAACBlmCYeGEtpFftVv5oB5ZvaCrnf9hgoG9VrAzEPAAAAAG4+KMs5rd8ppMkAfC3eQI8gK9nWiZQRb0NhGoxN5nTXIW/7Wfz/Axxz6LjB +AAAAMbQFOdgtHlHZABKcYAAAACCWBwc3Ka5loQ4tTcAKwNUI9rl5o7d+xBsQzE8AAAAAAHEmtXTQwHyscffq87mKiAzufJzxKsS9zAuLBb7rDJ+CiYj7Wf//ABz2T1Pa +AAAAMbQNHOAQJjTZABKkQAAAACBtRhh6/1jOIihI+EdAk+Fpft/w3o6D2j7K1UQAAAAAABxOQbk0j1733bZGhZ/H9SUb6KqLPLpFutSlsWGGG6GrfMX7WcD/PxsX4nhw +AAAAMbQsqP+cRcDZABKsIAAAACBQRr+bwait8bY9zdj9itlwM93EANTmexX7vSkAAAAAADwkQcbHRGEZPzdtvUya5lz+XimV3Qixo/pEIMhgydwoCnf8WfD/Dxu6U0Gq +AAAAMbSq2X3Mw/DZABK0AAAAACComiAHNptFd6q4hwwEwhp4r3NPqlwD2Jpp6QUAAAAAAC8vpamz3Mn/BbJZOp8B26IkgLTQsjxUuXYZwfn1ujiB9AABWvz/AxumKSZX +AAAAMbaVW6FO53ETABK74AAAACBCmGeBm23/p1MGJJLh4L3JZ6HqvoV9PwO6wAEAAAAAABLpB2/05OLsfrjIbjsjff2bJbO0WGBZOdSAAcixEnNjihIFWv//ABtV34Oy +AAAAMb5mY4RWynj2ABLDwAAAACBJqspr8KTxFfdNv9smcox7nLQA6CkfEXYBDgAAAAAAALbIpjxV2mDPSNhJHHSt/1iJC//9ZkZes2YUp1273Wdd4g0GWsD/Pxp+IAoE +AAAAMdEOeWJsqI7UABLLoAAAACDS3dWlc1fU5Oy0cD0riiJVQzj1U5AuGULPsjusAAAAAN//DaCp4LUdULP4YCJvchTJi59qdG3G6EpVu0SyVGzDDmcZWv//AB2AEZtA +AAAAMdEOgUV0i5a3ABLTgAAAACAaHy9YbZwVjf7Gji/Zy5uOmbctSwSxQgi29ykPAAAAAMPXh9kioIIYpXV2flkl6rOdiECFbn07zT59d1jiCbcS5XIZWsD/PxzBvhnV +AAAAMdEOoNGUF7ZDABLbYAAAACDvfsl9yPt48zcLzbKLUECZAN4lGpZgl1+8hi81AAAAALFlLWBwkUFH1t76ulgXnREK2R1n5DEtWnYdMYUqNwBASIUZWvD/Dxz+UwkZ +AAAAMdEPHwISSDRzABLjQAAAACDNOViN+NsXgQihizZUOUz2V5j1V+15hgCGMJsOAAAAAFzTg/caSaQHK+Ex/exUF0Nq8ov8p/1zD+c95/XoP9w3RZwZWvz/AxymZdTl +AAAAMdERF8QLCi0zABLrIAAAACCv0KjkkkpJPKCOuSyGO2ZsHb1iG+bQ/thLMhUAAAAAAGG49ycXfND27cOH/9TtU6cUU3bm+Mb968QeInDpsJgeTsQZWv//ABwMs7ue +AAAAMdEY+svuEhAzABLzAAAAACAPS0RxBOuthTrB/P3BwC+nYBg0Swh6GxhaRyAAAAAAAJeAHTVrhTkfc7ZbzAqvXLWYl3JsGGncerSPkQcT+QzoOCkaWsD/PxuIJtFi +AAAAMdE4hut6MZwzABL64AAAACDY4XNkZlBiDu51JziAOHNO8nR7cHWpCMUFGQMAAAAAAKUAgujGH3hmB4n7wE8IbHtKvxLBEYp0TfZJC//13IQqG7saWvD/DxvEHUWf +AAAAMdG2t2mqr8wzABMCwAIAACC9qXXFo6mb94KIoUAVZpxaUO9uOqMuXbE4ygAAAAAAANlF+uo0EtBHUFIHY4DOsMSuyI4BsElbV8HYkY1Fxwhat98aWvz/AxutY3s3 +AAAAMdOo+XXsvAxOABMKoAAAACCSsyLy1zfWHukZC3VeiAZmOzB5op+op3mZcwIAAAAAAEis13LP6krkG+dXnDGPbVp2v5tFTvBsBf36jsxd898OluobWv//ABvgd51C +AAAAMds6AVj0nxQxABMSgAAAACBndbypGbV2a77lrjmKRtVZJ8nCrG8m/DXpkQAAAAAAACWCjWpXAnWWhi90iCUfANqMdsCkCmKT6JU43wiz6aq3P6AeWsD/PxrCYuSN +AAAAMfrGIOUUKzO9ABMaYAAAACBYbuOZZmT8NZXCP3APQDx8tgOCIFSCkznjBgAAAAAAADHxpipwtdnRsTogbVQJDig8Zs/8qHsU8fkG4DZ7oDwVtCUhWvD/DxpBFFLV +AAAAMnQi90KiA0RaABMiQAAAACB8maYs6bIlGBtH61y2JKobfzr5dw0V0fH2CAAAAAAAAKnJ2n+15GuIp0jiBnmlxxqvFeVDFcT3H5f7/huFxc9f288nWkbFBRpJ4cNM +AAAAMyLwY6TGuYRzABMqIAAAACCzniwkHD//LHvyC8XFR33HztshVMzt4ZRIAAAAAAAAALdn0csJ2541WDW36Uo4Wk+CrM6oXKxeawZwlrvV3PBV+6dBWqMTCBqR/+/O +AAAAM9FjZ5Zsgh3jABMyAAAAACCODFvHsxENpKphV4mFkzJ8D5DXixpe+5dOBQAAAAAAAOxQtzMxz0Gtg1L/GK7ydxsxzOj4QZb650pRO7o5X6BcrZZUWiVICBonfRtv +AAAANIEUU4MM7yKvABM54AAAACDcAgFLU6txE+4uwS0009FD3u4nlajFvPQABgAAAAAAACAsivvdRcW8yvJuGCSsSOAiFeyiSJC6v13XnUhxv795TlJmWvf0BxoBOoSC +AAAANUVC8hZAoX/WABNBwAAAACBIacl9XiGLwbrIxFHqoUxOJkyzEEa7w/g+XK3RAAAAAFlgbCTIxsYzi7SZFVq4M/fpZ3q1vP00hXGB4Tsz42lwdTt2Wq3cAB1VCKhw +AAAANUVC+zxwkLqsABNJoAAAACAjl6t/OL/SPN2etQL9jzS3wp26uCSXH+F6oPZiAAAAABoRfErQpl0/p4a6Zn0Mrcuzpm6J6IFoTyJQY2CfI83Nj0l2WkArNxwMd5Yx +AAAANUVDH9UwTbXFABNRgAAAACA5bnVE9fHOFNIZ/ScJazPzEzIgIP8Jm3zJ5BQAAAAAABOBleESxvmr2C8WB21wXuIGpYLdJbuecE+8uKyB9VdoUWR2WtDKDRxrAo6k +AAAANUVDsjgvQbnGABNZYAAAACCUshRmUlXVDFjI5cJ1Llolc/rTrifhgBhoBNAAAAAAAFzJKs/tbe66phvaNheDFjoUzm0YaOEXtPymn2l+3b9LVoR2WrRyAxxAufV6 +AAAANUVF+8QrEcnNABNhQAAAACALbO6LLuO7gRZcm/zAwa9kaS1mQI2iu4kxCQgAAAAAALoSrF6nCSltwfpcmoxoP9NAU1TR1qa8wtIUUPFv7DQ136d2Wq3cABwUqnuv +AAAANUVPIfQaUiGJABNpIAAAACC/ayYcyPtAsGME1hkhjBxytk3vBCJbFvgM8S0AAAAAALRZ+R3b77LhMSnYmhfd8YNnjcLVX6bqmjnIxFYX4kGfGbl2WkArNxsP8qgO +AAAANUVzurPXU5gYABNxAAAAACD7rBRqhe1H/xaBBFTaLS7THYy5AJjN54nHKAwAAAAAAADBlxvCAaPMq6UnU+w51MUDRnmT0+mdp7U6yTxxi+XJatN2WtDKDRvjR+PX +AAAANUYCtYHV1DHgABN44AAAACDd4exkNOzu88lb8gmJ4fDZcYyymYKbXXXn6AMAAAAAAL5Sa7CVJCWJmZjBG3049Fn0bDf9xvocu7xjkAUi/pF7cyF4WrRyAxuz0WaS +AAAANUhIxpoHocYNABOAwAAAACAi+H1GSs8D8F2cLZL2dsrKBni5LL46EKCVSwAAAAAAACMUqdCRLjVHp2OZONA6nYFtp4R57+5EXLZZpZ6IEQOs5xt5Wq3cABuEdXiF +AAAANVEgFECYpMwuABOIoAAAACDABpXoHAQt3Al+914mgcJEBIYVp79yq9ISXwAAAAAAACQc7OrakgozDnEdRGRFhxD3U7NIeTsZAHbahgEQGFPN5Qp8WkArNxqTxgOy +AAAANXRUKEq/FC/QABOQgAAAACCzEgj2kLmCAs0B+3e4jRg0M+mu2mfzs+3uFgAAAAAAAKjSsEZMIQWTXave1NmbblUKnC8b6HfYHRjyRRooZT0iFe6BWryXERrYgyBa +AAAANeA/9Ixvdv9aABOYYAIAACAAMMuw3D9JfaMqrIdtZar4mon2tgaDvfPaAQAAAAAAAAyxoSJ+qbAp+Ty1aOUgUvDe3+uRf57w8Zr9GdidiQEJx0qNWmTUChq9w5bI +AAAANpao7fTakzawABOgQAAAACAcRwCqSYDOKjbXymFpBYMHUpvtC62SaLiQAwAAAAAAAJSLMCr8uUaLUYnPUj2o5YdA9t0K34zkUU3odKXWos3Cv6qSWpYmAxrZHlQ9 +AAAAN9fKvUq9vuB1ABOoIAAAACDMIxqV45PRXQvVUZEjenWc2jo+fd60AV0QAgAAAAAAAAKPZ2gsSWD8rtkvrkRQug/Ebix2UtlyxyjHdA8lelcfksmpWmbxAxql3T5H +AAAAOMhnNyNNwgf+ABOwAAAAACD0jwlUXW+kFSmJkYr98v+xpAGCMN8CqC82AgAAAAAAADcg5ZQ8BdWOVV+spnyNA7Le/wxJK9ZXQVHVPasZfHMjbWTDWkZ4BRry5bg4 +AAAAOgbYImm4kfEtABO34AAAACDLmI2eiyfFn9b6Vs2mkebX2Llky67Gpor7BAAAAAAAAO5BKXXuMbX0d3ngBnzn2IYyBB0WalSH63fLNHGhBk8g05DNWpsDAxoNSLJc +AAAAPAHMe4/+VvD1ABO/wAAAACD3csS4EH4NPDoWBekyE+cert0IJSt12TvOAAAAAAAAAIg3FAXqSjIykeIi1BcGCl3RFnQWd5kT9NknfbynOdi1tfbfWhwBAxrZiTys +AAAAPnYOdyWF940YABPHoAAAACBH9mo4Q1jZE21riGFMMwWk1EVa8YIJUtjQAgAAAAAAAJam/QAr5ZvugHponyI8/VTVWSBZrQ4c00jUnEJvzr3DOVbsWjECAhpIaXZU +AAAAQMt/25p1Iv8oABPPgAAAACBwX9pYjWVpXAhu6HMxvVuPyd+eQF191ipQJf3KAAAAAArpRZ56wi+GauZJDTKNced7//+V9S+5iIc8P8UhcURrapkDW///AB1qtLbm +AAAAQMt/4319BgcLABPXYAAAACAF2g7lzRf8tIvtQGoBKaAvCeoxd6ApejF1Bh6wAAAAAP8bbWYB+E4p9pGLDxDRxbIy2K+Y4iRjeZPwZIQTgZGByasDW8D/Pxx4hFvv +AAAAQMuAAwmckiaXABPfQAAAACCcmVRcuhYkrvtZdWJ0qaAaQK3IzIMo7ZZTgHYmAAAAACXIbGnjjjxgimPyCraXRKGl/F8GzGgy35LbEp2Dfoe0F9gDW/D/DxywVZ/U +AAAAQMuAgToawqTHABPnIAAAACB4GKTjnuK+BNSR2zIfycWNIiRYQj2PFVm3CgMAAAAAAMvDZ3YxPxDVL2g0wF2D3UE/Cxn4Eew4XoyZEDdCSrPZAgEEW/z/AxzZ54bC +AAAAQMuCefwThJ2HABPvAAAAACAXMH0ZrlEz/Q6c8nE1+6EcIPuQdI7ADgTIoQ0AAAAAAB+7vF3YjZs9arqfFQqPQ1jaqOaSUc5t2FHUOtk27Q+Y1FQEW///AByKeWxN +AAAAQMuKXQP2jICHABP24AAAACAwk2qOi7DhZFExOfqKuEqdARx9/G/BkKjL4wQAAAAAAKxMOGAHeRx9ogHMLi053ZPAFKW7UFmUv/jU2STWb+p5S6kEW8D/PxtlYFeE +AAAAQMup6SOCrAyHABP+wAAAACBg0JtmrN5nCUoBmdECqk3lKUXke5dDC2P73AsAAAAAANdI2qGCTAXqPDBb7Uyl8LWPExdiSSW0w5f5/hVKBUS3/CQFW/D/DxuWrxT2 +AAAAQMwoGaGzKjyHABQGoAAAACCQN59Xr9qQYM1wQX5ptZC9nw3f1iHG+ThAJQcAAAAAAM9CYRM49e5K8NfhFvAmboUXdc2DXHgD/ch6xy3z+2GksJQFW/z/AxsApTZb +AAAAQM4g25p1IvyIABQOgAAAACC0gQlUSGuaO96/8GewKJpGvzI2lOwc6ic+JgIAAAAAAOrOvggGSNKRAib5rOzGlwaT17rxZVQdNH5Sk8a4soxYS3IGW///ABukd8K4 +AAAAQNVEC8vOsmbsABQWYAAAACCg34XXPyaXRNzUNPAykDrSIvIXPWh+0aiU8QAAAAAAAIgXrj6Gf+8sJK763xq3/CDJ4ZDhY7jn4JdLq/Np6Hsjw0oMW58TURo8xbHD +AAAAQO37sEK/KAF2ABQeQADgACC6azH1Z+HNnD8PGlByPCoTW83jRHCwt1njNgAAAAAAAI8cPbVkcMTIDTaoCRFQV9hQZ00bulyQiseo5ivdXzZWzXMOW+dEFBp0e5le +AAAAQVGXuOLPTCXtABQmIAAAACBIb68RhxX7xmDp5uh2WvzV9/sf/eCQiukQCwAAAAAAAMaYphjrhuVOgdoEivDxMCupfPQSPrUpzHm2EitYh+86yk0QWzkRBRq9fcTK +AAAAQt35AJkPyTfOABQuAAAAACCXw9VLRsauQv6zPo/D5rSbOHiujTsw5OEEAQAAAAAAAO3Eri3WCwyRYaJeggaY3SbFvmSpwfZHz4Abf2K9G2L5vnoVW7BrARoa9d0J +AAAASGCXHcfIC/MhABQ14AAAACDHkXbCIM8k1iTeUUZ4S+ta0lofkjWmaOO9AAAAAAAAAGtmE7eUskixn4cyaFRO2EjbISUuA1pFHKa2hUv0I5iFLHEZWwDsWhmM/G3u +AAAAUoo1YPUNpKUVABQ9wAAAACDAB1gJXV2IdGclllxzsWcKD9xWfk4T/zyjJqaBAAAAADw4nzgCnnztcCgclnD/o4QuUJGgGh/BV1bAQBriRGLOXzQ0W///AB3yTJ3i +AAAAUoo1aNgVh6z4ABRFoAAAACAgaUS2PPQopb4GWVElkOcBYUF/duswkfPNV9NVAAAAAN4h8kd07tzUgjMExUvTHSb2LTnHLQ7eyxmAHyNHpB4zBDo0W8D/PxwrNUy0 +AAAAUoo1iGQ1E8yEABRNgAAAACCmoFFJuO6ttA814H/M3+hRJLCA/Vahwfkwi1USAAAAAOxhzLTlGf04IuW4T67VAJZUc2Gdaopdteb2E+ILz7uRsEI0W/D/DxyUFCor +AAAAUoo2BpSzREq0ABRVYAAAACCV35shfJoF2sFBuH/ASd3E/zQDynz8JImGpAEAAAAAAP5uLmX/nnAn3e1ecRGPDChmsbqSUjKepRQ4oHANRr0QTFM0W/z/Axw6BPtu +AAAAUoo3/1asBkN0ABRdQAAAACDAUp0EgtPmAygKYT0YJH78jLBxjKi70msIEAAAAAAAAOthEbLzUndybgQgY1O5mRT+fw07venbiGNk0ekRn0TBa2s0W///ABy4d0xo +AAAAUoo/4l6PDiZ0ABRlIAAAACBk44XD1Jq4XOZZP354k94ocgkLgl3gL5mDjAEAAAAAAGdcI1LV+MlAXHfwDz5GePOw6NyGJn1fbOUN9N7uKCA5TYU0W8D/PxsgPlJv +AAAAUopfbn4bLbJ0ABRtAAAAACAZf9QEOXhkjiqN530eXp7QAmhM/3BHMoCrIwAAAAAAAEC50fga+6Ez+J+Y6qIjp/no6QN5AP178ZFe9PchOtXvbqA0W/D/DxvgFfez +AAAAUordnvxLq+J0ABR04AAAACATcZVoLbbxEFuNsdjElP6Lydg1eFXqlKJ/KAAAAAAAAGn2Vf+lyTvKJR8q71MhVg4kGlIN5zrJqWYNatn+hFBhn7s0W/z/AxvB40Mh +AAAAUozWYPUNpKJ1ABR8wAAAACCzHD86H+QLimSO4obI2B8VHrogOPHfoPVC2AEAAAAAADCpOpkUebs/ZJOMlyOhdeCLVg/WlOW0kG0chlBGGoCnI9c0W///ABtpoRVS +AAAAUpS5aNgVh6pYABSEoAAAACA9fMBgEB6z2/Dz7nSwJYu38AkUxS47l1RokAAAAAAAAB8WyizNlUeoaEFAyOrbdyXxavZeYS3MCqB5FpF6Gw9t7Qc1W8D/PxoknERK +AAAAUrRFiGQ1E8nkABSMgAAAACCFOm5jcVGBPos5MtjM8HTv/3t1pJcr4pTmCQAAAAAAACUmcDy/XVZ8Xwtj8IMPxSRXk+NzTfjnSiJ20w7aFYBjfs81W/D/DxqpF+K3 +AAAAUzIGBiuy20erABSUYAAAACBlPBqnaT8HoNTB0XW1LhSxcXbUC2F4VrmUBQAAAAAAAB9wLoA+VFS373D+6uHhdg08zrUmyWpFn7s7S8Twg9iffgE5W/z/AxrZrspQ +AAAAVSOH98qkejlIABScQAAAACDC+De1Tb5AG+D0NOP2hO+uUPMymYwFXG2sAgAAAAAAALNwh/0qcsWC0YNESW6wtCtzUO+0lu5z8rZ9OPwEKheeqn49W///ABrdXKHx +AAAAXOQ1na3R43dpABSkIAAAACC9zJdVnhIWQtUtFGgwnaX9O6uOxXeCHanJAAAAAAAAADuREFWKmxbWORZdE5s5uTs9PObb13XNSe2Z3ttHWGwHj3pEW56sYBnN7C9A +AAAAb1PlQUUXg7a5ABSsAAAAACB/5Cdwa4wEoP/DpZsgKmptNPt4kEz28r9RAAAAAAAAAKdi30YT+4ft542bxCTOD1eWQzIzQ3a44zqluafhByyZC5JSW1WYSRkG7Edj +AAAAhX1SIdIdoYW8ABSz4AAAACDNx0IluupPiZNkc/AZnnyRpILt6VwQajY83wAAAAAAAB7/e0E1kHqlALIbPa9XOENZQu3Mb/ll+YPYCJcMlCOWRGtkW473AB3x+cKy +AAAAhX1SKfn+ad2ZABS7wAAAACASi+hwG1Z/ribIeSIXoWRivy1bZ5GflQaAfAAAAAAAAJ6W1XkOhn1CLFbXT2bVpYeSASr3LNYo9ZlGUxZF+B83SXFkW4DjPRwI8m7f +AAAAhX1SSpmBi0zLABTDoAAAACCGyuAEw4PYuDfZCTWuKjlMyDGHnmbnDXKzWgEAAAAAAHZlPzXsNlkD69UscVEQkVvd5jDQ7oLKZu6C43HwEnMH8HdkW+B4DxwqgCSz +AAAAhX1SzReOEQmVABTLgAAAACA5YGodViTj91SMSRFe8DBd+yCQONpD7lZxbQEAAAAAADWYDwY9k5be2MjJ+tyxV+mULCMu80+be/CAlMI5fdsK6n5kWzjeAxz4ml2g +AAAAhX1U1w/AKAx8ABTTYAAAACAqcrcj+HblvFIc1+FT0s6AkIM5/mM9+JKVnAAAAAAAAIWGJ89mxyRovK4M4TvJiMMVKEH71jiYnuSgUplQqvAayoVkW473ABxPbZj0 +AAAAhX1c/vCIhB/6ABTbQAAAACCZOgt0XKjjZlivSmSR7tK57fsxuFHfjUNr7j0AAAAAAPUHReZ+cRfZmz09sZx1g8jTdkTlxpeMCBbjDIJTgUxneoxkW4DjPRtBCIlg +AAAAhX19nnOp9IWPABTjIAAAACDkueuYRpduUcTxiWNvmu4EFV1g3jqKTVn1VQEAAAAAAHoChCo5z1pfO+Rb6wXmn9dTAzebQwebDLy6kZPCl+hwaJNkW+B4Dxsc0YzE +AAAAhX4AHIAvthvmABTrAAAAACAWNFnx+QvHvRs9JkNKqsLjkK2iSr8Gw98a4gEAAAAAAPvNKXh3QnwAvYeS7wdEIGwSSCZwa8biAmL8rDPiWOr/dJpkWzjeAxtuk3Vp +AAAAhYAKFLJGvIzgABTy4AAAACCClpL++IbL/b18jjTRxX63ElwKhdmnS+gYggAAAAAAAKlEr8OTHjL8sndgOW/hiB0gRAgqjoAf4sakEDi4aUc2n6FkW473ABvTPawm +AAAAhYgx9Xqi1liqABT6wAAAACA0nku8ZTVNXsdHq23K7cMEgXPx/zArtEH7VAAAAAAAAHkQmBApj6+KUIdOx92uuHUC9hcEhOLl6WAEBNE5mIz12KtkW4DjPRpcqz7n +AAAAhajReJwTPZ9vABUCoAAAACBIgs7uwpp23p6UJ0YR2BvMnZU1uhXDJbhjJwAAAAAAAH81xCJvWlFJ4hocTNxpbjICFeAOTfzcK002R9kx/wGeJs9kW+B4DxoJAeUY +AAAAhitPhSHU2rqGABUKgAAAACC7rDWcjZpzkV5Jf1JqHJniKIrTrgvaRHBDBwAAAAAAAIJxv4re068WAKI8Kcba5p0t5fNVJafugdixbPufKAMenFBlWzjeAxp485/T +AAAAiDVHtzjbTz5/ABUSYAAAACCvr3oIFa46qX1TqJy9leC4OWu+oLCOrXQtAwAAAAAAAGhCpkMY8FUBCPGxNeg6rF9iw0PUTgdLf+ObspWzpsbe1lNnW473ABoKjEMM +AAAAkFGNUw04m+MwABUaQAAAACCzg8xi7ambUsYXODEmzDDXpsxUWx8YJUBtAAAAAAAAADc5BNQme+pVFLRmyym/4QtrQE9QCjtZ5wIfzDGZabhnGfptWzsnWRl8R0Nm +AAAApAxzYhaVGKBuABUiIAAAACAJ3l6BeIrIlo9WfGtQgTP9hmhrNU23oP4f2DocAAAAAMFCFuiZVFdSBX/YiyAuTiUndGtVZHvFtXvw4UXBuvQuY5R9W2fYAB0jC590 +AAAApAxza2sFuD7qABUqAAAAACBJzleRRY5iLPK6gbOCkPvpxNgSRyXxSiKdc0ABAAAAAJyZ8+tskjbX6JfmRNXmiZTqWRR9qYYRx5505RFEminRa5h9W8AZNhygBDmD +AAAApAxzkLzINsC5ABUx4AAAACA4HTTZp7GKuqw9WdOWUkNFduHRyAMaX4McSQYDAAAAABzgF9fVoi3YGpLviZmjPmCGUhIrdw+B98YMMgRJ75iQc5x9W3CGDRwJVOvQ +AAAApAx0JgPSMMf4ABU5wAAAACDRmnCVAgfdDdbGGrlHppsmz8/kuI4ii2GDswIAAAAAAEx0G1WpfOymqAUjFhove5ELAfnE31r5ZVJm/81oQjFL+aF9W5xhAxyYp8+a +AAAApAx2ex/6GPyUABVBoAAAACBqbIWWiyOt9xeYZNHIT0cyUEC0B2SnJMcPyQAAAAAAAOJ1mlS2TMo5HeQmzgBtB7Ogz0Z0quwyNIPuoRklWdlH96l9W2fYABwRB/qD +AAAApAx/z5CZueahABVJgAAAACApMtutygw8BChsPFpBL07YLRUT8qUjs2JaLQAAAAAAAKrNgr1PaU5xhV/zzdB2hURsFo6Gz9JDm5YP60uile1qFrJ9W8AZNhse1jyn +AAAApAylIVMYPY7WABVRYAAAACB6ZCl6Zs1QBkgKADgFdmw1grYjpXtFIWoIjAAAAAAAANWAkhNajZemKT5sxWNB/xg8RJcwkYs0dGchx4lflkdsJbp9W3CGDRtpRHSz +AAAApA06aF0STDeMABVZQAAAACCHmF8PWqzuxgHM4zGfoipwzNiDqmZi04bFBwAAAAAAANLgvrexzbUEnfXQOpcPTufGjz6s/pr390Bb7QsPORuCT8J9W5xhAxtJZzgd +AAAApA+PhIT6hvICABVhIAAAACB/OuT65k264wfnZsL1sUCNv1hH8gNW43XxOwEAAAAAAEsvEMFdNtzf2w0+9HzpdhSC/27LmrJqFxk/IRSW4CgMnsp9W2fYABu7VDaX +AAAApBjj9SSbceO8ABVpAAAAACDx/pwueLkjNybnV76aVCUl+zozpINpSByElgAAAAAAAH8akDkPLACRd/6iK2ZvmOpVxondOYZ3ir7KwLh2cUKTAdV9W8AZNhpgx4hp +AAAApD41t6MfHcJCABVw4AAAACBByUZBiHE4BVzGP6MQ0KI9g9QdCvThxNffEgAAAAAAAANsycE9DYKCWX7A+qlHevHSP52D7sLD5STwuPr4gE13dfd9W3CGDRrbKIoF +AAAApNN8wZ0tzUQ6ABV4wAAAACAz6zCll3TMYW9/oyMW+PzOMForVsbIa+tHCwAAAAAAAG8x5q1kCWc6M4bYE1FgMNPvJJQxyUx50xAwuGFTad0Af3B+W5xhAxrfU7co +AAAApyiY6YVoi1P5ABWAoAAAACCz6/apm65pGp74UdcpwIHlAPmWt65YRcohAAAAAAAAALVjI3XSG9vKQlufM9WZ1YsXyTz5LdRGmQpFQcGywF/k7WuAW2fYABpLTnuK +AAAAsHEVQxW9gmNwABWIgAAAACCyoh/YEuQ6MvEJvDHu/wQDIFYZu64pkL+lAAAAAAAAAOJNIPsgC7IJifqgQB28zEnII28OwUJbeIaPRvPxdvDfCs2GWwXBShnhgdef +AAAAwaGUIiP2ByqNABWQYAAAACDEiRkpv/VQGZ66wPPIaR8Z2EYz3lPaivNGAAAAAAAAAAwQnP5bJPqh4BdLeIx+Po8vs0C87gelRJFLFnrUwnuCuamZW/BfTBkIaYpL +AAAAxuhkPfrZdNa4ABWYQAAAACBRndnx/6N/sRP2xd8KfdaFz9yhSawHpC5XWotlAAAAAK5A+39U5EIw3c/nYlAQJnb85o48qG4pxjNqj0cRZHG/nDu6W///AB0gZeZD +AAAAxuhkRd3hV96bABWgIAAAACBSafnRztDkdMAsmB8jCs2Z7v5hAAtSjHWu6WtdAAAAAPCyv5E51KkPLlpBwyYf96TPsV4UE3Mjbp2F36EyZneDQkK6W8D/PxzsLi5h +AAAAxuhkZWoA4/4nABWoAAAAACDNSv2f8qfWxRpVjibyqgSMOUypKdMHd3RxRqoBAAAAAJFGEQbDKnNind26y9zmMED9dMW+fwXG/dwaUPQc/mlAski6W/D/Dxwe5jPG +AAAAxuhk45p/FHxXABWv4AAAACAqGVxeRJ4VITYkez76q59GM6mnrD9GY3m5QbQEAAAAAK5Q1INBvF2Mp2xDWf1W3rYAOv0350B+SRV4Srsu490Phk66W/z/Axw3m9+3 +AAAAxuhm3Fx31nUXABW3wAAAACDGdjzvHzJzu8BeaoCQSpg7f6cPM3XIOxFCiM4CAAAAAAjh/7eKqS1irOyBoTzAousOqTfcAWYpN3RZ1qItIUN4mFa6W///AByidNyO +AAAAxuhuv2Ra3lgXABW/oAAAACB3k4w2Ac7Pvc2swW0/W7SfDoibZ28cmr9hlwEAAAAAAB3e+HFQT6xzv0lIX8v16rAUbIjB24/o0vUtlhcUJSAdfGW6W8D/Pxt+HpjR +AAAAxuiOS4Pm/eQXABXHgAAAACDhg+B0btZWCtxVJXswTVjZ43tVI3ka/Z4GigAAAAAAALZfcAhsxOHbn6eX0Bwz5W5IaEucXW+EsjwdJqw+pEcBn4i6W/D/DxuUmHKI +AAAAxukMfAIXfBQXABXPYAAAACCJUuTXm4ycnvY+Y/F0pYwxRX3vWQYDUuxMwQEAAAAAAFKtmxIcRCJAf6vfaQ+EACVy0/6sJq/7+IsWEQlhFuczeqS6W/z/AxtGNtOl +AAAAxusFPfrZdNQYABXXQADg/3+3KljK2mUk7i/YeJ9ER50M9J8db/Ynisv8nAAAAAAAADoAIB7WHhcZZh6OrykZbRb4t4VXJph5z82U1oA50VSletC6W///ABsybEgq +AAAAxvLoRd3hV9v7ABXfIAAAACAtkgxQGKR+1R0rIL6YC1AN92X5j7t/rs8wfwAAAAAAAJ8IhdxIW+Ph/XlgOlRyqldJ/VSFeUZi+haXQ7npPXD/TT+7W8D/Pxq5pLp4 +AAAAxxJ0ZWoA4/uHABXnAAAAwCCKR6UgtjsOrnX1HvJH/FoNnpfkLzW0ui29LAAAAAAAAJpd9uSq/3j/12pUZxwB7THSnqninGDb3vZ+86uUvliSOV+9W/D/DxohQ4bk +AAAAx43Eddvf0QeNABXu4AAAACDSFr3nppzBhYuEWZcAns8ebsuzFXpcKod6AQAAAAAAALSDdy83dtx8ZP0KmUh/VELym7QtBJi0bGSMWpKglK6+VwTCW7gGBBrueE4v +AAAAyXyz7RsncUHcABX2wAAAACCeYiQAjdOgVmyMP/1hEGqWTIVJKwMWkuetAQAAAAAAAMroGwX8byHCCoYXeVYCibBBgKS8/Iy17slpxWkyHCLl/MfIW2B5ARr5DwGu +AAAAzZo/VC+xAlpGABX+oAAAACB2zWT80WB/FL1JNnu5LWR11vj56d3k37iQAAAAAAAAALes4/dEB4rtAanw3oheqL3mKhPISR54Hs5jwvlwoUvpjlTaW6hmARqduvWQ +AAAA0gWa1z4P92XmABYGgAAAACDsQETGfwYx4dxq9yLlsV3EG62Hw28NX+0JAQAAAAAAAO6QS6IRlqpQNbCPo3jYvh4IMhEaOxyYnuwzaeuAXBNRGzXsWz1bARrbZ+Zn +AAAA1rgew2B1HfndABYOYAAAACCnZK1c/5qHBpn1CFb/Ms1mtEx7jgsPZQUxAAAAAAAAAL4EpUhK6O04jBkLxnbhVi6PAuJZaQVscg9HZglb7o4ohR/9Wzg+ARqrbbre +AAAA203OQQZsu26SABYWQADg/z/YhSCLJeX+Ly06iYHqwCjpTzqqqBkkJtQTAQAAAAAAAJ99n1NG8ZKu30eEVfFx9Hl0w9LzH4N0ab9jtk8/u6O60cQQXGlSARoyZV/d +AAAA4CKvRBrPcX/HABYeIAAAACAo/4JlJkhVf3rvL9pWeFpL0Wg1HGsZilvxAAAAAAAAAASo0MRrIXRJZx838aOoZKUuOdMPpPZbflAFpMQdWZs9bGIiXMlCARqkvxOx +AAAA5QVluk2AfW0cABYmAAAAACD/HX1J+PY9GG0mmtGJ8KTRexT3tciRnvs0AAAAAAAAANEFJTL/6nve9E2i1B3wU8FTvLWu2xn2GE9R2SMZXS3wHOI0XGtDARppK7hd +AAAA6h3mrc5teECeABYt4AAAACDgAWOvyEChDxJrqF5flEu7GrGKngyArVFeAAAAAAAAAOZeIyud/MIv9ZmYTCC5H5W88Elg3VTx34RdARlTcQueWodGXAM1ARoUsWQq +AAAA7zhnKDgYb8nvABY1wAAA/z/Ciq9H4ldNuGvC2vKhDjjVL3OK0C8A8gPpAAAAAAAAAL6kirvBq5n9SXwkIJcwqsfbJn5PLWyxl3ZWyR2it9KCAiVYXMImARrsiAXX +AAAA9Jj9ogrH79/nABY9oAAAACDDlaL1QJiTF+9c8fW25toI9VliKYQoQ6xdERUAAAAAAC3ZsnEs4DQQlO6nsFXHKEKNSYdjW/js5Q3L07IL6xkVkFBqXAT8AB1oenW3 +AAAA9Jj9qg2zkrbaABZFgAAAACCKb75zeQds94fhp3Z71l8f2QM2QZ305YA8kSkAAAAAAAPU1f6dnCgPlaQe8nsKCnMmBySD8c9nQJQ8bWC92whrS1NqXAABPxz6wY6s +AAAA9Jj9yhliHipDABZNYAAAACCpg/YwTStI80CKl7ipyVMgX8rA/sHODEdOhi4AAAAAAMHPQwsN1MjAjtcaLQVNYZwKVvL9lxn6JPULG0PsdrOLElZqXEDADxwOq6jg +AAAA9Jj+SkgcS/fnABZVQAAAACA78JFy3gBH66ikEr38xdW6W3zbq/ld+D1SzxwAAAAAAJVOuevfRCABta/vKFbkvGmEP9i9lbHZLJDegpINmBfW0FhqXBDwAxwM6FTv +AAAA9JkASwMFAy54ABZdIAAAACAaL3/hLI8wJ3eiq6QBf27/jeTNPB4C3ycQqCoAAAAAANF3xLFtuZHzVZ1CfeLKfEsAsuOUnVQdqx/VkqKqPJuyoVtqXAT8ABw9QuGt +AAAA9JkITe6n4BCeABZlAAAAACBZEtw4C1H3hEUC3BhiM1iUNfXo571lHdd3pRUAAAAAAIpg9DgQPiwpPmirt/fmbr0aqrNHMR08LwsQOM3ASjNqPV9qXAABPxs+Uum1 +AAAA9JkoWZ0zU7DTABZs4AAAACAp/Mmilk8Z6AgpujYzr5XrekmsfIHGvHcY7zoAAAAAAJcqK5/e9aeizamig7gz91CAwq7pBULYyeWxFQlkhvXjE2JqXEDADxuVDxhg +AAAA9JmoiFdhIjGoABZ0wAAAACDzpxrVX1NBRiMIcTJkJdupwg4waMrdQtVCzQwAAAAAAOKF00aLWALocqskVcPpy80R7TDUp6lQJtpLuVXb60ctgmtqXBDwAxsXdM+f +AAAA9JupQ0AYXDzbABZ8oAAgaTY5ob9IKKvyJ7Y51ATCwX3A1cTzZjhSLitlogAAAAAAAKbR1f1TTT+yNxiYfBabol6SXijjcDhVLRSBbXB3cgB6PIVqXAT8ABsc+NEe +AAAA9KOsLuL1RGmnABaEgAAAACCNRCuQSsIWbAfvHdL+tS0uTHXa2JSjeVEGLAAAAAAAAG7yWJ9wS8OwGFRpE0CAHXgEDZPTkC2+vIagEc4aYjuMZsJqXAABPxq7KsQp +AAAA9MO33W5o5RzXABaMYAAAACADzvybFmMtRvB7aJ9+zpTkWbVTbhhYhYdaFwAAAAAAAEJbyLuA9wB5zdxmmHVDD1RFyGz2mAqQUmtzMQqYhX+2XG1rXEDADxoPf0g4 +AAAA9UPml5w3Z+mZABaUQACg7XvB0XBL3upyKq2BgPpIiNP53dHbyoHMcUpSBwAAAAAAAEYscVotjjIekjPdaNUEtI9yYqcD8qT0+5ebGl0jdluXD6FtXBDwAxoA2y0d +AAAA90B2RoSyBYM8ABacIAAAACCtdUF8gCaZlEoMo+z/ab/2HROZTdIFFDJZAQAAAAAAAGBOkoOIuJeB8BT3xXYJUS8B/UG8fndBxpw2pfdvRlQ9ecZ0XBmGARq5zwGe +AAAA/BFBSdZRB1gCABakAAAAACDDcPKuoKffp+I7JIoX7DKzZzXKjUzkGidsAQAAAAAAAEXsmh9JTHBEyOwlWe04CIDLXkB4wtzHCKrLolBI+VkvsaiAXCH7ABpZutzb +AAABAVGnHV/0rANDABar4AAA/z+oL9R/Zfx01f+UfHGZHJzEJT73zQHmLdanAAAAAAAAAAfpaHl3FzUmKF3vzcyS9X3v6gvStSNnEfBFZc9E2vRUPUGXXGozARpBIo6E +AAABBpydjL1XFS7BABazwAAAACDDGJpvP5aeAFTZ9EiuC2RdNcGR5A0XktRSAAAAAAAAAGOWuxEh43N1LYtuzRoZC4diwGluoy+uyl8lqRoSyNdCJzioXIQaARpk8IVR +AAABC9NdzWJ+r1EyABa7oAAAACCtEhJY//0eyqQdEwshEekjl3PizN25B/fyB1dvAAAAAEkLPGmJN1fWIwXIxSP9VxlqlKuDuC8G8l5itNsv39bfdoS6XMr9AB2RtFyT +AAABC9Nd1VcVdKLTABbDgAAAACC0q8JFPBOSUo9Og0P8BizNly4iwrA9w8mN5hsCAAAAAP+hVB1fy1CYQ3NIOYSIp0cQYQSjDkliFRg+MBej451qwYm6XIByPxxTC23b +AAABC9Nd9SlwigD1ABbLYAAAACDFupZklKqRXby+hfirIUD3Nqehp3Cfw/fqo5wBAAAAAF4KSZRp+0AaObZuJ4J9eJQ3Rqo05irSFigS5/+NNniQHZC6XKDcDxwLp0Eh +AAABC9NedHLc34FeABbTQAAAACAJUjOVcXIkM5efIagIeUP3GAEtu35i4vtSR24AAAAAAGF5v7b51cIsXIMRGFaCUGrBQ9BWxbRO9hN7wgjzTrg02pa6XCj3Axx98PSI +AAABC9NgcZiONZLBABbbIAAAACCh/tiuXBT5gizkwH5whYmIcXA2fzBVCSphcYQDAAAAAJ7vULL178GQrHeduxmVte4nOJOSUM+cC+vAIZdHWWEIbp26XMr9ABw6wYlk +AAABC9NoZi9TjeAtABbjAAAAACBj1yMgB0edxX6HM6/T3oLeKm/YIRuSfS1NYAQAAAAAAKjzCvlWGdTve4ip9s5XVIGQX981fpLJkp9m4AfwQ0wS5Ki6XIByPxuIOwm0 +AAABC9OIOIpo7x2/ABbq4AAAACCYsRsV/rXHLMrzs5lRWjcYoueUu9yoZVbkozQAAAAAAEaF+Dl7YAg0By+h0LDwhTHH60EYqQh3TJPJnk6T+wGAaLe6XKDcDxsjxccC +AAABC9QHgfa+dCunABbywAAAACDmf3ikVZtqCUrghYVudDOmNS1tDLZd/8KyPwQAAAAAADgBJ4NgqfRNQPq5cVemNbtNr3BGjjvJJHcjvn3akFJy2Mu6XCj3Axth6G82 +AAABC9YEp6gUiHrkABb6oAAAACB1+yfClu9oCffzn1sTPT8d3fzBNGG2NYdNDQIAAAAAAHOgX9n3U7KqvP+1D8rFbbw0kCn2P2W3fktcot1212eUBuW6XMr9ABtAEZmR +AAABC935Pm1s2bfbABcCgAAAACBfgvZB0xSaWcSZtS4EO0Eg05utphH6wU0/TwAAAAAAAMYqVLAg4EplmmMk7BmndQhBjFl8E9TJardsK51kHyFSYSG7XIByPxrCzaVn +AAABC/3LmYLOHsNVABcKYAAAACDpdxuHCZFe2jAHx5qjrWm1+ERq9KwjXUkKHgAAAAAAAGn4MpX4xxlHfatZ5blXYokCpalRBsP32dY540LOS15dzBa8XKDcDxr1VkAm +AAABDHzEU3Lb1AIiABcSQACA/y8yLTfpGG5PRvxlFsynWLKrnyOPhcqsCHolAgAAAAAAADrMbZp5BfoRMEZgvc03o54C+ZApPEWjO0tEzQ7gJ4O3yk3AXCj3AxoJ1XkQ +AAABDlW+eSCx60IUABcaIAAA/z8I1h/PUyoEQ2TwZIpBpVu6QF1aoL9vQV2EAgAAAAAAAKT7FmTQCuREjb34+Z8aePfFu4A2/WnW80rtXuYjhvZcHRLMXPiGAhrnxSsX +AAABEQDFdRo6fgZcABciAAAA/z/knC6s/W//yCNXGLAom3xomrvRuOgqGun+AAAAAAAAACMkj1fbMlkz1auPMGodJWihdO6CYAQYQWrnhDj3NSNdc/jbXBotAhonfG0W diff --git a/core/src/main/resources/wallet/checkpoints.txt b/core/src/main/resources/wallet/checkpoints.txt new file mode 100644 index 0000000000..0b438e3ffc --- /dev/null +++ b/core/src/main/resources/wallet/checkpoints.txt @@ -0,0 +1,337 @@ +TXT CHECKPOINTS 1 +0 +334 +AAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv +AAAAAAAAD8EPwQ/BAAAPwAEAAADfP83Sx8MZ9RsrnZCvqzAwqB2Ma+ZesNAJrTfwAAAAACwESaNKhvRgz6WuE7UFdFk1xwzfRY/OIdIOPzX5yaAdjnWUSf//AB0GrNq5 +AAAAAAAAF6EXoRehAAAXoAEAAADonWzAaUAKd30XT3NnHKobZMnLOuHdzm/xtehsAAAAAD8cUJA6NBIHHcqPHLc4IrfHw+6mjCGu3e+wRO81EvpnMVqrSf//AB1ffy8G +AAAAAAAAH4EfgR+BAAAfgAEAAAAcYL1NItllvrX81+LuAq6qIdoXrrUiBRLemDJWAAAAAKut4Vhu9v71myuelA4ZqO3kP4eTuqb+uLQE8+CxjfkhsuLCSf//AB0pFg7j +AAAAAAAAJ2EnYSdhAAAnYAEAAABGqWHkclp5E4ehRawBs45b5x4XYaqgtDMoSwqbAAAAALTV1vKUrdjXiTPqPUgmGGmaDGPvVaSoLzWx8iK3xMoSZ3zaSf//AB06PHGe +AAAAAAAAL0EvQS9BAAAvQAEAAACEBXV8WKSX5CaMJjF7nEwm317Fjsj99uhjc4r6AAAAABvhLcOGl2UGckHYybNe0um9fV26bfiZMFaB2f8rNxSAOgrySf//AB0nOH0B +AAAAAAAANyE3ITchAAA3IAEAAACSILqxEicC8TnZGcfxX9/p8FckbxdcatdcW8WKAAAAAEvnJo5A8+Y0mjJYV7kKVLj6Ul/9j/ODgf0w6gefxnUm+eoISv//AB0Usifw +AAAAAAAAPwE/AT8BAAA/AAEAAACEwGnx0ildnoyPILwYNY1jLUALLFQP8DFmeQx/AAAAAOiv4/rCGnLDfU5qDhbuLSwqU96lkyYJaTm8IJmezWdu/eshSv//AB0sAszi +AAAAAAAARuFG4UbhAABG4AEAAAD9pdvx0xs07by26iadZjBdkL97+LVOnWwpYrD/AAAAAK28IuIFDZQEvOwRV/f5ojcFS/6weEx11ir8PLW1uu5XhT5FSv//AB25x8ke +AAAAAAAATsFOwU7BAABOwAEAAAAvgrh2cIRfqt3j/t0Nv1BA22K6KyXCPiyECMF0AAAAAO1z31AjyOj0d/uWX+TDy/5e40t9i1bD76Pz+cCydckTGFJqSv//AB3ZU5cE +AAAAAAAAVqFWoVahAABWoAEAAAAin9TRvZzHe1UspvNuSaHkjGdwPF2/P6eEL9xnAAAAAGZsu6+oXMrRFQzwfmOzChP3du3xlBraNR1IelOBukZvsg+hSv//AB0GJxxm +AAAAAAAAXoFegV6BAABegAEAAADMWpXIGkCsCDeXLbyq2mvPvJex7GJCYsBHb/DbAAAAAM5RkurS0BrKra+/IAxkYqN4q0lMod9qZ/oqjbLLUGH3DHPFSv//AB3B8ocA +AAAAAAAAZmFmYWZhAABmYAEAAACsBE+FLo8osvJgV3Nhn8XcKfQicTbvieww/mCSAAAAAH+sbigSSrJl6CmsC6CyPQhLaDKza0QbkIv5vZli/8QtlFfsSv//AB3Nm/kq +AAAAAAAAbkFuQW5BAABuQAEAAAALyHOUlLDHxaV1oJszrx5E/W6vcQZOjIkAp/H5AAAAADdPJYtimG1A8VllMhUVZ+MISrORF352dqsEbm4rfNWUy0kQS///AB3cT9YE +AAAAAAAAdiF2IXYhAAB2IAEAAADmv3/X93kKY3hvqoeNDcf9jy/zZXMuRYYsZgdRAAAAAHANNC9lx7aDTf+2FTWKGJcBbwRIkTNyGQy+PSektTNVsVErS///AB2/sCUZ +AAAAAAAAfgGs1C3SAAB+AAEAAABLA2DYNKMw7Hgz4w4fUj7gWgeTNh4ppzQhlk+YAAAAACe2SgIK8pTpA/7tk3aHBTNqIAkGEqBD9Hr0YqL15bVk+O46S2rYAB3TpDcH +AAAAAAAAh1KPIu8gAACF4AEAAADOUzZxCACLCrZb56MIxIoEIWeq6nsqRTl+BDZeAAAAAED71QTWHLGhEIJrymjIHUFzaQjcvbP2UVS/r5ySz1DZxapLSyjEAB3+ZlwF +AAAAAAAAkZmkySCQAACNwAEAAABYktUu5mb9Udk13yfunLPLcaMDAsC+/3mVeI0SAAAAABI/8VBwhXPOzYzAuO8REO+I8QPR9GREyfye8xg/SeKFr5ddS3G+AB3Rki4H +AAAAAAAAnDAd9NKmAACVoAEAAACFilxtRYgzqoP3t+VtccYEy3EWXruBBLgvZN6NAAAAAOQIwRAptf27kuoO6436E4/6Oszg9p197r6xQAyFBC4Bcj9rS8OMAB0JvYvV +AAAAAAAAqoNHCwIiAACdgAEAAAAaIxCXtqtiecgPJGdKLI7luahI4dRXFa2JtjWBAAAAAKgiuv5u2GAOP/zm1h0Q3xkn6v6bv2d8tExNIJ8UPGuo24x4S1dGZRzOIiEY +AAAAAAAAvmyHarHKAAClYAEAAADnCfys/hFGQgTkzB2vSntj33KnQqWfTz7vloQwAAAAAABWa9XPFhrodgII7+bi7V7ZKY4LdK32GsS2u4/OLRurIOaES+WzQxxj7JoP +AAAAAAAA3DRAqLy1AACtQAEAAADVXhtGjCJ5iXEnIDfWzAT9rHORPAAS0NdjDC4aAAAAAOq7qNFZRow8B/dQ23du37fM4u2Iq0TzBUvkToq3/g+geU+US29/OBxybZMS +AAAAAAAA/+MWO9HLAAC1IAEAAAB0sfE3ZrOH9/bpE7QmobN2hQGrURvpzWzCCgwuAAAAAMHzQY7OI3TA/mkBu2E506clAApINgYlj7dWP5YemTVAoKOmS3UTOBwat3W4 +AAAAAAABI9gdtV3rAAC9AAEAAABVUxeUXAvmF4DxYzNSXjTs3IfwlYe24l/4JjIQAAAAAILT2J+G8f74uNmyAW9JnWDFnSCl8rRYYeO/CGV2cQT5an60SxURKhzFK+US +AAAAAAABU8Zb6gtBAADE4AEAAABzkQWrX5ewFL+e88S/O5SY51fz1vhH3tRv9uYaAAAAAJTaTzK11MIXSRfGlZ6U4TmS6ig5c5ursTao8hscqvB1UtzCS6e8IByBlYEF +AAAAAAABkV70dMN4AADMwAEAAABzQ8F6sjmLrNKzryUaZi7JdLbAO010YgFpIj8KAAAAAP1LgY2K8OcupHdUOpaCsyr9fUIXlpzbdBt0avYxCqcvtHPPS29UFhwF+4oC +AAAAAAAB66jAzOGJAADUoAEAAAChpKuAo68wSmzShRxQOiDBWPhqRJdaUTxsQ9wGAAAAAM4DdGfpuItjzlXnWMf7MSwgVK8RgKssPtXqwdXN88sT6OzfS1PsExwAKkUF +AAAAAAACUNgZQS8CAADcgAEAAABGy7ccvE9OZlfB8ooqhI7/kAfljEnM/4AW/kMJAAAAAMW9Vt6y4cOodVR1e6CC+h8KszLNFJOJ1/MS2NVV9c3wI/LzSyScFRyE42MA +AAAAAAACric7YGodAADkYAEAAADpBN+TLkJyTO13QgCm1Seg1aF+BfvBdkMUskcIAAAAAGLtcb3OjIaURBNKOaF4p0mBbLm7wNHFqhwx4c5GMsJKSB0BTFxnDxxWg7AB +AAAAAAADMQiRgEj1AADsQAEAAACTTCvVpFYYC0BDQaOA0g9R0IYrODEd602VBUUJAAAAACmaFwLknPabw9Cm7uJ1EMw8ylpCfh0ACyzK+QcRaq9IIsYSTGS6DhxUI8IE +AAAAAAADuew03p6EAAD0IAEAAAA6WlncsAaw4UiyqiobivpJ8SzBiun3s+wsJwkBAAAAADRA95fGOcOU0tS3CRxB1QIXkXUSwHLdWnY8eXTGzKBGLk8jTEIxDRyYDQAB +AAAAAAAEUsD4KRv9AAD8AAEAAAB3mRBUPOiZ5hS9pWBQ/9aiGOv/JQZ5m+MPquYJAAAAAE547xZxUzivsp41YiAUODrIlX/mG1ZpjCF6FO7PapRSmI0yTJPkChyl2HIE +AAAAAAAFC+ocnh7zAAED4AEAAABOjlzzxOS49jqc+IvrLbq6GUkYIQGuTlz1StEAAAAAAJ8qI0ToESsNe9gIlBQQbuXxe7bNZAeIg+G2YfolGqxr7R08TPSjBRxNzSsC +AAAAAAAGcd18PyutAAELwAEAAAD7V8cczSEbPeTMwuI7UKfNtyqrkeYHN7Oiv98DAAAAAIioitnfaJJeiA5dUrflDO8iWHHGi0CizQvKEITNQ2A384hATP1oARyusfgB +AAAAAAAMB8kDIyyqAAEToAEAAADD6WaCS7mIy8rpGowIPm8ADfcBKSRt1oHRacEAAAAAAIXKGhu6Zhlug2n/7LzalLkMyfbryxZSk3HmsnB0OWijnkdOTFoMARzC4UAe +AAAAAAATi2pVCXgAAAEbgAEAAABuuUVfO7THPxPwWyd0NoSO21akwS9qnUPR9wkAAAAAAEiIfxRoBMCVZJ3RAgadDm8U2Emkwiaa+C718mk4wDOmGxVbTBi6AByPS6Mb +AAAAAAAeYVnHFS1UAAEjYAEAAABPNXbFSDLkdMOHdA9poStLbQ+1SDWX1/qouBEAAAAAAOQ4wJ3d+Vwy2ler4Em9KqKYX13FSuZO5Uwrn8MuTnvKT8tnTA6AABwRqw5P +AAAAAAAuIBCUxPZNAAErQAEAAAAMLHyqT8JOZ3uNrJxMIRGYEYT3nF1HzLZW6HcAAAAAADzTVc2lDiLbmEP5ncM8q1TEw0P1d1kZkZqI0qOstTWCE/V2TJggaRtSQENM +AAAAAABBTaleRTLoAAEzIAEAAAAlZmyAjIigoxs18ypRBJig75D2ZTq/LE9Hu0wAAAAAAAxuq8lWaFwsEaZxcX1OTJA15F0DjrkhHxnywMPxRSpvcRmHTObtWxuucMl/ +AAAAAABXPIQiUz9iAAE7AAEAAAAOhg3mXDWpTS4zW+fXmqu2493zkY5tZcYeWyMAAAAAAONqvCEnIp06lK4OIGegp1yrYWKdWy8BuSffQ7bAAloIl2+VTO1mRxv7EbsD +AAAAAABzehv9C0l3AAFC4AEAAACT7yN15axxWGpjC1n+47CGMLk9SeWolSw6FSoAAAAAABA+E4IjPusGLerk7HrCivj8acYXn2aCEu372jArVFnn5EiiTKOyMRsHDdHI +AAAAAACcCwa7161CAAFKwAEAAAAYu2Hy5/kJBwLEG10x8fl0i04a+YyQhOBQlQ4AAAAAAArXQ6PkzrrBgvhKUkTtzFqIxoUXiyHff/hsoJdImTapifOzTJ2OLxvHJJZv +AAAAAADGcizpOqWNAAFSoAEAAACN8pQ3PhGo4sERrtXTpnOb0/zTY7SFu/P+LCwAAAAAAGOKxXywyaEdnE6m2j/R329FjWcT7Ik00IteEtdUebhr68u/TMp+HhusHmHS +AAAAAAEIkam6YfY0AAFagAEAAADh30gW11VBqbrSQNM6ukmK6ZeqdJbifnUrqQ8AAAAAACDHJEysJCNUsN8EbEVwz6SQmJoqti5smpttkZ65j6jzJ6PMTGMyFRuA3aKW +AAAAAAFnsxsL7bFwAAFiYAEAAAAc4v5GJY2z3zyrtqhu7k4ARQ3QE468HWBxBQMAAAAAALGcBXeoG0iIxmCRWbmxxJuqTw2MbxO7yaWFqtPGnA7vqD7ZTFZyDhuzuSCd +AAAAAAHzSIpICAQtAAFqQAEAAABumKkpH8OkEjWXUUwyG9yx0g+ygjDmQu+ttgsAAAAAANaKSEb9lDLzLNfwpprjRq0Uvin0MuE2pITyu95WXMzbEnTlTCqLCRuVWmqp +AAAAAALGiuyDfLOtAAFyIAEAAAAvM57vt+IeRLFZmxK3SNIwrK2425iKeJRQdgAAAAAAAP7NnzKGhchO/AA4XJp1+Iz4RdKW5I2Ta00HmYMK0VYfYyj1TNIcCBsMkObi +AAAAAAO/G/yGVhusAAF6AAEAAACEIIeZFQWvEpVVTNBkT288n3B1KfgllqqgMQQAAAAAACiqr64Z2XrwiNMiuoDw3A8CUEq+Gh8m52ZSwRBwb40OElYBTVNZBRsC1GJ2 +AAAAAAU4Cq0i+3IeAAGB4AEAAAD56JBkoYAvTbuvRNqKp7TLA01rjlf4uFC4kgIAAAAAAJNpGsZYATB7pFLumTP7IhNBHlmmwGrXksjxewgYcrfRG/MQTUyGBBvh1Pog +AAAAAAb1olFOzFrgAAGJwAEAAADd91CQvr4E/QC9XVSUWn53X/IaASN04oT+WgIAAAAAAHpxEA2jK0VPFeGGO23aFIyDD5LA6ZgGwQ9prGOS6jy5M1ohTcsEBBsk2gT4 +AAAAAAjrUYnT6V5sAAGRoAEAAABlHWfOi38ecTZB4glpgP105XtDQCquzJw4DgAAAAAAABST87fUh282dXEyCJyhQITFUHXXxa4fx7UMfHzLTIMSf64xTe6NAxvI2xyO +AAAAAAsijirlaxLgAAGZgAEAAAAOpFxTktzl/dOPLErGEEBuLs8ml2TBa78QBQMAAAAAAB+2TsOI4Y1317Y5+F63s2rL6djntagB7QZyZUVj2ARuyylBTSn6AhuqgXZp +AAAAAA3Hw+JhyZf6AAGhYAEAAAAi6PTZIB2swuWyr3tAauRsnHc1yE/xV4BkggAAAAAAADsWVAiBrukpas4xPzUaJeMtomg9TH+XAOeTxuaSr8HYQMxQTVKFAhs5IzHm +AAAAABDnrS48ncEpAAGpQAEAAABOJQECIjzF1KnyO9RZ8dmmcQQBWSmy1syK8wAAAAAAAF5sBpLOm6f8Htf6nHUv6vCeEywzNMm6cF7aLHM+VdbHiABeTSbMARu5m1XJ +AAAAABVJjeXJZ4UuAAGxIAEAAADu9Ez5w+BKEGyy3xS/gojNg7hei2czMafXTgAAAAAAABFxY2+JKb1awlsDJVEzV/ZNb72GbemXrE1P6F7mU9wqeCBqTc0tARvDXXY/ +AAAAABv37IPNI3JzAAG5AAEAAACWH5gseRQiSptSk/gQzI4C1FcXmmTP9D+7NwAAAAAAAOWKot7bLs8JHrB3lAe+ZHerymH/PYljo7c8vtDmbd8qA5x3TTHcABvvW21U +AAAAACUfql1QEymxAAHA4AEAAAAv8ohrUL/oduWhcjQhj2TAnR4Yf/5ZZfQIgAAAAAAAAElAgSvR2+lo/Oed1Io1FCCTJWyMZrjt+lhRLfLvvEQccQCMTTnzABsay2jS +AAAAAC1pxu0ZLTeuAAHIwAEAAADI2DEKu2nCXMXNN1rJTISIwPY7cdtEJAwdaQAAAAAAAO4A1DU45uhYzf/CTTZf+ophrOJVgxxfedWYV9th+niwFXebTb3LABt8jSBp +AAAAADdPD9bcmy/0AAHQoAEAAAB19iO31C1znhGXB2FJl8JChF9CSsb8CjsVcQAAAAAAAEKihOUu5fWUpyoyUnLOrWnULP+0/Ajx71vtFMyNb5xtEO2rTay1ABvUDtpP +AAAAAEJoI+HXgBG2AAHYgAEAAAA9A+9n6SMQ8fEWH89uNjG80lqT5eQitayEowAAAAAAAHCWFzCW5z2xc8SyGudru79lXrtb2WYukdpyEUTFTurajHm7TfqYABtYmLhU +AAAAAE+Wj98/9B88AAHgYAEAAACXSksZfv5mthMtrzxYxrcfEpISkD7UkyBTLgAAAAAAAMd5iTUg08WipCJhx8CS8T5hurf7gPrOAG8H7U/2rVtz5FnITbOTahrmxhR4 +AAAAAGKCW50/mAYZAAHoQAEAAABNFN3lnjqvgymDGuYYf6pGUSXrx927xDXkMwAAAAAAAPKsZcG8icpZYbkeJtcPV5U8uAvDD09roLWB3nEWSIEhf0LUTfK5RBoalF1a +AAAAAH/as/mBH3ZiAAHwIAEAAAATL2aG9zIfGmonitIhGPqRP3E+Dl/UC0v/KAAAAAAAAFB4Cs4JOIIEgOcbrugep4UTC8gp2hRS/bWJzFgWLwpm9J7eTSGUJhpFQ3Lb +AAAAALQefiGP+OX+AAH4AAEAAADVoF/7eWfud4Z4fPa0j+ZNrwemFuEYU8f5HgAAAAAAAHmhgeO1687EXKlOfzCSyKj3L34b3Gt415bo3my4eJenIcfsTS+THRohHwoF +AAAAAPhNq5KCvJBVAAH/4AEAAAAjYupMfkApQ+F7WuwrHqo61qpHgstMaZhuDgAAAAAAAJjY1qtpNn506DxHOfKO2yv3KZQ14pMjv2NeVvYXsSe9brj4TYUhExoLyjiL +AAAAAWG2X3vudTCOAAIHwAEAAAC069TF2G2EdSVeCC24PEyE0aYjJUtXUyEKAAAAAAAAAHsOGnYqwwLFqEwJl4Rlz5ejh9cc+cPOTWZfXVncIjDa03gEThIqDBoCuZRI +AAAAAgd0Jpp8J4p3AAIPoAEAAAA96H6SIrmh4lozEuOYB/Ocl4iDKvsfGyypAgAAAAAAAFtPgBPFjrDD9Abvtt7rIfpF/ZCmXtK/hiyElBCjLnAsIscUTs+7ChraDJcW +AAAAAsNIQ2KTHCwRAAIXgAEAAACm2s1lZeANbPnchswuTipn5yI5c84iFEvVBAAAAAAAAJ2hK4JwdDtinLnXvrKRr/4jA/ow0xRerF5EztB/g36WndklTgTsCRql2r6K +AAAAA4568Asa4obxAAIfYAEAAAAtJnC0MPYlp2pgsxXHCws0Qq9nv8uWrpHdCQAAAAAAACfx8m614tr8PBSy8Kext/FHqJjZJvH7VGT5PCP0UE80p182TuXhCBosiyag +AAAABHFw4cEr+kswAAInQAEAAAAKXYjM0MVrm75MhKyuclCi1NxbqS9SeD3TBwAAAAAAACySAw5igb5Xvsd2sITcMW/r/LJIerluxxcIrzZTGVXpdq9JToZKCRod2gnt +AAAABUprw7t3ffo+AAIvIAEAAADSRgUSvXLJuiZdImMAcIa1kIP2KBLd9VfdAwAAAAAAAObyH80swCvzJBXItN3SAM8ogjQDawFZivOgF6TUzi4x13FcTuNvCRoWqGlT +AAAABiALqBnpgDLzAAI3AAEAAAAEdlPrXTB9zxKW5rFDpb0f9ZLDR4uHyt5rBwAAAAAAAMiW3XWBljdNYF3OLD6ME/g0Sawo1F/HNFUW11FluM+xDCNvTqWOCRqHj8OE +AAAABvL7X0R+mhkXAAI+4AEAAAAwPla4OPbkCMpZTRG/HHYNIWL+nY3htxvrCAAAAAAAAPy77i4U+Lzixlo7w3Utr1lkWBDW2i83x8zth5Ch/JARaFKCTl3uCRoOcCKg +AAAAB733nt1u769iAAJGwAEAAAAJX4QVGRlNwJoij6ar/fHoUXx91+SF2NiwAQAAAAAAABJbmEq55/zj1QA2CMl2Y5+RccWFa+eKzO/4dAGyq2RmA5SXTkttCxoExfRL +AAAACG5gfCYS94FlAAJOoAEAAAAwfCgqsDrim0wjAIl5O/zJcrCaX21xOqobCAAAAAAAAOD0xYkK2PKCePrC5H16jhMbyyvoZYVMuEQHvKYmiom2FhmuTsrwDRoWLVAc +AAAACP79cJpvSRE8AAJWgAEAAABXRhwPRJeSHLmbYJMdpuk9XbbUsLkkAo24BQAAAAAAANzBngmWFfbH3uV5Ibn+h/YmnIZttuByKXOAYZNz2qA9irvATpoRDhoDxfA8 +AAAACY5HuhbVBVwFAAJeYAEAAAAf79lak+p5Wjc/JGFumn0LZTqkP0BwaGp9AgAAAAAAAPXPYczT7AW2lY4zD1T7vUC0MVSV8xf9J2MTzoLTxmY/JOrUTrFhDxpRYTHJ +AAAAChFZgmZAeLs+AAJmQAEAAACUJ/cMj+wJKwxiyMUhln9V9gLz5IYLSaMQCAAAAAAAAB2r9e2ixMAVCFmJswQQWEWnpRiVgr4pAjAERg1xlQhnK1nmTmiGDhpKDsil +AAAACpwlFy5EWSkdAAJuIAEAAABg7Z7T1Yqlqsyzt3YVJX5x3p+Q6tCovDM4BwAAAAAAANFu2klbATzndp0N9uY2vlDa6LPMt+TLFqlJQWHv7t3YTcD4Trp2DhrnmPoE +AAAACyeIcihEzkv/AAJ2AAEAAADEg4HEOx0uvThscJcSiapp6XT/KB/t0n8bAwAAAAAAAPzsAUUCW4rIEbSG/JHwf1o5ohcMLu4QZiON2kVFr3C2yN8JT9dpDRo1CZm1 +AAAAC73VXDIumXtpAAJ94AEAAACmS77xVScJJnHVjObyMbVAHYGDKTi6vDgHCwAAAAAAAOlTxivTggIx0puQOlthPcJsCIx1jzjl02blLi/e3xVkmIgbTz/UDBrwMAUO +AAAADFr7Cx3r9hwYAAKFwAEAAACkmkpjgOn7AT64NAk6wxR2wJekP179y2QGAAAAAAAAAP0D8cC8X6ruMAajeJmA4tp7e7VsKB/M2VkpV04hazk2SQktTwspDBpqP8jm +AAAADQDD92shJKLCAAKNoAEAAADEVRQMfQ6UxE2SR4wqglkM5HCaKDnfILUeAgAAAAAAAIDF6zgqmMVXOTTQlUNF41Jz7gJAJmgWAER/CytVTkiBX4o/T5wwDBqqr1mz +AAAADaYn4I9tHzJ9AAKVgAEAAACW1DNxxihcn0Squo4M5sWH6A8lvBEbxOB1AgAAAAAAAM3n58PZtu3N0n69nzHusMHcYCtwflEcvElqVAIwzF0vAIRQTww1CxoGNrB5 +AAAADloKPAUfc8b1AAKdYAEAAADQvnrUsCSWVD2f1Pr4ThpOj37uDO6+/bB/CQAAAAAAAM4luTI2jCl6KmxqQgCcPh8KqmfiVME5nP97OfQI4IauUvViT4cyCxonLq60 +AAAADw4XATBzG8RtAAKlQAEAAAD1mgn8pFdHzWlsL+p4+TTld1I2NJ3hmhk/BQAAAAAAAF02L/fQ0eSSvHWa8uw4/tcYhScUjm8xpNa71eNTLgszG/ZzT35QChpQiMH5 +AAAAD9GKm05qR8mfAAKtIAEAAAD2Mh/cMirqRe0nn9Iq1leTS9KCFva057IXAgAAAAAAAHfoU9TLw51Oniu2LYCbHyq0JcA3+7Y0D/RPDpIK1EXeIf2GT+OhChpQgJaI +AAAAEI8lntC9UcgKAAK1AAEAAABZI0tpHBTemiDAT8aDGaaM9JMjcNSsdx2cAwAAAAAAAHXvjFwT3zqD3uCFgRyZucyBn4+FjLpKUl0euFNzZfNYMk2aT/ceCxr60a6O +AAAAEURwkTBm0hBWAAK84AEAAACw0l+csvrkIyh0w5uQUifOvi9sDzN4DD/lCgAAAAAAAIsgmncZxVK6S4NyzNFMUetb9Fi18GyH0Zk9YlP5pacwQV6qTwKuCRpdiCRi +AAAAEhSzp2ra1Su2AALEwAEAAAC4B8LeyLc19xu6Exlvadwm0sdeqDGGK9e0BAAAAAAAAJBhxxlqAJuWFrDLwak+cMYzThvW7+J5CIUKsDTGWf7ylXm+T1+LChoiXXen +AAAAEtPkexWmwmgDAALMoAEAAAAOVsA9yv1hZqT0M/wRg93KXSEvffu/NWfgBgAAAAAAAHuzpeREEX+2b6wH0wGNPrt8qLDKEavOcaQOrDQ+CtMQhQnRT9aYChrRBrot +AAAAE5Ikrv8DV4yUAALUgAEAAABgqLa1IT1k1FXzrrDIMLzqOLP/OEoNEcBfBQAAAAAAAKcijpw/NlTmWMDTDHopVig40nBlHhfaZb+mpe4+Az4IRPfhT4q3CRpgkXRL +AAAAFGGeB1erGaGZAALcYAEAAADoP3M4pxcBIDcMFEoOH4bEhEDBBuBNy2kbCAAAAAAAAFNVavLdhzXOWO+njASenf6QnHLa761w7Y0m669TbsVo8Sr0TzGUCRrNjfIK +AAAAFTQWW0ADOWr9AALkQAEAAAAa5Cec2+NWDxvJVbhp2TO6impbC9TycAeSCQAAAAAAAJwfJZ2A9SLpm7f2CwGKjZbc4TA/3oFGB/udJk7KUA08zn0FUC79CBopvPmW +AAAAFhRfPDePwG9uAALsIAEAAABB6bFBFgVuPvzw/1n0Ud7MckylhKa3HN2RBwAAAAAAAK1J//wVA6Cm1OtFEXe1ixsLmnO2OfqJzKF//X4x92IlWW8WUMk8CBorBXkf +AAAAFwkeDBwPjXffAAL0AAEAAABJtKkDwmds89DuFLLZf0tTKM+1mKWhdKSdAQAAAAAAAKmblvkfiTWWyaVnF2BTlnbhBIieMMWhEzm2d1ncX8UK95onUF6oBxpA6qlO +AAAAGBBlsJgRRBm8AAL74AEAAACryGtgxuPGIrHtB53M5GUrd/kwx7qI6DNDBwAAAAAAAFzIff2abwkijY+AnPqk7dMjm9ZPSqbfo4jp0SU2VwCtfy44UL7fBhrISt0+ +AAAAGTWxEEhUcQSWAAMDwAEAAAAgd13snTsmAw6NVYeXwd7zePNFxBWRZoBwBgAAAAAAAF9p3DIalr/1m5D5HHwIioy0Mp6vqQvXAc96m9ZQTcYtHedIUDg6BhoaJmL3 +AAAAGnlueBNyjGqcAAMLoAEAAADxMAVyLCW6TmMToi8xd/5fHCSs9IP1C3YEAwAAAAAAADx5cYOZfXdScyl5oKbjMjjNawr0xA7EYHrK+JA9lZFNs0NaUIvbBRoA5+c0 +AAAAG9GcmthZbdjQAAMTgAEAAACdb04J1XnJMBWoPpCB/ug6XIsbo8hlFrYfBAAAAAAAACU5kxe7XHxNrv6P4sTfrAzqfk6FkTzWZwMDdyQMrf6TpJBrUAh+BRqEKX33 +AAAAHUCr5swt9qOtAAMbYAEAAAAOv2o9GRg/8y1XLWbrxHEtdKsre8il8scvAgAAAAAAABeUG1JXau20Seou1gCnOZlXswxzeP9ML1f0Pc1AZN7PCOZ9UO91BRpFpYNz +AAAAHrHeyAV5YSc0AAMjQAEAAABAZmBk3O8vEZQmYHidW1GZ5iRfvDi6Q208AQAAAAAAANfNnMW7CBMDtOb30L0LvjerUrWi3VgqqQ4bziAHWSLiYxyPUMUTBRoLaE5N +AAAAID7w/0q0w+tEAAMrIAEAAACEAu3/yPyJMhG/gP38DabMJ9/9EKi5dlFmAQAAAAAAAHvvIIvSBR3lDrxUytZSfsnG6fpXIKLJ1rmh1lUQKZ2IEjmhUOv6BBolhGb6 +AAAAIdPAt1OPHJhgAAMzAAEAAAAL63cL6rTa61a2v+kslB1HkRAPoFYP+G7cAgAAAAAAAPgG0PwzKZZLrkwGcp6TuLOU1F3Wt4JkaXWywHxYCxtdJU6zUOrgBBqXcK0c +AAAAI3D8CCdlC05fAAM64AEAAAATq9as/QRQZ7NbiW0sDnHluYjKSOxGVfqhAgAAAAAAANQbrFUPE8FvhfwF+W7JNRJHVh14heISWRWR7b5VfmSh9SPGUGL6BBqMTELu +AAAAJQXwPTXDhACjAANCwAIAAAAxPP6YZ6jlPWT7RK6E2qfsj7lJ5hF8URf1AAAAAAAAABl/qnGZKjJp0IIx5S8o4aDy3kg4LNgKtRqq5z614rqxNATbUGuhBRomOKY5 +AAAAJmwAf/q9P+cfAANKoAEAAACPblqJbabQH2TbrMtrZR8l+axIM5IqucqZAgAAAAAAAC09O54ycjSjp8dTzItlFq538Jt1bF0CvcEpFWOs4q/BovfrULEpBRpX6ilq +AAAAJ/J3eKXIz1D3AANSgAIAAAABRZOAGGYwZlN50BoxbvPxBg86LPd3uQh1AgAAAAAAALzgN4eDziYqEw119Dzzwzurc5+pUwsJ+JDVgr6ARRJt1y4AUbGmBRp1B5Gy +AAAAKVc6LSc1FRwRAANaYAIAAAAbH93FOSRPWG/NfO3rBAHvv2/MUpQiWVmSAQAAAAAAAKY7Csuyrndi76Fs/hyUdggrElc5MqdWbxukEnHoaooGquoQUTwfBRomGdg6 +AAAAKuDYXV70w/Z4AANiQAEAAAB0fMxQfLC+i0WNqq+UwWj0ilVf2gmVqEyzAwAAAAAAABRg8vGFXXX8G+iq8htYsAT+yq4E/8aBucbP2mQfYiHXNnohUVyYBBojbQ3/ +AAAALJecaPrZYKPwAANqIAEAAACzrQE86vYXPV1BiJTXT+NSxWzIsEoxnlV2AAAAAAAAAK2LAZvcSpmHGAjlUShbA7ZaLT3IfXRkE/gsz1XgLbsplOgwUUvXAxoIBach +AAAALqSBJ3xxQCqZAANyAAIAAAA7RsEWj0exJINl8W5+sX/z0g7DzVIyBFg6AAAAAAAAAC4B+s05egCjPETbDmhebyCCcbBpdbT3u32O/8ay+YpJGotBUfp1AxqiOdh6 +AAAAMOshluKShCFIAAN54AIAAAAzU7TKXCHZoH19IRxxk7aVgWVAAuZn96sbAQAAAAAAAPL2RBMH+xgPoT10EWBjnN1BWoWmBDPMkwMq5bpLx4sS2epOUW6BAhrKeVCM +AAAANA/KxJsaP77MAAOBwAIAAABdlYLIwE/94xkZTJ0h0RYljq3MgLSP0nl1AgAAAAAAAD9CSF2KOy5Zk39VyYYdA1kJupx14DRVM/JLdZ6aCsr3mwxfUb4vAhqsfhrZ +AAAAN6nk5ERfML0DAAOJoAIAAAAAtdqp/fl9ct41VWrFfomB/BW6gH/gb1R0AAAAAAAAADNrYZ0Eb5RiuhZPF7JFQBTvEDG3/WnZbaX8nL0zfFk3ltVuUZTeARobGMYL +AAAAO+BasIUNWPGiAAORgAIAAACjLHUiLhYdG7zd5Ya2O01MT4kHDLgzWMW2AAAAAAAAAK5mqGpC6ZQ7ZnXyK0LXFF8BYbzSI3wGim3WOOFZb+C910l/UT2qARrazPpz +AAAAQJs8dZBfNdApAAOZYAIAAAANsIJdf+Ui6vXdRtBYM9+/NS4SUweXL28yAQAAAAAAABsqQ5um7w9WRee0f7Vb7HNcdvueFJCBObOC9hVPuFhbS+qPUel/ARqqe+Tl +AAAARdubuHv8LyGEAAOhQAIAAAAG0xnwlWIw3fYL/bp0yXP/ZCSRE1Z0hBBkAAAAAAAAAGfFE8jd5wD3Ca0oDZpjKeKdgNrievB4JELPLSNyKE6NYuigUWRhARoG14wH +AAAAS5A5744UbeXgAAOpIAIAAABZRaZ2UpQ6X1IBwpWNV6Yxaay7pbHdBeEaAQAAAAAAAMQVh2dfNpqzbdKuLC0oArnPECnFYm3bzluL38N0NMKaTkqvUTcTARol+LMX +AAAAUuOyUN2GANbtAAOxAAIAAABBCr6r8AfBJHlh0qzRMzk/r+qJrxnub7bZAAAAAAAAADfrE8EX9QmSiaQ6u9Whc/7wQEfbmAwMuExqGTDzucYUHjC+URXeABqMFDHt +AAAAW/e2rO3bMUVEAAO44AIAAABXsaSPxeKYWXcGqFcGrESz7nUvDGdCbZURAAAAAAAAAPzO8HRUDoriCg9qFxl3JwrlqXrNON+E1NbYJ2d5pHIfje3OUU7JABroq+pP +AAAAZfvBv1lJV2loAAPAwAIAAAAAATOj+83Z3UVyvRPuaE5JbdGPNpCpHpuVAAAAAAAAABEhIC2Z6rg2k3BxFgONOwCfOhA5to4O60uuuFykKaEX0/rdUSmkABoCIMgB +AAAAckPqyEdtb+UEAAPIoAIAAABgqXEY9fKFjHPC/BwY23KMNf4kPkIea/iRAAAAAAAAAP598fQiaQ7fEf3dWvsPLTY040Kyx0sGDg9gbgI29y0yc3LtUWiJABr2oHhz +AAAAgPBE6lomxjQnAAPQgAIAAAAmzJ9N+BjMCrhyPsO49PKhVuaftHPkRuFVAAAAAAAAAFG/m5KlGJnTl9fAp1tiV4HXoOk/8bTey9tBrIB//iE+y+D8UfLbchlwVoHM +AAAAkn5c9io9/C99AAPYYAIAAADBZC/d504+FDryeR8KwyspNUcnYTIaGWpPAAAAAAAAAG1hClgMQacQYbiM42rd2qMyZcr1SUoe7bDVYbeXtIzY0nYKUjKHVBlIs5nQ +AAAAqljeIvXX4fgVAAPgQAIAAABYIwxBtfqfi8hO52IzM3cCxgMoJxTnyCdTAAAAAAAAADh1tnsooz0WNQJmx8VhdQ+oRhYUvJMf4sdx18lRoEL9Z74YUldSQRk0RO1f +AAAAyTb5Fqr47ML+AAPoIAIAAACX7rVSkZhpCkUal478RqfWOExWBYcrpuICAAAAAAAAACZt+4ZLsZe8wnMe31jhPrKk217aW2kZbfGAgDXUZRy+LLUmUpxnMRnQtVj1 +AAAA8gbLBpqIXmztAAPwAAIAAAAEFLb5LL5O9l6rOGRcB7XZvziVEDD/8ysgAAAAAAAAADxSdp5+Gl5BFl6OSTmLOnqTASbVjBjHsHOw7YKx0ppLyPQ0UiIiJhkRNGjN +AAABJubv3Tndk1a0AAP34AIAAAAKM90ssmxG+APltlb4X3ZFy5FZP4kFcZkcAAAAAAAAAJwm+n3C0u+pTKFYGN/E+YGiyya3lPrXI/ykIP8M4c3Wgu5CUiDcHBlSx+6b +AAABbMQtf9ALFvRRAAP/wAIAAADB/4TpX5pz12CzfkRAVrdIZ/zYo4LhPMEQAAAAAAAAADmYdBvx9oBrJryElvTvvM1Syjaye4J8LftvHwVecsOk2HZRUsqwFhkbRYB2 +AAABxaHL+NVHoUz/AAQHoAIAAADoHUz2iigStnsjLSd4bTeTg8yet2jbSbsLAAAAAAAAAJE1SXE8H/4K1acSmQz/coP+ODNPp1MO7xcyFtYOvWi9nINeUrYKEBlp4UEx +AAACQ1UC6hrwW/WNAAQPgAIAAACPhaRB9cw7pd7dS+nZFiTvDtNnw3MJ/bsBAAAAAAAAAG7JA7pYje2+TKqKIqcQK1GlzMPAYxrdh99GQVjdxkEq4CdrUoX8ChlyPJP6 +AAAC+twK7X+2FztrAAQXYAIAAAA1cxzFmotPC0zH2yyV3ebxRrdqmc5qW+4EAAAAAAAAAFGUvhEXMIryB1ag6FKcDCOu4svzxBRLWYEcJgwfBNFAY0x5UvNnCBmTtj5x +AAAD6rWvenKyMXmGAAQfQAIAAACsDY1USGpUoXzLTDAkKHrzydCFUFE78vICAAAAAAAAAKokpuJ8XW7yyzLGH5Gz2xplbbp8koaWC6KK/BcOCTCpa9aIUvsLBxkl15zS +AAAFCNHimKfmYljJAAQnIAIAAABUFjK7kH+hlwj6cEphh4oUPXWh7P3DkFUEAAAAAAAAAG9DyVe/GGRD9XOHS1cohSqeNooOwSvIT6wGPdmdZEzbOMGYUkISBhkH1JlO +AAAGVOtvey/80pQOAAQvAAIAAAA0mHeB65bRwhvbelQLBBYh/jUUv8DtBLcCAAAAAAAAALY2n76ecRwvbQw8sOzw7hRlerxUpre5FS0sWl+35DMBDiGnUm66BBnBgNK3 +AAAH/1t6zJkptGOSAAQ24AIAAADfcmfSNpovmr1d5j2K7S2+D65o3cadxYwDAAAAAAAAAPhaPQ0zPVLKuwrBNs1CMA28iE3BvPNX99H3d3XKWNo+WFu1UgyjAxmJ1R/W +AAAKKbsJOIfFC1GEAAQ+wAIAAAA/VPbqGot5LQuRFHNPJ6UN33xeB1C8eakAAAAAAAAAADJ3SNw46mKUdoJEC3uGPBs+37NpUqZICKAs32SY1ijgvrnEUh8HAxlPXdfJ +AAAMw6Su4LKwrErjAARGoAIAAACFcF4ulUEs9DkuVM0xVm7efLf1jIxUjt0BAAAAAAAAAIFj2Y/SNomyfmn93vY9LKpgDuFqgdsxcAjrjgVYn7widFvTUmZmAhkGG1Ph +AAAQC71UHVphkj4dAAROgAIAAADgP0Ab19JISjI+zUtr+ZRaGjXeYaRCNVECAAAAAAAAAD6y3yI4LkP1tSfApriyMLzLk5RwQ7f5NXrT9lkfKJdJjmriUiz1ARmiymc1 +AAAUEZ4ZJqWngOInAARWYAIAAADwfcFacIKz/xtkQBANbOWnmdV4PhYHzFABAAAAAAAAAG6cdoXTGFMoIltzbAilUhwYcqO4aXk6LAviFEIJt1VYyOTxUm6jARlAIc26 +AAAY4DTsS2yFIG3GAAReQAIAAAC7sd2bA0Oh9qwHKXz2EnZUXK1tJz3YUZAAAAAAAAAAABHTtpH4Mq5+Z126IM8dCRM+zDPdicm76CtCnxt7fBKbT1sBU1NfARlEoVum +AAAenV5ZFhaJCAWSAARmIAIAAADgYdTpsw4Pj43roethu8sLq5AnfZTTgwsAAAAAAAAAAHNuoPIB8WKYIFws2Vq9HY1JB6H8uFsPD8G6rfBoqeLyOYEQUyYgARmxDAjQ +AAAlnIvscf0+S/mBAARuAAIAAAD9M61GTCYwmwz5WcXYnTL6Um3u/667T1oAAAAAAAAAAKMaMQbYsZ3TSGf0Udjf2VcvSzx+1irNj7tDD+3i9brlHBQhU7ECARnEQZUl +AAAtZ7+ZYBB+PISnAAR14AIAAABIMc8GBdlAgDhBgYxqAsV4Wok+6Lo0aGsAAAAAAAAAAHRh3QIKzF7zCY8VulZBVJmhzbPwOcUE/3WAnd9YbU5cGMAwU5nbABkWcEB+ +AAA2ljJSEaMyaO0FAAR9wAIAAABeKZ5rt3eIXLOREy9n8//nCLDNmmwGPL8AAAAAAAAAALkftQx9tn66T1C7W5JL6xyPbgfCegMyTVhLJU1G0hBzzt0/U6qzABlkLE1Y +AABBzvUssHCgSb9TAASFoAIAAAC6PytCCOwElbLjdDRlyuK0TY8cd4tEz2sAAAAAAAAAANKH5S6ARcBgwc7kfRzHVZx7irjbWAU5+1X8V5qZjqFO/g5QU4ydABkmwMGA +AABOmwVOeMsHc2uJAASNgAIAAADcDIDt9Mu4Dsyj1XYt+uR37C/CTLBqaJgAAAAAAAAAAETbmnnhaJC1AjaIKUWqGwtnW+w1Hb433Eme8bZBdgjpJypgU2yJABkjpMw4 +AABdRsdNbDb1KtjbAASVYAIAAAAarq5w/eZlSZDo73f4MPqxlSWYgH1jzDYAAAAAAAAAACiEa2cQS2gGiskqmbA9Dzwg4Z9gwSF0hILuRl6+bz3UN9hwU1MwfBg7lmHj +AABtguPJOmnD9J1AAASdQAIAAABmUzMXiUQto4/0BanzgHx9NAen4IW16Q4AAAAAAAAAAMVlK9IfJ6CHNRXFpTj8dB7mEgHXJBPBhW4JhnB842BN7nmAU0IoaRhr2iTz +AACArwrQuiUdIFFfAASlIAIAAADcqCVUPO/WYrMZngKvoTxqrUsBiQGAAQQAAAAAAAAAAAGHdD1IHbUgFw8icB80oUSThERqVVdeLEbhg94otIVHAeaQU5qFXRixibXk +AACWPeQvCcdpQdWbAAStAAIAAACys9IE+9H9pfG/qOg9b2e+cwfAWmTURBsAAAAAAAAAAM0Zs2jqdvVKYE1cUiLRkBEvNk3xpa83+8JMs/vTK5dkIAShU6KrURiCTR+s +AACu7exs4xD7A/iuAAS04AIAAAAQRk0sLUuMJa1ZdBfAg3MNkZjuUjGl8j8AAAAAAAAAAGiejOiJWhvDUIQZ+cStIlPEBXjFUWuJ/y4QSpoKY8tTdMqvU9FfQRjdeDT7 +AADNxIZL2EwV/jmGAAS8wAIAAAArHIYnynCwrIPXi9SmmWqeG1E7RslCshsAAAAAAAAAAMWrKgOiWAzUWX2/FBR48LCfOo+izuaTtNPCRxNYYpzohr3BU+ZrPxjH6MFA +AADtjmjE/SjRXGm9AATEoAIAAADpWcsIJjhutzPI2IbHsfh+ioRrNUcy1BkAAAAAAAAAAO1gHyh8+IQmcQ/clbfzVr6Wa6F/COOxRMSMdB/crB/rZNHSU6KuOhjH/Npj +AAEP6WisrWA2CCybAATMgAIAAABLsmfKSHJrF7yozzRT4sfUd4ze+yS2ETcAAAAAAAAAANB1cGqQoX60u5AEz1yLxlvFL5WmftI0wbD1gw6LYM7RwFnkU2K6Nxi4XNGm +AAE0F1bcWn2c11U6AATUYAIAAACiw5fpBhLcKlyaQObfcw79B20LbpkfLAkAAAAAAAAAAJ2aASe6Uu8icLpRGmEQBC6asItujpoOKkDubRXc8ugefqHzU1gcLhgmDweu +AAFf0LZuDMI3wQVTAATcQAIAAACDMnWpkXDGHgeWeOz86a4oPJ3fZg0YziAAAAAAAAAAAJ147/j4n1VrTsLaJh9plJi5kePFJfC29+U7jbeV5TwMcK0DVO4VKBixqVjj +AAGSHBls3JawQjhAAATkIAIAAADqk8Ja9r3BmT4lRpKN++40NuECrlN90xgAAAAAAAAAAONJaW0QOG0g/swxcTI5Y5Dja9+7gfoEI9AGaOK0HNIRMKQUVOnbJBhKVXAa +AAHIzx1CaD2mEO5gAATsAAIAAAAPavk4Mgp++zVN+dqY8+XAod4HFaLRBxYAAAAAAAAAALq6UKIRa2UCK0N6nJEsg9GMOaFh2I1dJhARQTx5VwtzUIckVJO4Hxhp5XAr +AAIIXQzQMYMX3DJQAATz4AIAAABfim6ucWDYDDzRcTKcq91Q9mEwUkTiyhoAAAAAAAAAAGNlYsg+8U5nM/D07mezkVzm5if3fxIYMMjo7wMZlm3SKs42VHNpHxhfVFYE +AAJIizmTz/JAfUm/AAT7wAIAAAAPJXFbyppgcSKMToKQ3eOwGRK6nRgwRBAAAAAAAAAAAPmc3IyXtbFEHExxUy/soFPQADf2akeZtu6hkPYgfIJZ4cVIVMCNHhis0IKo +AAKKh4BnqEMeRTn8AAUDoAIAAABryq6Of7GLJ3NZ5TWvkiCqj49tO3NgohoAAAAAAAAAAPhpvLQRDzvi3PmC/oX5D4hFa798Pp++VaYGBUfacUbbx4tZVDDDGxhLrvor +AALTJWBx7kRjY3l1AAULgAIAAAAr27E4C0e1x8x7E5mTPdihs5xqhtVakgkAAAAAAAAAAJRyWd2c95lOzQxx18cFywd2EGSnYiZQElZA7f2dn14YJLZrVGFIGxhRGzza +AAMdCeP85TvOJPIDAAUTYAIAAAD+31BVesiYSho2I3lIOvZK4kMuuaysMxEAAAAAAAAAAA2KxHtLwtB5Iczh5LrDq6xugBYSJ5yiroqMNMfqbCkFOU5+VHR7GxiFv4U5 +AANmZQTHJBTIcqplAAUbQAIAAACvJGyzrYOYeow2qZ0G7iROIGnKdkprxhgAAAAAAAAAAEqjhDBvu40xnz9VbmK220mLsKHT+k/dnJh/ckUWCCHXcAiRVHzdGxiXP4PJ +AAOuvnpjW2Yg+9FGAAUjIAIAAAD5GNf87na5jTBRWxIHX81cw5YLIu06sRAAAAAAAAAAAKJQM880j/xFqgWWD6wicStgyihqdNDEHQBZijlxQyqylPSiVMoNGxh8Hgue +AAP5Q90LP/fABJRQAAUrAAIAAAAravTkVTH3NGhn0jxHTncjDtrP4ewUGwMAAAAAAAAAAAz2K1twsw0W53aH90CPYQIWHsJwDt1bs2LA3wV/OJ3ZqQW0VC8BGRgFaH30 +AARJ40ILi2s9531gAAUy4AIAAABUx0Uw+/UR2daHbXfkXqth2AXTyCekvxAAAAAAAAAAAIVkvibj0bsg3IQLFalyFxBhHG6W/Ob16bbLXbrsMcxNUrDHVMCjGhjD2lhm +AASVkTpGAVaKeJ0JAAU6wAIAAADcCuFcrYcxYvJ9sv8z2fviGTqkkuHp0QUAAAAAAAAAADORZ8K98E9ap6pWsauJJWGefYUeJYbXccUZQCxkDtXn39TYVIe7GBhknHpS +AATnFNcvmhfCEIsUAAVCoAIAAAD4QZrnTwgYXJxMZzomI2kf6UfWS3gwng8AAAAAAAAAAGviTewuZJ+ALDtXrEvc2ilz0Ae5pCOcQAVwdqd+i0xz6WrqVDqNFxgyfc4g +AAU8rl4badO1uBLFAAVKgAIAAADohgw1YB6WZa7pbDfWSj6alUtuw+HTNgAAAAAAAAAAAJggHsJXPRATg1XbaCCv2fx5jRdX2cn83yIU7LsQvCZHJZj8VMAuFxixFpaM +AAWTpGZSBCtYHUPeAAVSYAIAAACo7EaE50E1m38zUaYbSRK7GcPret/JPgsAAAAAAAAAAN+76k321Ttgblny9CWJGm+V8jHBXT7EA9F1tdmbktOb+1UPVfKIFxiUXtM1 +AAXpTfXdPxpBx6liAAVaQAIAAABEaLzDhKHbwFL3iM4rjxRMHcjjIuJM0hIAAAAAAAAAAHTPj5EVdrg7q4Y4nUT4W+JX8oQ31qYqC/Jdx4tu6ZIVkcYgVXE8Fhg0fHpw +AAZD91v1txqyGqYbAAViIAIAAAB0xRwcxTqvR4xkO7YS2mvReyaM2b3MxAAAAAAAAAAAAMzAomGKH5c9+sN4J0NbRjq9GMv9DygKkEMtPXhJejbMAvMzVfAXFxi3Kh3H +AAabQ3fB6l1KDS6dAAVqAAIAAAA/mYFKNtKiBDsdS/YaQQ9xgo7KHey/VgAAAAAAAAAAALN2LtJ4rES7lT4kJiz+uVLQq+bTt/i3T9JOAJuWtsuWXWdGVd0TFxhkNued +AAbyn0BqaMRnY/EsAAVx4AIAAAD2RpmBinoT4pBhSm1/93RvVJDm3MrRdRIAAAAAAAAAAEuJqkMTnRSvQtA85jDmCibXYY79jW3Vfn2mE1CimTZbAW1YVfWGFhhFIRcL +AAdMHOWj80CPTV4vAAV5wAIAAABWsh5xyS6HtXpJujn24r5MuBNkDOhqgQMAAAAAAAAAAILMtOTaiINGA9Up/LXhxssWMKwdnEMw4QzSQhgoVx3isVtrVYsaFxgJPMyK +AAejX6SPTLkyaECxAAWBoAMAAAD29DiiCRqzxKMc2pQxVQkbUg9XJDKVkg0AAAAAAAAAAHzG9cuYYQmV95kdCdPiN/nipIbtjwJbzlnj4M3cUvc3Ywl9VUMgFhi2Ml5r +AAf+fN7K4F8UAv+UAAWJgAIAAAAGPC75AWvzKpBO6tYqfMEgz/o88kMZcgYAAAAAAAAAABi2Rh3rDU2fycZjoQFHQzKuqvd1DMewA/tx4ElOUD2MLp2PVY5BFhgmsH7a +AAhZEkG11cDZSx+GAAWRYAMAAADEVA0hTsdntv2BMSH7YQNBAsw4/vKaHQMAAAAAAAAAAPd272QzhiykKFteshui+Sj+XFekHj9YlkOep5WBi2IfjnuhVciGFRgox3uT +AAi2uXaQKvTZ+PGtAAWZQAMAAAB1PkcF7tLrq/Dj7GroyFUWlpRp7xOKLhQAAAAAAAAAANv05xtUiRP+8W4H00okNPbjrGfMdt4NeAXuVVw+tLcBvYSzVRUIFRingz3n +AAkWlKuZ6iMxEO/QAAWhIAMAAADnlPPiXnH9CFZzKKtuau83Zm10erONegwAAAAAAAAAAHZHfpKkuUHl62wICqHNv7q1FFlwPkxkNjhO3yfEE/hzwtbFVQTdFBg6fjkM +AAl3NgJIUTuzEvYHAAWpAAMAAACBWl5t1uTPvhf41vIgDfSapHFsxndIhQcAAAAAAAAAAP6nJYLyiWbEIybbqB8DsLiZ+pkphIOy0S/yW3dlqoP4J8fXVcRDFBiaEXGE +AAnaslxFZ2oPdv/9AAWw4AMAAAB6aDb6o44WSorZWegzgtP03rmuEC0dzgUAAAAAAAAAAHzLZzcUwL2LXqKQ8ewJIinFp5b2aE3C2NfshA1FQ3I9w17pVcFNExgnIqOG +AApDInpI0hyEKEPOAAW4wAMAAAAq86VtjVrbZTFxLkqqwp3ZWoV0/uwdxwcAAAAAAAAAAP5PNK63I/35bvFzoTOk2J4j01sw/q9znqdo6Jax+V/8bBn7VbqHEhjH1UZ/ +AAqv7nbV8gI4gQVWAAXAoAcAACBnp0LqRjXO6+s3Xmh8h2kFduYbFjRPzggAAAAAAAAAAOx90gednMp8jwoRnPuL430VF6vl3TCv0MKRTfo+8Jzmph4NVnIUEhhp7WAg +AAsfb9PKyG915FuEAAXIgAMAAAD53Ve9nlz9+3dVCpNYy7pH64I2hPQzYggAAAAAAAAAANHq67w9uwfVnA9Laa+5lRjg5PxlEsHNNJWztuvLrCejz44fVhQPEhisoAWx +AAuPEqIEXhjZL5S4AAXQYAMAAACAFfcaPPZS1o82T4h404hrPHb85O4EqRAAAAAAAAAAAFuabbGwhWZQ+jLvOayxrCx1haRssKUd374jDMf82FoGop8xVlSpERi5kRXr +AAwBORlyRT6CKd6yAAXYQAMAAAAuPeiomStbNHwU14SM96JPLWKddFRHEQwAAAAAAAAAAFC7sDvw917FPxkr13125Kr6GWDoAVgG29U0GXgH+ugLVBRDVomyEBgVk5G5 +AAx593VzlH8jtrJaAAXgIAMAAAAd8AR5EiSXrsmLHjnhvf+uiZrMMp4DpQAAAAAAAAAAAHS7CaZQHNLKgrZmaLm472N/XgfLjwnI8WnyGL/wNEBNrsxTVnYeDxgcuacc +AAz/UI/qgIbZgN+/AAXoAAQAAAD6czd/P1hFRda1NWOKpMwYw1kymz23sAMAAAAAAAAAAFIcelNdLVK8TQYGtH6yy+VDEXumqnd4De9k+zYzuSTmLMVkVk/mDRgbmM5N +AA2QXhCcuWjDvZMgAAXv4AQAAABCUOD+y5YCqA4X4Ek7fhzNDP1lvcW8mAEAAAAAAAAAAFQt2ikzPQa5wHMP5SPZEzw5Yt6yFJIbTl9QVsVyqR7vqWd0VgnECxhMqn1Z +AA47uLb+n9qZN5rSAAX3wAQAAADn3BPGQhTIArt8qQkINh9JExTc03nzYAoAAAAAAAAAAC4SBqkpKQ20aa6NvaSJFHQLVkKbjqulcxtzrRkoI1hDogaFVpGVChj2TOSo +AA76M7POkqug+Z9NAAX/oAQAAAAHKZZL8pX2fjMkS4/MHTd+3Iy6+KVziQMAAAAAAAAAAKSFN4eizDLUmtQ5PfjBNPpmfmpaQcJ+wjCSqsN+dzFhKvGVVhuzCRhZxxSe +AA/KDQ5jmSg3XpPsAAYHgAQAAAAZex89L6zCAW+MKtz3OioFPZF0GUfsuAQAAAAAAAAAADLy7iIjtSj6g5Q+8aIgDxvKKG1lW4AEriAH4zQlNwM6OWSnVvAoCRg6ZpfC +ABCmKY/w5K9ccX99AAYPYAQAAADE/juFAwCo8NeGCeT0q8etEEIc7y5XXQcAAAAAAAAAAN17xvJ5b6zUu6ZM4I8ALN6/1Ga9wQD/uegKKCxXpIeROMS2VhShBxgC8+Oh +ABGubW6S48XcALNeAAYXQAQAAAAgvLMxNKb+P5jwSIK6kkPHjWQH/NueTwcAAAAAAAAAAKqOJteTwad0SMWRI8hg+EKqq9ybNkMoXsmulA+kudEOyAvHVp+5Bhj0mtfn +ABLaMjZMj0fVGWBNAAYfIAQAAADqsdQ60ilyN2qK707hr5NIfzorxtmkxAMAAAAAAAAAAAdmm8141IyLhH6c6nbSJ6VrVLyibtpcSvr9VSpQoK+0hBnaVqjwBhjZyz3h +ABP8sJiJmTFclF8ZAAYnAAQAAABHPte37y/Ogowxj9XlhoNEpTVsnpO2BAQAAAAAAAAAAEQJyuW3svjxjqVfVYyb+nxfR3ihpTFypI/FfhctDtPSZMXrVsOkBhivm8HH +ABUsJBbzwzhj/RTNAAYu4AQAAAD37yiBuKDLQVuoHoicebxfGwmBZ8lWRgMAAAAAAAAAAKSIaf6NZ3eCH6hVJROct30SxEDBYYLGN+lD3+p9k32qexb+VvSWBhhWKCct +ABZeFeYwv+kd3mJ7AAY2wAQAAACJUWxLbjfVgA+fnjYJOiuNqEDXrOJFVgQAAAAAAAAAAJtNP14hQ/GkVCrywXZ4CgxX72aDphhfHF+Nih2UWbLkBVQPV0snBhhclofi +ABeltF+z3kz9tZTwAAY+oAQAAADR7x73u97v/1ByQ7M+/tZHZYwwOKHmgQMAAAAAAAAAAETWTugwPNlatypt7HduiToghEoXJhzY1x2YAHP6aZITrskhV3YnBhhaaUYC +ABjtTYrOHh0qfIZKAAZGgAEAACC8uMT/qxBBSnWtThC/67Ff5gIhu90AIQEAAAAAAAAAAO7KSzplx9PRqtr6XfVNa77PAZgw+qVEYJBCnVfp9e5DYsQyV/qoBRhCJKGr +ABpRfLdgJt4nqRNjAAZOYAQAAADmLvKMuXk/T5zSpnpYwee1kxKbm+VWbwQAAAAAAAAAAGSfiOKI1oe8gZu13AlKjE939fRsL2osAVIllw97PFFXdsFEVzaEBRigCrKE +ABu+78vXOVeKMGz1AAZWQAQAAAC7kyyQhvxEfu3SQkVNRZNvR/SRHikcvQEAAAAAAAAAAPrreBAfjCpsAEti4TRoZ1m3UmWvpBs/zI7c31XPADDj9oVXV6CbBRi2fCdV +AB0mcOta9RlcQHkgAAZeIAEAACDNHXKw/agnsGcB2HlbfO3Wb/91M0h6wwEAAAAAAAAAAHO4pz6FxGfyz45cs3SgR4/MpfRnt7UwhSKVEaFATK6uQs5oV9Y/BRgO0kEv +AB6mfdbrgpnwJCwPAAZmAAAAACBCLsOepCYOv0JCHb+GM1fEmFLawrQEGAUAAAAAAAAAAPnen2pzKIQ99/GjckUdhUzoQxvp3eaGMNlal3NweVcOMe56V/0mBRi+cadQ +ACAtxfsiGV2z0xFqAAZt4AAAACDh2VuHjAFXFHz3IpMZ8BFDKffxiHj/SwMAAAAAAAAAAGA+JCR1kdW3pIFA/zuSI+PMFCTWoRZ0YDqTbhQyQcoHwmSNV2kmBRh4+yfw +ACG1N1Lmnz65gcjUAAZ1wAAAADAhYiwmpOYsr6jkNMfgg/VAvMyDkjy0BgUAAAAAAAAAAEj6xboior1IrAoZHNGBEYjQ2Kje4gVpfSmkFC3OZu+gguugVyhyBRj9s3TO +ACMnalaEJ1NTfQI5AAZ9oAAAACC5fXDa4zD0YCudAQ8FeDmjUAea4WhwkwMAAAAAAAAAADp+YYP4pmo57SYqnmIshNH5qkvcEszlYEEz5KjgAfqRAhGyV9wOBRgYcYix +ACS1/dDcjk/vG+0YAAaFgAAAADBxOa6PS2sbIN7ep414bHCqZbmePOz24gIAAAAAAAAAAG6AhNzsYuvOL4V4labW7wL878syYnk9WxrOFWY2xQXkdULEVwj7BBiJ1sZx +ACZKxHB79ab4pJEeAAaNYAAAACCxliE8dmLJ4CSzvJX4FUO7YjWxNmYHswAAAAAAAAAAANxevoKSYvotUK1wKjEkvrYhWtFRgroZhfZ6HQEdJRcMi03WV17eBBgvWejH +ACfo3I9pX8Vg+F38AAaVQAAAACDVu7BS4xgWVM6zEOwFAjuj+wwdHZa4JQAAAAAAAAAAAP2sl0wjNKxVo9NGX5riTpBQZyeLPYvLOFh64GhzY1WMB5bnV9SOBBhlmsXv +ACmjLx/5YpHTniFvAAadIAAAACAlCbO45PmCkMfJVR0YDrKkY/C5eIvVIQQAAAAAAAAAAKqNoGCd1mcCs+i1I7Wi1Lv1jroo7bHepqVSZRYRkszZKtD4V8RABBh++Xtk +ACt9MvFVV3nfpKwMAAalAAAAACCjLo4nRVIWoC1HBLOjxHMc80EX1GjZfwIAAAAAAAAAAJSPpGVCwtMaN3M9ygOyzIL/rcBn/WA0efZNdz/PwTgB0qkLWNJVBBg9TuO0 +AC1OOf5J2N/elcOrAAas4AAAACClkM9dyJtNLsIhRAlyMdEJwKK4hDGiZAMAAAAAAAAAAKN+LTmq8x2L18AKB/qXhP7oirNhCC8DYBEZ2sSg2HyNAg0eWHRRBBibZltl +AC8hHXGkPdcuVDR1AAa0wAAAACD/cu5pp48ULk+ldNcleEBgQEIEpdQuKgQAAAAAAAAAAHU7sAH14dJP9aV9uCd4sKl/gx396hdhNq3gFi01w1CsF7wuWNTmAxhrynAR +ADEl0i+0xC2+0MbMAAa8oAAAACDTxa+JdxdIkPF+GouU2Rntb4DyE592XwIAAAAAAAAAAC5Atl/p/FL5QjniJgRHOm9GFoTgaEkkyfndzyMUoN6hguBAWInVAxho9k9N +ADMzpbAMPo21HkkvAAbEgAAAACD/RceD0JcG41ncx2CD4V5Rg55O1TH/swAAAAAAAAAAAIQVlwvcyDUpOhEO4jh5dEs+FTj1GaP2+QmNotoCqdQzwfFRWIWLAxjG3f4O +ADVsV7hkrnpwZcyeAAbMYAAAACDwZ24cV3C7qIhqq9iL/T7CZs5oI8vNHgIAAAAAAAAAABjU10kV7C0+g1eqjT5QD4ZeokqZ0NKsN41JYm5nuMmzJ/ljWP91Axh6IlTj +ADey3SPIpnwtZ+nrAAbUQAIAACBsEgkEvq5p/Rx9P3I6P9zBJyuGO7MslwEAAAAAAAAAABusRqpnJ9rdKjeMBijz6TSo4tfR+TPF98Qw2ggdq0CN9GJ1WHlDAxggViTG +ADocpOmmgGMKNFU2AAbcIAAAACA+evIAa711Ial9nNhBzAxngI0BsdImywAAAAAAAAAAAI24Czc2VsD4Mg6PTX+zNtohrAfjoruMFLnRJPSjLyRnxDeFWEfMAhh7mQNN +ADztMjVjN2XegxWfAAbkAAAAACD0IUtha4NA5/e01gxpnJLNymYvArTedgIAAAAAAAAAAIlSU/fahyQ4LTjcyd0FqnYoFn2u8Ty+LZs6tWv+iXkUCWaWWLmaAhg8hAWG +AD/zSsNCs+c6akJ0AAbr4AIAACAXD60LaxzL3EQB17HI7oaMaXfWzhJ5WAAAAAAAAAAAAOYp8InMviL4HtmJZ+qXbpQZa8lqYCLwZB7gwEnpTc4LEhaoWJN+Ahi5Hn+P +AEMbgr5qXa66E8ZpAAbzwAIAACDI57WbtoNcjZuu68zYzr0eIsRqJJqYnwAAAAAAAAAAAPX/tG09B7hqaw09bNTpqlu6LSU3NCZW9Wc5u2QaeMaXTr65WN9iAhhUYqAB +AEZoYHlBPSBbr3gxAAb7oAIAACAH1iE5ncNJo/fFTdZtBxMs6MSJi+2LAwAAAAAAAAAAAEguaQj5jPDyXdaqhlieIH6e4iBSliCangqNRT67w1A0/5/LWLFPAhhCxCu3 +AEnQoxdDwQphqRzIAAcDgAIAACDNtgpyTJf65DaaZkbLtrSlowAHWH30IgEAAAAAAAAAAPP4LeHjZPYWiIpTNZtu4pShjH6ZQAPVMaUuCUpSQJEGrDTdWFozAhgqr7Qk +AE1kxf+UqjKJBDiUAAcLYAAAACCZ7QwAT9f0nw5QAqlk89D+vZBqpZ6tcgEAAAAAAAAAAFxkwm2ofXOccNo4M0KkS7ST+vKvrvhUEcxoEAL/sKsOpunuWHMcAhjUm2BO +AFEftqLxzn0eR9qEAAcTQAIAACBx3BDH0Kh8Fnu1v8WZll4FxzOynULXAQIAAAAAAAAAACdBXTfPF0ByEQdNFp52bRgRyS8fEEbSJQgHrASPAxmcUVUBWT4bAhiVIS9H +AFTc0w4mC90KhU9hAAcbIAAAACABgs9JpUR8CNbE/WGfkGy/79uKKptUwAAAAAAAAAAAANQNMFOfWTFb4YvHrCLuS3dwGavPGkXzJl6+H9boQj/TwIsSWaf2ARgA0Z/e +AFjfmkHxo0TiewZzAAcjAAAAACD990Cw5Jz3W7PVFo+zWG92E9zFzYlnWwEAAAAAAAAAAC43sUTAus7Qfrfntk2pFs0xIfJCcAVVGusOxqZAKsfX8OQjWVTYARh/Xan1 +AF0kV5ADzGNCCJLEAAcq4AAAACCoGJH1orG0in8CAflT/1tGl12En7IxOgEAAAAAAAAAABngc7dr0++rCbkOMxmNUC6BwrSKgFVWaXqbC9Z1kGs2mxo0Wa+eARhX3jJH +AGIA7PpiYqjrFEnBAAcywAAAACA0cRAbvaP+MHZksyg6nvDpfZo4p+rNiAAAAAAAAAAAABDIq6hHm7ql4ISBUv08IonKUOHD5YyaT6qvvfWAPFRI3bhFWX6LARjkOoHT +AGcZ3nr5KS2Pc8sQAAc6oAIAACCZ1qcMVHu6oaggSQvQLMN407xuIEaUOAEAAAAAAAAAALZqCwJM/fB9Ddl+GK1u8aQRsEUhKdO/4+brrlXe/sTdlUJYWTCNARi8JgoI +AGwtVR5QbbP7+NYNAAdCgAAAACDc8lFxBEpdPYP10NB3dPTebmsFfyse9AAAAAAAAAAAAIqaQsinfGejMjtzlgVoO1H9HkGbtAp0H9y7Ea8y+i0itoRoWdxdARi51QOB +AHHwiS0wpgoU4a2uAAdKYAIAACC4eVrYZELObYKJcY195BPnOHWbcXMDDgEAAAAAAAAAAC9LtwzWjKpXXeYF6TpIN+wGjqi8hahEW63uWpIUruMomsh5WTVHARjEptgJ +AHgZ3qmCxIkEdFyPAAdSQAIAACBmozoTm7ZwYR1xZUlhh3dbrQPXC9HiUwAAAAAAAAAAACB01a+exTUMb8xLXN3oEu5GAt1QA4rgb6aIs/4iQJCf4gGLWeAwARhJp86v +AH62plJTHFrWpLjpAAdaIAIAACCAG4FikzS+jnr16/ud8JwY4fgztfDvywAAAAAAAAAAAEDRygd/7+f7eXcRuqDAY+ypuO2Uaa4BKJgrRK0MJThkkTKeWek8ARgi/1Qi +AIUTNK0P3kp+jZI2AAdiAAAAACB4IgQMwEdDBLImi4thEzFMfB7WimOt0AAAAAAAAAAAADxwUP3ctm4wgFMTeDsSsSxsYsDrwyO5JXbZSA4DQLmHw/avWQsxARh2Zc5p +AIuvP/EAUEY/eZ48AAdp4AAAACChi8JZfSP7eFbEyvTKqNeidphjQ6m9pwAAAAAAAAAAABMYNEx8WGnQzO0yKAG2lntPZvbc3DGn+5T2DU5f3XYFNGi/WRj/ABhT01Vf +AJOWbjJFnoH8w2ISAAdxwAAAACBhjkMKwWl3WZliupk0hyPpY4N8MGps+wAAAAAAAAAAAI3r4znB2OjMHzJ4LgRcsFKQed5nBd7UAnTHketY2a4yAojRWXP6ABjYNa/f +AJujLZFGK2U7jmYBAAd5oAAAACDZXG7YWfHa/yfFmfHx3LEPqrz/R4FAbwAAAAAAAAAAAHeJUflLt8RCKl7vCcWt2zkGOhJvC8pwh05/YSs+PSwzbt7iWTDrABgbBtzU +AKQ10C7DKtkVjGQHAAeBgAAAACC06p4elhmvzrndOTx91MCSdz009zy+4wAAAAAAAAAAAMhj08Y+iZ7h9uMg5zsbKn9utdrVasel4WhCGpMt79Ub2hLyWb3BABgwGeUD +AK6dnX5YX6bYxakBAAeJYAAAACBpopofHqz8DGjp1cH5IH5ll/He1gn7swAAAAAAAAAAAEcfGy5Yer3X5Jf6aFd7YUvadTrELBgixG5yiuiz8mMcL8IFWkvOABhGVPBx +ALhjXJ75VIpo6JKGAAeRQAAAACCYKvxq+oBeISruI6Wz3y+04UVBBB/nvwAAAAAAAAAAAEGhEf0MnVYLWlZWQM2e/GZkkCsKTxocdj1ppfi/z9xjrHgYWvbQABgNbgyL +AMIJZ3KMyNccgCcuAAeZIAAAACBl165IP0v4xn2hf9HL0ZYPnORu9b/jzgAAAAAAAAAAAIqwdYAuU8F/SVUQIJH3F0Q8KyGIaWt/4QwykQuEUZIdeRkoWu2wABjQAO0Q +AM1urjbAzuVkCJbTAAehAAAAACDjfICnqLhQ4+ia3QkJC2uJuHRJZRPviQAAAAAAAAAAAChd3HmF58NOsgXSwxKXJ/e8EIc+EeBUtI0aknhqQtfSyMg3WkWWABibOqeS +ANrZM7W6oNFt8uZnAAeo4AAAACDdX5Qm4nuyoj9Y4VriMauNMXxBZ6AFMQAAAAAAAAAAAGLVx5fnY6BKSpQjKjfcc8C9mTHoCoOlzz1Xb5CwEbUZWrFJWsGRABgSLQaq +AOiuWCg9RKgiU0urAAewwAAAACAjkiq3e+Ve/QXO4SOSork3k/Jmn6MNcgAAAAAAAAAAADy5mlToNW73oLuFQh7YzHg3NFhngjaNfLCu1d4M/pVhYrFZWoxXfhdJt4bf +APijmUI2IMbP+2pXAAe4oAAAACBM/YPL1ZJj9/n3BvEUFYY4ICzVy1eJagAAAAAAAAAAALp9U/IU2smDwyedYEAoN8kvXRZmTbNpL4oGbgFQkLGCtH1pWkYhbBflmY/7 +AQtIxK9jt5XeADXIAAfAgAAAACCBBqw8e44DUlaAnv6UjDiUF1QyVOVoawAAAAAAAAAAAM5recvHh4mNgVvBlCSK4/Jhi+vcsxryfJnhJD1eKEYyVzV6WvjpYRcRjX1F +AR/fzayx9nuiacKMAAfIYAAAACDO+kcNtN8CqD3VGt9QEtvqAgOf6t7JAQAAAAAAAAAAAAJVqpnEuysv0y+KFVCd0QW055B31OyVHpJXOuquqbjba9qLWtyXXReVC7vR +ATVqTsduFrCR3WRiAAfQQAAAACDRBJtmkoyHSm9OfuU/amIKUxGBd6iqEwAAAAAAAAAAAJqS+ximZ7/e6Oqu6Y2826uwkqPJ9R7yuHuQJ01UhIp/vsWcWqOJVRcEraJw +AUz8BKngasmFxGtjAAfYIAAAACAwTKyssD5pyDS7/0vlMkowmJt4mKpnLwAAAAAAAAAAADezJ/Yacj838NQgdAlCR6Tp95q8LTWAQwdaZM9suE+3IlCuWklKURd9IESy +AWXI367hnoV6V9zpAAfgAAAAACB5yZNwQc6+27xRkPbcbFuyJ+6YIwdfLgAAAAAAAAAAAHO/8/9O8Ui1rvLaH5hEzap0MPcwldOTvrvTlWSaj/9I1YTAWrcqUBcMmrXm +AX7u8fTowemGBM0dAAfn4AAAACAYDV0zdLyxX+sWip70ujsF4q0gfETXKAAAAAAAAAAAAI1E1QMy+cLTC20QKlRrr7Af1S03Ni1yaPT9k4gGli3NZGfRWg1QSRdLHQYe +AZpuw9LU3K9Yi6dQAAfvwAAAACAQZWFUV2wXJJWeqglMLb6HMwS5RpTYQgAAAAAAAAAAAMforTYxahXrERK4dhIyr2OWT/bCi0HXBX5YWXb/t2JMXQjjWlP7RReXH+lF +Abc9mZiFlmDZF06CAAf3oAAAACDWa9SFVsinzt44IaA0SBcTLKubNk3sGAAAAAAAAAAAAKtczARVeFx0TAiZB0DG74AGATVTY4oYVswDDRTv6Uxox/f0WqnsQxdBDCJJ +AdTr1i38PmyiFZ5sAAf/gAAAACB4ZuuNURVJDIWcDfYXO5ZrxMY3ImxKDQAAAAAAAAAAAJ5OdtT4l3HpapzTwfdfSuEc6sNOOITWBqJEjjdZZ9IUNbwGW0laQRdCG9nt +AfPFgOhy1wDEwBC6AAgHYAAAACBqtiGOq5D48LeQU65Xa+B8IyHahrmtMQAAAAAAAAAAADlUYADGVX+lwB8xxK51Aof5PKccBvhJNPUFvzJh+wMl3tMWW0H4OBfe970O +AhcowLPm4MSblXVuAAgPQAAAACATm0wBBag0mPkplc0zk1WJN80BEvQdAwAAAAAAAAAAAIlmBPoOz5WwdnN3xVsIAdIMAVXSmbuy/uYrbfC0dzkJ7cooW1ZvNxdWhKeI +AjuG9ZjLnxX5qtizAAgXIAAAACDYzyjPVWQMfqZhUfRsLd3JS+GN/IvGIAAAAAAAAAAAAFHksafEa/I1XtqntoZ32R2wlMPh8K6/sbKQa8woDPSj/UU6Wyh6NBc545gl +AmHxd50qYovic1m8AAgfAAAAACDwK5wok0gExoguH4yK872mRn0iLuXcCQAAAAAAAAAAAEBuodyPhcLelI0f+rpug3n/VDPohe/T0TQiidj9B+5DmWdNWxdaNhf6H6J4 +AocJntsfncQRNhF5AAgm4AAAACD+Fi95ceJM/+CsiRaFFUEUz03k9JLKLgAAAAAAAAAAAKprEDp2Vq3kFitaooGc74BKhddkkRfoJqbsaUZb1sjhk3hdW3tPLxdc6RrD +ArGmue8BkhE2O2eFAAguwAAAACDF7sKQecJh3F4TffJV/1PwlURf6D2UAAAAAAAAAAAAAEHVzdgjWDQ1xUVucTIqz3mb/0iksYF40IiT1IEJ93sWK69uW6cNLBcKESRc +At9qSfVMp/iybC+YAAg2oAAAACDPsRJxflyQ79R2wRdtX1uoqpM32UidHQAAAAAAAAAAAFGjv1vfVTUk9oQdbi43LK7dar/Kp74eg7UNEIwO+tA6VzeAWy3XKReXHySN +Aw+ZY3Zvo76cg6xfAAg+gAAAACDOK/C5+6SApOvZ7bjo+Vy60Ua1FuZ3GAAAAAAAAAAAAGQ3cXV+MYgH/kPPpmqjJAo7rUgxz5hU9n08OQubdnZ6H+2RW6EZKBePcQzX +A0HfsrWhrUX/AS70AAhGYAAAACCke7zeOAAMRUgCSw8mtIrWKS8/eCzrJQAAAAAAAAAAANtrcrSw1Vf2lYZTvXiofzXjV7uhkbydtdfvFYJfbR9W+w2kWx9aJxctEW7n +A3Ua0mxdHmZ6i9CgAAhOQAAAACDihzv+Fzl2xvyjuUOjjOhGzJ13p7hhDAAAAAAAAAAAAAa0s5HbL10ZtHHgWwcQShZ3cNG/WD6FxZh1d3MukiaGAsa1W5HBJRd47Eoj +A6p/xq47klb8dUKnAAhWIAAAACDGSqyFPFpIQQ3a7mcBZCu5oKbb/qnbAwAAAAAAAAAAABrjq2aDGqfl7vaTs/7YYWxgGRjhdjRyh791ZinAmqQHG+/IW70vJxeCTESF +A93yCPQZPI+uNBqlAAheAAAAACB72/e2Vw4+TvIo3pk/iVWSFrZ+7VvHGgAAAAAAAAAAAH3IV1wi9LLm56mnIQsX+BbEzUTE2/VXs8aaXzOM5jIJ+GfbW5ItJxcEKAsy +BBFmqAAfbeNmLFCnAAhl4AAAACBmF+NNB4n+fX0oF/2E3qKoyM3F6ZZXDgAAAAAAAAAAANfsscIddXjoVx+x2mAvgTWqjkMmq0LNUHDuzFOMrwTs/FfvWy9OKhdhIu7X +BEENCGeaYj2soniZAAhtwAAAACDirLPnHk5EOvSOgdOB3qfTXi6NXmn+FQAAAAAAAAAAAH8q2iJNxK+6bKNwELCZwCMiy13yT87bD/W4f7PKZO6uoBoFXHzZMRdx8oYe +BGl9nUzOC8OwGd2XAAh1oAAAQCDTstfWGtLZX/vVVtngDweHdCNgCo2gFQAAAAAAAAAAANGSdDosGQp0Ifkv7+klBVede47aVoys7hOyV1GscExmnYMZXPQeNxchuuPn +BI4RD4GFzbbLp2U9AAh9gAAAACASCzJkVi1J31nEAKDydkSNsqmqS/b0CAAAAAAAAAAAAFy0tSFQ/n3sIXt020JORC74skEFwkTrrrWfY425xI7zyU8qXKUYMhe0EqUw +BLZPYwRzFg1IUIWVAAiFYADA/y9lULXa52VZWJ4+PhNSNwcra8SYlJ2mKAAAAAAAAAAAAFmIeDQ19QbSzPuttITlbW8dXf3UgGUKyuHjtD00ZOpzyvE7XDPWLxcdUI/b +BOB0BjYhODsJmRMKAAiNQAAAACBrBb0sSgaz2FA6AzwlkzlqJaeeHcrbFAAAAAAAAAAAABsI3z1CzZo42LZq353F60ZPUDYzvYYQhf//cjY0UxWWoaJOXDVoMBe/Z7cq +BQoZ1ZncIovd5BnrAAiVIAAAACCuVddkC3OOHBYJHMc2ZlJuf6Eq9mwEGQAAAAAAAAAAAPeCX+BxQnX+VFIfZuiYz3Q+1D3ZPxhctijfmVgj5O4tfVhgXIhvLhdtCFpM +BTWEDv/oRyjopOpwAAidAAAAACAM1Taz6xzZwCjggfFFUAYnayk0Z8PlFwAAAAAAAAAAAHvBsnSJ2wHIXTikvG0igGEemAT1Btg60A0qM+vWY5kvdsdyXFBbLhdPuQ9V +BWEBNNZ028ybHZsOAAik4AAAACAbYeiWFxCZGkf/gYfZRtk+T7M1acCWIgAAAAAAAAAAANAJhlj1NTHm5n/JRImGtaj5lNpC10YHnqvhD1XlYeJDED+FXBdhLhc1xK/b +BYx5OM08JlUHiLPDAAiswAAAACAUwxRraAi40KvExvnuCdfkB396o3rPHAAAAAAAAAAAAGgUmmK5PCyfkfuqKXPKG3m6Efxe6MbM6fAYYeitAs2CVc6WXGwfLBd3+r94 +BboqD0YQ/vPy0wHrAAi0oAAAQCAhMy8YgNByq9ZaTqGwn6zbMBFxp+fjDQAAAAAAAAAAAHGzphJHpM9riSBV8ngkfzPXb6kLSMdv2mn1gyaMuWX4rzmpXB0HLBcAIgYt +Bef0E0Y8iSEIXavFAAi8gAAAACCqJG7LeBacfvZ1ehdco+WM6V0kwSloDgAAAAAAAAAAAOR/Y00XFPF2REkCTfUOyiXU8RgRXO8sRknJjuFeKUdQVM27XBFOLBfhaHV2 +BhV1HVeWh249HUpfAAjEYAAAgCAqUgoJ8YtRhvZ5wz5zvinPnxERXFCyAQAAAAAAAAAAAJDPhQlQdAWfEiNUUxhvEUmEP/drtJ5rgoXJzNhf2QnMDU7NXDj/KRfWGLus +BkV2AoDsDE62cnaTAAjMQAAAACC/Jis5jV91U1f7bLXc1DWFEpJRbN9KCgAAAAAAAAAAAF4Pe1dyXuphL2uI+WbWooL5R7btdffTz+USC8sK5iEd6MLfXEX7KRd8Gs1V +BnV8GunthUu4x0TVAAjUIAAAACDXdi8B59cAJ+SzQJf8rDogYOr1lKuFBAAAAAAAAAAAAHkiwaMxpVidI1uP2PSAZvDJ1VPm+H9LJkbMzj0NxXYueFzwXHa7JReuEDNC +Bqrp5qOY/694OiFZAAjcAAAAACBzxqRKrJh4FvPRDuMnFO3W+yo/+228DAAAAAAAAAAAABAw3deFEkJ3ewNS6pskyikSV+f0b60vDw1At9NVCQ6udvICXQP9JRc33+M3 +Bt/8BpMe+l+0cvlVAAjj4AAAACA0pUTAWqILhKoJ/P6ZJlUA8/VY32WYHQAAAAAAAAAAAKQR1SRjcuTNE/r79SrUYD1HUiH5qda9A2NpSdUJXSEWEjEUXSx5IxennK4/ +BxjR5kMqYX5AXwC1AAjrwAAAACCmOTRY8Cy7u4/sugJgpCO/mHoLwbZeEQAAAAAAAAAAAOZIsqYbzRmDz1PwaqkAMuLWLgo7eKT0+M9L+DDA29a5vFskXZsNHxfYMtrv +B1m9nmok83qfrm9oAAjzoAAAgCDP8OB6s52w8x1N7YG6IzkXMVW5xXg5EQAAAAAAAAAAAHotddzlmB7EIaVN9wbT1Af2bckXDx4NbkjtHoocrXck6e02XQg6Hxe8Q7EK +B5pN58sRlHzFOR//AAj7gAAAACBSYArI6b5PONfRi67pcVj7cWTbJI1rCQAAAAAAAAAAAKh1PP1FNXL/yUwEMhXpYBPx8f6iC77T2BDBwemNb+RO2JlHXTkwHBcAFrew +B+HS6cYPTvV6+snbAAkDYAAAwCD/aBvTWiuCtoAhxbQRpupP82voj93gFgAAAAAAAAAAAIDkuWjcUvoGa94vzaFmniQN1z6cVysCVsBwbetPXZvp5rlZXdGjGxdcc04A +CCrDlIUnc1LZ8Nj7AAkLQAAA/z9FV3KiSlGDfga9ToHe8ysq0WioRw7iEQAAAAAAAAAAANUoIlkSoxVo6aV0yNQwBWNFJQHWUc5jhRxig6Tw4XsQVS5rXT4hGherRNd9 +CHfrzMTnNH4hN5sYAAkTIAAA/z+HbkJNgw5nAYXntDFX57V72pTVvvZIDAAAAAAAAAAAAIaAZN99p5iv3Uy5KzLVIn4oJdXuWwQtoiPuVvR37fNGt+h7XfWrFxeUDV+5 +CM0W0f4hiP5fWVciAAkbAAAAACD949mnNSIOTWH92aZDY80eG/VA3HF4DwAAAAAAAAAAAHz7539g8IjaBVxv7M2N+/bPcmY6Nik4r0IZMxOPFbbAkSCNXSQPFhcRiJ3V +CSh7DyaX/kiv4GZrAAki4AAAQCBkJtVoaqbUq7I+EEmnUlsQDTY688BYFQAAAAAAAAAAAN8JpiIqwffNT/xQxjdHpDrq3zyloPsBWmA2IQWQQO0LdzufXVyjFReQWFDz +CYWm6xCIhvXC3VCXAAkqwAAgACBa35Ut2KFYNsnRSMn44AHqcTe/OkZ2BgAAAAAAAAAAAF4Xg+DOwujvmF1htTEQCwMy9wsWY5yhtvG2eTnKXEEkXcmwXd+OFBc2QzNB +Cee2T+SyTIWzqlyJAAkyoAAAACAwzYQY7c/FVOgHRQts7iXBTtoSZjkkDgAAAAAAAAAAAMBIbtwe998H0gZ6M6rpIxqKxjCjoZalWQSeOQmxxfDZHKnEXdEgFhcHWk2u +CkLRjbL/8nH1LHItAAk6gAAAACCOJE0sVbxAPKpdbq8PkiFw5BPrHgL7AgAAAAAAAAAAAOA7TZ33LY2yMqILsv81xDOpnxRn85H3W19iGA2W8G1qpMTWXT6yFRee+RYz +Cp+8ywGs/RHkC2XvAAlCYAAAACBNintTpR6/Rnifd3mKRLpTtkf3SpB7BgAAAAAAAAAAAHMiuQ07JARU4fxwJSnQB4yP90UIdJITeDi20Qyg1qGFUWLpXdLbFRfl/eXh +Cvv3cMfIKnGruuwSAAlKQAAAQCDg8QXQk2sKxfrBy12fCB1tGy/PfOAKCAAAAAAAAAAAAOiNwY4aE0xXaHdQI2qJS2bk7RCoBCNjsq8gyukcuqQlnb77XdC8FRdSYvKb +C1i2W21eJGmELC2pAAlSIAAAACBhQw6GlSeLc+kpkzn8iZZFwb5N8t35CAAAAAAAAAAAAEEoFPY0abk13Y9PYgozJiGsehGP8W+ysLDDiBRgvvnxExUNXvJlFBcVxSxe +C7uMRcpKNSmZq1xhAAlaAAAAgCBXprmLUh7/IK8/40oeg7BWe7seVLqsCgAAAAAAAAAAABBSeAj6PaVvE7iUEenN6XcKknPUytIDifV0wS1sL3Ow7VEeXngMExdc/Jps +DCViZM3ipYUuK6mLAAlh4ADA/z/MI1qZk8vf6Ti1S5hQSFxGtqQI3SP+EAAAAAAAAAAAAG15Ry3Qz4Cf9jJWW23i+wUfX4egal2vebuVQS81f0NDU/QvXv8yEhdhE5lJ +DJQooZqbnri+wMP4AAlpwAAA/z9MIHjQOI44RP5iQXI+lUMHS9OpdMFmEQAAAAAAAAAAAAtDll4GoIe0dybzWqiG31YwBXIVu0alghYkPiNlZawpYFNCXtQaEheBuYI2 +DQOCnM/692T/c/vpAAlxoAAgACBpKU8xBXETUdlfq3ziC5HqY2sbxpLaBgAAAAAAAAAAAC2Podb2UVE3Lz98swMLSdirCyqr/Lp6GODK5wBbDTIfutpUXrwsEhd5M2Bk +DXJv6TyZC6dJNCJfAAl5gAAAwCC821K+00n7cVERL9d6Jzo0wkE8NSY/BAAAAAAAAAAAANEvjjQwmKBYHwf7qsdzHSrM+7AC4wGhHxMBliBwDnPi7SJmXhkBERcyCjYC +Dej8cVmdICmGlGmsAAmBYAAAgCAMNQrVauCVnJhLXbUm+wi+s6FYCIDGCwAAAAAAAAAAAHSgbycFfzUIokW6wOwg6yoHf88c9CrCsoU3LGXqo65Awhh8XkE7FBcTwPq1 +Dkyiv/CRkmMKmkEzAAmJQADgACBD4xnvCIUw3EDxERdHu3/bAmPjwJ15DAAAAAAAAAAAAHiPGbRWrMMMyL6BA6eaJfNIo5XiUMXo0B7u72S1QqDZLpGNXrwgExchnuav +DrYJPtE6p0g5WpqUAAmRIAAAACAUOgCoJeMzIDa34vdMEznyR1cSUIbpDQAAAAAAAAAAAKlIg5av2zB0dTVJcJhbennqbQ9kisot+0OheDaM0jzyNJueXjOjEReyl0xD +DyhWrFlPqEc/c9+uAAmZAAAgACDcrvMMuRSy9GQxh8BFNt6wIDWYhvU4EQAAAAAAAAAAABVOP5QKoWx9atpYrUh6L+UeYRrgD4sj3ZOBngyn3WuRAeawXjl6EReMHTLH +D5uvExILinkBRJMsAAmg4ADg/yfO2pclcPlRE5XVKCPZeF63mn30v+yHDgAAAAAAAAAAAPaDA4g+h9tjjVWtJ587WMHb07Mcfa1BoDJCANQd+z94wJDEXvaXEhf5l8cN +EAgacoNqeblmwryDAAmowAAAACBrw4jKPLZgZpVWF50edwCXFDSCydrEBwAAAAAAAAAAAJzD7FqR8uJdE2JDv+AVjncVcclu70I3QzG/aH2H/bHffOnYXjV/FBdyiZsc +EGp3iH6JaGGCuz8LAAmwoAAAQCAgBboCfncZ/s4WkKcsLjW+V4vlPQoDAwAAAAAAAAAAALs0nxXJbP7VZ+Fc3dhRpL6RN6QfYphMw8iCNdqEKGL6e/joXvLUERcyQVAj +ENuF9IkxgIodMy/3AAm4gAAAACCTOuNmt+7x2vxFMkHRbDxRElZiABP9DAAAAAAAAAAAAAERAwV0KNK6qPCLxwDpQCkrbHi2jFc/Kldv0N5pJMkE+XP7XhnVERet5rYy +EUyU1REypM3OngUzAAnAYAAAgCB898coFK4fvpBHu54gmeu1eEuspbAwCAAAAAAAAAAAAHQ93L5j0nbFj5FI6biHU0sUuxN+qzGCaK4mc2P3nQYZpEoMXxU6EBeWCMCJ +EcjRYNcojJYAgaG7AAnIQAAAACCjAhx+1E7mE4bleOFpzqtEG3mGLs/VDgAAAAAAAAAAAFr7gwTRRSFysbOK9K7JXVQxXaq23POoYGxre0mJgYM5pEwfX/i0EBckQiPZ +EkF8pCJpsBtpkPH6AAnQIADg/z9tCoMocT196c1IBJkoKNhgXJT3k1ysCgAAAAAAAAAAAPESYEteACbYsgdEuoUXLrDqFqut46D/RLAoOw+IITSkU6cxX6ybEBdqMa4t +ErrgKoeevYMpfx+WAAnYAADg/y/Zjrsqarpkd5PIhR21HJ55cSMyymaaBAAAAAAAAAAAAKPhdir1YiPGjqsC309lxumCEY8aSu2HOTrVU6Ihc4oti3tDX+oHEBeswM1c +EzihxS/9MDJCHTIfAAnf4AAAQCAGq48tARXjK5m5ygIMjtFJqlySqsdXBgAAAAAAAAAAAPJaEbuOk1ZnpJ4y6SR6yn+dY6fB5QaJHhb6QWgMXit2KCxWXxI6EBdJ5KeT +E7TgpleovIarz4zNAAnnwADg/zelTTDUGas2KxqgEH2WV5EilGEY8VASBgAAAAAAAAAAAJVJFZNI4Kxh1AUjFLdY7k6MLVY47YSfxE1acJIuEMlMpcRmX6qSDhc6QrMB +FD83ZUplM/sW8MloAAnvoAAAQCBUTUlenrKovQvyeNaLUYJBmxfT5hcuDQAAAAAAAAAAAIaxf5jfjr8MifLPauOJeh6OWDYxSUUPluG5E8hVh+rrWD95X96VDhf7Be0Q +FMlwaWsoL8M1y3sGAAn3gAAAICD2It4VF1dZC7XR51SjkuBPy2c1ot7iBQAAAAAAAAAAAOto2NUucHczD9ZpnC5X1L05HcifvGpMh8gwIt7mZ2DW3BWLX04TDhdKX1k6 +FVin/nTPxZEaUhFJAAn/YADg/zdLnpRRoj5nb9aeuWAA1TG1ZV/ig3PhBQAAAAAAAAAAAP7jjrVesvd94nsFrp03Zj496k0mgpUa+z2F/FaP1R7omhShXzPEEBcdNBS1 +FdDmSXs4LVrTs1CaAAoHQAAAACDvuQVfiWazEOmUBkUubcV9HD6bQ4YDCQAAAAAAAAAAAIpAWQyuvdv0i6cfyxgL/cqONcYtUdP6eDTQOlOgmOc5h7CyX93+DxdiV5Q4 +Fk7wqQvRa3I8HIU0AAoPIAAAACClMk0cUuhV+1lsN2AW0bLOzyOjOxhvBAAAAAAAAAAAAHvW0itHeUSYGKmni03ZQ4CQz/twR/T8aM+iCLhkzgVuGaXDX1axDheZrIxe +FtgmMCmxHYDfZVDnAAoXAADg/ydUahAkyDODrqzZ/A9vg1WNyOWPKEjSBgAAAAAAAAAAAIF1YVHIYoFF9C7GrkXnuXeYn2j+2cNXhc5hrQ9iuMiyW5nWX3ITDxcvOs69 +F13fK/nssCNHFGj5AAoe4AAAQCDp4pHELBlNIcdX51i41BvbBjtbfHsECgAAAAAAAAAAAIcgs1NcVR2veWwlmJK7rkICXTqwbukwNfe5scvzGUskkCDpXxciDxc/NQHe +F+MYpAKDBczv4JWTAAomwAAAACBS1/Bd73vGgmzadPW9r4Vf4TzSyKulDgAAAAAAAAAAALu0Rd+bUPZVV1LffkjQnE9dvY5cjr8wr/G1X47UTZ6Atcr5X6GoDRcUdkaH +GHax93pF7gaxq0XZAAouoAAAoCCqU8YdV933DGrl49Dn/tEwGvNAEx6tCAAAAAAAAAAAAFYHby9tEq0dadLbjJDzL1bca6HRgkdDHRIWmM7snd7JCQ8MYFeEDRdjn+Zp +GQvX6N/GOKeq1c5DAAo2gAAAACC4g9jQzny0CwJI9mI2feK6gSKFkJPyBAAAAAAAAAAAACYuf376jLmC/rlDvhOOvKnh4emKgnHG9GJw3WPHFB2b//0dYLkhDRcLx0qQ +GaVdn56xd5nImNufAAo+YAAAACAHKVwriCjnfwq5aifRmVw5M7Iuu3iqCAAAAAAAAAAAAMPe34CBzFKsON/ycucEEY0p3fNEZHYcCGAK7L4ZvypzBDUwYOP0DBd5TI71 +GkD2FFHSldaV1ylFAApGQAAAoCCuIeiM9eCK9ZhitwDrjbRFc/59vyyECwAAAAAAAAAAAIXwebYkSFSbeLbTeMKy9Qdj4KCSw+pp3ozDK+//1/8PzOhCYIwfDRcwPZvi diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java new file mode 100644 index 0000000000..55a6e8879b --- /dev/null +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java @@ -0,0 +1,507 @@ +/* + * 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 bisq.core.account.sign; + + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; + +import com.google.common.base.Charsets; + +import java.security.KeyPair; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +import static bisq.core.account.sign.SignedWitness.VerificationMethod.ARBITRATOR; +import static bisq.core.account.sign.SignedWitness.VerificationMethod.TRADE; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +public class SignedWitnessServiceTest { + private SignedWitnessService signedWitnessService; + private byte[] account1DataHash; + private byte[] account2DataHash; + private byte[] account3DataHash; + private AccountAgeWitness aew1; + private AccountAgeWitness aew2; + private AccountAgeWitness aew3; + private byte[] signature1; + private byte[] signature2; + private byte[] signature3; + private byte[] signer1PubKey; + private byte[] signer2PubKey; + private byte[] signer3PubKey; + private byte[] witnessOwner1PubKey; + private byte[] witnessOwner2PubKey; + private byte[] witnessOwner3PubKey; + private long date1; + private long date2; + private long date3; + private long tradeAmount1; + private long tradeAmount2; + private long tradeAmount3; + private long SIGN_AGE_1 = SignedWitnessService.SIGNER_AGE_DAYS * 3 + 5; + private long SIGN_AGE_2 = SignedWitnessService.SIGNER_AGE_DAYS * 2 + 4; + private long SIGN_AGE_3 = SignedWitnessService.SIGNER_AGE_DAYS + 3; + private KeyRing keyRing; + private P2PService p2pService; + private FilterManager filterManager; + private ECKey arbitrator1Key; + KeyPair peer1KeyPair; + KeyPair peer2KeyPair; + KeyPair peer3KeyPair; + + @Before + public void setup() throws Exception { + AppendOnlyDataStoreService appendOnlyDataStoreService = mock(AppendOnlyDataStoreService.class); + ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); + when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); + keyRing = mock(KeyRing.class); + p2pService = mock(P2PService.class); + filterManager = mock(FilterManager.class); + signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager); + account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); + account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); + account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); + long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); + long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); + aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); + aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); + aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); + arbitrator1Key = new ECKey(); + peer1KeyPair = Sig.generateKeyPair(); + peer2KeyPair = Sig.generateKeyPair(); + peer3KeyPair = Sig.generateKeyPair(); + signature1 = arbitrator1Key.signMessage(Utilities.encodeToHex(account1DataHash)).getBytes(Charsets.UTF_8); + signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); + signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); + date1 = getTodayMinusNDays(SIGN_AGE_1); + date2 = getTodayMinusNDays(SIGN_AGE_2); + date3 = getTodayMinusNDays(SIGN_AGE_3); + signer1PubKey = arbitrator1Key.getPubKey(); + signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + signer3PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); + witnessOwner1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + witnessOwner2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); + witnessOwner3PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); + tradeAmount1 = 1000; + tradeAmount2 = 1001; + tradeAmount3 = 1001; + } + + @Test + public void testIsValidAccountAgeWitnessOk() { + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidAccountAgeWitnessArbitratorSignatureProblem() { + signature1 = new byte[]{1, 2, 3}; + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidAccountAgeWitnessPeerSignatureProblem() { + signature2 = new byte[]{1, 2, 3}; + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidSelfSignatureOk() throws Exception { + KeyPair peer1KeyPair = Sig.generateKeyPair(); + signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + + signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); + signature3 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, signer2PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, signer2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer2PubKey, signer2PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidSimpleLoopSignatureProblem() throws Exception { + // A reasonable case where user1 is signed by user2 and later switches account and the new + // account gets signed by user2. This is not allowed. + KeyPair peer1KeyPair = Sig.generateKeyPair(); + KeyPair peer2KeyPair = Sig.generateKeyPair(); + byte[] user1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + byte[] user2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); + + signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); + signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, user1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, user1PubKey, user2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, user2PubKey, user1PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidAccountAgeWitnessDateTooSoonProblem() { + date3 = getTodayMinusNDays(SIGN_AGE_2 - 1); + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + @Test + public void testIsValidAccountAgeWitnessDateTooLateProblem() { + date3 = getTodayMinusNDays(3); + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + + @Test + public void testIsValidAccountAgeWitnessEndlessLoop() throws Exception { + byte[] account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); + byte[] account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); + byte[] account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); + long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); + long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); + AccountAgeWitness aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); + AccountAgeWitness aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); + AccountAgeWitness aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); + + KeyPair peer1KeyPair = Sig.generateKeyPair(); + KeyPair peer2KeyPair = Sig.generateKeyPair(); + KeyPair peer3KeyPair = Sig.generateKeyPair(); + + String account1DataHashAsHexString = Utilities.encodeToHex(account1DataHash); + String account2DataHashAsHexString = Utilities.encodeToHex(account2DataHash); + String account3DataHashAsHexString = Utilities.encodeToHex(account3DataHash); + + byte[] signature1 = Sig.sign(peer3KeyPair.getPrivate(), account1DataHashAsHexString.getBytes(Charsets.UTF_8)); + byte[] signature2 = Sig.sign(peer1KeyPair.getPrivate(), account2DataHashAsHexString.getBytes(Charsets.UTF_8)); + byte[] signature3 = Sig.sign(peer2KeyPair.getPrivate(), account3DataHashAsHexString.getBytes(Charsets.UTF_8)); + + byte[] signer1PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); + byte[] signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + byte[] signer3PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); + byte[] witnessOwner1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); + byte[] witnessOwner2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); + byte[] witnessOwner3PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); + long date1 = getTodayMinusNDays(SIGN_AGE_1); + long date2 = getTodayMinusNDays(SIGN_AGE_2); + long date3 = getTodayMinusNDays(SIGN_AGE_3); + + long tradeAmount1 = 1000; + long tradeAmount2 = 1001; + long tradeAmount3 = 1001; + + SignedWitness sw1 = new SignedWitness(TRADE, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + + @Test + public void testIsValidAccountAgeWitnessLongLoop() throws Exception { + AccountAgeWitness aew = null; + KeyPair signerKeyPair; + KeyPair signedKeyPair = Sig.generateKeyPair(); + int iterations = 1002; + for (int i = 0; i < iterations; i++) { + byte[] accountDataHash = org.bitcoinj.core.Utils.sha256hash160(String.valueOf(i).getBytes(Charsets.UTF_8)); + long accountCreationTime = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); + aew = new AccountAgeWitness(accountDataHash, accountCreationTime); + String accountDataHashAsHexString = Utilities.encodeToHex(accountDataHash); + byte[] signature; + byte[] signerPubKey; + if (i == 0) { + // use arbitrator key + ECKey arbitratorKey = new ECKey(); + signedKeyPair = Sig.generateKeyPair(); + String signature1String = arbitratorKey.signMessage(accountDataHashAsHexString); + signature = signature1String.getBytes(Charsets.UTF_8); + signerPubKey = arbitratorKey.getPubKey(); + } else { + signerKeyPair = signedKeyPair; + signedKeyPair = Sig.generateKeyPair(); + signature = Sig.sign(signedKeyPair.getPrivate(), accountDataHashAsHexString.getBytes(Charsets.UTF_8)); + signerPubKey = Sig.getPublicKeyBytes(signerKeyPair.getPublic()); + } + byte[] witnessOwnerPubKey = Sig.getPublicKeyBytes(signedKeyPair.getPublic()); + long date = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); + SignedWitness sw = new SignedWitness(i == 0 ? ARBITRATOR : TRADE, accountDataHash, signature, signerPubKey, witnessOwnerPubKey, date, tradeAmount1); + signedWitnessService.addToMap(sw); + } + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew)); + } + + + private long getTodayMinusNDays(long days) { + return Instant.ofEpochMilli(new Date().getTime()).minus(days, ChronoUnit.DAYS).toEpochMilli(); + } + + @Test + public void testSignAccountAgeWitness_withTooLowTradeAmount() throws CryptoException { + long accountCreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + + KeyPair peerKeyPair = Sig.generateKeyPair(); + KeyPair signerKeyPair = Sig.generateKeyPair(); + + when(keyRing.getSignatureKeyPair()).thenReturn(signerKeyPair); + + AccountAgeWitness accountAgeWitness = new AccountAgeWitness(account1DataHash, accountCreationTime); + signedWitnessService.signAndPublishAccountAgeWitness(Coin.ZERO, accountAgeWitness, peerKeyPair.getPublic()); + + verify(p2pService, never()).addPersistableNetworkPayload(any(PersistableNetworkPayload.class), anyBoolean()); + } + + @Test + public void testSignAccountAgeWitness_withSufficientTradeAmount() throws CryptoException { + long accountCreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + + KeyPair peerKeyPair = Sig.generateKeyPair(); + KeyPair signerKeyPair = Sig.generateKeyPair(); + + when(keyRing.getSignatureKeyPair()).thenReturn(signerKeyPair); + + + AccountAgeWitness accountAgeWitness = new AccountAgeWitness(account1DataHash, accountCreationTime); + signedWitnessService.signAndPublishAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, peerKeyPair.getPublic()); + + verify(p2pService, times(1)).addPersistableNetworkPayload(any(PersistableNetworkPayload.class), anyBoolean()); + } + + /* Signed witness tree + Each edge in the graph represents one signature + + Arbitrator + | + sw1 + | + sw2 + | + sw3 + */ + @Test + public void testBanFilterSingleTree() { + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + // Second account is banned, first account is still a signer but the other two are no longer signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + + // First account is banned, no accounts in the tree below it are signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + /* Signed witness trees + Each edge in the graph represents one signature + + Arbitrator + | | + sw1 sw2 + | + sw3 + */ + @Test + public void testBanFilterTwoTrees() { + // Signer 2 is signed by arbitrator + signer2PubKey = arbitrator1Key.getPubKey(); + signature2 = arbitrator1Key.signMessage(Utilities.encodeToHex(account2DataHash)).getBytes(Charsets.UTF_8); + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(ARBITRATOR, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + + // Only second account is banned, first account is still a signer but the other two are no longer signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + + // Only first account is banned, account2 and account3 are still signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } + + /* Signed witness tree + Each edge in the graph represents one signature + + Arbitrator + | | + sw1 sw2 + \ / + sw3 + */ + @Test + public void testBanFilterJoinedTrees() throws Exception { + // Signer 2 is signed by arbitrator + signer2PubKey = arbitrator1Key.getPubKey(); + signature2 = arbitrator1Key.signMessage(Utilities.encodeToHex(account2DataHash)).getBytes(Charsets.UTF_8); + + // Peer1 owns both account1 and account2 +// witnessOwner2PubKey = witnessOwner1PubKey; +// peer2KeyPair = peer1KeyPair; +// signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); + + // sw1 also signs sw3 (not supported yet but a possible addition for a more robust system) + var signature3p = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); + var signer3pPubKey = witnessOwner1PubKey; + var date3p = date3; + var tradeAmount3p = tradeAmount3; + + SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); + SignedWitness sw2 = new SignedWitness(ARBITRATOR, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); + SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); + SignedWitness sw3p = new SignedWitness(TRADE, account3DataHash, signature3p, signer3pPubKey, witnessOwner3PubKey, date3p, tradeAmount3p); + + signedWitnessService.addToMap(sw1); + signedWitnessService.addToMap(sw2); + signedWitnessService.addToMap(sw3); + signedWitnessService.addToMap(sw3p); + + // First account is banned, the other two are still signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); + + // Second account is banned, the other two are still signers + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); + + // First and second account is banned, the third is no longer a signer + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); + assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); + } +} + diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessTest.java new file mode 100644 index 0000000000..7b4381c81d --- /dev/null +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessTest.java @@ -0,0 +1,62 @@ +package bisq.core.account.sign; + +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import com.google.common.base.Charsets; + +import java.time.Instant; + +import org.junit.Before; +import org.junit.Test; + +import static bisq.core.account.sign.SignedWitness.VerificationMethod.ARBITRATOR; +import static bisq.core.account.sign.SignedWitness.VerificationMethod.TRADE; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class SignedWitnessTest { + + private ECKey arbitrator1Key; + private byte[] witnessOwner1PubKey; + private byte[] witnessHash; + private byte[] witnessHashSignature; + + @Before + public void setUp() { + arbitrator1Key = new ECKey(); + witnessOwner1PubKey = Sig.getPublicKeyBytes(Sig.generateKeyPair().getPublic()); + witnessHash = Utils.sha256hash160(new byte[]{1}); + witnessHashSignature = arbitrator1Key.signMessage(Utilities.encodeToHex(witnessHash)).getBytes(Charsets.UTF_8); + } + + @Test + public void testProtoRoundTrip() { + SignedWitness signedWitness = new SignedWitness(ARBITRATOR, witnessHash, witnessHashSignature, arbitrator1Key.getPubKey(), witnessOwner1PubKey, Instant.now().getEpochSecond(), 100); + assertEquals(signedWitness, SignedWitness.fromProto(signedWitness.toProtoMessage().getSignedWitness())); + } + + @Test + public void isImmutable() { + byte[] signerPubkey = arbitrator1Key.getPubKey(); + SignedWitness signedWitness = new SignedWitness(TRADE, witnessHash, witnessHashSignature, signerPubkey, witnessOwner1PubKey, Instant.now().getEpochSecond(), 100); + byte[] originalWitnessHash = signedWitness.getAccountAgeWitnessHash().clone(); + witnessHash[0] += 1; + assertArrayEquals(originalWitnessHash, signedWitness.getAccountAgeWitnessHash()); + + byte[] originalWitnessHashSignature = signedWitness.getSignature().clone(); + witnessHashSignature[0] += 1; + assertArrayEquals(originalWitnessHashSignature, signedWitness.getSignature()); + + byte[] originalSignerPubkey = signedWitness.getSignerPubKey().clone(); + signerPubkey[0] += 1; + assertArrayEquals(originalSignerPubkey, signedWitness.getSignerPubKey()); + byte[] originalwitnessOwner1PubKey = signedWitness.getWitnessOwnerPubKey().clone(); + witnessOwner1PubKey[0] += 1; + assertArrayEquals(originalwitnessOwner1PubKey, signedWitness.getWitnessOwnerPubKey()); + } + +} diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java new file mode 100644 index 0000000000..cf47c712d4 --- /dev/null +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -0,0 +1,361 @@ +/* + * 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 bisq.core.account.witness; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CountryUtil; +import bisq.core.offer.OfferPayload; +import bisq.core.payment.ChargeBackRisk; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SepaAccountPayload; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.TraderDataItem; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.trade.Contract; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.KeyStorage; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; + +import java.security.KeyPair; +import java.security.PublicKey; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +// Restricted default Java security policy on Travis does not allow long keys, so test fails. +// Using Utilities.removeCryptographyRestrictions(); did not work. +//@Ignore +public class AccountAgeWitnessServiceTest { + private PublicKey publicKey; + private KeyPair keypair; + private SignedWitnessService signedWitnessService; + private AccountAgeWitnessService service; + private ChargeBackRisk chargeBackRisk; + private FilterManager filterManager; + private File dir1; + private File dir2; + private File dir3; + + @Before + public void setup() throws IOException { + KeyRing keyRing = mock(KeyRing.class); + setupService(keyRing); + keypair = Sig.generateKeyPair(); + publicKey = keypair.getPublic(); + // Setup temp storage dir + dir1 = makeDir("temp_tests1"); + dir2 = makeDir("temp_tests1"); + dir3 = makeDir("temp_tests1"); + } + + private void setupService(KeyRing keyRing) { + chargeBackRisk = mock(ChargeBackRisk.class); + AppendOnlyDataStoreService dataStoreService = mock(AppendOnlyDataStoreService.class); + P2PService p2pService = mock(P2PService.class); + ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); + when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); + AppendOnlyDataStoreService appendOnlyDataStoreService = mock(AppendOnlyDataStoreService.class); + filterManager = mock(FilterManager.class); + signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager); + service = new AccountAgeWitnessService(null, null, null, signedWitnessService, chargeBackRisk, null, dataStoreService, null, filterManager); + } + + private File makeDir(String name) throws IOException { + var dir = File.createTempFile(name, ""); + dir.delete(); + dir.mkdir(); + return dir; + } + + @After + public void tearDown() { + // Do teardown stuff + } + + @Ignore + @Test + public void testIsTradeDateAfterReleaseDate() { + Date ageWitnessReleaseDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); + Date tradeDate = new GregorianCalendar(2017, Calendar.NOVEMBER, 1).getTime(); + assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { + })); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); + assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { + })); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22, 0, 0, 1).getTime(); + assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { + })); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22).getTime(); + assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { + })); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 21).getTime(); + assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { + })); + } + + @Ignore + @Test + public void testVerifySignatureOfNonce() throws CryptoException { + byte[] nonce = new byte[]{0x01}; + byte[] signature = Sig.sign(keypair.getPrivate(), nonce); + assertTrue(service.verifySignature(publicKey, nonce, signature, errorMessage -> { + })); + assertFalse(service.verifySignature(publicKey, nonce, new byte[]{0x02}, errorMessage -> { + })); + assertFalse(service.verifySignature(publicKey, new byte[]{0x03}, signature, errorMessage -> { + })); + assertFalse(service.verifySignature(publicKey, new byte[]{0x02}, new byte[]{0x04}, errorMessage -> { + })); + } + + @Test + public void testArbitratorSignWitness() { + KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1)); + KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2)); + + // Setup dispute for arbitrator to sign both sides + List disputes = new ArrayList<>(); + PubKeyRing buyerPubKeyRing = buyerKeyRing.getPubKeyRing(); + PubKeyRing sellerPubKeyRing = sellerKeyRing.getPubKeyRing(); + PaymentAccountPayload buyerPaymentAccountPayload = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries()); + PaymentAccountPayload sellerPaymentAccountPayload = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries()); + AccountAgeWitness buyerAccountAgeWitness = service.getNewWitness(buyerPaymentAccountPayload, buyerPubKeyRing); + service.addToMap(buyerAccountAgeWitness); + AccountAgeWitness sellerAccountAgeWitness = service.getNewWitness(sellerPaymentAccountPayload, sellerPubKeyRing); + service.addToMap(sellerAccountAgeWitness); + long now = new Date().getTime() + 1000; + Contract contract = mock(Contract.class); + disputes.add(new Dispute(new Date().getTime(), + "trade1", + 0, + true, + true, + buyerPubKeyRing, + now - 1, + now - 1, + contract, + null, + null, + null, + null, + null, + "contractAsJson", + null, + null, + null, + true, + SupportType.ARBITRATION)); + disputes.get(0).setIsClosed(); + disputes.get(0).getDisputeResultProperty().set(new DisputeResult( + "trade1", + 1, + DisputeResult.Winner.BUYER, + DisputeResult.Reason.OTHER.ordinal(), + true, + true, + true, + "summary", + null, + null, + 100000, + 0, + null, + now - 1, + false)); + + // Filtermanager says nothing is filtered + when(filterManager.isNodeAddressBanned(any())).thenReturn(false); + when(filterManager.isCurrencyBanned(any())).thenReturn(false); + when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); + when(filterManager.arePeersPaymentAccountDataBanned(any())).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false); + + when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); + + when(contract.getPaymentMethodId()).thenReturn(PaymentMethod.SEPA_ID); + when(contract.getTradeAmount()).thenReturn(Coin.parseCoin("0.01")); + when(contract.getBuyerPubKeyRing()).thenReturn(buyerPubKeyRing); + when(contract.getSellerPubKeyRing()).thenReturn(sellerPubKeyRing); + when(contract.getBuyerPaymentAccountPayload()).thenReturn(buyerPaymentAccountPayload); + when(contract.getSellerPaymentAccountPayload()).thenReturn(sellerPaymentAccountPayload); + when(contract.getOfferPayload()).thenReturn(mock(OfferPayload.class)); + List items = service.getTraderPaymentAccounts(now, getPaymentMethodById(PaymentMethod.SEPA_ID), disputes); + assertEquals(2, items.size()); + + // Setup a mocked arbitrator key + ECKey arbitratorKey = mock(ECKey.class); + when(arbitratorKey.signMessage(any())).thenReturn("1"); + when(arbitratorKey.signMessage(any())).thenReturn("2"); + when(arbitratorKey.getPubKey()).thenReturn("1".getBytes()); + + // Arbitrator signs both trader accounts + items.forEach(item -> service.arbitratorSignAccountAgeWitness( + item.getTradeAmount(), + item.getAccountAgeWitness(), + arbitratorKey, + item.getPeersPubKey())); + + // Check that both accountAgeWitnesses are signed + SignedWitness foundBuyerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey( + buyerPubKeyRing.getSignaturePubKeyBytes()).stream() + .findFirst() + .orElse(null); + assert foundBuyerSignedWitness != null; + assertEquals(Utilities.bytesAsHexString(foundBuyerSignedWitness.getAccountAgeWitnessHash()), + Utilities.bytesAsHexString(buyerAccountAgeWitness.getHash())); + SignedWitness foundSellerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey( + sellerPubKeyRing.getSignaturePubKeyBytes()).stream() + .findFirst() + .orElse(null); + assert foundSellerSignedWitness != null; + assertEquals(Utilities.bytesAsHexString(foundSellerSignedWitness.getAccountAgeWitnessHash()), + Utilities.bytesAsHexString(sellerAccountAgeWitness.getHash())); + } + + // Create a tree of signed witnesses Arb -(SWA)-> aew1 -(SW1)-> aew2 -(SW2)-> aew3 + // Delete SWA signature, none of the account age witnesses are considered signed + // Sign a dummy AccountAgeWitness using the signerPubkey from SW1; aew2 and aew3 are not considered signed. The + // lost SignedWitness isn't possible to recover so aew1 is still not signed, but it's pubkey is a signer. + @Test + public void testArbitratorSignDummyWitness() throws CryptoException { + ECKey arbitratorKey = new ECKey(); + // Init 2 user accounts + var user1KeyRing = new KeyRing(new KeyStorage(dir1)); + var user2KeyRing = new KeyRing(new KeyStorage(dir2)); + var user3KeyRing = new KeyRing(new KeyStorage(dir3)); + var pubKeyRing1 = user1KeyRing.getPubKeyRing(); + var pubKeyRing2 = user2KeyRing.getPubKeyRing(); + var pubKeyRing3 = user3KeyRing.getPubKeyRing(); + var account1 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries()); + var account2 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries()); + var account3 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "3", CountryUtil.getAllSepaCountries()); + var aew1 = service.getNewWitness(account1, pubKeyRing1); + var aew2 = service.getNewWitness(account2, pubKeyRing2); + var aew3 = service.getNewWitness(account3, pubKeyRing3); + // Backdate witness1 70 days + aew1 = new AccountAgeWitness(aew1.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(70)); + aew2 = new AccountAgeWitness(aew2.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(35)); + aew3 = new AccountAgeWitness(aew3.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(1)); + service.addToMap(aew1); + service.addToMap(aew2); + service.addToMap(aew3); + + // Test as user1. It's still possible to sign as arbitrator since the ECKey is passed as an argument. + setupService(user1KeyRing); + + // Arbitrator signs user1 + service.arbitratorSignAccountAgeWitness(aew1, arbitratorKey, pubKeyRing1.getSignaturePubKeyBytes(), + aew1.getDate()); + // user1 signs user2 + signAccountAgeWitness(aew2, pubKeyRing2.getSignaturePubKey(), aew2.getDate(), user1KeyRing); + // user2 signs user3 + signAccountAgeWitness(aew3, pubKeyRing3.getSignaturePubKey(), aew3.getDate(), user2KeyRing); + signedWitnessService.signAndPublishAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, aew2, + pubKeyRing2.getSignaturePubKey()); + assertTrue(service.accountIsSigner(aew1)); + assertTrue(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew3)); + + // Remove SignedWitness signed by arbitrator + @SuppressWarnings("OptionalGetWithoutIsPresent") + var signedWitnessArb = signedWitnessService.getSignedWitnessMapValues().stream() + .filter(sw -> sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR) + .findAny() + .get(); + signedWitnessService.removeSignedWitness(signedWitnessArb); + assertEquals(signedWitnessService.getSignedWitnessMapValues().size(), 2); + + // Check that no account age witness is a signer + assertFalse(service.accountIsSigner(aew1)); + assertFalse(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertFalse(signedWitnessService.isSignedAccountAgeWitness(aew2)); + + // Sign dummy AccountAgeWitness using signer key from SW_1 + assertEquals(signedWitnessService.getRootSignedWitnessSet(false).size(), 1); + + // TODO: move this to accountagewitnessservice + @SuppressWarnings("OptionalGetWithoutIsPresent") + var orphanedSignedWitness = signedWitnessService.getRootSignedWitnessSet(false).stream().findAny().get(); + var dummyAccountAgeWitnessHash = Hash.getRipemd160hash(orphanedSignedWitness.getSignerPubKey()); + var dummyAEW = new AccountAgeWitness(dummyAccountAgeWitnessHash, + orphanedSignedWitness.getDate() - + (TimeUnit.DAYS.toMillis(SignedWitnessService.SIGNER_AGE_DAYS + 1))); + service.arbitratorSignAccountAgeWitness( + dummyAEW, arbitratorKey, orphanedSignedWitness.getSignerPubKey(), dummyAEW.getDate()); + + assertFalse(service.accountIsSigner(aew1)); + assertTrue(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew2)); + } + + private void signAccountAgeWitness(AccountAgeWitness accountAgeWitness, + PublicKey witnessOwnerPubKey, + long time, + KeyRing signerKeyRing) throws CryptoException { + byte[] signature = Sig.sign(signerKeyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); + SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE, + accountAgeWitness.getHash(), + signature, + signerKeyRing.getSignatureKeyPair().getPublic().getEncoded(), + witnessOwnerPubKey.getEncoded(), + time, + SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING.value); + signedWitnessService.addToMap(signedWitness); + } + +} diff --git a/core/src/test/java/bisq/core/app/BisqHelpFormatterTest.java b/core/src/test/java/bisq/core/app/BisqHelpFormatterTest.java new file mode 100644 index 0000000000..7ac9777bc2 --- /dev/null +++ b/core/src/test/java/bisq/core/app/BisqHelpFormatterTest.java @@ -0,0 +1,142 @@ +/* + * 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 bisq.core.app; + +import bisq.common.config.BisqHelpFormatter; + +import joptsimple.OptionParser; + +import java.net.URISyntaxException; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +public class BisqHelpFormatterTest { + + @Test + public void testHelpFormatter() throws IOException, URISyntaxException { + + OptionParser parser = new OptionParser(); + + parser.formatHelpWith(new BisqHelpFormatter("Bisq Test", "bisq-test", "0.1.0")); + + parser.accepts("name", + "The name of the Bisq node") + .withRequiredArg() + .ofType(String.class) + .defaultsTo("Bisq"); + + parser.accepts("another-option", + "This is a long description which will need to break over multiple linessssssssssss such " + + "that no line is longer than 80 characters in the help output.") + .withRequiredArg() + .ofType(String.class) + .defaultsTo("WAT"); + + parser.accepts("exactly-72-char-description", + "012345678911234567892123456789312345678941234567895123456789612345678971") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("exactly-72-char-description-with-spaces", + " 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("90-char-description-without-spaces", + "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("90-char-description-with-space-at-char-80", + "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("90-char-description-with-spaces-at-chars-5-and-80", + "-123 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("90-char-description-with-space-at-char-73", + "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 3456789-923456789") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("1-char-description-with-only-a-space", " ") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("empty-description", "") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("no-description") + .withRequiredArg() + .ofType(String.class); + + parser.accepts("no-arg", "Some description"); + + parser.accepts("optional-arg", + "Option description") + .withOptionalArg(); + + parser.accepts("with-default-value", + "Some option with a default value") + .withRequiredArg() + .ofType(String.class) + .defaultsTo("Wat"); + + parser.accepts("data-dir", + "Application data directory") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File("/Users/cbeams/Library/Application Support/Bisq")); + + parser.accepts("enum-opt", + "Some option that accepts an enum value as an argument") + .withRequiredArg() + .ofType(AnEnum.class) + .defaultsTo(AnEnum.foo); + + ByteArrayOutputStream actual = new ByteArrayOutputStream(); + String expected = new String(Files.readAllBytes(Paths.get(getClass().getResource("cli-output.txt").toURI()))); + if (System.getProperty("os.name").startsWith("Windows")) { + // Load the expected content from a different file for Windows due to different path separator + // And normalize line endings to LF in case the file has CRLF line endings + expected = new String(Files.readAllBytes(Paths.get(getClass().getResource("cli-output_windows.txt").toURI()))) + .replaceAll("\\r\\n?", "\n"); + } + + parser.printHelpOn(new PrintStream(actual)); + assertThat(actual.toString(), equalTo(expected)); + } + + + enum AnEnum {foo, bar, baz} +} diff --git a/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java b/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java new file mode 100644 index 0000000000..9a626efac9 --- /dev/null +++ b/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.core.arbitration; + +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorService; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; + +import java.util.ArrayList; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class ArbitratorManagerTest { + + + + @Test + public void testIsArbitratorAvailableForLanguage() { + User user = mock(User.class); + ArbitratorService arbitratorService = mock(ArbitratorService.class); + + ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false); + + ArrayList languagesOne = new ArrayList() {{ + add("en"); + add("de"); + }}; + + ArrayList languagesTwo = new ArrayList() {{ + add("en"); + add("es"); + }}; + + Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null, null, + languagesOne, 0L, null, "", null, + null, null); + + Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null, null, + languagesTwo, 0L, null, "", null, + null, null); + + manager.addDisputeAgent(one, () -> { + }, errorMessage -> { + }); + manager.addDisputeAgent(two, () -> { + }, errorMessage -> { + }); + + assertTrue(manager.isAgentAvailableForLanguage("en")); + assertFalse(manager.isAgentAvailableForLanguage("th")); + } + + @Test + public void testGetArbitratorLanguages() { + User user = mock(User.class); + ArbitratorService arbitratorService = mock(ArbitratorService.class); + + ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false); + + ArrayList languagesOne = new ArrayList() {{ + add("en"); + add("de"); + }}; + + ArrayList languagesTwo = new ArrayList() {{ + add("en"); + add("es"); + }}; + + Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null, null, + languagesOne, 0L, null, "", null, + null, null); + + Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null, null, + languagesTwo, 0L, null, "", null, + null, null); + + ArrayList nodeAddresses = new ArrayList() {{ + add(two.getNodeAddress()); + }}; + + manager.addDisputeAgent(one, () -> { + }, errorMessage -> { + }); + manager.addDisputeAgent(two, () -> { + }, errorMessage -> { + }); + + assertThat(manager.getDisputeAgentLanguages(nodeAddresses), containsInAnyOrder("en", "es")); + assertThat(manager.getDisputeAgentLanguages(nodeAddresses), not(containsInAnyOrder("de"))); + } + +} diff --git a/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java b/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java new file mode 100644 index 0000000000..172dbc285a --- /dev/null +++ b/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java @@ -0,0 +1,60 @@ +/* + * 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 bisq.core.arbitration; + +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import com.google.common.collect.Lists; + +import org.apache.commons.lang3.RandomUtils; + +import java.util.Date; + +import org.junit.Ignore; + +@SuppressWarnings({"SameParameterValue", "UnusedAssignment"}) +public class ArbitratorTest { + + @Ignore("TODO InvalidKeySpecException at bisq.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") + public void testRoundtrip() { + Arbitrator arbitrator = getArbitratorMock(); + + + Arbitrator newVo = Arbitrator.fromProto(arbitrator.toProtoMessage().getArbitrator()); + } + + public static Arbitrator getArbitratorMock() { + return new Arbitrator(new NodeAddress("host", 1000), + getBytes(100), + "btcaddress", + new PubKeyRing(getBytes(100), getBytes(100)), + Lists.newArrayList(), + new Date().getTime(), + getBytes(100), + "registrationSignature", + null, null, null); + } + + public static byte[] getBytes(@SuppressWarnings("SameParameterValue") int count) { + return RandomUtils.nextBytes(count); + } +} diff --git a/core/src/test/java/bisq/core/arbitration/MediatorTest.java b/core/src/test/java/bisq/core/arbitration/MediatorTest.java new file mode 100644 index 0000000000..035069f75f --- /dev/null +++ b/core/src/test/java/bisq/core/arbitration/MediatorTest.java @@ -0,0 +1,56 @@ +/* + * 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 bisq.core.arbitration; + +import bisq.core.support.dispute.mediation.mediator.Mediator; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import com.google.common.collect.Lists; + +import java.util.Date; + +import org.junit.Ignore; + +import static bisq.core.arbitration.ArbitratorTest.getBytes; + +public class MediatorTest { + + @Ignore("TODO InvalidKeySpecException at bisq.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") + public void testRoundtrip() { + Mediator Mediator = getMediatorMock(); + + + //noinspection AccessStaticViaInstance + Mediator.fromProto(Mediator.toProtoMessage().getMediator()); + } + + public static Mediator getMediatorMock() { + return new Mediator(new NodeAddress("host", 1000), + new PubKeyRing(getBytes(100), getBytes(100)), + Lists.newArrayList(), + new Date().getTime(), + getBytes(100), + "registrationSignature", + "email", + "info", + null); + } +} diff --git a/core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java b/core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java new file mode 100644 index 0000000000..7909a6fdf9 --- /dev/null +++ b/core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java @@ -0,0 +1,67 @@ +package bisq.core.arbitration; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.support.dispute.arbitration.TraderDataItem; +import bisq.core.payment.payload.PaymentAccountPayload; + +import org.bitcoinj.core.Coin; + +import java.security.PublicKey; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.mock; + +/* + * 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 . + */ +public class TraderDataItemTest { + private TraderDataItem traderDataItem1; + private TraderDataItem traderDataItem2; + private TraderDataItem traderDataItem3; + private AccountAgeWitness accountAgeWitness1; + private AccountAgeWitness accountAgeWitness2; + private byte[] hash1 = "1".getBytes(); + private byte[] hash2 = "2".getBytes(); + + @Before + public void setup() { + accountAgeWitness1 = new AccountAgeWitness(hash1, 123); + accountAgeWitness2 = new AccountAgeWitness(hash2, 124); + traderDataItem1 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(546), + mock(PublicKey.class)); + traderDataItem2 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(547), + mock(PublicKey.class)); + traderDataItem3 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness2, Coin.valueOf(548), + mock(PublicKey.class)); + } + + @Test + public void testEquals() { + assertEquals(traderDataItem1, traderDataItem2); + assertNotEquals(traderDataItem1, traderDataItem3); + assertNotEquals(traderDataItem2, traderDataItem3); + } + + @Test + public void testHashCode() { + assertEquals(traderDataItem1.hashCode(), traderDataItem2.hashCode()); + assertNotEquals(traderDataItem1.hashCode(), traderDataItem3.hashCode()); + } +} diff --git a/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java b/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java new file mode 100644 index 0000000000..a2f00aaec1 --- /dev/null +++ b/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java @@ -0,0 +1,141 @@ +/* + * 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 bisq.core.btc; + +import bisq.core.btc.wallet.BtcWalletService; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import java.util.List; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TxFeeEstimationServiceTest { + + @Test + public void testGetEstimatedTxVsize_withDefaultTxVsize() throws InsufficientMoneyException { + List outputValues = List.of(Coin.valueOf(2000), Coin.valueOf(3000)); + int initialEstimatedTxVsize; + Coin txFeePerVbyte; + BtcWalletService btcWalletService = mock(BtcWalletService.class); + int result; + int realTxVsize; + Coin txFee; + + initialEstimatedTxVsize = 175; + txFeePerVbyte = Coin.valueOf(10); + realTxVsize = 175; + + txFee = txFeePerVbyte.multiply(initialEstimatedTxVsize); + when(btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee)).thenReturn(realTxVsize); + result = TxFeeEstimationService.getEstimatedTxVsize(outputValues, initialEstimatedTxVsize, txFeePerVbyte, btcWalletService); + assertEquals(175, result); + } + + // FIXME @Bernard could you have a look? + @Test + @Ignore + public void testGetEstimatedTxVsize_withLargeTx() throws InsufficientMoneyException { + List outputValues = List.of(Coin.valueOf(2000), Coin.valueOf(3000)); + int initialEstimatedTxVsize; + Coin txFeePerVbyte; + BtcWalletService btcWalletService = mock(BtcWalletService.class); + int result; + int realTxVsize; + Coin txFee; + + initialEstimatedTxVsize = 175; + txFeePerVbyte = Coin.valueOf(10); + realTxVsize = 1750; + + txFee = txFeePerVbyte.multiply(initialEstimatedTxVsize); + when(btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee)).thenReturn(realTxVsize); + + // repeated calls to getEstimatedFeeTxVsize do not work (returns 0 at second call in loop which cause test to fail) + result = TxFeeEstimationService.getEstimatedTxVsize(outputValues, initialEstimatedTxVsize, txFeePerVbyte, btcWalletService); + assertEquals(1750, result); + } + + // FIXME @Bernard could you have a look? + @Test + @Ignore + public void testGetEstimatedTxVsize_withSmallTx() throws InsufficientMoneyException { + List outputValues = List.of(Coin.valueOf(2000), Coin.valueOf(3000)); + int initialEstimatedTxVsize; + Coin txFeePerVbyte; + BtcWalletService btcWalletService = mock(BtcWalletService.class); + int result; + int realTxVsize; + Coin txFee; + + initialEstimatedTxVsize = 1750; + txFeePerVbyte = Coin.valueOf(10); + realTxVsize = 175; + + txFee = txFeePerVbyte.multiply(initialEstimatedTxVsize); + when(btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee)).thenReturn(realTxVsize); + result = TxFeeEstimationService.getEstimatedTxVsize(outputValues, initialEstimatedTxVsize, txFeePerVbyte, btcWalletService); + assertEquals(175, result); + } + + @Test + public void testIsInTolerance() { + int estimatedSize; + int txVsize; + double tolerance; + boolean result; + + estimatedSize = 100; + txVsize = 100; + tolerance = 0.0001; + result = TxFeeEstimationService.isInTolerance(estimatedSize, txVsize, tolerance); + assertTrue(result); + + estimatedSize = 100; + txVsize = 200; + tolerance = 0.2; + result = TxFeeEstimationService.isInTolerance(estimatedSize, txVsize, tolerance); + assertFalse(result); + + estimatedSize = 120; + txVsize = 100; + tolerance = 0.2; + result = TxFeeEstimationService.isInTolerance(estimatedSize, txVsize, tolerance); + assertTrue(result); + + estimatedSize = 200; + txVsize = 100; + tolerance = 1; + result = TxFeeEstimationService.isInTolerance(estimatedSize, txVsize, tolerance); + assertTrue(result); + + estimatedSize = 201; + txVsize = 100; + tolerance = 1; + result = TxFeeEstimationService.isInTolerance(estimatedSize, txVsize, tolerance); + assertFalse(result); + } +} diff --git a/core/src/test/java/bisq/core/btc/nodes/BtcNetworkConfigTest.java b/core/src/test/java/bisq/core/btc/nodes/BtcNetworkConfigTest.java new file mode 100644 index 0000000000..04825ff7d4 --- /dev/null +++ b/core/src/test/java/bisq/core/btc/nodes/BtcNetworkConfigTest.java @@ -0,0 +1,78 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.setup.WalletConfig; + +import bisq.network.Socks5MultiDiscovery; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class BtcNetworkConfigTest { + private static final int MODE = 0; + + private WalletConfig delegate; + + @Before + public void setUp() { + delegate = mock(WalletConfig.class); + } + + @Test + public void testProposePeersWhenProxyPresentAndNoPeers() { + BtcNetworkConfig config = new BtcNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + mock(Socks5Proxy.class)); + config.proposePeers(Collections.emptyList()); + + verify(delegate, never()).setPeerNodes(any()); + verify(delegate).setDiscovery(any(Socks5MultiDiscovery.class)); + } + + @Test + public void testProposePeersWhenProxyNotPresentAndNoPeers() { + BtcNetworkConfig config = new BtcNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + null); + config.proposePeers(Collections.emptyList()); + + verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); + verify(delegate, never()).setPeerNodes(any()); + } + + @Test + public void testProposePeersWhenPeersPresent() { + BtcNetworkConfig config = new BtcNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + null); + config.proposePeers(Collections.singletonList(mock(PeerAddress.class))); + + verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); + verify(delegate).setPeerNodes(any()); + } +} diff --git a/core/src/test/java/bisq/core/btc/nodes/BtcNodeConverterTest.java b/core/src/test/java/bisq/core/btc/nodes/BtcNodeConverterTest.java new file mode 100644 index 0000000000..7b7d4e1f5a --- /dev/null +++ b/core/src/test/java/bisq/core/btc/nodes/BtcNodeConverterTest.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.nodes.BtcNodeConverter.Facade; +import bisq.core.btc.nodes.BtcNodes.BtcNode; + +import bisq.network.DnsLookupException; + +import org.bitcoinj.core.PeerAddress; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import java.net.InetAddress; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BtcNodeConverterTest { + @Test + public void testConvertOnionHost() { + BtcNode node = mock(BtcNode.class); + when(node.getOnionAddress()).thenReturn("aaa.onion"); + + //InetAddress inetAddress = mock(InetAddress.class); + + Facade facade = mock(Facade.class); + //when(facade.onionHostToInetAddress(any())).thenReturn(inetAddress); + + PeerAddress peerAddress = new BtcNodeConverter(facade).convertOnionHost(node); + // noinspection ConstantConditions + assertEquals(node.getOnionAddress(), peerAddress.getHostname()); + } + + @Test + public void testConvertClearNode() { + final String ip = "192.168.0.1"; + + BtcNode node = mock(BtcNode.class); + when(node.getHostNameOrAddress()).thenReturn(ip); + + PeerAddress peerAddress = new BtcNodeConverter().convertClearNode(node); + // noinspection ConstantConditions + InetAddress inetAddress = peerAddress.getAddr(); + assertEquals(ip, inetAddress.getHostAddress()); + } + + @Test + public void testConvertWithTor() throws DnsLookupException { + InetAddress expected = mock(InetAddress.class); + + Facade facade = mock(Facade.class); + when(facade.torLookup(any(), anyString())).thenReturn(expected); + + BtcNode node = mock(BtcNode.class); + when(node.getHostNameOrAddress()).thenReturn("aaa.onion"); + + PeerAddress peerAddress = new BtcNodeConverter(facade).convertWithTor(node, mock(Socks5Proxy.class)); + + // noinspection ConstantConditions + assertEquals(expected, peerAddress.getAddr()); + } +} diff --git a/core/src/test/java/bisq/core/btc/nodes/BtcNodesRepositoryTest.java b/core/src/test/java/bisq/core/btc/nodes/BtcNodesRepositoryTest.java new file mode 100644 index 0000000000..bead4cceb8 --- /dev/null +++ b/core/src/test/java/bisq/core/btc/nodes/BtcNodesRepositoryTest.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.nodes.BtcNodes.BtcNode; + +import org.bitcoinj.core.PeerAddress; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; + +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BtcNodesRepositoryTest { + @Test + public void testGetPeerAddressesWhenClearNodes() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + BtcNodesRepository repository = new BtcNodesRepository(converter, + Collections.singletonList(node)); + + List peers = repository.getPeerAddresses(null, false); + + assertFalse(peers.isEmpty()); + } + + @Test + public void testGetPeerAddressesWhenConverterReturnsNull() { + BtcNodeConverter converter = mock(BtcNodeConverter.class); + when(converter.convertClearNode(any())).thenReturn(null); + + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNodesRepository repository = new BtcNodesRepository(converter, + Collections.singletonList(node)); + + List peers = repository.getPeerAddresses(null, false); + + verify(converter).convertClearNode(any()); + assertTrue(peers.isEmpty()); + } + + @Test + public void testGetPeerAddressesWhenProxyAndClearNodes() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNode onionNode = mock(BtcNode.class); + when(node.hasOnionAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + BtcNodesRepository repository = new BtcNodesRepository(converter, + Lists.newArrayList(node, onionNode)); + + List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), true); + + assertEquals(2, peers.size()); + } + + @Test + public void testGetPeerAddressesWhenOnionNodesOnly() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNode onionNode = mock(BtcNode.class); + when(node.hasOnionAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + BtcNodesRepository repository = new BtcNodesRepository(converter, + Lists.newArrayList(node, onionNode)); + + List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), false); + + assertEquals(1, peers.size()); + } +} diff --git a/core/src/test/java/bisq/core/btc/nodes/BtcNodesSetupPreferencesTest.java b/core/src/test/java/bisq/core/btc/nodes/BtcNodesSetupPreferencesTest.java new file mode 100644 index 0000000000..12ea77c41c --- /dev/null +++ b/core/src/test/java/bisq/core/btc/nodes/BtcNodesSetupPreferencesTest.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.core.btc.nodes; + +import bisq.core.btc.nodes.BtcNodes.BtcNode; +import bisq.core.user.Preferences; + +import java.util.List; + +import org.junit.Test; + +import static bisq.core.btc.nodes.BtcNodes.BitcoinNodesOption.CUSTOM; +import static bisq.core.btc.nodes.BtcNodes.BitcoinNodesOption.PUBLIC; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BtcNodesSetupPreferencesTest { + @Test + public void testSelectPreferredNodesWhenPublicOption() { + Preferences delegate = mock(Preferences.class); + when(delegate.getBitcoinNodesOptionOrdinal()).thenReturn(PUBLIC.ordinal()); + + BtcNodesSetupPreferences preferences = new BtcNodesSetupPreferences(delegate); + List nodes = preferences.selectPreferredNodes(mock(BtcNodes.class)); + + assertTrue(nodes.isEmpty()); + } + + @Test + public void testSelectPreferredNodesWhenCustomOption() { + Preferences delegate = mock(Preferences.class); + when(delegate.getBitcoinNodesOptionOrdinal()).thenReturn(CUSTOM.ordinal()); + when(delegate.getBitcoinNodes()).thenReturn("aaa.onion,bbb.onion"); + + BtcNodesSetupPreferences preferences = new BtcNodesSetupPreferences(delegate); + List nodes = preferences.selectPreferredNodes(mock(BtcNodes.class)); + + assertEquals(2, nodes.size()); + } +} diff --git a/core/src/test/java/bisq/core/btc/wallet/RestrictionsTest.java b/core/src/test/java/bisq/core/btc/wallet/RestrictionsTest.java new file mode 100644 index 0000000000..f66b2d4c38 --- /dev/null +++ b/core/src/test/java/bisq/core/btc/wallet/RestrictionsTest.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.core.btc.wallet; + +import org.bitcoinj.core.Coin; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings("ConstantConditions") +public class RestrictionsTest { + @Test + public void testIsMinSpendableAmount() { + Coin amount = null; + Coin txFee = Coin.valueOf(20000); + + amount = Coin.ZERO; + assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); + + amount = txFee; + assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); + + amount = Restrictions.getMinNonDustOutput(); + assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); + + amount = txFee.add(Restrictions.getMinNonDustOutput()); + assertTrue(Restrictions.isAboveDust(amount.subtract(txFee))); + + amount = txFee.add(Restrictions.getMinNonDustOutput()).add(Coin.valueOf(1)); + assertTrue(Restrictions.isAboveDust(amount.subtract(txFee))); + } +} diff --git a/core/src/test/java/bisq/core/crypto/EncryptionTest.java b/core/src/test/java/bisq/core/crypto/EncryptionTest.java new file mode 100644 index 0000000000..15928da85d --- /dev/null +++ b/core/src/test/java/bisq/core/crypto/EncryptionTest.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.core.crypto; + +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.KeyStorage; +import bisq.common.file.FileUtil; + +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.junit.After; +import org.junit.Before; + +public class EncryptionTest { + private static final Logger log = LoggerFactory.getLogger(EncryptionTest.class); + private KeyRing keyRing; + private File dir; + + @Before + public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { + + dir = File.createTempFile("temp_tests", ""); + //noinspection ResultOfMethodCallIgnored + dir.delete(); + //noinspection ResultOfMethodCallIgnored + dir.mkdir(); + KeyStorage keyStorage = new KeyStorage(dir); + keyRing = new KeyRing(keyStorage); + } + + @After + public void tearDown() throws IOException { + FileUtil.deleteDirectory(dir); + } + + +} diff --git a/core/src/test/java/bisq/core/crypto/SigTest.java b/core/src/test/java/bisq/core/crypto/SigTest.java new file mode 100644 index 0000000000..4255d10fad --- /dev/null +++ b/core/src/test/java/bisq/core/crypto/SigTest.java @@ -0,0 +1,89 @@ +/* + * 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 bisq.core.crypto; + +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.KeyStorage; +import bisq.common.crypto.Sig; +import bisq.common.file.FileUtil; + +import java.io.File; +import java.io.IOException; + +import java.util.Random; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class SigTest { + private static final Logger log = LoggerFactory.getLogger(SigTest.class); + private KeyRing keyRing; + private File dir; + + @Before + public void setup() throws IOException { + + dir = File.createTempFile("temp_tests", ""); + //noinspection ResultOfMethodCallIgnored + dir.delete(); + //noinspection ResultOfMethodCallIgnored + dir.mkdir(); + KeyStorage keyStorage = new KeyStorage(dir); + keyRing = new KeyRing(keyStorage); + } + + @After + public void tearDown() throws IOException { + FileUtil.deleteDirectory(dir); + } + + + @Test + public void testSignature() { + long ts = System.currentTimeMillis(); + log.trace("start "); + for (int i = 0; i < 100; i++) { + String msg = String.valueOf(new Random().nextInt()); + String sig = null; + try { + sig = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), msg); + } catch (CryptoException e) { + log.error("sign failed"); + e.printStackTrace(); + assertTrue(false); + } + try { + assertTrue(Sig.verify(keyRing.getSignatureKeyPair().getPublic(), msg, sig)); + } catch (CryptoException e) { + log.error("verify failed"); + e.printStackTrace(); + assertTrue(false); + } + } + log.trace("took {} ms.", System.currentTimeMillis() - ts); + } +} + + diff --git a/core/src/test/java/bisq/core/dao/governance/ballot/BallotListServiceTest.java b/core/src/test/java/bisq/core/dao/governance/ballot/BallotListServiceTest.java new file mode 100644 index 0000000000..3c5da9c169 --- /dev/null +++ b/core/src/test/java/bisq/core/dao/governance/ballot/BallotListServiceTest.java @@ -0,0 +1,46 @@ +package bisq.core.dao.governance.ballot; + +import bisq.core.dao.governance.ballot.BallotListService.BallotListChangeListener; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.ProposalValidatorProvider; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; + +import bisq.common.persistence.PersistenceManager; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BallotListServiceTest { + @Test + @SuppressWarnings("unchecked") + public void testAddListenersWhenNewPayloadAdded() { + // given + ObservableList payloads = FXCollections.observableArrayList(); + + ProposalService proposalService = mock(ProposalService.class); + when(proposalService.getProposalPayloads()).thenReturn(payloads); + + BallotListService service = new BallotListService(proposalService, mock(PeriodService.class), + mock(ProposalValidatorProvider.class), mock(PersistenceManager.class)); + + BallotListChangeListener listener = mock(BallotListChangeListener.class); + service.addListener(listener); + + service.addListeners(); + + // when + payloads.add(mock(ProposalPayload.class, RETURNS_DEEP_STUBS)); + + // then + verify(listener).onListChanged(any()); + } +} diff --git a/core/src/test/java/bisq/core/dao/governance/proposal/MyProposalListServiceTest.java b/core/src/test/java/bisq/core/dao/governance/proposal/MyProposalListServiceTest.java new file mode 100644 index 0000000000..a851e6cdab --- /dev/null +++ b/core/src/test/java/bisq/core/dao/governance/proposal/MyProposalListServiceTest.java @@ -0,0 +1,30 @@ +package bisq.core.dao.governance.proposal; + +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.persistence.PersistenceManager; + +import javafx.beans.property.SimpleIntegerProperty; + +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MyProposalListServiceTest { + @Test + public void canInstantiate() { + P2PService p2PService = mock(P2PService.class); + when(p2PService.getNumConnectedPeers()).thenReturn(new SimpleIntegerProperty(0)); + PersistenceManager persistenceManager = mock(PersistenceManager.class); + MyProposalListService service = new MyProposalListService(p2PService, + mock(DaoStateService.class), + mock(PeriodService.class), mock(WalletsManager.class), persistenceManager, mock(PubKeyRing.class) + ); + } +} diff --git a/core/src/test/java/bisq/core/dao/governance/proposal/ProposalServiceP2PDataStorageListenerTest.java b/core/src/test/java/bisq/core/dao/governance/proposal/ProposalServiceP2PDataStorageListenerTest.java new file mode 100644 index 0000000000..7945a241b2 --- /dev/null +++ b/core/src/test/java/bisq/core/dao/governance/proposal/ProposalServiceP2PDataStorageListenerTest.java @@ -0,0 +1,127 @@ +/* + * 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 bisq.core.dao.governance.proposal; + +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStorageService; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; +import bisq.core.dao.governance.proposal.storage.temp.TempProposalStorageService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; +import bisq.network.p2p.storage.persistence.ProtectedDataStoreService; + +import javafx.collections.ListChangeListener; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + + +/** + * Tests of the P2PDataStorage::onRemoved callback behavior to ensure that the proper number of signal events occur. + */ +public class ProposalServiceP2PDataStorageListenerTest { + private ProposalService proposalService; + + @Mock + private PeriodService periodService; + + @Mock + private DaoStateService daoStateService; + + @Mock + private ListChangeListener tempProposalListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + this.proposalService = new ProposalService( + mock(P2PService.class), + this.periodService, + mock(ProposalStorageService.class), + mock(TempProposalStorageService.class), + mock(AppendOnlyDataStoreService.class), + mock(ProtectedDataStoreService.class), + this.daoStateService, + mock(ProposalValidatorProvider.class), + true); + + // Create a state so that all added/removed Proposals will actually update the tempProposals list. + when(this.periodService.isInPhase(anyInt(), any(DaoPhase.Phase.class))).thenReturn(true); + when(this.daoStateService.isParseBlockChainComplete()).thenReturn(false); + } + + private static ProtectedStorageEntry buildProtectedStorageEntry() { + ProtectedStorageEntry protectedStorageEntry = mock(ProtectedStorageEntry.class); + TempProposalPayload tempProposalPayload = mock(TempProposalPayload.class); + Proposal tempProposal = mock(Proposal.class); + when(protectedStorageEntry.getProtectedStoragePayload()).thenReturn(tempProposalPayload); + when(tempProposalPayload.getProposal()).thenReturn(tempProposal); + + return protectedStorageEntry; + } + + // TESTCASE: If an onRemoved callback is called which does not remove anything the tempProposals listeners + // are not signaled. + @Test + public void onRemoved_noSignalIfNoChange() { + this.proposalService.onRemoved(Collections.singletonList(mock(ProtectedStorageEntry.class))); + + verify(this.tempProposalListener, never()).onChanged(any()); + } + + // TESTCASE: If an onRemoved callback is called with 1 element AND it creates a remove of 1 element, the tempProposal + // listeners are signaled once. + @Test + public void onRemoved_signalOnceOnOneChange() { + ProtectedStorageEntry one = buildProtectedStorageEntry(); + this.proposalService.onAdded(Collections.singletonList(one)); + this.proposalService.getTempProposals().addListener(this.tempProposalListener); + + this.proposalService.onRemoved(Collections.singletonList(one)); + + verify(this.tempProposalListener).onChanged(any()); + } + + // TESTCASE: If an onRemoved callback is called with 2 elements AND it creates a remove of 2 elements, the + // tempProposal listeners are signaled once. + @Test + public void onRemoved_signalOnceOnMultipleChanges() { + ProtectedStorageEntry one = buildProtectedStorageEntry(); + ProtectedStorageEntry two = buildProtectedStorageEntry(); + this.proposalService.onAdded(Arrays.asList(one, two)); + this.proposalService.getTempProposals().addListener(this.tempProposalListener); + + this.proposalService.onRemoved(Arrays.asList(one, two)); + + verify(this.tempProposalListener).onChanged(any()); + } +} diff --git a/core/src/test/java/bisq/core/dao/governance/proposal/param/ChangeParamValidatorTest.java b/core/src/test/java/bisq/core/dao/governance/proposal/param/ChangeParamValidatorTest.java new file mode 100644 index 0000000000..0ba4bed83d --- /dev/null +++ b/core/src/test/java/bisq/core/dao/governance/proposal/param/ChangeParamValidatorTest.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.dao.governance.proposal.param; + +import bisq.core.dao.governance.param.Param; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ChangeParamValidatorTest { + @Before + public void setup() { + Res.setup(); + } + + @Test + public void testGetChangeValidationResult() throws ParamValidationException { + ChangeParamValidator changeParamValidator = new ChangeParamValidator(null, null, new BsqFormatter()); + try { + changeParamValidator.validationChange(0, 0, 2, 2, Param.UNDEFINED); + Assert.fail(); + } catch (ParamValidationException e) { + Assert.assertEquals(e.getError(), ParamValidationException.ERROR.SAME); + } + + try { + changeParamValidator.validationChange(0, 1, 2, 2, Param.UNDEFINED); + Assert.fail(); + } catch (ParamValidationException e) { + Assert.assertEquals(e.getError(), ParamValidationException.ERROR.NO_CHANGE_POSSIBLE); + } + + try { + changeParamValidator.validationChange(0, -1, 2, 2, Param.UNDEFINED); + Assert.fail(); + } catch (ParamValidationException e) { + Assert.assertEquals(e.getError(), ParamValidationException.ERROR.NO_CHANGE_POSSIBLE); + } + + try { + changeParamValidator.validationChange(2, 4, 2, 1.1, Param.UNDEFINED); + Assert.fail(); + } catch (ParamValidationException e) { + Assert.assertEquals(e.getError(), ParamValidationException.ERROR.TOO_HIGH); + } + + try { + changeParamValidator.validationChange(4, 2, 1.5, 2, Param.UNDEFINED); + Assert.fail(); + } catch (ParamValidationException e) { + Assert.assertEquals(e.getError(), ParamValidationException.ERROR.TOO_LOW); + } + + changeParamValidator.validationChange(4, 2, 2, 2, Param.UNDEFINED); + changeParamValidator.validationChange(2, 4, 2, 2, Param.UNDEFINED); + changeParamValidator.validationChange(0, 1, 0, 0, Param.UNDEFINED); + changeParamValidator.validationChange(0, -1, 0, 0, Param.UNDEFINED); + changeParamValidator.validationChange(-1, 0, 0, 0, Param.UNDEFINED); + changeParamValidator.validationChange(1, 0, 0, 0, Param.UNDEFINED); + } +} diff --git a/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java new file mode 100644 index 0000000000..21b34eb59a --- /dev/null +++ b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java @@ -0,0 +1,205 @@ +/* + * 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 bisq.core.dao.node.full; + +// not converting this test because it is already ignored. +// Intro to jmockit can be found at http://jmockit.github.io/tutorial/Mocking.html +//@Ignore +/* +public class BlockParserTest { + // @Tested classes are instantiated automatically when needed in a test case, + // using injection where possible, see http://jmockit.github.io/tutorial/Mocking.html#tested + // To force instantiate earlier, use availableDuringSetup + @Tested(fullyInitialized = true, availableDuringSetup = true) + BlockParser blockParser; + + @Tested(fullyInitialized = true, availableDuringSetup = true) + DaoStateService daoStateService; + + // @Injectable are mocked resources used to for injecting into @Tested classes + // The naming of these resources doesn't matter, any resource that fits will be used for injection + + // Used by daoStateService + @Injectable + PersistenceProtoResolver persistenceProtoResolver; + @Injectable + File storageDir; + @Injectable + String genesisTxId = "genesisTxId"; + @Injectable + int genesisBlockHeight = 200; + + // Used by fullNodeParser + @Injectable + RpcService rpcService; + @Tested(fullyInitialized = true, availableDuringSetup = true) + DaoStateService writeModel; + @Tested(fullyInitialized = true, availableDuringSetup = true) + TxParser txParser; + + //FIXME + @Test + public void testIsBsqTx() { + // Setup a basic transaction with two inputs + int height = 200; + String hash = "abc123"; + long time = new Date().getTime(); + final List inputs = asList(new TxInput("tx1", 0, null), + new TxInput("tx1", 1, null)); + final List outputs = asList(new RawTxOutput(0, 101, "tx1", null, null, null, height)); + RawTx rawTx = new RawTx("vo", height, hash, time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(outputs)); + + // Return one spendable txoutputs with value, for three test cases + // 1) - null, 0 -> not BSQ transaction + // 2) - 100, null -> BSQ transaction + // 3) - 0, 100 -> BSQ transaction + new Expectations(daoStateService) {{ + // Expectations can be recorded on mocked instances, either with specific matching arguments or catch all + // http://jmockit.github.io/tutorial/Mocking.html#results + // Results are returned in the order they're recorded, so in this case for the first call to + // getSpendableTxOutput("tx1", 0) the return value will be Optional.empty() + // for the second call the return is Optional.of(new TxOutput(0,... and so on + daoStateService.getUnspentTxOutput(new TxOutputKey("tx1", 0)); + result = Optional.empty(); + result = Optional.of(new RawTxOutput(0, 100, "txout1", null, null, null, height)); + result = Optional.of(new RawTxOutput(0, 0, "txout1", null, null, null, height)); + + daoStateService.getUnspentTxOutput(new TxOutputKey("tx1", 1)); + result = Optional.of(new RawTxOutput(0, 0, "txout2", null, null, null, height)); + result = Optional.empty(); + result = Optional.of(new RawTxOutput(0, 100, "txout2", null, null, null, height)); + }}; + String genesisTxId = "genesisTxId"; + int blockHeight = 200; + String blockHash = "abc123"; + Coin genesisTotalSupply = Coin.parseCoin("2.5"); + + // First time there is no BSQ value to spend so it's not a bsq transaction + assertFalse(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + // Second time there is BSQ in the first txout + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + // Third time there is BSQ in the second txout + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + } + + @Test + public void testParseBlocks() { + // Setup blocks to test, starting before genesis + // Only the transactions related to bsq are relevant, no checks are done on correctness of blocks or other txs + // so hashes and most other data don't matter + long time = new Date().getTime(); + int genesisHeight = 200; + int startHeight = 199; + int headHeight = 201; + Coin issuance = Coin.parseCoin("2.5"); + RawTransaction genTx = new RawTransaction("gen block hash", 0, 0L, 0L, genesisTxId); + + // Blockhashes + String bh199 = "blockhash199"; + String bh200 = "blockhash200"; + String bh201 = "blockhash201"; + + // Block 199 + String cbId199 = "cbid199"; + RawTransaction tx199 = new RawTransaction(bh199, 0, 0L, 0L, cbId199); + RawTx cbTx199 = new RawTx(cbId199, 199, bh199, time, + ImmutableList.copyOf(new ArrayList()), + ImmutableList.copyOf(asList(new RawTxOutput(0, 25, cbId199, null, null, null, 199)))); + RawBlock block199 = new RawBlock(bh199, 10, 10, 199, 2, "root", asList(tx199), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", "previousBlockHash", bh200); + + // Genesis Block + String cbId200 = "cbid200"; + RawTransaction tx200 = new RawTransaction(bh200, 0, 0L, 0L, cbId200); + RawTx cbTx200 = new RawTx(cbId200, 200, bh200, time, + ImmutableList.copyOf(new ArrayList()), + ImmutableList.copyOf(asList(new RawTxOutput(0, 25, cbId200, null, null, null, 200)))); + RawTx genesisTx = new RawTx(genesisTxId, 200, bh200, time, + ImmutableList.copyOf(asList(new TxInput("someoldtx", 0, null))), + ImmutableList.copyOf(asList(new RawTxOutput(0, issuance.getValue(), genesisTxId, null, null, null, 200)))); + RawBlock block200 = new RawBlock(bh200, 10, 10, 200, 2, "root", asList(tx200, genTx), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", bh199, bh201); + + // Block 201 + // Make a bsq transaction + String cbId201 = "cbid201"; + String bsqTx1Id = "bsqtx1"; + RawTransaction tx201 = new RawTransaction(bh201, 0, 0L, 0L, cbId201); + RawTransaction txbsqtx1 = new RawTransaction(bh201, 0, 0L, 0L, bsqTx1Id); + long bsqTx1Value1 = Coin.parseCoin("2.4").getValue(); + long bsqTx1Value2 = Coin.parseCoin("0.04").getValue(); + RawTx cbTx201 = new RawTx(cbId201, 201, bh201, time, + ImmutableList.copyOf(new ArrayList()), + ImmutableList.copyOf(asList(new RawTxOutput(0, 25, cbId201, null, null, null, 201)))); + RawTx bsqTx1 = new RawTx(bsqTx1Id, 201, bh201, time, + ImmutableList.copyOf(asList(new TxInput(genesisTxId, 0, null))), + ImmutableList.copyOf(asList(new RawTxOutput(0, bsqTx1Value1, bsqTx1Id, null, null, null, 201), + new RawTxOutput(1, bsqTx1Value2, bsqTx1Id, null, null, null, 201)))); + RawBlock block201 = new RawBlock(bh201, 10, 10, 201, 2, "root", asList(tx201, txbsqtx1), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", bh200, "nextBlockHash"); + + // TODO update test with new API + /* + new Expectations(rpcService) {{ + rpcService.requestBlock(199); + result = block199; + rpcService.requestBlock(200); + result = block200; + rpcService.requestBlock(201); + result = block201; + + rpcService.requestTx(cbId199, 199); + result = cbTx199; + rpcService.requestTx(cbId200, genesisHeight); + result = cbTx200; + rpcService.requestTx(genesisTxId, genesisHeight); + result = genesisTx; + rpcService.requestTx(cbId201, 201); + result = cbTx201; + rpcService.requestTx(bsqTx1Id, 201); + result = bsqTx1; + }}; + + // Running parseBlocks to build the bsq blockchain + fullNodeParser.parseBlocks(startHeight, headHeight, block -> { + }); +*/ + +// Verify that the genesis tx has been added to the bsq blockchain with the correct issuance amount + /* assertTrue(daoStateService.getGenesisTx().get() == genesisTx); + assertTrue(daoStateService.getGenesisTotalSupply().getValue() == issuance.getValue()); + + // And that other txs are not added + assertFalse(daoStateService.containsTx(cbId199)); + assertFalse(daoStateService.containsTx(cbId200)); + assertFalse(daoStateService.containsTx(cbId201)); + + // But bsq txs are added + assertTrue(daoStateService.containsTx(bsqTx1Id)); + TxOutput bsqOut1 = daoStateService.getUnspentAndMatureTxOutput(bsqTx1Id, 0).get(); + assertTrue(daoStateService.isUnspent(bsqOut1)); + assertTrue(bsqOut1.getValue() == bsqTx1Value1); + TxOutput bsqOut2 = daoStateService.getUnspentAndMatureTxOutput(bsqTx1Id, 1).get(); + assertTrue(daoStateService.isUnspent(bsqOut2)); + assertTrue(bsqOut2.getValue() == bsqTx1Value2); + assertFalse(daoStateService.isTxOutputSpendable(genesisTxId, 0)); + assertTrue(daoStateService.isTxOutputSpendable(bsqTx1Id, 0)); + assertTrue(daoStateService.isTxOutputSpendable(bsqTx1Id, 1)); + + } + } + */ diff --git a/core/src/test/java/bisq/core/dao/node/full/RpcServiceTest.java b/core/src/test/java/bisq/core/dao/node/full/RpcServiceTest.java new file mode 100644 index 0000000000..4b10a835ef --- /dev/null +++ b/core/src/test/java/bisq/core/dao/node/full/RpcServiceTest.java @@ -0,0 +1,140 @@ +/* + * 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 bisq.core.dao.node.full; + +import bisq.core.dao.node.full.rpc.dto.RawDtoInput; +import bisq.core.dao.node.full.rpc.dto.DtoSignatureScript; + +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class RpcServiceTest { + private static final String SIGNATURE = "3045" + + "022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b" + + "02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5"; + private static final String SIGNATURE_1 = "3044" + + "02201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f48" + + "0220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b3576"; + private static final String SIGNATURE_2 = "3045" + + "022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46" + + "022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e"; + private static final String PUB_KEY = + "03dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416"; + private static final String PUB_KEY_1 = + "0229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845"; + private static final String PUB_KEY_2 = + "0398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b31"; + private static final String MULTISIG_REDEEM_SCRIPT_HEX = + "5221" + PUB_KEY_1 + "21" + PUB_KEY_2 + "52ae"; // OP_2 pub1 pub2 OP_2 OP_CHECKMULTISIG + private static final String P2WPKH_REDEEM_SCRIPT_HEX = + "0014" + "9bc809698674ec7c01d35d438e9d0de1aa87b6c8"; // 0 hash160 + private static final String P2WSH_REDEEM_SCRIPT_HEX = + "0020" + "223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f"; // 0 hash256 + + @Test + public void testExtractPubKeyAsHex_coinbase() { + checkExtractPubKeyAsHexReturnsNull(new RawDtoInput()); + } + + @Test + public void testExtractPubKeyAsHex_P2PK() { + var input = rawInput(SIGNATURE + "[ALL]"); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2PKH() { + var input = rawInput(SIGNATURE + "[ALL] " + PUB_KEY); + assertEquals(PUB_KEY, RpcService.extractPubKeyAsHex(input, true)); + assertEquals(PUB_KEY, RpcService.extractPubKeyAsHex(input, false)); + } + + @Test + public void testExtractPubKeyAsHex_P2WPKH() { + var input = rawInput("", SIGNATURE + "01", PUB_KEY); + assertEquals(PUB_KEY, RpcService.extractPubKeyAsHex(input, true)); + assertNull(RpcService.extractPubKeyAsHex(input, false)); + } + + @Test + public void testExtractPubKeyAsHex_P2SH_P2WPKH() { + var input = rawInput(P2WPKH_REDEEM_SCRIPT_HEX, SIGNATURE + "01", PUB_KEY); + assertEquals(PUB_KEY, RpcService.extractPubKeyAsHex(input, true)); + assertNull(RpcService.extractPubKeyAsHex(input, false)); + } + + @Test + public void testExtractPubKeyAsHex_P2PKH_nonDefaultSighash() { + var input = rawInput(SIGNATURE + "[SINGLE|ANYONECANPAY] " + PUB_KEY); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2WPKH_nonDefaultSighash() { + var input = rawInput("", SIGNATURE + "82", PUB_KEY); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2SH_P2WPKH_nonDefaultSighash() { + var input = rawInput(P2WPKH_REDEEM_SCRIPT_HEX, SIGNATURE + "82", PUB_KEY); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2SH_multisig() { + var input = rawInput("0 " + SIGNATURE_1 + "[ALL] " + SIGNATURE_2 + "[ALL] " + MULTISIG_REDEEM_SCRIPT_HEX); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2SH_multisig_nonDefaultSighash() { + var input = rawInput("0 " + SIGNATURE_1 + "[ALL] " + SIGNATURE_2 + "[NONE] " + MULTISIG_REDEEM_SCRIPT_HEX); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2WSH_multisig() { + var input = rawInput("", "", SIGNATURE_1 + "01", SIGNATURE_2 + "01", MULTISIG_REDEEM_SCRIPT_HEX); + checkExtractPubKeyAsHexReturnsNull(input); + } + + @Test + public void testExtractPubKeyAsHex_P2SH_P2WSH_multisig() { + var input = rawInput(P2WSH_REDEEM_SCRIPT_HEX, "", SIGNATURE_1 + "01", SIGNATURE_2 + "01", MULTISIG_REDEEM_SCRIPT_HEX); + checkExtractPubKeyAsHexReturnsNull(input); + } + + private static void checkExtractPubKeyAsHexReturnsNull(RawDtoInput input) { + assertNull(RpcService.extractPubKeyAsHex(input, true)); + assertNull(RpcService.extractPubKeyAsHex(input, false)); + } + + private static RawDtoInput rawInput(String asm, String... txInWitness) { + var input = new RawDtoInput(); + var scriptSig = new DtoSignatureScript(); + scriptSig.setAsm(asm); + input.setScriptSig(scriptSig); + input.setTxInWitness(txInWitness.length > 0 ? List.of(txInWitness) : null); + return input; + } +} diff --git a/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindClientTest.java b/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindClientTest.java new file mode 100644 index 0000000000..ab3548cb98 --- /dev/null +++ b/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindClientTest.java @@ -0,0 +1,234 @@ +/* + * 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 bisq.core.dao.node.full.rpc; + +import bisq.core.dao.node.full.rpc.dto.RawDtoBlock; +import bisq.core.dao.node.full.rpc.dto.RawDtoTransaction; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + + +import com.googlecode.jsonrpc4j.HttpException; +import com.googlecode.jsonrpc4j.JsonRpcClientException; +import com.googlecode.jsonrpc4j.RequestIDGenerator; +import kotlin.text.Charsets; + +public class BitcoindClientTest { + private static final String TEST_BLOCK_HASH = "015f37a20d517645a11a6cdd316049f41bc77b4a4057b2dd092114b78147f42c"; + private static final String TEST_BLOCK_VERBOSITY_0 = readFromResourcesUnPrettified("getblock-result-verbosity-0.txt"); + private static final String TEST_BLOCK_VERBOSITY_1 = readFromResourcesUnPrettified("getblock-result-verbosity-1.json"); + private static final String TEST_BLOCK_VERBOSITY_2 = readFromResourcesUnPrettified("getblock-result-verbosity-2.json"); + private static final String TEST_NETWORK_INFO = readFromResourcesUnPrettified("getnetworkinfo-result.json"); + + private BitcoindClient client; + private int mockResponseCode = 200; + private boolean canConnect = true; + private ByteArrayInputStream mockResponse; + private ByteArrayInputStream mockErrorResponse; + private ByteArrayOutputStream mockOutputStream = new ByteArrayOutputStream(); + + @Before + public void setUp() throws Exception { + var mockURLStreamHandler = mock(MyURLStreamHandler.class); + var mockRequestIDGenerator = mock(RequestIDGenerator.class); + + client = BitcoindClient.builder() + .rpcHost("127.0.0.1") + .rpcPort(18443) + .rpcUser("bisqdao") + .rpcPassword("bsq") + .urlStreamHandler(mockURLStreamHandler) + .requestIDGenerator(mockRequestIDGenerator) + .build(); + + when(mockURLStreamHandler.openConnection(any(), any())).then(inv -> { + var connection = mock(HttpURLConnection.class); + if (canConnect) { + when(connection.getOutputStream()).thenReturn(mockOutputStream); + if (mockResponseCode < 400) { + when(connection.getInputStream()).thenReturn(mockResponse); + } else { + when(connection.getInputStream()).thenThrow(IOException.class); + when(connection.getErrorStream()).thenReturn(mockErrorResponse); + } + } else { + doThrow(ConnectException.class).when(connection).connect(); + } + return connection; + }); + when(mockRequestIDGenerator.generateID()).thenReturn("987654321"); + } + + @Test + public void testGetBlockCount() throws Exception { + var expectedRequest = toJson("{'id':'987654321','jsonrpc':'2.0','method':'getblockcount','params':[]}"); + mockResponse = toJsonIS("{'result':'150','error':null,'id':'123456789'}"); + + assertEquals((Integer) 150, client.getBlockCount()); + assertEquals(expectedRequest, mockOutputStream.toString(UTF_8)); + } + + @Test(expected = ConnectException.class) + public void testGetBlockCount_noConnection() throws Exception { + canConnect = false; + + client.getBlockCount(); + } + + @Test(expected = HttpException.class) + public void testGetBlockCount_wrongCredentials() throws Exception { + mockResponseCode = 401; +// mockResponseCustomHeaders.put("WWW-Authenticate", "[Basic realm=\"jsonrpc\"]"); + + client.getBlockCount(); + } + + @Test + public void testGetBlockHash() throws Exception { + var expectedRequest = toJson("{'id':'987654321','jsonrpc':'2.0','method':'getblockhash','params':[139]}"); + mockResponse = toJsonIS("{'result':'" + TEST_BLOCK_HASH + "','error':null,'id':'123456789'}"); + + assertEquals(TEST_BLOCK_HASH, client.getBlockHash(139)); + assertEquals(expectedRequest, mockOutputStream.toString(UTF_8)); + } + + @Test + public void testGetBestBlockHash() throws Exception { + var expectedRequest = toJson("{'id':'987654321','jsonrpc':'2.0','method':'getbestblockhash','params':[]}"); + mockResponse = toJsonIS("{'result':'" + TEST_BLOCK_HASH + "','error':null,'id':'123456789'}"); + + assertEquals(TEST_BLOCK_HASH, client.getBestBlockHash()); + assertEquals(expectedRequest, mockOutputStream.toString(UTF_8)); + } + + @Test(expected = JsonRpcClientException.class) + public void testGetBlockHash_heightOutOfRange() throws Exception { + mockResponseCode = 500; + mockErrorResponse = toJsonIS("{'result':null,'error':{'code':-8,'message':'Block height out of range'},'id':'123456789'}"); + + client.getBlockHash(151); + } + + @Test + public void testGetBlock_verbosity_0() throws Exception { + doTestGetBlock(0, "\"" + TEST_BLOCK_VERBOSITY_0 + "\""); + } + + @Test + public void testGetBlock_verbosity_1() throws Exception { + doTestGetBlock(1, TEST_BLOCK_VERBOSITY_1); + } + + @Test + public void testGetBlock_verbosity_2() throws Exception { + doTestGetBlock(2, TEST_BLOCK_VERBOSITY_2); + } + + private void doTestGetBlock(int verbosity, String blockJson) throws Exception { + var expectedRequest = toJson("{'id':'987654321','jsonrpc':'2.0','method':'getblock','params':['" + + TEST_BLOCK_HASH + "'," + verbosity + "]}"); + mockResponse = toJsonIS("{'result':" + blockJson + ",'error':null,'id':'123456789'}"); + + var block = client.getBlock(TEST_BLOCK_HASH, verbosity); + var blockJsonRoundTripped = new ObjectMapper().writeValueAsString(block); + + assertEquals(verbosity == 0, block instanceof RawDtoBlock.Summarized); + assertEquals(verbosity == 1, block.getTx() != null && + block.getTx().stream().allMatch(tx -> tx instanceof RawDtoTransaction.Summarized)); + + assertEquals(blockJson, blockJsonRoundTripped); + assertEquals(expectedRequest, mockOutputStream.toString(UTF_8)); + } + + @Test(expected = JsonRpcClientException.class) + public void testGetBlock_blockNotFound() throws Exception { + mockResponseCode = 500; + mockErrorResponse = toJsonIS("{'result':null,'error':{'code':-5,'message':'Block not found'},'id':'123456789'}"); + + client.getBlock(TEST_BLOCK_HASH.replace('f', 'e'), 2); + } + + @Test(expected = JsonRpcClientException.class) + public void testGetBlock_malformedHash() throws Exception { + mockResponseCode = 500; + mockErrorResponse = toJsonIS("{'result':null,'error':{'code':-8,'message':'blockhash must be of length 64 " + + "(not 3, for \\'foo\\')'},'id':'123456789'}"); + + client.getBlock("foo", 2); + } + + @Test + public void testGetNetworkInfo() throws Exception { + var expectedRequest = toJson("{'id':'987654321','jsonrpc':'2.0','method':'getnetworkinfo','params':[]}"); + mockResponse = toJsonIS("{'result':" + TEST_NETWORK_INFO + ",'error':null,'id':'123456789'}"); + + var networkInfo = client.getNetworkInfo(); + var networkInfoRoundTripped = new ObjectMapper().writeValueAsString(networkInfo); + var expectedNetworkInfoStr = TEST_NETWORK_INFO.replace("MY_CUSTOM_SERVICE", "UNKNOWN"); + + assertEquals(expectedNetworkInfoStr, networkInfoRoundTripped); + assertEquals(expectedRequest, mockOutputStream.toString(UTF_8)); + } + + private static String toJson(String json) { + return json.replace("'", "\"").replace("\\\"", "'"); + } + + private static ByteArrayInputStream toJsonIS(String json) { + return new ByteArrayInputStream(toJson(json).getBytes(UTF_8)); + } + + private static String readFromResourcesUnPrettified(String resourceName) { + try { + var path = Paths.get(BitcoindClientTest.class.getResource(resourceName).toURI()); + return new String(Files.readAllBytes(path), Charsets.UTF_8).replaceAll("(\\s+\\B|\\B\\s+|\\v)", ""); + } catch (Exception e) { + return ""; + } + } + + private static abstract class MyURLStreamHandler extends URLStreamHandler { + @Override + public abstract URLConnection openConnection(URL u, Proxy p); + } +} diff --git a/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindDaemonTest.java b/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindDaemonTest.java new file mode 100644 index 0000000000..0cc9dc2065 --- /dev/null +++ b/core/src/test/java/bisq/core/dao/node/full/rpc/BitcoindDaemonTest.java @@ -0,0 +1,183 @@ +/* + * 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 bisq.core.dao.node.full.rpc; + +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; + +public class BitcoindDaemonTest { + private BitcoindDaemon daemon; + private int acceptAnotherCount; + private CountDownLatch errorHandlerLatch = new CountDownLatch(1); + private Consumer errorHandler = mock(ThrowableConsumer.class); + private BitcoindDaemon.BlockListener blockListener = mock(BitcoindDaemon.BlockListener.class); + private Socket socket = mock(Socket.class); + private volatile boolean socketClosed; + + @Before + public void setUp() throws Exception { + var serverSocket = mock(ServerSocket.class); + + when(serverSocket.accept()).then(invocation -> waitToAccept(() -> { + if (socketClosed) { + throw new SocketException(); + } + return socket; + })); + doAnswer((VoidAnswer) invocation -> { + socketClosed = true; + acceptAnother(1); + }).when(serverSocket).close(); + + doAnswer((VoidAnswer) invocation -> errorHandlerLatch.countDown()).when(errorHandler).accept(any()); + + daemon = new BitcoindDaemon(serverSocket, errorHandler); + daemon.setBlockListener(blockListener); + } + + @After + public void tearDown() { + daemon.shutdown(); + } + + @Test + public void testNoBlocksMissedDuringFloodOfIncomingBlocks() throws Exception { + var latch = new CountDownLatch(1); // to block all the daemon worker threads until shutdown, as if stuck + + doAnswer((VoidAnswer) invocation -> latch.await()).when(blockListener).blockDetected(any()); + when(socket.getInputStream()).then(invocation -> new ByteArrayInputStream("foo".getBytes())); + + acceptAnother(50); + waitUntilAllAccepted(); + + // Unblock all the daemon worker threads and shut down. + latch.countDown(); + daemon.shutdown(); + + verify(blockListener, times(50)).blockDetected("foo"); + } + + @Test + public void testBlockHashIsTrimmed() throws Exception { + when(socket.getInputStream()).then(invocation -> new ByteArrayInputStream("\r\nbar \n".getBytes())); + + acceptAnother(1); + waitUntilAllAccepted(); + daemon.shutdown(); + + verify(blockListener).blockDetected("bar"); + } + + @Test + public void testBrokenSocketRead() throws Exception { + when(socket.getInputStream()).thenThrow(IOException.class); + + acceptAnother(1); + errorHandlerLatch.await(5, TimeUnit.SECONDS); + + verify(errorHandler).accept(argThat(t -> t instanceof NotificationHandlerException && + t.getCause() instanceof IOException)); + } + + @Test + public void testRuntimeExceptionInBlockListener() throws Exception { + daemon.setBlockListener(blockHash -> { + throw new IndexOutOfBoundsException(); + }); + when(socket.getInputStream()).then(invocation -> new ByteArrayInputStream("foo".getBytes())); + + acceptAnother(1); + errorHandlerLatch.await(5, TimeUnit.SECONDS); + + verify(errorHandler).accept(argThat(t -> t instanceof NotificationHandlerException && + t.getCause() instanceof IndexOutOfBoundsException)); + } + + + @Test + public void testErrorInBlockListener() throws Exception { + synchronized (this) { + daemon.setBlockListener(blockHash -> { + throw new Error(); + }); + when(socket.getInputStream()).then(invocation -> new ByteArrayInputStream("foo".getBytes())); + acceptAnother(1); + } + errorHandlerLatch.await(5, TimeUnit.SECONDS); + + verify(errorHandler).accept(any(Error.class)); + } + + @Test(expected = NotificationHandlerException.class) + public void testUnknownHost() throws Exception { + new BitcoindDaemon("[", -1, errorHandler).shutdown(); + } + + private synchronized void acceptAnother(int n) { + acceptAnotherCount += n; + notifyAll(); + } + + private synchronized V waitToAccept(Callable onAccept) throws Exception { + while (acceptAnotherCount == 0) { + wait(); + } + var result = onAccept.call(); + acceptAnotherCount--; + notifyAll(); + return result; + } + + private synchronized void waitUntilAllAccepted() throws InterruptedException { + while (acceptAnotherCount > 0) { + wait(); + } + notifyAll(); + } + + private interface ThrowableConsumer extends Consumer { + } + + private interface VoidAnswer extends Answer { + void voidAnswer(InvocationOnMock invocation) throws Throwable; + + @Override + default Void answer(InvocationOnMock invocation) throws Throwable { + voidAnswer(invocation); + return null; + } + } +} diff --git a/core/src/test/java/bisq/core/dao/node/parser/GenesisTxParserTest.java b/core/src/test/java/bisq/core/dao/node/parser/GenesisTxParserTest.java new file mode 100644 index 0000000000..62a591f3ab --- /dev/null +++ b/core/src/test/java/bisq/core/dao/node/parser/GenesisTxParserTest.java @@ -0,0 +1,200 @@ +/* + * 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 bisq.core.dao.node.parser; + +import bisq.core.dao.node.full.RawTx; +import bisq.core.dao.node.full.RawTxOutput; +import bisq.core.dao.node.parser.exceptions.InvalidGenesisTxException; +import bisq.core.dao.state.model.blockchain.TxInput; +import bisq.core.dao.state.model.blockchain.TxOutputType; +import bisq.core.dao.state.model.blockchain.TxType; + +import org.bitcoinj.core.Coin; + +import com.google.common.collect.ImmutableList; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class GenesisTxParserTest { + + @Test + public void testIsGenesis() { + // fixme(chirhonul): Assert.assertEquals(2, 3); + + int blockHeight = 200; + String blockHash = "abc123"; + Coin genesisTotalSupply = Coin.parseCoin("2.5"); + long time = new Date().getTime(); + final List inputs = Arrays.asList( + new TxInput("tx0", 0, null), + new TxInput("tx1", 1, null) + ); + RawTxOutput output = new RawTxOutput( + 0, + genesisTotalSupply.value, + null, + null, + null, + null, + blockHeight + ); + RawTx rawTx = new RawTx( + "tx2", + blockHeight, + blockHash, + time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(Arrays.asList(output)) + ); + + String genesisTxId = "genesisTxId"; + int genesisBlockHeight = 150; + + // With mismatch in block height and tx id, we should not get genesis tx back. + boolean result = GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight); + boolean want = false; + Assert.assertEquals(want, result); + + // With correct block height but mismatch in tx id, we should still not get genesis tx back. + blockHeight = 150; + rawTx = new RawTx( + "tx2", + blockHeight, + blockHash, + time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(Arrays.asList(output)) + ); + result = GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight); + want = false; + Assert.assertEquals(want, result); + + } + + @Test + public void testGetGenesisTempTx() { + int blockHeight = 200; + String blockHash = "abc123"; + Coin genesisTotalSupply = Coin.parseCoin("2.5"); + long time = new Date().getTime(); + final List inputs = Arrays.asList( + new TxInput("tx0", 0, null), + new TxInput("tx1", 1, null) + ); + RawTxOutput output = new RawTxOutput( + 0, + genesisTotalSupply.value, + null, + null, + null, + null, + blockHeight + ); + + String genesisTxId = "genesisTxId"; + // With correct tx id and block height, we should find our genesis tx with correct tx and output type. + RawTx rawTx = new RawTx( + genesisTxId, + blockHeight, + blockHash, + time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(Arrays.asList(output)) + ); + TempTx resultTempTx = GenesisTxParser.getGenesisTempTx(rawTx, genesisTotalSupply); + + TempTx tempTx = TempTx.fromRawTx(rawTx); + tempTx.setTxType(TxType.GENESIS); + for (int i = 0; i < tempTx.getTempTxOutputs().size(); ++i) { + tempTx.getTempTxOutputs().get(i).setTxOutputType(TxOutputType.GENESIS_OUTPUT); + } + TempTx wantTempTx = tempTx; + + Assert.assertEquals(wantTempTx, resultTempTx); + + // With correct tx id and block height, but too low sum of outputs (lower than genesisTotalSupply), we + // should see an exception raised. + output = new RawTxOutput( + 0, + genesisTotalSupply.value - 1, + null, + null, + null, + null, + blockHeight + ); + rawTx = new RawTx( + genesisTxId, + blockHeight, + blockHash, + time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(Arrays.asList(output)) + ); + try { + GenesisTxParser.getGenesisTempTx(rawTx, genesisTotalSupply); + Assert.fail("Expected an InvalidGenesisTxException to be thrown when outputs are too low"); + } catch (InvalidGenesisTxException igtxe) { + String wantMessage = "Genesis tx is invalid; not using all available inputs. Remaining input value is 1 sat"; + Assert.assertTrue("Unexpected exception, want message starting with " + + "'" + wantMessage + "', got '" + igtxe.getMessage() + "'", igtxe.getMessage().startsWith(wantMessage)); + } + + // With correct tx id and block height, but too high sum of outputs (higher than from genesisTotalSupply), we + // should see an exception raised. + RawTxOutput output1 = new RawTxOutput( + 0, + genesisTotalSupply.value - 2, + null, + null, + null, + null, + blockHeight + ); + RawTxOutput output2 = new RawTxOutput( + 0, + 3, + null, + null, + null, + null, + blockHeight + ); + rawTx = new RawTx( + genesisTxId, + blockHeight, + blockHash, + time, + ImmutableList.copyOf(inputs), + ImmutableList.copyOf(Arrays.asList(output1, output2)) + ); + try { + GenesisTxParser.getGenesisTempTx(rawTx, genesisTotalSupply); + Assert.fail("Expected an InvalidGenesisTxException to be thrown when outputs are too high"); + } catch (InvalidGenesisTxException igtxe) { + String wantMessage = "Genesis tx is invalid; using more than available inputs. Remaining input value is 2 sat"; + Assert.assertTrue("Unexpected exception, want message starting with " + + "'" + wantMessage + "', got '" + igtxe.getMessage() + "'", igtxe.getMessage().startsWith(wantMessage)); + } + } +} diff --git a/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java b/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java new file mode 100644 index 0000000000..96857e59bb --- /dev/null +++ b/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java @@ -0,0 +1,72 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.util.coin.BsqFormatter; + +import org.bitcoinj.core.Coin; + +import org.junit.Assert; +import org.junit.Test; + +public class DaoStateServiceTest { + @Test + public void testIsBlockHashKnown() { + DaoStateService stateService = new DaoStateService( + new DaoState(), + new GenesisTxInfo("fakegenesistxid", 100, Coin.parseCoin("2.5").value), + new BsqFormatter()); + Assert.assertEquals( + "Unknown block should not exist.", + false, + stateService.isBlockHashKnown("fakeblockhash0") + ); + + Block block = new Block(0, 1534800000, "fakeblockhash0", null); + stateService.onNewBlockHeight(0); + stateService.onNewBlockWithEmptyTxs(block); + Assert.assertEquals( + "Block has to be genesis block to get added.", + false, + stateService.isBlockHashKnown("fakeblockhash0") + ); + + Assert.assertEquals( + "Block that was never added should still not exist.", + false, + stateService.isBlockHashKnown("fakeblockhash1") + ); + + block = new Block(1, 1534800001, "fakeblockhash1", null); + stateService.onNewBlockHeight(1); + stateService.onNewBlockWithEmptyTxs(block); + block = new Block(2, 1534800002, "fakeblockhash2", null); + stateService.onNewBlockHeight(2); + stateService.onNewBlockWithEmptyTxs(block); + block = new Block(3, 1534800003, "fakeblockhash3", null); + stateService.onNewBlockHeight(3); + stateService.onNewBlockWithEmptyTxs(block); + Assert.assertEquals( + "Block that was never added should still not exist after adding more blocks.", + false, + stateService.isBlockHashKnown("fakeblockhash4") + ); + } +} diff --git a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java new file mode 100644 index 0000000000..a9e773878a --- /dev/null +++ b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.core.dao.state; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.state.storage.DaoStateStorageService; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class DaoStateSnapshotServiceTest { + + private DaoStateSnapshotService daoStateSnapshotService; + + @Before + public void setup() { + daoStateSnapshotService = new DaoStateSnapshotService(mock(DaoStateService.class), + mock(GenesisTxInfo.class), + mock(DaoStateStorageService.class), + mock(DaoStateMonitoringService.class), + null); + } + + @Test + public void testGetSnapshotHeight() { + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 0, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 100, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 102, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 119, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 120, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 121, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 130, 10)); + assertEquals(120, daoStateSnapshotService.getSnapshotHeight(102, 139, 10)); + assertEquals(130, daoStateSnapshotService.getSnapshotHeight(102, 140, 10)); + assertEquals(130, daoStateSnapshotService.getSnapshotHeight(102, 141, 10)); + assertEquals(990, daoStateSnapshotService.getSnapshotHeight(102, 1000, 10)); + } + + @Test + public void testSnapshotHeight() { + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 0, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 80, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 90, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 100, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 119, 10)); + assertTrue(daoStateSnapshotService.isSnapshotHeight(102, 120, 10)); + assertTrue(daoStateSnapshotService.isSnapshotHeight(102, 130, 10)); + assertTrue(daoStateSnapshotService.isSnapshotHeight(102, 140, 10)); + assertTrue(daoStateSnapshotService.isSnapshotHeight(102, 200, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 201, 10)); + assertFalse(daoStateSnapshotService.isSnapshotHeight(102, 199, 10)); + } +} diff --git a/core/src/test/java/bisq/core/dao/voting/voteresult/VoteResultConsensusTest.java b/core/src/test/java/bisq/core/dao/voting/voteresult/VoteResultConsensusTest.java new file mode 100644 index 0000000000..c6d6de3c4f --- /dev/null +++ b/core/src/test/java/bisq/core/dao/voting/voteresult/VoteResultConsensusTest.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.core.dao.voting.voteresult; + +import bisq.core.dao.governance.merit.MeritConsensus; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertEquals; + +@Slf4j +public class VoteResultConsensusTest { + @Before + public void setup() { + } + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void testGetWeightedMeritAmount() { + int currentChainHeight; + int blocksPerYear = 50_000; // 144*365=51264; + currentChainHeight = 1_000_000; + + assertEquals("fresh issuance", 100000, MeritConsensus.getWeightedMeritAmount(100_000, 1_000_000, + currentChainHeight, blocksPerYear)); + assertEquals("0.5 year old issuance", 75000, MeritConsensus.getWeightedMeritAmount(100_000, 975_000, + currentChainHeight, blocksPerYear)); + assertEquals("1 year old issuance", 50000, MeritConsensus.getWeightedMeritAmount(100_000, 950_000, + currentChainHeight, blocksPerYear)); + assertEquals("1.5 year old issuance", 25000, MeritConsensus.getWeightedMeritAmount(100_000, 925_000, + currentChainHeight, blocksPerYear)); + assertEquals("2 year old issuance", 0, MeritConsensus.getWeightedMeritAmount(100_000, 900_000, + currentChainHeight, blocksPerYear)); + assertEquals("3 year old issuance", 0, MeritConsensus.getWeightedMeritAmount(100_000, 850_000, + currentChainHeight, blocksPerYear)); + + + assertEquals("1 block old issuance", 99999, MeritConsensus.getWeightedMeritAmount(100_000, 999_999, + currentChainHeight, blocksPerYear)); + assertEquals("2 block old issuance", 99998, MeritConsensus.getWeightedMeritAmount(100_000, 999_998, + currentChainHeight, blocksPerYear)); + assertEquals("10 blocks old issuance", 99990, MeritConsensus.getWeightedMeritAmount(100_000, 999_990, + currentChainHeight, blocksPerYear)); + assertEquals("100 blocks old issuance", 99900, MeritConsensus.getWeightedMeritAmount(100_000, 999_900, + currentChainHeight, blocksPerYear)); + assertEquals("99_999 blocks old issuance", 1, MeritConsensus.getWeightedMeritAmount(100_000, 900_001, + currentChainHeight, blocksPerYear)); + assertEquals("99_990 blocks old issuance", 10, MeritConsensus.getWeightedMeritAmount(100_000, 900_010, + currentChainHeight, blocksPerYear)); + assertEquals("100_001 blocks old issuance", 0, MeritConsensus.getWeightedMeritAmount(100_000, 899_999, + currentChainHeight, blocksPerYear)); + assertEquals("1_000_000 blocks old issuance", 0, MeritConsensus.getWeightedMeritAmount(100_000, 0, + currentChainHeight, blocksPerYear)); + } + + @Test + public void testInvalidChainHeight() { + exception.expect(IllegalArgumentException.class); + MeritConsensus.getWeightedMeritAmount(100_000, 2_000_000, 1_000_000, 1_000_000); + } + + @Test + public void testInvalidIssuanceHeight() { + exception.expect(IllegalArgumentException.class); + MeritConsensus.getWeightedMeritAmount(100_000, -1, 1_000_000, 1_000_000); + } + + @Test + public void testInvalidAmount() { + exception.expect(IllegalArgumentException.class); + MeritConsensus.getWeightedMeritAmount(-100_000, 1, 1_000_000, 1_000_000); + } + + @Test + public void testInvalidCurrentChainHeight() { + exception.expect(IllegalArgumentException.class); + MeritConsensus.getWeightedMeritAmount(100_000, 1, -1, 1_000_000); + } + + @Test + public void testInvalidBlockPerYear() { + exception.expect(IllegalArgumentException.class); + MeritConsensus.getWeightedMeritAmount(100_000, 1, 11, -1); + } +} diff --git a/core/src/test/java/bisq/core/locale/BankUtilTest.java b/core/src/test/java/bisq/core/locale/BankUtilTest.java new file mode 100644 index 0000000000..faed124eb2 --- /dev/null +++ b/core/src/test/java/bisq/core/locale/BankUtilTest.java @@ -0,0 +1,47 @@ +package bisq.core.locale; + +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class BankUtilTest { + + @Before + public void setup() { + Locale.setDefault(new Locale("en", "US")); + GlobalSettings.setLocale(new Locale("en", "US")); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); + } + + @Test + public void testBankFieldsForArgentina() { + final String argentina = "AR"; + + assertTrue(BankUtil.isHolderIdRequired(argentina)); + assertEquals("CUIL/CUIT", BankUtil.getHolderIdLabel(argentina)); + assertEquals("CUIT", BankUtil.getHolderIdLabelShort(argentina)); + + assertTrue(BankUtil.isNationalAccountIdRequired(argentina)); + assertEquals("CBU number", BankUtil.getNationalAccountIdLabel(argentina)); + + assertTrue(BankUtil.isBankNameRequired(argentina)); + + assertTrue(BankUtil.isBranchIdRequired(argentina)); + assertTrue(BankUtil.isAccountNrRequired(argentina)); + assertEquals("Número de cuenta", BankUtil.getAccountNrLabel(argentina)); + + assertTrue(BankUtil.useValidation(argentina)); + + assertFalse(BankUtil.isBankIdRequired(argentina)); + assertFalse(BankUtil.isStateRequired(argentina)); + assertFalse(BankUtil.isAccountTypeRequired(argentina)); + + } + +} diff --git a/core/src/test/java/bisq/core/locale/CurrencyUtilTest.java b/core/src/test/java/bisq/core/locale/CurrencyUtilTest.java new file mode 100644 index 0000000000..592b438e46 --- /dev/null +++ b/core/src/test/java/bisq/core/locale/CurrencyUtilTest.java @@ -0,0 +1,155 @@ +/* + * 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 bisq.core.locale; + +import bisq.common.config.BaseCurrencyNetwork; + +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; +import bisq.asset.Coin; +import bisq.asset.coins.Ether; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CurrencyUtilTest { + + @Before + public void setup() { + + Locale.setDefault(new Locale("en", "US")); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); + } + + @Test + public void testGetTradeCurrency() { + Optional euro = CurrencyUtil.getTradeCurrency("EUR"); + Optional naira = CurrencyUtil.getTradeCurrency("NGN"); + Optional fake = CurrencyUtil.getTradeCurrency("FAK"); + + assertTrue(euro.isPresent()); + assertTrue(naira.isPresent()); + assertFalse("Fake currency shouldn't exist", fake.isPresent()); + } + + @Test + public void testFindAsset() { + MockAssetRegistry assetRegistry = new MockAssetRegistry(); + + // test if code is matching + boolean daoTradingActivated = false; + // Test if BSQ on mainnet is failing + Assert.assertFalse(CurrencyUtil.findAsset(assetRegistry, "BSQ", + BaseCurrencyNetwork.BTC_MAINNET, daoTradingActivated).isPresent()); + + // on testnet/regtest it is allowed + assertEquals(CurrencyUtil.findAsset(assetRegistry, "BSQ", + BaseCurrencyNetwork.BTC_TESTNET, daoTradingActivated).get().getTickerSymbol(), "BSQ"); + + + daoTradingActivated = true; + // With daoTradingActivated we can request BSQ + assertEquals(CurrencyUtil.findAsset(assetRegistry, "BSQ", + BaseCurrencyNetwork.BTC_MAINNET, daoTradingActivated).get().getTickerSymbol(), "BSQ"); + + // Test if not matching ticker is failing + Assert.assertFalse(CurrencyUtil.findAsset(assetRegistry, "BSQ1", + BaseCurrencyNetwork.BTC_MAINNET, daoTradingActivated).isPresent()); + + // Add a mock coin which has no mainnet version, needs to fail if we are on mainnet + MockTestnetCoin.Testnet mockTestnetCoin = new MockTestnetCoin.Testnet(); + try { + assetRegistry.addAsset(mockTestnetCoin); + CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", + BaseCurrencyNetwork.BTC_MAINNET, daoTradingActivated); + Assert.fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { + String wantMessage = "We are on mainnet and we could not find an asset with network type mainnet"; + Assert.assertTrue("Unexpected exception, want message starting with " + + "'" + wantMessage + "', got '" + e.getMessage() + "'", e.getMessage().startsWith(wantMessage)); + } + + // For testnet its ok + assertEquals(CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", + BaseCurrencyNetwork.BTC_TESTNET, daoTradingActivated).get().getTickerSymbol(), "MOCK_COIN"); + assertEquals(Coin.Network.TESTNET, mockTestnetCoin.getNetwork()); + + // For regtest its still found + assertEquals(CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", + BaseCurrencyNetwork.BTC_REGTEST, daoTradingActivated).get().getTickerSymbol(), "MOCK_COIN"); + + + // We test if we are not on mainnet to get the mainnet coin + Coin ether = new Ether(); + assertEquals(CurrencyUtil.findAsset(assetRegistry, "ETH", + BaseCurrencyNetwork.BTC_TESTNET, daoTradingActivated).get().getTickerSymbol(), "ETH"); + assertEquals(CurrencyUtil.findAsset(assetRegistry, "ETH", + BaseCurrencyNetwork.BTC_REGTEST, daoTradingActivated).get().getTickerSymbol(), "ETH"); + assertEquals(Coin.Network.MAINNET, ether.getNetwork()); + + // We test if network matches exactly if there are distinct network types defined like with BSQ + Coin bsq = (Coin) CurrencyUtil.findAsset(assetRegistry, "BSQ", BaseCurrencyNetwork.BTC_MAINNET, daoTradingActivated).get(); + assertEquals("BSQ", bsq.getTickerSymbol()); + assertEquals(Coin.Network.MAINNET, bsq.getNetwork()); + + bsq = (Coin) CurrencyUtil.findAsset(assetRegistry, "BSQ", BaseCurrencyNetwork.BTC_TESTNET, daoTradingActivated).get(); + assertEquals("BSQ", bsq.getTickerSymbol()); + assertEquals(Coin.Network.TESTNET, bsq.getNetwork()); + + bsq = (Coin) CurrencyUtil.findAsset(assetRegistry, "BSQ", BaseCurrencyNetwork.BTC_REGTEST, daoTradingActivated).get(); + assertEquals("BSQ", bsq.getTickerSymbol()); + assertEquals(Coin.Network.REGTEST, bsq.getNetwork()); + } + + @Test + public void testGetNameAndCodeOfRemovedAsset() { + assertEquals("Bitcoin Cash (BCH)", CurrencyUtil.getNameAndCode("BCH")); + assertEquals("N/A (XYZ)", CurrencyUtil.getNameAndCode("XYZ")); + } + + class MockAssetRegistry extends AssetRegistry { + private List registeredAssets = new ArrayList<>(); + + MockAssetRegistry() { + for (Asset asset : ServiceLoader.load(Asset.class)) { + registeredAssets.add(asset); + } + } + + void addAsset(Asset asset) { + registeredAssets.add(asset); + } + + public Stream stream() { + return registeredAssets.stream(); + } + } +} diff --git a/core/src/test/java/bisq/core/locale/MockTestnetCoin.java b/core/src/test/java/bisq/core/locale/MockTestnetCoin.java new file mode 100644 index 0000000000..8ed0e85e9e --- /dev/null +++ b/core/src/test/java/bisq/core/locale/MockTestnetCoin.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.locale; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; + + + +import bisq.asset.AddressValidationResult; +import bisq.asset.Base58AddressValidator; +import bisq.asset.Coin; + +public class MockTestnetCoin extends Coin { + + public MockTestnetCoin(Network network, NetworkParameters networkParameters) { + super("MockTestnetCoin", "MOCK_COIN", new BSQAddressValidator(networkParameters), network); + } + + public static class Mainnet extends MockTestnetCoin { + + public Mainnet() { + super(Network.MAINNET, MainNetParams.get()); + } + } + + public static class Testnet extends MockTestnetCoin { + + public Testnet() { + super(Network.TESTNET, TestNet3Params.get()); + } + } + + public static class Regtest extends MockTestnetCoin { + + public Regtest() { + super(Network.REGTEST, RegTestParams.get()); + } + } + + public static class BSQAddressValidator extends Base58AddressValidator { + + public BSQAddressValidator(NetworkParameters networkParameters) { + super(networkParameters); + } + + @Override + public AddressValidationResult validate(String address) { + return super.validate(address); + } + } +} diff --git a/core/src/test/java/bisq/core/message/MarshallerTest.java b/core/src/test/java/bisq/core/message/MarshallerTest.java new file mode 100644 index 0000000000..a4f2e11dbd --- /dev/null +++ b/core/src/test/java/bisq/core/message/MarshallerTest.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.message; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +@Slf4j +public class MarshallerTest { + + @Test + public void getBaseEnvelopeTest() { + protobuf.Ping Ping = protobuf.Ping.newBuilder().setNonce(100).build(); + protobuf.Pong Pong = protobuf.Pong.newBuilder().setRequestNonce(1000).build(); + protobuf.NetworkEnvelope envelope1 = protobuf.NetworkEnvelope.newBuilder().setPing(Ping).build(); + protobuf.NetworkEnvelope envelope2 = protobuf.NetworkEnvelope.newBuilder().setPong(Pong).build(); + log.info(Ping.toString()); + log.info(Pong.toString()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + envelope1.writeDelimitedTo(outputStream); + envelope2.writeDelimitedTo(outputStream); + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + protobuf.NetworkEnvelope envelope3 = protobuf.NetworkEnvelope.parseDelimitedFrom(inputStream); + protobuf.NetworkEnvelope envelope4 = protobuf.NetworkEnvelope.parseDelimitedFrom(inputStream); + + + log.info("message: {}", envelope3.getPing()); + //log.info("peerseesd empty: '{}'",envelope3.getPong().equals(PB.NetworkEnvelope.) == ""); + assertTrue(isPing(envelope3)); + assertTrue(!isPing(envelope4)); + + log.info("3 = {} 4 = {}", isPing(envelope3), isPing(envelope4)); + log.info(envelope3.toString()); + log.info(envelope4.toString()); + + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public boolean isPing(protobuf.NetworkEnvelope envelope) { + return !envelope.getPing().getDefaultInstanceForType().equals(envelope.getPing()); + } +} diff --git a/core/src/test/java/bisq/core/monetary/PriceTest.java b/core/src/test/java/bisq/core/monetary/PriceTest.java new file mode 100644 index 0000000000..f72aca7ae8 --- /dev/null +++ b/core/src/test/java/bisq/core/monetary/PriceTest.java @@ -0,0 +1,133 @@ +/* + * 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 bisq.core.monetary; + +import org.junit.Assert; +import org.junit.Test; + +public class PriceTest { + + @Test + public void testParse() { + Price result = Price.parse("USD", "0.1"); + Assert.assertEquals( + "Fiat value should be formatted with two decimals.", + "0.10 BTC/USD", + result.toFriendlyString() + ); + + result = Price.parse("EUR", "0.1234"); + Assert.assertEquals( + "Fiat value should be given two decimals", + "0.1234 BTC/EUR", + result.toFriendlyString() + ); + + try { + Price.parse("EUR", "0.12345"); + Assert.fail("Expected IllegalArgumentException to be thrown when too many decimals are used."); + } catch (IllegalArgumentException iae) { + Assert.assertEquals( + "Unexpected exception message.", + "java.lang.ArithmeticException: Rounding necessary", + iae.getMessage() + ); + } + + Assert.assertEquals( + "Negative value should be parsed correctly.", + -100000000L, + Price.parse("LTC", "-1").getValue() + ); + + Assert.assertEquals( + "Comma (',') as decimal separator should be converted to period ('.')", + "0.0001 BTC/USD", + Price.parse("USD", "0,0001").toFriendlyString() + ); + + Assert.assertEquals( + "Too many decimals should get rounded up properly.", + "10000.2346 LTC/BTC", + Price.parse("LTC", "10000,23456789").toFriendlyString() + ); + + Assert.assertEquals( + "Too many decimals should get rounded down properly.", + "10000.2345 LTC/BTC", + Price.parse("LTC", "10000,23454999").toFriendlyString() + ); + + Assert.assertEquals( + "Underlying long value should be correct.", + 1000023456789L, + Price.parse("LTC", "10000,23456789").getValue() + ); + + try { + Price.parse("XMR", "56789.123456789"); + Assert.fail("Expected IllegalArgumentException to be thrown when too many decimals are used."); + } catch (IllegalArgumentException iae) { + Assert.assertEquals( + "Unexpected exception message.", + "java.lang.ArithmeticException: Rounding necessary", + iae.getMessage() + ); + } + } + @Test + public void testValueOf() { + Price result = Price.valueOf("USD", 1); + Assert.assertEquals( + "Fiat value should have four decimals.", + "0.0001 BTC/USD", + result.toFriendlyString() + ); + + result = Price.valueOf("EUR", 1234); + Assert.assertEquals( + "Fiat value should be given two decimals", + "0.1234 BTC/EUR", + result.toFriendlyString() + ); + + Assert.assertEquals( + "Negative value should be parsed correctly.", + -1L, + Price.valueOf("LTC", -1L).getValue() + ); + + Assert.assertEquals( + "Too many decimals should get rounded up properly.", + "10000.2346 LTC/BTC", + Price.valueOf("LTC", 1000023456789L).toFriendlyString() + ); + + Assert.assertEquals( + "Too many decimals should get rounded down properly.", + "10000.2345 LTC/BTC", + Price.valueOf("LTC", 1000023454999L).toFriendlyString() + ); + + Assert.assertEquals( + "Underlying long value should be correct.", + 1000023456789L, + Price.valueOf("LTC", 1000023456789L).getValue() + ); + } +} diff --git a/core/src/test/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepositoryTest.java b/core/src/test/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepositoryTest.java new file mode 100644 index 0000000000..c7c776b038 --- /dev/null +++ b/core/src/test/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepositoryTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.network.p2p.seed; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.config.Config; + +import org.junit.Assert; +import org.junit.Test; + +import static java.lang.String.format; + +public class DefaultSeedNodeRepositoryTest { + + @Test + public void getSeedNodes() { + DefaultSeedNodeRepository DUT = new DefaultSeedNodeRepository(new Config()); + Assert.assertFalse(DUT.getSeedNodeAddresses().isEmpty()); + } + + @Test + public void manualSeedNodes() { + String seed1 = "asdf:8001"; + String seed2 = "fdsa:6001"; + String seedNodesOption = format("--%s=%s,%s", Config.SEED_NODES, seed1, seed2); + DefaultSeedNodeRepository DUT = new DefaultSeedNodeRepository(new Config(seedNodesOption)); + Assert.assertFalse(DUT.getSeedNodeAddresses().isEmpty()); + Assert.assertEquals(2, DUT.getSeedNodeAddresses().size()); + Assert.assertTrue(DUT.getSeedNodeAddresses().contains(new NodeAddress(seed1))); + Assert.assertTrue(DUT.getSeedNodeAddresses().contains(new NodeAddress(seed2))); + } +} diff --git a/core/src/test/java/bisq/core/notifications/MobileModelTest.java b/core/src/test/java/bisq/core/notifications/MobileModelTest.java new file mode 100644 index 0000000000..4790d54c3a --- /dev/null +++ b/core/src/test/java/bisq/core/notifications/MobileModelTest.java @@ -0,0 +1,90 @@ +/* + * 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 bisq.core.notifications; + +import bisq.common.util.Tuple2; + +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +@Slf4j +public class MobileModelTest { + + @Test + public void testParseDescriptor() { + MobileModel mobileModel = new MobileModel(); + List> list = Arrays.asList( + new Tuple2<>("iPod Touch 5", false), + new Tuple2<>("iPod Touch 6", false), + new Tuple2<>("iPhone 4", false), + new Tuple2<>("iPhone 4s", false), + new Tuple2<>("iPhone 5", false), + new Tuple2<>("iPhone 5c", false), + new Tuple2<>("iPhone 5s", false), + + new Tuple2<>("iPhone 6", false), + new Tuple2<>("iPhone 6 Plus", false), + new Tuple2<>("iPhone 6s", true), + new Tuple2<>("iPhone 6s Plus", true), + + new Tuple2<>("iPhone 7", true), + new Tuple2<>("iPhone 7 Plus", true), + new Tuple2<>("iPhone SE", false), // unclear + new Tuple2<>("iPhone 8", true), + new Tuple2<>("iPhone 8 Plus", true), + new Tuple2<>("iPhone X", true), + new Tuple2<>("iPhone XS", true), + new Tuple2<>("iPhone XS Max", true), + new Tuple2<>("iPhone XR", true), + new Tuple2<>("iPhone 11", true), + new Tuple2<>("iPhone 11 Pro", true), + new Tuple2<>("iPhone 11 Pro Max", true), + new Tuple2<>("iPhone 11S", true), // not sure if this model will exist, but based on past versioning it is possible + // need to ensure it will be parsed correctly just in case + + new Tuple2<>("iPad 2", false), + new Tuple2<>("iPad 3", false), + new Tuple2<>("iPad 4", false), + new Tuple2<>("iPad Air", false), + new Tuple2<>("iPad Air 2", false), + new Tuple2<>("iPad 5", false), + new Tuple2<>("iPad 6", false), + new Tuple2<>("iPad Mini", false), + new Tuple2<>("iPad Mini 2", false), + new Tuple2<>("iPad Mini 3", false), + new Tuple2<>("iPad Mini 4", false), + + new Tuple2<>("iPad Pro 9.7 Inch", true), + new Tuple2<>("iPad Pro 12.9 Inch", true), + new Tuple2<>("iPad Pro 12.9 Inch 2. Generation", true), + new Tuple2<>("iPad Pro 10.5 Inch", true) + ); + + list.forEach(tuple -> { + log.info(tuple.toString()); + assertEquals("tuple: " + tuple, mobileModel.parseDescriptor(tuple.first), tuple.second); + }); + + } +} diff --git a/core/src/test/java/bisq/core/offer/OfferMaker.java b/core/src/test/java/bisq/core/offer/OfferMaker.java new file mode 100644 index 0000000000..aa9d294c2e --- /dev/null +++ b/core/src/test/java/bisq/core/offer/OfferMaker.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.core.offer; + +import com.natpryce.makeiteasy.Instantiator; +import com.natpryce.makeiteasy.Maker; +import com.natpryce.makeiteasy.Property; + +import static com.natpryce.makeiteasy.MakeItEasy.a; + +public class OfferMaker { + + public static final Property price = new Property<>(); + public static final Property minAmount = new Property<>(); + public static final Property amount = new Property<>(); + public static final Property baseCurrencyCode = new Property<>(); + public static final Property counterCurrencyCode = new Property<>(); + public static final Property direction = new Property<>(); + public static final Property useMarketBasedPrice = new Property<>(); + public static final Property marketPriceMargin = new Property<>(); + public static final Property id = new Property<>(); + + public static final Instantiator Offer = lookup -> new Offer( + new OfferPayload(lookup.valueOf(id, "1234"), + 0L, + null, + null, + lookup.valueOf(direction, OfferPayload.Direction.BUY), + lookup.valueOf(price, 100000L), + lookup.valueOf(marketPriceMargin, 0.0), + lookup.valueOf(useMarketBasedPrice, false), + lookup.valueOf(amount, 100000L), + lookup.valueOf(minAmount, 100000L), + lookup.valueOf(baseCurrencyCode, "BTC"), + lookup.valueOf(counterCurrencyCode, "USD"), + null, + null, + "SEPA", + "", + null, + null, + null, + null, + null, + "", + 0L, + 0L, + 0L, + false, + 0L, + 0L, + 0L, + 0L, + false, + false, + 0L, + 0L, + false, + null, + null, + 0)); + + public static final Maker btcUsdOffer = a(Offer); +} diff --git a/core/src/test/java/bisq/core/offer/OfferTest.java b/core/src/test/java/bisq/core/offer/OfferTest.java new file mode 100644 index 0000000000..9300c01574 --- /dev/null +++ b/core/src/test/java/bisq/core/offer/OfferTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.offer; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OfferTest { + + @Test + public void testHasNoRange() { + OfferPayload payload = mock(OfferPayload.class); + when(payload.getMinAmount()).thenReturn(1000L); + when(payload.getAmount()).thenReturn(1000L); + + Offer offer = new Offer(payload); + assertFalse(offer.isRange()); + } + + @Test + public void testHasRange() { + OfferPayload payload = mock(OfferPayload.class); + when(payload.getMinAmount()).thenReturn(1000L); + when(payload.getAmount()).thenReturn(2000L); + + Offer offer = new Offer(payload); + assertTrue(offer.isRange()); + } +} diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java new file mode 100644 index 0000000000..63b4e406bb --- /dev/null +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -0,0 +1,176 @@ +package bisq.core.offer; + +import bisq.core.api.CoreContext; +import bisq.core.trade.TradableList; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.persistence.PersistenceManager; + +import java.nio.file.Files; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static bisq.core.offer.OfferMaker.btcUsdOffer; +import static com.natpryce.makeiteasy.MakeItEasy.make; +import static junit.framework.TestCase.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class OpenOfferManagerTest { + private PersistenceManager> persistenceManager; + private CoreContext coreContext; + + @Before + public void setUp() throws Exception { + var corruptedStorageFileHandler = mock(CorruptedStorageFileHandler.class); + var storageDir = Files.createTempDirectory("storage").toFile(); + persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler); + coreContext = new CoreContext(); + } + + @After + public void tearDown() { + persistenceManager.shutdown(); + } + + @Test + public void testStartEditOfferForActiveOffer() { + P2PService p2PService = mock(P2PService.class); + OfferBookService offerBookService = mock(OfferBookService.class); + + when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); + + final OpenOfferManager manager = new OpenOfferManager(coreContext, + null, + null, + null, + p2PService, + null, + null, + null, + offerBookService, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + persistenceManager); + + AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); + + + doAnswer(invocation -> { + ((ResultHandler) invocation.getArgument(1)).handleResult(); + return null; + }).when(offerBookService).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); + + final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); + + ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); + + manager.editOpenOfferStart(openOffer, resultHandler, null); + + verify(offerBookService, times(1)).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); + + assertTrue(startEditOfferSuccessful.get()); + + } + + @Test + public void testStartEditOfferForDeactivatedOffer() { + P2PService p2PService = mock(P2PService.class); + OfferBookService offerBookService = mock(OfferBookService.class); + when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); + + final OpenOfferManager manager = new OpenOfferManager(coreContext, + null, + null, + null, + p2PService, + null, + null, + null, + offerBookService, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + persistenceManager); + + AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); + + ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); + + final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); + openOffer.setState(OpenOffer.State.DEACTIVATED); + + manager.editOpenOfferStart(openOffer, resultHandler, null); + assertTrue(startEditOfferSuccessful.get()); + + } + + @Test + public void testStartEditOfferForOfferThatIsCurrentlyEdited() { + P2PService p2PService = mock(P2PService.class); + OfferBookService offerBookService = mock(OfferBookService.class); + + when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); + + final OpenOfferManager manager = new OpenOfferManager(coreContext, + null, + null, + null, + p2PService, + null, + null, + null, + offerBookService, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + persistenceManager); + + AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); + + ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); + + final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); + openOffer.setState(OpenOffer.State.DEACTIVATED); + + manager.editOpenOfferStart(openOffer, resultHandler, null); + assertTrue(startEditOfferSuccessful.get()); + + startEditOfferSuccessful.set(false); + + manager.editOpenOfferStart(openOffer, resultHandler, null); + assertTrue(startEditOfferSuccessful.get()); + } + +} diff --git a/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java b/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java new file mode 100644 index 0000000000..9dcf0d4168 --- /dev/null +++ b/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.core.offer.availability; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ArbitratorSelectionTest { + @Test + public void testGetLeastUsedArbitrator() { + // We get least used selected + List lastAddressesUsedInTrades; + Set arbitrators; + String result; + + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb1"); + arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb2", result); + + // if all are same we use first according to alphanumeric sorting + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); + arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb1", result); + + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1"); + arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb2", result); + + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1", "arb2"); + arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb3", result); + + lastAddressesUsedInTrades = Arrays.asList("xxx", "ccc", "aaa"); + arbitrators = new HashSet<>(Arrays.asList("aaa", "ccc", "xxx")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("aaa", result); + lastAddressesUsedInTrades = Arrays.asList("333", "000", "111"); + arbitrators = new HashSet<>(Arrays.asList("111", "333", "000")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("000", result); + + // if winner is not in our arb list we use our arb from arbitrators even if never used in trades + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); + arbitrators = new HashSet<>(Arrays.asList("arb4")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb4", result); + + // if winner (arb2) is not in our arb list we use our arb from arbitrators + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb1", "arb1", "arb2"); + arbitrators = new HashSet<>(Arrays.asList("arb1")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb1", result); + + // arb1 is used least + lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb2", "arb2", "arb1", "arb1", "arb2"); + arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); + assertEquals("arb1", result); + } +} diff --git a/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java b/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java new file mode 100644 index 0000000000..b869045683 --- /dev/null +++ b/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentAccountPayload; + +import java.util.Collections; + +import org.junit.Test; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PaymentAccountsTest { + @Test + public void testGetOldestPaymentAccountForOfferWhenNoValidAccounts() { + PaymentAccounts accounts = new PaymentAccounts(Collections.emptySet(), mock(AccountAgeWitnessService.class)); + PaymentAccount actual = accounts.getOldestPaymentAccountForOffer(mock(Offer.class)); + + assertNull(actual); + } + +// @Test +// public void testGetOldestPaymentAccountForOffer() { +// AccountAgeWitnessService service = mock(AccountAgeWitnessService.class); +// +// PaymentAccount oldest = createAccountWithAge(service, 3); +// Set accounts = Sets.newHashSet( +// oldest, +// createAccountWithAge(service, 2), +// createAccountWithAge(service, 1)); +// +// BiFunction dummyValidator = (offer, account) -> true; +// PaymentAccounts testedEntity = new PaymentAccounts(accounts, service, dummyValidator); +// +// PaymentAccount actual = testedEntity.getOldestPaymentAccountForOffer(mock(Offer.class)); +// assertEquals(oldest, actual); +// } + + private static PaymentAccount createAccountWithAge(AccountAgeWitnessService service, long age) { + PaymentAccountPayload payload = mock(PaymentAccountPayload.class); + + PaymentAccount account = mock(PaymentAccount.class); + when(account.getPaymentAccountPayload()).thenReturn(payload); + + AccountAgeWitness witness = mock(AccountAgeWitness.class); + when(service.getAccountAge(eq(witness), any())).thenReturn(age); + + when(service.getMyWitness(payload)).thenReturn(witness); + + return account; + } +} diff --git a/core/src/test/java/bisq/core/payment/ReceiptPredicatesTest.java b/core/src/test/java/bisq/core/payment/ReceiptPredicatesTest.java new file mode 100644 index 0000000000..8ee55d018e --- /dev/null +++ b/core/src/test/java/bisq/core/payment/ReceiptPredicatesTest.java @@ -0,0 +1,99 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.locale.CryptoCurrency; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; + +import com.google.common.collect.Lists; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ReceiptPredicatesTest { + private final ReceiptPredicates predicates = new ReceiptPredicates(); + + @Test + public void testIsMatchingCurrency() { + Offer offer = mock(Offer.class); + when(offer.getCurrencyCode()).thenReturn("USD"); + + PaymentAccount account = mock(PaymentAccount.class); + when(account.getTradeCurrencies()).thenReturn(Lists.newArrayList( + new CryptoCurrency("BTC", "Bitcoin"), + new CryptoCurrency("ETH", "Ether"))); + + assertFalse(predicates.isMatchingCurrency(offer, account)); + } + + @Test + public void testIsMatchingSepaOffer() { + Offer offer = mock(Offer.class); + PaymentMethod.SEPA = mock(PaymentMethod.class); + when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SEPA); + + assertTrue(predicates.isMatchingSepaOffer(offer, mock(SepaInstantAccount.class))); + assertTrue(predicates.isMatchingSepaOffer(offer, mock(SepaAccount.class))); + } + + @Test + public void testIsMatchingSepaInstant() { + Offer offer = mock(Offer.class); + PaymentMethod.SEPA_INSTANT = mock(PaymentMethod.class); + when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SEPA_INSTANT); + + assertTrue(predicates.isMatchingSepaInstant(offer, mock(SepaInstantAccount.class))); + assertFalse(predicates.isMatchingSepaInstant(offer, mock(SepaAccount.class))); + } + + @Test + public void testIsMatchingCountryCodes() { + CountryBasedPaymentAccount account = mock(CountryBasedPaymentAccount.class); + when(account.getCountry()).thenReturn(null); + + assertFalse(predicates.isMatchingCountryCodes(mock(Offer.class), account)); + } + + @Test + public void testIsSameOrSpecificBank() { + PaymentMethod.SAME_BANK = mock(PaymentMethod.class); + + Offer offer = mock(Offer.class); + when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SAME_BANK); + + assertTrue(predicates.isOfferRequireSameOrSpecificBank(offer, mock(NationalBankAccount.class))); + } + + @Test + public void testIsEqualPaymentMethods() { + PaymentMethod method = PaymentMethod.getDummyPaymentMethod("1"); + + Offer offer = mock(Offer.class); + when(offer.getPaymentMethod()).thenReturn(method); + + PaymentAccount account = mock(PaymentAccount.class); + when(account.getPaymentMethod()).thenReturn(method); + + assertTrue(predicates.isEqualPaymentMethods(offer, account)); + } +} diff --git a/core/src/test/java/bisq/core/payment/ReceiptValidatorTest.java b/core/src/test/java/bisq/core/payment/ReceiptValidatorTest.java new file mode 100644 index 0000000000..12dbe054b9 --- /dev/null +++ b/core/src/test/java/bisq/core/payment/ReceiptValidatorTest.java @@ -0,0 +1,267 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.offer.Offer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class ReceiptValidatorTest { + private ReceiptValidator validator; + private PaymentAccount account; + private Offer offer; + private ReceiptPredicates predicates; + + @Before + public void setUp() { + this.predicates = mock(ReceiptPredicates.class); + this.account = mock(CountryBasedPaymentAccount.class); + this.offer = mock(Offer.class); + this.validator = new ReceiptValidator(offer, account, predicates); + } + + @After + public void tearDown() { + verifyZeroInteractions(offer); + } + + @Test + public void testIsValidWhenCurrencyDoesNotMatch() { + when(predicates.isMatchingCurrency(offer, account)).thenReturn(false); + + assertFalse(validator.isValid()); + verify(predicates).isMatchingCurrency(offer, account); + } + + @Test + public void testIsValidWhenNotCountryBasedAccount() { + account = mock(PaymentAccount.class); + assertFalse(account instanceof CountryBasedPaymentAccount); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenNotMatchingCodes() { + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(false); + + assertFalse(validator.isValid()); + verify(predicates).isMatchingCountryCodes(offer, account); + } + + @Test + public void testIsValidWhenSepaOffer() { + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(true); + + assertTrue(validator.isValid()); + verify(predicates).isMatchingSepaOffer(offer, account); + } + + @Test + public void testIsValidWhenSepaInstant() { + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(true); + + assertTrue(validator.isValid()); + verify(predicates).isMatchingSepaOffer(offer, account); + } + + @Test + public void testIsValidWhenSpecificBankAccountAndOfferRequireSpecificBank() { + account = mock(SpecificBanksAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); + when(predicates.isMatchingBankId(offer, account)).thenReturn(false); + + assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenSameBankAccountAndOfferRequireSpecificBank() { + account = mock(SameBankAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); + when(predicates.isMatchingBankId(offer, account)).thenReturn(false); + + assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenSpecificBankAccount() { + account = mock(SpecificBanksAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); + when(predicates.isMatchingBankId(offer, account)).thenReturn(true); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenSameBankAccount() { + account = mock(SameBankAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); + when(predicates.isMatchingBankId(offer, account)).thenReturn(true); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenNationalBankAccount() { + account = mock(NationalBankAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + // Same or Specific Bank offers can't be taken by National Bank accounts. TODO: Consider partially relaxing to allow Specific Banks. + public void testIsValidWhenNationalBankAccountAndOfferIsNot() { + account = mock(NationalBankAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + + assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); + + verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); + verify(predicates, never()).isMatchingBankId(offer, account); + } + + @Test + // National or Same Bank offers can't be taken by Specific Banks accounts. TODO: Consider partially relaxing to allow National Bank. + public void testIsValidWhenSpecificBanksAccountAndOfferIsNot() { + account = mock(SpecificBanksAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + + assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); + + verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); + verify(predicates, never()).isMatchingBankId(offer, account); + } + + @Test + // National or Specific Bank offers can't be taken by Same Bank accounts. + public void testIsValidWhenSameBankAccountAndOfferIsNot() { + account = mock(SameBankAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + + assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); + + verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); + verify(predicates, never()).isMatchingBankId(offer, account); + } + + @Test + public void testIsValidWhenWesternUnionAccount() { + account = mock(WesternUnionAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + } + + @Test + public void testIsValidWhenWesternIrregularAccount() { + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); + when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); + when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); + when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); + + assertTrue(validator.isValid()); + } + + @Test + public void testIsValidWhenMoneyGramAccount() { + account = mock(MoneyGramAccount.class); + + when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); + when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); + + assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); + + verify(predicates, never()).isMatchingCountryCodes(offer, account); + verify(predicates, never()).isMatchingSepaOffer(offer, account); + verify(predicates, never()).isMatchingSepaInstant(offer, account); + verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); + } +} diff --git a/core/src/test/java/bisq/core/payment/TradeLimitsTest.java b/core/src/test/java/bisq/core/payment/TradeLimitsTest.java new file mode 100644 index 0000000000..17e256b50f --- /dev/null +++ b/core/src/test/java/bisq/core/payment/TradeLimitsTest.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.core.payment; + +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateService; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class TradeLimitsTest { + @Test + public void testGetFirstMonthRiskBasedTradeLimit() { + TradeLimits tradeLimits = new TradeLimits(mock(DaoStateService.class), mock(PeriodService.class)); + long expected, result; + + expected = 0; + result = tradeLimits.getFirstMonthRiskBasedTradeLimit(0, 1); + assertEquals(expected, result); + + expected = 25000000; + result = tradeLimits.getFirstMonthRiskBasedTradeLimit(100000000, 1); + assertEquals(expected, result); + + expected = 3130000; //0.03125 -> 0.0313 -> 0.0313 + result = tradeLimits.getFirstMonthRiskBasedTradeLimit(100000000, 8); + assertEquals(expected, result); + + expected = 6250000; + result = tradeLimits.getFirstMonthRiskBasedTradeLimit(200000000, 8); + assertEquals(expected, result); + } +} diff --git a/core/src/test/java/bisq/core/payment/validation/AltCoinAddressValidatorTest.java b/core/src/test/java/bisq/core/payment/validation/AltCoinAddressValidatorTest.java new file mode 100644 index 0000000000..acce0539fb --- /dev/null +++ b/core/src/test/java/bisq/core/payment/validation/AltCoinAddressValidatorTest.java @@ -0,0 +1,55 @@ +/* + * 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 bisq.core.payment.validation; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; + +import bisq.asset.AssetRegistry; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AltCoinAddressValidatorTest { + + @Test + public void test() { + AltCoinAddressValidator validator = new AltCoinAddressValidator(new AssetRegistry()); + + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + String currencyCode = baseCurrencyNetwork.getCurrencyCode(); + Res.setBaseCurrencyCode(currencyCode); + Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); + CurrencyUtil.setBaseCurrencyCode(currencyCode); + + validator.setCurrencyCode("BTC"); + assertTrue(validator.validate("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem").isValid); + + validator.setCurrencyCode("LTC"); + assertTrue(validator.validate("Lg3PX8wRWmApFCoCMAsPF5P9dPHYQHEWKW").isValid); + + validator.setCurrencyCode("BOGUS"); + + assertFalse(validator.validate("1BOGUSADDR").isValid); + } +} diff --git a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java new file mode 100644 index 0000000000..7b546af698 --- /dev/null +++ b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java @@ -0,0 +1,280 @@ +/* + * 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 bisq.core.provider.mempool; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; + +import com.google.gson.Gson; + +import org.apache.commons.io.IOUtils; + +import org.bitcoinj.core.Coin; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.junit.Test; +import org.junit.Assert; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TxValidatorTest { + private static final Logger log = LoggerFactory.getLogger(TxValidatorTest.class); + + private List btcFeeReceivers = new ArrayList<>(); + + public TxValidatorTest() { + btcFeeReceivers.add("1EKXx73oUhHaUh8JBimtiPGgHfwNmxYKAj"); + btcFeeReceivers.add("1HpvvMHcoXQsX85CjTsco5ZAAMoGu2Mze9"); + btcFeeReceivers.add("3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU"); + btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR"); + btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c"); + btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq"); + btcFeeReceivers.add("38bZBj5peYS3Husdz7AH3gEUiUbYRD951t"); + btcFeeReceivers.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); + btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7"); + btcFeeReceivers.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); + btcFeeReceivers.add("34VLFgtFKAtwTdZ5rengTT2g2zC99sWQLC"); + log.warn("Known BTC fee receivers: {}", btcFeeReceivers.toString()); + } + + @Test + public void testMakerTx() throws InterruptedException { + String mempoolData, offerData; + + // paid the correct amount of BSQ fees + offerData = "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390"; + mempoolData = "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID expected 1.01 BSQ, actual fee paid 0.80 BSQ (USED 8.00 RATE INSTEAD OF 10.06 RATE) + // PASS due to leniency rule of accepting old DAO rate parameters: https://github.com/bisq-network/bisq/issues/5329#issuecomment-803223859 + offerData = "48067552,3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3,10000000,80,0,667656"; + mempoolData = "{\"txid\":\"3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9717}},{\"vout\":0,\"prevout\":{\"value\":4434912}},{\"vout\":2,\"prevout\":{\"value\":12809932}}],\"vout\":[{\"scriptpubkey_address\":\"1Nzqa4J7ck5bgz7QNXKtcjZExAvReozFo4\",\"value\":9637},{\"scriptpubkey_address\":\"bc1qhmmulf5prreqhccqy2wqpxxn6dcye7ame9dd57\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1qx6hg8km2jdjc5ukhuedmkseu9wlsjtd8zeajpj\",\"value\":5721894}],\"size\":553,\"weight\":1879,\"fee\":23030,\"status\":{\"confirmed\":true,\"block_height\":667660}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID Expected fee: 0.61 BSQ, actual fee paid: 0.35 BSQ (USED 5.75 RATE INSTEAD OF 10.06 RATE) + // PASS due to leniency rule of accepting old DAO rate parameters: https://github.com/bisq-network/bisq/issues/5329#issuecomment-803223859 + offerData = "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195"; + mempoolData = "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID expected 0.11 BSQ, actual fee paid 0.08 BSQ (USED 5.75 RATE INSTEAD OF 7.53) + // PASS due to leniency rule of accepting old DAO rate parameters: https://github.com/bisq-network/bisq/issues/5329#issuecomment-803223859 + offerData = "F1dzaFNQ,f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b,1440000,8,0,670822, bsq paid too little"; + mempoolData = "{\"txid\":\"f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":15163}},{\"vout\":2,\"prevout\":{\"value\":6100000}}],\"vout\":[{\"scriptpubkey_address\":\"1MEsc2m4MSomNJWSr1p6fhnUQMyA3DRGrN\",\"value\":15155},{\"scriptpubkey_address\":\"bc1qztgwe9ry9a9puchjuscqdnv4v9lsm2ut0jtfec\",\"value\":2040000},{\"scriptpubkey_address\":\"bc1q0nstwxc0vqkj4x000xt328mfjapvlsd56nn70h\",\"value\":4048308}],\"size\":406,\"weight\":1291,\"fee\":11700,\"status\":{\"confirmed\":true,\"block_height\":670823}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + } + + @Test + public void testTakerTx() throws InterruptedException { + String mempoolData, offerData; + + // The fee was more than what we expected: Expected BTC fee: 5000 sats , actual fee paid: 6000 sats + offerData = "00072328,3524364062c96ba0280621309e8b539d152154422294c2cf263a965dcde9a8ca,1000000,6000,1,614672"; + mempoolData = "{\"txid\":\"3524364062c96ba0280621309e8b539d152154422294c2cf263a965dcde9a8ca\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":2971000}}],\"vout\":[{\"scriptpubkey_address\":\"3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV\",\"value\":6000},{\"scriptpubkey_address\":\"1Hxu2X9Nr2fT3qEk9yjhiF54TJEz1Cxjoa\",\"value\":1607600},{\"scriptpubkey_address\":\"16VP6nHDDkmCMwaJj4PeyVHB88heDdVu9e\",\"value\":1353600}],\"size\":257,\"weight\":1028,\"fee\":3800,\"status\":{\"confirmed\":true,\"block_height\":614672}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee matched what we expected + offerData = "00072328,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; + mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":19980}},{\"vout\":2,\"prevout\":{\"value\":2086015}},{\"vout\":0,\"prevout\":{\"value\":1100000}},{\"vout\":2,\"prevout\":{\"value\":938200}}],\"vout\":[{\"scriptpubkey_address\":\"17qiF1TYgT1YvsCPJyXQoKMtBZ7YJBW9GH\",\"value\":19792},{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee was more than what we expected: Expected BTC fee: 5000 sats , actual fee paid: 7000 sats + offerData = "bsqtrade,dfa4555ab78c657cad073e3f29c38c563d9dafc53afaa8c6af28510c734305c4,1000000,10,1,662390"; + mempoolData = "{\"txid\":\"dfa4555ab78c657cad073e3f29c38c563d9dafc53afaa8c6af28510c734305c4\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":678997}}],\"vout\":[{\"scriptpubkey_address\":\"3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU\",\"value\":7000},{\"scriptpubkey_address\":\"bc1qu6vey3e7flzg8gmhun05m43uc2vz0ay33kuu6r\",\"value\":647998}],\"size\":224,\"weight\":566,\"fee\":23999,\"status\":{\"confirmed\":true,\"block_height\":669720}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee matched what we expected + offerData = "89284,e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588,900000,9,0,666473"; + mempoolData = "{\"txid\":\"e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":72738}},{\"vout\":0,\"prevout\":{\"value\":1600000}}],\"vout\":[{\"scriptpubkey_address\":\"17Kh5Ype9yNomqRrqu2k1mdV5c6FcKfGwQ\",\"value\":72691},{\"scriptpubkey_address\":\"bc1qdr9zcw7gf2sehxkux4fmqujm5uguhaqz7l9lca\",\"value\":629016},{\"scriptpubkey_address\":\"bc1qgqrrqv8q6l5d3t52fe28ghuhz4xqrsyxlwn03z\",\"value\":956523}],\"size\":404,\"weight\":1286,\"fee\":14508,\"status\":{\"confirmed\":true,\"block_height\":672388}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID: Expected fee: 7.04 BSQ, actual fee paid: 1.01 BSQ + offerData = "VOxRS,e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91,10000000,101,0,669129"; + mempoolData = "{\"txid\":\"e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":16739}},{\"vout\":2,\"prevout\":{\"value\":113293809}}],\"vout\":[{\"scriptpubkey_address\":\"1F14nF6zoUfJkqZrFgdmK5VX5QVwEpAnKW\",\"value\":16638},{\"scriptpubkey_address\":\"bc1q80y688ev7u43vqy964yf7feqddvt2mkm8977cm\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1q9whgyc2du9mrgnxz0nl0shwpw8ugrcae0j0w8p\",\"value\":101784485}],\"size\":406,\"weight\":1291,\"fee\":9425,\"status\":{\"confirmed\":true,\"block_height\":669134}}"; + Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID: Expected fee: 1029000 sats BTC, actual fee paid: 441000 sats BTC because they used the default rate of 0.003 should have been 0.007 per BTC + // after 1.6.0 we introduce additional leniency to allow the default rate (which is not stored in the DAO param change list) + offerData = "AKA,6779b7571f21a5a1af01d675bf032b8a3c571416d05345491018cbc2d016e888,147000000,441000,1,676543"; + mempoolData = "{'txid':'6779b7571f21a5a1af01d675bf032b8a3c571416d05345491018cbc2d016e888','version':1,'locktime':0,'vin':[{'txid':'94c36c0a9c5c99844ddfe17ef05a3ebbe94b14d76ee4bed7b00c7d45e681b441','vout':0,'prevout':{'scriptpubkey_address':'bc1qt5uprdzeh9g4el0k9cttn40qzagvpca9q0q6vl','value':177920825},'sequence':4294967295}],'vout':[{'scriptpubkey_address':'19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq','value':441000},{'scriptpubkey_address':'bc1qxxcl9dz6usrx4z456g6fg8n3u9327hl458d6mx','value':177008388},{'scriptpubkey_address':'bc1qdq0894p2nmg04ceyqgapln6addfl80zy7z36jd','value':467243}],'size':256,'weight':697,'fee':4194,'status':{'confirmed':true,'block_height':676543}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + } + + @Test + public void testGoodOffers() throws InterruptedException { + Map goodOffers = loadJsonTestData("offerTestData.json"); + Map mempoolData = loadJsonTestData("txInfo.json"); + Assert.assertTrue(goodOffers.size() > 0); + Assert.assertTrue(mempoolData.size() > 0); + log.warn("TESTING GOOD OFFERS"); + testOfferSet(goodOffers, mempoolData, true); + } + + @Test + public void testBadOffers() throws InterruptedException { + Map badOffers = loadJsonTestData("badOfferTestData.json"); + Map mempoolData = loadJsonTestData("txInfo.json"); + Assert.assertTrue(badOffers.size() > 0); + Assert.assertTrue(mempoolData.size() > 0); + log.warn("TESTING BAD OFFERS"); + testOfferSet(badOffers, mempoolData, false); + } + + private void testOfferSet(Map offers, Map mempoolData, boolean expectedResult) { + Set knownValuesList = new HashSet<>(offers.values()); + knownValuesList.forEach(offerData -> { + TxValidator txValidator = createTxValidator(offerData); + log.warn("TESTING {}", txValidator.getTxId()); + String jsonTxt = mempoolData.get(txValidator.getTxId()); + if (jsonTxt == null || jsonTxt.isEmpty()) { + log.warn("{} was not found in the mempool", txValidator.getTxId()); + Assert.assertFalse(expectedResult); // tx was not found in explorer + } else { + txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers); + Assert.assertTrue(expectedResult == txValidator.getResult()); + } + }); + } + + private Map loadJsonTestData(String fileName) { + String json = ""; + try { + json = IOUtils.toString(this.getClass().getResourceAsStream(fileName), "UTF-8"); + } catch (IOException e) { + log.error(e.toString()); + } + Map map = new Gson().fromJson(json, Map.class); + return map; + } + + // initialize the TxValidator with offerData to be validated + // and mock the used DaoStateService + private TxValidator createTxValidator(String offerData) { + try { + String[] y = offerData.split(","); + String txId = y[1]; + long amount = Long.parseLong(y[2]); + boolean isCurrencyForMakerFeeBtc = Long.parseLong(y[4]) > 0; + DaoStateService mockedDaoStateService = mock(DaoStateService.class); + + Answer mockGetFeeRate = invocation -> { + return mockedLookupFeeRate(invocation.getArgument(0), invocation.getArgument(1)); + }; + Answer mockGetParamValueAsCoin = invocation -> { + return mockedGetParamValueAsCoin(invocation.getArgument(0), invocation.getArgument(1)); + }; + Answer> mockGetParamChangeList = invocation -> { + return mockedGetParamChangeList(invocation.getArgument(0)); + }; + when(mockedDaoStateService.getParamValueAsCoin(Mockito.any(Param.class), Mockito.anyInt())).thenAnswer(mockGetFeeRate); + when(mockedDaoStateService.getParamValueAsCoin(Mockito.any(Param.class), Mockito.anyString())).thenAnswer(mockGetParamValueAsCoin); + when(mockedDaoStateService.getParamChangeList(Mockito.any())).thenAnswer(mockGetParamChangeList); + TxValidator txValidator = new TxValidator(mockedDaoStateService, txId, Coin.valueOf(amount), isCurrencyForMakerFeeBtc); + return txValidator; + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + return null; + } + + Coin mockedLookupFeeRate(Param param, int blockHeight) { + BsqFormatter bsqFormatter = new BsqFormatter(); + LinkedHashMap feeMap = mockedGetFeeRateMap(param); + for (Map.Entry entry : feeMap.entrySet()) { + if (blockHeight >= entry.getKey()) { + if (param.equals(Param.DEFAULT_MAKER_FEE_BTC) || param.equals(Param.DEFAULT_TAKER_FEE_BTC)) + return bsqFormatter.parseToBTC(entry.getValue()); + else + return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter); + } + } + if (param.equals(Param.DEFAULT_MAKER_FEE_BTC) || param.equals(Param.DEFAULT_TAKER_FEE_BTC)) + return bsqFormatter.parseToBTC(param.getDefaultValue()); + else + return ParsingUtils.parseToCoin(param.getDefaultValue(), bsqFormatter); + } + + private LinkedHashMap mockedGetFeeRateMap(Param param) { + LinkedHashMap feeMap = new LinkedHashMap<>(); + if (param == Param.DEFAULT_MAKER_FEE_BSQ) { + feeMap.put(674707L, "8.66"); // https://github.com/bisq-network/proposals/issues/318 + feeMap.put(670027L, "7.53"); + feeMap.put(660667L, "10.06"); + feeMap.put(655987L, "8.74"); + feeMap.put(641947L, "7.6"); + feeMap.put(632587L, "6.6"); + feeMap.put(623227L, "5.75"); + feeMap.put(599827L, "10.0"); + feeMap.put(590467L, "13.0"); + feeMap.put(585787L, "8.0"); + feeMap.put(581107L, "1.6"); + } else if (param == Param.DEFAULT_TAKER_FEE_BSQ) { + feeMap.put(674707L, "60.59"); // https://github.com/bisq-network/proposals/issues/318 + feeMap.put(670027L, "52.68"); + feeMap.put(660667L, "70.39"); + feeMap.put(655987L, "61.21"); + feeMap.put(641947L, "53.23"); + feeMap.put(632587L, "46.30"); + feeMap.put(623227L, "40.25"); + feeMap.put(599827L, "30.00"); + feeMap.put(590467L, "38.00"); + feeMap.put(585787L, "24.00"); + feeMap.put(581107L, "4.80"); + } else if (param == Param.DEFAULT_MAKER_FEE_BTC) { + feeMap.put(623227L, "0.0010"); + feeMap.put(585787L, "0.0020"); + } else if (param == Param.DEFAULT_TAKER_FEE_BTC) { + feeMap.put(623227L, "0.0070"); + feeMap.put(585787L, "0.0060"); + } + return feeMap; + } + + public Coin mockedGetParamValueAsCoin(Param param, String paramValue) { + BsqFormatter bsqFormatter = new BsqFormatter(); + return bsqFormatter.parseParamValueToCoin(param, paramValue); + } + + public List mockedGetParamChangeList(Param param) { + BsqFormatter bsqFormatter = new BsqFormatter(); + List retVal = new ArrayList(); + Map feeMap = mockedGetFeeRateMap(param); + for (Map.Entry entry : feeMap.entrySet()) { + retVal.add(ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter)); + } + return retVal; + } +} diff --git a/core/src/test/java/bisq/core/provider/price/MarketPriceFeedServiceTest.java b/core/src/test/java/bisq/core/provider/price/MarketPriceFeedServiceTest.java new file mode 100644 index 0000000000..9a42582624 --- /dev/null +++ b/core/src/test/java/bisq/core/provider/price/MarketPriceFeedServiceTest.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.core.provider.price; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.junit.Ignore; +import org.junit.Test; + +import static junit.framework.TestCase.assertTrue; + +@Ignore +public class MarketPriceFeedServiceTest { + private static final Logger log = LoggerFactory.getLogger(MarketPriceFeedServiceTest.class); + + @Test + public void testGetPrice() throws InterruptedException { + PriceFeedService priceFeedService = new PriceFeedService(null, null, null); + priceFeedService.setCurrencyCode("EUR"); + priceFeedService.requestPriceFeed(tradeCurrency -> { + log.debug(tradeCurrency.toString()); + assertTrue(true); + }, + (errorMessage, throwable) -> { + log.debug(errorMessage); + assertTrue(false); + } + ); + Thread.sleep(10000); + } +} diff --git a/core/src/test/java/bisq/core/trade/TradableListTest.java b/core/src/test/java/bisq/core/trade/TradableListTest.java new file mode 100644 index 0000000000..e6a41bca6a --- /dev/null +++ b/core/src/test/java/bisq/core/trade/TradableListTest.java @@ -0,0 +1,48 @@ +/* + * 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 bisq.core.trade; + +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOffer; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static protobuf.PersistableEnvelope.MessageCase.TRADABLE_LIST; + +public class TradableListTest { + + @Test + public void protoTesting() { + OfferPayload offerPayload = mock(OfferPayload.class, RETURNS_DEEP_STUBS); + TradableList openOfferTradableList = new TradableList<>(); + protobuf.PersistableEnvelope message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); + assertEquals(message.getMessageCase(), TRADABLE_LIST); + + // test adding an OpenOffer and convert toProto + Offer offer = new Offer(offerPayload); + OpenOffer openOffer = new OpenOffer(offer); + openOfferTradableList.add(openOffer); + message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); + assertEquals(message.getMessageCase(), TRADABLE_LIST); + assertEquals(1, message.getTradableList().getTradableList().size()); + } +} diff --git a/core/src/test/java/bisq/core/trade/txproof/xmr/XmrTxProofParserTest.java b/core/src/test/java/bisq/core/trade/txproof/xmr/XmrTxProofParserTest.java new file mode 100644 index 0000000000..af91a7d4d3 --- /dev/null +++ b/core/src/test/java/bisq/core/trade/txproof/xmr/XmrTxProofParserTest.java @@ -0,0 +1,178 @@ +package bisq.core.trade.txproof.xmr; + +import bisq.core.user.AutoConfirmSettings; + +import java.time.Instant; + +import java.util.Collections; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +import static bisq.core.trade.txproof.xmr.XmrTxProofParser.MAX_DATE_TOLERANCE; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +public class XmrTxProofParserTest { + private XmrTxProofModel xmrTxProofModel; + private String recipientAddressHex = "e957dac72bcec80d59b2fecacfa7522223b6a5df895b7e388e60297e85f3f867b42f43e8d9f086a99a997704ceb92bd9cd99d33952de90c9f5f93c82c62360ae"; + private String txHash = "488e48ab0c7e69028d19f787ec57fd496ff114caba9ab265bfd41a3ea0e4687d"; + private String txKey = "6c336e52ed537676968ee319af6983c80b869ca6a732b5962c02748b486f8f0f"; + private XmrTxProofParser parser; + private Date tradeDate; + + @Before + public void prepareMocksAndObjects() { + long amount = 100000000000L; + tradeDate = new Date(1574922644000L); + String serviceAddress = "127.0.0.1:8081"; + AutoConfirmSettings autoConfirmSettings = new AutoConfirmSettings(true, + 10, + 1, + Collections.singletonList(serviceAddress), + "XMR"); + + // TODO using the mocking framework would be better... + String recipientAddress = "4ATyxmFGU7h3EWu5kYR6gy6iCNFCftbsjATfbuBBjsRHJM4KTwEyeiyVNNUmsfpK1kdRxs8QoPLsZanGqe1Mby43LeyWNMF"; + xmrTxProofModel = new XmrTxProofModel( + "dummyTest", + txHash, + txKey, + recipientAddress, + amount, + tradeDate, + autoConfirmSettings); + + parser = new XmrTxProofParser(); + } + + @Test + public void testJsonRoot() { + // checking what happens when bad input is provided + assertSame(parser.parse(xmrTxProofModel, + "invalid json data").getDetail(), XmrTxProofRequest.Detail.API_INVALID); + assertSame(parser.parse(xmrTxProofModel, + "").getDetail(), XmrTxProofRequest.Detail.API_INVALID); + assertSame(parser.parse(xmrTxProofModel, + "[]").getDetail(), XmrTxProofRequest.Detail.API_INVALID); + assertSame(parser.parse(xmrTxProofModel, + "{}").getDetail(), XmrTxProofRequest.Detail.API_INVALID); + } + + @Test + public void testJsonTopLevel() { + // testing the top level fields: data and status + assertSame(parser.parse(xmrTxProofModel, + "{'data':{'title':''},'status':'fail'}") + .getDetail(), XmrTxProofRequest.Detail.TX_NOT_FOUND); + assertSame(parser.parse(xmrTxProofModel, + "{'data':{'title':''},'missingstatus':'success'}") + .getDetail(), XmrTxProofRequest.Detail.API_INVALID); + assertSame(parser.parse(xmrTxProofModel, + "{'missingdata':{'title':''},'status':'success'}") + .getDetail(), XmrTxProofRequest.Detail.API_INVALID); + } + + @Test + public void testJsonAddress() { + assertSame(parser.parse(xmrTxProofModel, + "{'data':{'missingaddress':'irrelevant'},'status':'success'}") + .getDetail(), XmrTxProofRequest.Detail.API_INVALID); + assertSame(parser.parse(xmrTxProofModel, + "{'data':{'address':'e957dac7'},'status':'success'}") + .getDetail(), XmrTxProofRequest.Detail.ADDRESS_INVALID); + } + + @Test + public void testJsonTxHash() { + String missing_tx_hash = "{'data':{'address':'" + recipientAddressHex + "'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, missing_tx_hash).getDetail(), XmrTxProofRequest.Detail.API_INVALID); + + String invalid_tx_hash = "{'data':{'address':'" + recipientAddressHex + "', 'tx_hash':'488e48'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, invalid_tx_hash).getDetail(), XmrTxProofRequest.Detail.TX_HASH_INVALID); + } + + @Test + public void testJsonTxKey() { + String missing_tx_key = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, missing_tx_key).getDetail(), XmrTxProofRequest.Detail.API_INVALID); + + String invalid_tx_key = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'cdce04'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, invalid_tx_key).getDetail(), XmrTxProofRequest.Detail.TX_KEY_INVALID); + } + + @Test + public void testJsonTxTimestamp() { + String missing_tx_timestamp = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "'," + + "'viewkey':'" + txKey + "'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, missing_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.API_INVALID); + + String invalid_tx_timestamp = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'" + txKey + "'," + + "'tx_timestamp':'12345'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, invalid_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING); + + long tradeTimeSec = tradeDate.getTime() / 1000; + String ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE - 1); + String invalid_tx_timestamp_1ms_too_old = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'" + txKey + "'," + + "'tx_timestamp':'" + ts + "'}, 'status':'success'}"; + assertSame(parser.parse(xmrTxProofModel, invalid_tx_timestamp_1ms_too_old).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING); + + ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE); + String valid_tx_timestamp_exact_MAX_DATE_TOLERANCE = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'" + txKey + "'," + + "'tx_timestamp':'" + ts + "'}, 'status':'success'}"; + parser.parse(xmrTxProofModel, valid_tx_timestamp_exact_MAX_DATE_TOLERANCE); + assertNotSame(parser.parse(xmrTxProofModel, valid_tx_timestamp_exact_MAX_DATE_TOLERANCE).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING); + + ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE + 1); + String valid_tx_timestamp_less_than_MAX_DATE_TOLERANCE = "{'data':{'address':'" + recipientAddressHex + "', " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'" + txKey + "'," + + "'tx_timestamp':'" + ts + "'}, 'status':'success'}"; + assertNotSame(parser.parse(xmrTxProofModel, valid_tx_timestamp_less_than_MAX_DATE_TOLERANCE).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING); + } + + @Test + public void testJsonTxConfirmation() { + long epochDate = Instant.now().toEpochMilli() / 1000; + String outputs = "'outputs':[" + + "{'amount':100000000000,'match':true,'output_idx':0,'output_pubkey':'972a2c9178876f1fae4ecd22f9d7c132a12706db8ffb5d1f223f9aa8ced75b61'}," + + "{'amount':0,'match':false,'output_idx':1,'output_pubkey':'658330d2d56c74aca3b40900c56cd0f0111e2876be677ade493d06d539a1bab0'}],"; + String json = "{'status':'success', 'data':{" + + "'address':'" + recipientAddressHex + "', " + + outputs + + "'tx_confirmations':777, " + + "'tx_hash':'" + txHash + "', " + + "'viewkey':'" + txKey + "', " + + "'tx_timestamp':'" + epochDate + "'}" + + "}"; + assertSame(parser.parse(xmrTxProofModel, json), XmrTxProofRequest.Result.SUCCESS); + json = json.replaceFirst("777", "0"); + + assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS); + + json = json.replaceFirst("100000000000", "100000000001"); + assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.AMOUNT_NOT_MATCHING); + + // Revert change of amount + json = json.replaceFirst("100000000001", "100000000000"); + json = json.replaceFirst("'match':true", "'match':false"); + assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.NO_MATCH_FOUND); + } + + @Test + public void testJsonFail() { + String failedJson = "{\"data\":null,\"message\":\"Cant parse tx hash: a\",\"status\":\"error\"}"; + assertSame(parser.parse(xmrTxProofModel, failedJson).getDetail(), XmrTxProofRequest.Detail.API_INVALID); + } +} diff --git a/core/src/test/java/bisq/core/user/PreferencesTest.java b/core/src/test/java/bisq/core/user/PreferencesTest.java new file mode 100644 index 0000000000..f067d8680c --- /dev/null +++ b/core/src/test/java/bisq/core/user/PreferencesTest.java @@ -0,0 +1,142 @@ +/* + * 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 bisq.core.user; + +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.Res; + +import bisq.common.config.Config; +import bisq.common.persistence.PersistenceManager; + +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Currency; +import java.util.List; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PreferencesTest { + + private Preferences preferences; + private PersistenceManager persistenceManager; + + @Before + public void setUp() { + final Locale en_US = new Locale("en", "US"); + Locale.setDefault(en_US); + GlobalSettings.setLocale(en_US); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); + + persistenceManager = mock(PersistenceManager.class); + Config config = new Config(); + LocalBitcoinNode localBitcoinNode = new LocalBitcoinNode(config); + preferences = new Preferences( + persistenceManager, config, null, localBitcoinNode, null, null, Config.DEFAULT_FULL_DAO_NODE, + null, null, Config.UNSPECIFIED_PORT); + } + + @Test + public void testAddFiatCurrency() { + final FiatCurrency usd = new FiatCurrency("USD"); + final FiatCurrency usd2 = new FiatCurrency("USD"); + final ObservableList fiatCurrencies = preferences.getFiatCurrenciesAsObservable(); + + preferences.addFiatCurrency(usd); + + assertEquals(1, fiatCurrencies.size()); + + preferences.addFiatCurrency(usd2); + + assertEquals(1, fiatCurrencies.size()); + } + + @Test + public void testGetUniqueListOfFiatCurrencies() { + PreferencesPayload payload = mock(PreferencesPayload.class); + + List fiatCurrencies = CurrencyUtil.getMainFiatCurrencies(); + final FiatCurrency usd = new FiatCurrency("USD"); + fiatCurrencies.add(usd); + + when(persistenceManager.getPersisted(anyString())).thenReturn(payload); + when(payload.getUserLanguage()).thenReturn("en"); + when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); + when(payload.getPreferredTradeCurrency()).thenReturn(usd); + when(payload.getFiatCurrencies()).thenReturn(fiatCurrencies); + + preferences.readPersisted(() -> { + assertEquals(7, preferences.getFiatCurrenciesAsObservable().size()); + assertTrue(preferences.getFiatCurrenciesAsObservable().contains(usd)); + }); + } + + @Test + public void testGetUniqueListOfCryptoCurrencies() { + PreferencesPayload payload = mock(PreferencesPayload.class); + + List cryptoCurrencies = CurrencyUtil.getMainCryptoCurrencies(); + final CryptoCurrency dash = new CryptoCurrency("DASH", "Dash"); + cryptoCurrencies.add(dash); + + when(persistenceManager.getPersisted(anyString())).thenReturn(payload); + when(payload.getUserLanguage()).thenReturn("en"); + when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); + when(payload.getPreferredTradeCurrency()).thenReturn(new FiatCurrency("USD")); + when(payload.getCryptoCurrencies()).thenReturn(cryptoCurrencies); + + preferences.readPersisted(() -> { + assertTrue(preferences.getCryptoCurrenciesAsObservable().contains(dash)); + }); + } + + @Test + public void testUpdateOfPersistedFiatCurrenciesAfterLocaleChanged() { + PreferencesPayload payload = mock(PreferencesPayload.class); + + List fiatCurrencies = new ArrayList<>(); + final FiatCurrency usd = new FiatCurrency(Currency.getInstance("USD"), new Locale("de", "AT")); + fiatCurrencies.add(usd); + + assertEquals("US-Dollar (USD)", usd.getNameAndCode()); + + when(persistenceManager.getPersisted(anyString())).thenReturn(payload); + when(payload.getUserLanguage()).thenReturn("en"); + when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); + when(payload.getPreferredTradeCurrency()).thenReturn(usd); + when(payload.getFiatCurrencies()).thenReturn(fiatCurrencies); + + preferences.readPersisted(() -> { + assertEquals("US Dollar (USD)", preferences.getFiatCurrenciesAsObservable().get(0).getNameAndCode()); + }); + } +} diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java new file mode 100644 index 0000000000..7d98e95923 --- /dev/null +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.core.user; + +import bisq.core.alert.Alert; +import bisq.core.arbitration.ArbitratorTest; +import bisq.core.arbitration.MediatorTest; +import bisq.core.filter.Filter; +import bisq.core.proto.CoreProtoResolver; + +import com.google.common.collect.Lists; + +import java.util.HashSet; + +import org.junit.Ignore; + +public class UserPayloadModelVOTest { + @Ignore("TODO InvalidKeySpecException at bisq.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") + public void testRoundtrip() { + UserPayload vo = new UserPayload(); + vo.setAccountId("accountId"); + UserPayload newVo = UserPayload.fromProto(vo.toProtoMessage().getUserPayload(), new CoreProtoResolver()); + } + + @Ignore("TODO InvalidKeySpecException at bisq.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") + public void testRoundtripFull() { + UserPayload vo = new UserPayload(); + vo.setAccountId("accountId"); + vo.setDisplayedAlert(new Alert("message", true, false, "version", new byte[]{12, -64, 12}, "string", null)); + vo.setDevelopersFilter(new Filter(Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + false, + Lists.newArrayList(), + false, + null, + null, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + null, + 0, + null, + null, + null, + null, + false, + Lists.newArrayList(), + new HashSet<>(), + false, + false)); + + vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); + vo.setRegisteredMediator(MediatorTest.getMediatorMock()); + vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); + vo.setAcceptedMediators(Lists.newArrayList(MediatorTest.getMediatorMock())); + UserPayload newVo = UserPayload.fromProto(vo.toProtoMessage().getUserPayload(), new CoreProtoResolver()); + } +} diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java new file mode 100644 index 0000000000..fc6a1f534d --- /dev/null +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -0,0 +1,147 @@ +/* + * 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 bisq.core.util; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.param.Param; +import bisq.core.filter.Filter; +import bisq.core.filter.FilterManager; + +import com.google.common.collect.Lists; +import com.google.common.primitives.Longs; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class FeeReceiverSelectorTest { + @Mock + private DaoFacade daoFacade; + @Mock + private FilterManager filterManager; + + @Test + public void testGetAddress() { + Random rnd = new Random(123); + when(filterManager.getFilter()).thenReturn(filterWithReceivers( + List.of("", "foo#0.001", "ill-formed", "bar#0.002", "baz#0.001", "partial#bad"))); + + Map selectionCounts = new HashMap<>(); + for (int i = 0; i < 400; i++) { + String address = FeeReceiverSelector.getAddress(daoFacade, filterManager, rnd); + selectionCounts.compute(address, (k, n) -> n != null ? n + 1 : 1); + } + + assertEquals(3, selectionCounts.size()); + + // Check within 2 std. of the expected values (95% confidence each): + assertEquals(100.0, selectionCounts.get("foo"), 18); + assertEquals(200.0, selectionCounts.get("bar"), 20); + assertEquals(100.0, selectionCounts.get("baz"), 18); + } + + @Test + public void testGetAddress_noValidReceivers_nullFilter() { + when(daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS)).thenReturn("default"); + + when(filterManager.getFilter()).thenReturn(null); + assertEquals("default", FeeReceiverSelector.getAddress(daoFacade, filterManager)); + } + + @Test + public void testGetAddress_noValidReceivers_filterWithNullList() { + when(daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS)).thenReturn("default"); + + when(filterManager.getFilter()).thenReturn(filterWithReceivers(null)); + assertEquals("default", FeeReceiverSelector.getAddress(daoFacade, filterManager)); + } + + @Test + public void testGetAddress_noValidReceivers_filterWithEmptyList() { + when(daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS)).thenReturn("default"); + + when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of())); + assertEquals("default", FeeReceiverSelector.getAddress(daoFacade, filterManager)); + } + + @Test + public void testGetAddress_noValidReceivers_filterWithIllFormedList() { + when(daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS)).thenReturn("default"); + + when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of("ill-formed"))); + assertEquals("default", FeeReceiverSelector.getAddress(daoFacade, filterManager)); + } + + @Test + public void testWeightedSelection() { + Random rnd = new Random(456); + + int[] selections = new int[3]; + for (int i = 0; i < 6000; i++) { + selections[FeeReceiverSelector.weightedSelection(Longs.asList(1, 2, 3), rnd)]++; + } + + // Check within 2 std. of the expected values (95% confidence each): + assertEquals(1000.0, selections[0], 58); + assertEquals(2000.0, selections[1], 74); + assertEquals(3000.0, selections[2], 78); + } + + private static Filter filterWithReceivers(List btcFeeReceiverAddresses) { + return new Filter(Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + false, + Lists.newArrayList(), + false, + null, + null, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + btcFeeReceiverAddresses, + null, + 0, + null, + null, + null, + null, + false, + Lists.newArrayList(), + new HashSet<>(), + false, + false); + } +} diff --git a/core/src/test/java/bisq/core/util/FormattingUtilsTest.java b/core/src/test/java/bisq/core/util/FormattingUtilsTest.java new file mode 100644 index 0000000000..76f5a46e0b --- /dev/null +++ b/core/src/test/java/bisq/core/util/FormattingUtilsTest.java @@ -0,0 +1,69 @@ +package bisq.core.util; + +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; + +import org.bitcoinj.utils.Fiat; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import com.natpryce.makeiteasy.Instantiator; +import com.natpryce.makeiteasy.Maker; +import com.natpryce.makeiteasy.Property; + +import org.junit.Before; +import org.junit.Test; + +import static com.natpryce.makeiteasy.MakeItEasy.a; +import static com.natpryce.makeiteasy.MakeItEasy.make; +import static com.natpryce.makeiteasy.MakeItEasy.with; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FormattingUtilsTest { + private static final Property currencyCode = new Property<>(); + private static final Property priceString = new Property<>(); + private static final Maker usdPrice = a(lookup -> + new Price(Fiat.parseFiat(lookup.valueOf(currencyCode, "USD"), lookup.valueOf(priceString, "100")))); + + @Before + public void setUp() { + Locale.setDefault(new Locale("en", "US")); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); + } + + @Test + public void testFormatDurationAsWords() { + long oneDay = TimeUnit.DAYS.toMillis(1); + long oneHour = TimeUnit.HOURS.toMillis(1); + long oneMinute = TimeUnit.MINUTES.toMillis(1); + long oneSecond = TimeUnit.SECONDS.toMillis(1); + + assertEquals("1 hour, 0 minutes", FormattingUtils.formatDurationAsWords(oneHour)); + assertEquals("1 day, 0 hours, 0 minutes", FormattingUtils.formatDurationAsWords(oneDay)); + assertEquals("2 days, 0 hours, 1 minute", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneMinute)); + assertEquals("2 days, 0 hours, 2 minutes", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneMinute * 2)); + assertEquals("1 hour, 0 minutes, 0 seconds", FormattingUtils.formatDurationAsWords(oneHour, true, true)); + assertEquals("1 hour, 0 minutes, 1 second", FormattingUtils.formatDurationAsWords(oneHour + oneSecond, true, true)); + assertEquals("1 hour, 0 minutes, 2 seconds", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, true, true)); + assertEquals("2 days, 21 hours, 28 minutes", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneHour * 21 + oneMinute * 28)); + assertEquals("110 days", FormattingUtils.formatDurationAsWords(oneDay * 110, false, false)); + assertEquals("10 days, 10 hours, 10 minutes, 10 seconds", FormattingUtils.formatDurationAsWords(oneDay * 10 + oneHour * 10 + oneMinute * 10 + oneSecond * 10, true, false)); + assertEquals("1 hour, 2 seconds", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, true, false)); + assertEquals("1 hour", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, false, false)); + assertEquals("0 hours, 0 minutes, 1 second", FormattingUtils.formatDurationAsWords(oneSecond, true, true)); + assertEquals("1 second", FormattingUtils.formatDurationAsWords(oneSecond, true, false)); + assertEquals("0 hours", FormattingUtils.formatDurationAsWords(oneSecond, false, false)); + assertEquals("", FormattingUtils.formatDurationAsWords(0)); + assertTrue(FormattingUtils.formatDurationAsWords(0).isEmpty()); + } + + @Test + public void testFormatPrice() { + assertEquals("100.0000", FormattingUtils.formatPrice(make(usdPrice))); + assertEquals("7098.4700", FormattingUtils.formatPrice(make(usdPrice.but(with(priceString, "7098.4700"))))); + } +} diff --git a/core/src/test/java/bisq/core/util/ProtoUtilTest.java b/core/src/test/java/bisq/core/util/ProtoUtilTest.java new file mode 100644 index 0000000000..dee3b4a249 --- /dev/null +++ b/core/src/test/java/bisq/core/util/ProtoUtilTest.java @@ -0,0 +1,69 @@ +/* + * 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 bisq.core.util; + +import bisq.core.offer.OpenOffer; + +import bisq.common.proto.ProtoUtil; + +import protobuf.OfferPayload; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@SuppressWarnings("UnusedAssignment") +public class ProtoUtilTest { + + //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in bisq.common. + @Test + public void testEnum() { + OfferPayload.Direction direction = OfferPayload.Direction.SELL; + OfferPayload.Direction direction2 = OfferPayload.Direction.BUY; + OfferPayload.Direction realDirection = getDirection(direction); + OfferPayload.Direction realDirection2 = getDirection(direction2); + assertEquals("SELL", realDirection.name()); + assertEquals("BUY", realDirection2.name()); + } + + @Test + public void testUnknownEnum() { + protobuf.OpenOffer.State result = protobuf.OpenOffer.State.PB_ERROR; + try { + OpenOffer.State finalResult = OpenOffer.State.valueOf(result.name()); + fail(); + } catch (IllegalArgumentException ignore) { + } + } + + @Test + public void testUnknownEnumFix() { + protobuf.OpenOffer.State result = protobuf.OpenOffer.State.PB_ERROR; + try { + OpenOffer.State finalResult = ProtoUtil.enumFromProto(OpenOffer.State.class, result.name()); + assertEquals(OpenOffer.State.AVAILABLE, ProtoUtil.enumFromProto(OpenOffer.State.class, "AVAILABLE")); + } catch (IllegalArgumentException e) { + fail(); + } + } + + public static OfferPayload.Direction getDirection(OfferPayload.Direction direction) { + return OfferPayload.Direction.valueOf(direction.name()); + } +} diff --git a/core/src/test/java/bisq/core/util/RegexValidatorTest.java b/core/src/test/java/bisq/core/util/RegexValidatorTest.java new file mode 100644 index 0000000000..67d5075ade --- /dev/null +++ b/core/src/test/java/bisq/core/util/RegexValidatorTest.java @@ -0,0 +1,313 @@ +/* + * 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 bisq.core.util; + +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.Res; +import bisq.core.util.validation.RegexValidator; +import bisq.core.util.validation.RegexValidatorFactory; + +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +public class RegexValidatorTest { + + @Before + public void setup() { + Locale.setDefault(new Locale("en", "US")); + GlobalSettings.setLocale(new Locale("en", "US")); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); + } + + @Test + public void testAddressRegexValidator() { + RegexValidator regexValidator = RegexValidatorFactory.addressRegexValidator(); + + assertTrue(regexValidator.validate("").isValid); + assertFalse(regexValidator.validate(" ").isValid); + + // onion V2 addresses + assertTrue(regexValidator.validate("abcdefghij234567.onion").isValid); + assertTrue(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); + assertTrue(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); + assertTrue(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); + assertTrue(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertTrue(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertTrue(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); + assertFalse(regexValidator.validate("abcd.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); + assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); + + // onion v3 addresses + assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char + assertTrue(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); + + // ipv4 addresses + assertTrue(regexValidator.validate("12.34.56.78").isValid); + assertTrue(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); + assertTrue(regexValidator.validate("12.34.56.78:8888").isValid); + assertFalse(regexValidator.validate("12.34.56.788").isValid); + assertFalse(regexValidator.validate("12.34.56.78:").isValid); + + // ipv6 addresses + assertTrue(regexValidator.validate("FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); + assertTrue(regexValidator.validate("FE80::0202:B3FF:FE1E:8329").isValid); + assertTrue(regexValidator.validate("FE80::0202:B3FF:FE1E:8329,FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); + assertTrue(regexValidator.validate("::1").isValid); + assertTrue(regexValidator.validate("fe80::").isValid); + assertTrue(regexValidator.validate("2001::").isValid); + assertTrue(regexValidator.validate("[::1]:8333").isValid); + assertTrue(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); + assertTrue(regexValidator.validate("[2001:db8::1]:80").isValid); + assertTrue(regexValidator.validate("[aaaa::bbbb]:8333").isValid); + assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); + + // fqdn addresses + assertTrue(regexValidator.validate("example.com").isValid); + assertTrue(regexValidator.validate("mynode.local:8333").isValid); + assertTrue(regexValidator.validate("foo.example.com,bar.example.com").isValid); + assertTrue(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); + + assertFalse(regexValidator.validate("mynode.local:65536").isValid); + assertFalse(regexValidator.validate("-example.com").isValid); + assertFalse(regexValidator.validate("example-.com").isValid); + } + + @Test + public void testOnionAddressRegexValidator() { + RegexValidator regexValidator = RegexValidatorFactory.onionAddressRegexValidator(); + + assertTrue(regexValidator.validate("").isValid); + assertFalse(regexValidator.validate(" ").isValid); + + // onion V2 addresses + assertTrue(regexValidator.validate("abcdefghij234567.onion").isValid); + assertTrue(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); + assertTrue(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); + assertTrue(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); + assertTrue(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertTrue(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertTrue(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); + assertFalse(regexValidator.validate("abcd.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); + assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); + + // onion v3 addresses + assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char + assertTrue(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); + + } + + @Test + public void testLocalnetAddressRegexValidator() { + RegexValidator regexValidator = RegexValidatorFactory.localnetAddressRegexValidator(); + + assertTrue(regexValidator.validate("").isValid); + assertFalse(regexValidator.validate(" ").isValid); + + // onion V2 addresses + assertFalse(regexValidator.validate("abcdefghij234567.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); + assertFalse(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertFalse(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertFalse(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); + assertFalse(regexValidator.validate("abcd.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); + assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); + + // onion v3 addresses + assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char + assertFalse(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); + + // ipv4 addresses + assertFalse(regexValidator.validate("12.34.56.78").isValid); + assertFalse(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); + assertFalse(regexValidator.validate("12.34.56.78:8888").isValid); + assertFalse(regexValidator.validate("12.34.56.788").isValid); + assertFalse(regexValidator.validate("12.34.56.78:").isValid); + + // ipv4 local addresses + assertTrue(regexValidator.validate("10.10.10.10").isValid); + assertTrue(regexValidator.validate("172.19.1.1").isValid); + assertTrue(regexValidator.validate("172.19.1.1").isValid); + assertTrue(regexValidator.validate("192.168.1.1").isValid); + assertTrue(regexValidator.validate("192.168.1.1,172.16.1.1").isValid); + assertTrue(regexValidator.validate("192.168.1.1:8888,192.168.1.2:8888").isValid); + assertFalse(regexValidator.validate("192.168.1.888").isValid); + assertFalse(regexValidator.validate("192.168.1.1:").isValid); + + // ipv4 autolocal addresses + assertTrue(regexValidator.validate("169.254.123.232").isValid); + + // ipv6 local addresses + assertTrue(regexValidator.validate("fe80:2:3:4:5:6:7:8").isValid); + assertTrue(regexValidator.validate("fe80::").isValid); + assertTrue(regexValidator.validate("fc00::").isValid); + assertTrue(regexValidator.validate("fd00::,fe80::1").isValid); + assertTrue(regexValidator.validate("fd00::8").isValid); + assertTrue(regexValidator.validate("fd00::7:8").isValid); + assertTrue(regexValidator.validate("fd00::6:7:8").isValid); + assertTrue(regexValidator.validate("fd00::5:6:7:8").isValid); + assertTrue(regexValidator.validate("fd00::4:5:6:7:8").isValid); + assertTrue(regexValidator.validate("fd00::3:4:5:6:7:8").isValid); + assertTrue(regexValidator.validate("fd00:2:3:4:5:6:7:8").isValid); + assertTrue(regexValidator.validate("fd00::0202:B3FF:FE1E:8329").isValid); + assertTrue(regexValidator.validate("fd00::0202:B3FF:FE1E:8329,FE80::0202:B3FF:FE1E:8329").isValid); + // ipv6 local with optional port at the end + assertTrue(regexValidator.validate("[fd00::1]:8081").isValid); + assertTrue(regexValidator.validate("[fd00::1]:8081,[fc00::1]:8081").isValid); + assertTrue(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); + + // ipv6 loopback + assertFalse(regexValidator.validate("::1").isValid); + + // ipv6 unicast + assertFalse(regexValidator.validate("2001::").isValid); + assertFalse(regexValidator.validate("[::1]:8333").isValid); + assertFalse(regexValidator.validate("[2001:db8::1]:80").isValid); + assertFalse(regexValidator.validate("[aaaa::bbbb]:8333").isValid); + assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); + + // *.local fqdn hostnames + assertTrue(regexValidator.validate("mynode.local").isValid); + assertTrue(regexValidator.validate("mynode.local:8081").isValid); + + // non-local fqdn hostnames + assertFalse(regexValidator.validate("example.com").isValid); + assertFalse(regexValidator.validate("foo.example.com,bar.example.com").isValid); + assertFalse(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); + + // invalid fqdn hostnames + assertFalse(regexValidator.validate("mynode.local:65536").isValid); + assertFalse(regexValidator.validate("-example.com").isValid); + assertFalse(regexValidator.validate("example-.com").isValid); + } + + @Test + public void testLocalhostAddressRegexValidator() { + RegexValidator regexValidator = RegexValidatorFactory.localhostAddressRegexValidator(); + + assertTrue(regexValidator.validate("").isValid); + assertFalse(regexValidator.validate(" ").isValid); + + // onion V2 addresses + assertFalse(regexValidator.validate("abcdefghij234567.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); + assertFalse(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertFalse(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); + assertFalse(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); + assertFalse(regexValidator.validate("abcd.onion").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); + assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); + assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); + assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); + + // onion v3 addresses + assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char + assertFalse(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); + + // ipv4 addresses + assertFalse(regexValidator.validate("12.34.56.78").isValid); + assertFalse(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); + assertFalse(regexValidator.validate("12.34.56.78:8888").isValid); + assertFalse(regexValidator.validate("12.34.56.788").isValid); + assertFalse(regexValidator.validate("12.34.56.78:").isValid); + + // ipv4 loopback addresses + assertTrue(regexValidator.validate("127.0.0.1").isValid); + assertTrue(regexValidator.validate("127.0.1.1").isValid); + + // ipv4 local addresses + assertFalse(regexValidator.validate("10.10.10.10").isValid); + assertFalse(regexValidator.validate("172.19.1.1").isValid); + assertFalse(regexValidator.validate("172.19.1.1").isValid); + assertFalse(regexValidator.validate("192.168.1.1").isValid); + assertFalse(regexValidator.validate("192.168.1.1,172.16.1.1").isValid); + assertFalse(regexValidator.validate("192.168.1.1:8888,192.168.1.2:8888").isValid); + assertFalse(regexValidator.validate("192.168.1.888").isValid); + assertFalse(regexValidator.validate("192.168.1.1:").isValid); + + // ipv4 autolocal addresses + assertFalse(regexValidator.validate("169.254.123.232").isValid); + + // ipv6 local addresses + assertFalse(regexValidator.validate("fe80::").isValid); + assertFalse(regexValidator.validate("fc00::").isValid); + assertFalse(regexValidator.validate("fd00::8").isValid); + assertFalse(regexValidator.validate("fd00::7:8").isValid); + assertFalse(regexValidator.validate("fd00::6:7:8").isValid); + assertFalse(regexValidator.validate("fd00::5:6:7:8").isValid); + assertFalse(regexValidator.validate("fd00::3:4:5:6:7:8").isValid); + assertFalse(regexValidator.validate("fd00::4:5:6:7:8").isValid); + assertFalse(regexValidator.validate("fd00:2:3:4:5:6:7:8").isValid); + assertFalse(regexValidator.validate("fd00::0202:B3FF:FE1E:8329").isValid); + + assertFalse(regexValidator.validate("FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); + assertFalse(regexValidator.validate("FE80::0202:B3FF:FE1E:8329").isValid); + assertFalse(regexValidator.validate("FE80::0202:B3FF:FE1E:8329,FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); + // ipv6 local with optional port at the end + assertFalse(regexValidator.validate("[fd00::1]:8081").isValid); + assertFalse(regexValidator.validate("[fd00::1]:8081,[fc00::1]:8081").isValid); + + // ipv6 loopback + assertTrue(regexValidator.validate("::1").isValid); + assertTrue(regexValidator.validate("::2").isValid); + assertTrue(regexValidator.validate("[::1]:8333").isValid); + + // ipv6 unicast + assertFalse(regexValidator.validate("2001::").isValid); + assertFalse(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); + assertFalse(regexValidator.validate("[2001:db8::1]:80").isValid); + assertFalse(regexValidator.validate("[aaaa::bbbb]:8333").isValid); + assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); + + // localhost fqdn hostnames + assertTrue(regexValidator.validate("localhost").isValid); + assertTrue(regexValidator.validate("localhost:8081").isValid); + + // local fqdn hostnames + assertFalse(regexValidator.validate("mynode.local:8081").isValid); + + // non-local fqdn hostnames + assertFalse(regexValidator.validate("example.com").isValid); + assertFalse(regexValidator.validate("foo.example.com,bar.example.com").isValid); + assertFalse(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); + + // invalid fqdn hostnames + assertFalse(regexValidator.validate("mynode.local:65536").isValid); + assertFalse(regexValidator.validate("-example.com").isValid); + assertFalse(regexValidator.validate("example-.com").isValid); + } +} diff --git a/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java new file mode 100644 index 0000000000..d4c2e683ef --- /dev/null +++ b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java @@ -0,0 +1,123 @@ +/* + * 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 bisq.core.util.coin; + +import bisq.core.monetary.Price; + +import org.bitcoinj.core.Coin; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class CoinUtilTest { + + @Test + public void testGetFeePerBtc() { + assertEquals(Coin.parseCoin("1"), CoinUtil.getFeePerBtc(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.01"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("0.1"))); + assertEquals(Coin.parseCoin("0.015"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.3"), Coin.parseCoin("0.05"))); + } + + @Test + public void testMinCoin() { + assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); + assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); + assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); + } + + @Test + public void testMaxCoin() { + assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); + assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); + assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); + } + + @Test + public void testGetAdjustedAmount() { + Coin result = CoinUtil.getAdjustedAmount( + Coin.valueOf(100_000), + Price.valueOf("USD", 1000_0000), + 20_000_000, + 1); + assertEquals( + "Minimum trade amount allowed should be adjusted to the smallest trade allowed.", + "0.001 BTC", + result.toFriendlyString() + ); + + try { + CoinUtil.getAdjustedAmount( + Coin.ZERO, + Price.valueOf("USD", 1000_0000), + 20_000_000, + 1); + fail("Expected IllegalArgumentException to be thrown when amount is too low."); + } catch (IllegalArgumentException iae) { + assertEquals( + "Unexpected exception message.", + "amount needs to be above minimum of 10k satoshis", + iae.getMessage() + ); + } + + result = CoinUtil.getAdjustedAmount( + Coin.valueOf(1_000_000), + Price.valueOf("USD", 1000_0000), + 20_000_000, + 1); + assertEquals( + "Minimum allowed trade amount should not be adjusted.", + "0.01 BTC", + result.toFriendlyString() + ); + + result = CoinUtil.getAdjustedAmount( + Coin.valueOf(100_000), + Price.valueOf("USD", 1000_0000), + 1_000_000, + 1); + assertEquals( + "Minimum trade amount allowed should respect maxTradeLimit and factor, if possible.", + "0.001 BTC", + result.toFriendlyString() + ); + + // TODO(chirhonul): The following seems like it should raise an exception or otherwise fail. + // We are asking for the smallest allowed BTC trade when price is 1000 USD each, and the + // max trade limit is 5k sat = 0.00005 BTC. But the returned amount 0.00005 BTC, or + // 0.05 USD worth, which is below the factor of 1 USD, but does respect the maxTradeLimit. + // Basically the given constraints (maxTradeLimit vs factor) are impossible to both fulfill.. + result = CoinUtil.getAdjustedAmount( + Coin.valueOf(100_000), + Price.valueOf("USD", 1000_0000), + 5_000, + 1); + assertEquals( + "Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified.", + "0.00005 BTC", + result.toFriendlyString() + ); + } +} diff --git a/core/src/test/resources/bisq/core/app/cli-output.txt b/core/src/test/resources/bisq/core/app/cli-output.txt new file mode 100644 index 0000000000..02391b6fb2 --- /dev/null +++ b/core/src/test/resources/bisq/core/app/cli-output.txt @@ -0,0 +1,57 @@ +Bisq Test version 0.1.0 + +Usage: bisq-test [options] + +Options: + + --name= (default: Bisq) + The name of the Bisq node + + --another-option= (default: WAT) + This is a long description which will need to break over multiple + linessssssssssss such that no line is longer than 80 characters in the + help output. + + --exactly-72-char-description= + 012345678911234567892123456789312345678941234567895123456789612345678971 + + --exactly-72-char-description-with-spaces= + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1 + + --90-char-description-without-spaces= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789 + + --90-char-description-with-space-at-char-80= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 + 923456789 + + --90-char-description-with-spaces-at-chars-5-and-80= + -123 + 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 + 923456789 + + --90-char-description-with-space-at-char-73= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 + 3456789-923456789 + + --1-char-description-with-only-a-space= + + --empty-description= + + --no-description= + + --no-arg + Some description + + --optional-arg= + Option description + + --with-default-value= (default: Wat) + Some option with a default value + + --data-dir= (default: /Users/cbeams/Library/Application Support/Bisq) + Application data directory + + --enum-opt= (default: foo) + Some option that accepts an enum value as an argument + diff --git a/core/src/test/resources/bisq/core/app/cli-output_windows.txt b/core/src/test/resources/bisq/core/app/cli-output_windows.txt new file mode 100644 index 0000000000..e07fcca83c --- /dev/null +++ b/core/src/test/resources/bisq/core/app/cli-output_windows.txt @@ -0,0 +1,57 @@ +Bisq Test version 0.1.0 + +Usage: bisq-test [options] + +Options: + + --name= (default: Bisq) + The name of the Bisq node + + --another-option= (default: WAT) + This is a long description which will need to break over multiple + linessssssssssss such that no line is longer than 80 characters in the + help output. + + --exactly-72-char-description= + 012345678911234567892123456789312345678941234567895123456789612345678971 + + --exactly-72-char-description-with-spaces= + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1 + + --90-char-description-without-spaces= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789 + + --90-char-description-with-space-at-char-80= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 + 923456789 + + --90-char-description-with-spaces-at-chars-5-and-80= + -123 + 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 + 923456789 + + --90-char-description-with-space-at-char-73= + -123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 + 3456789-923456789 + + --1-char-description-with-only-a-space= + + --empty-description= + + --no-description= + + --no-arg + Some description + + --optional-arg= + Option description + + --with-default-value= (default: Wat) + Some option with a default value + + --data-dir= (default: \Users\cbeams\Library\Application Support\Bisq) + Application data directory + + --enum-opt= (default: foo) + Some option that accepts an enum value as an argument + diff --git a/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-0.txt b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-0.txt new file mode 100644 index 0000000000..e363535db5 --- /dev/null +++ b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-0.txt @@ -0,0 +1,53 @@ +00000020a33d8cf2a1567a148dad1a4099599bafa631135262413a4bdd1182be5673471abe69039afc7c93936e3e2860da8cab522281fe4139a453d1 +1a3d5cfe75e93761bf25b25fffff7f20010000000b020000000001010000000000000000000000000000000000000000000000000000000000000000 +ffffffff05028b000101ffffffff02495b062a0100000017a914f2d479a78b981e4a2b05be5f89ef7c468ac48e78870000000000000000266a24aa21 +a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c0120000000000000000000000000000000000000000000000000 +0000000000000000000000000100000001274c7174e814eb731712b80187660f7a6ab2949a3271ef80037685cdde2d78c3020000006b483045022100 +b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e1109 +3951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416ffffffff03881300000000000017a9144c0e48 +93237f85479f489b32c8ff0faf3ee2e1c987c247090000000000160014007128282856f8e8f3c75909f9f1474b55cb1f1605f8902500000000160014 +9bc809698674ec7c01d35d438e9d0de1aa87b6c800000000010000000114facc8cf47a984cedf9ba84db10ad767e18c6fb6edbac39ce8f138a1e5b43 +9100000000da0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64b +c6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073 +d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e999 +1f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152aefeffffff01b06a21000000000017a914 +4c0e4893237f85479f489b32c8ff0faf3ee2e1c9878a0000000100000000010133ba0d88494567a02bffc406b31bd5eb29f0b536a96326baca349b3a +96f241e90200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987067e180000000000160014f6da24 +d081a1b63dcacf4a5762a8ed91fd472c685b74b900000000001600144755561caa18d651bf59912545764811d0ab96f60247304402200d4f21475675 +3861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac6500471930 +012102772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c320000000001000000000101e06ec4548803dadb10ef6c66e4f3 +1f319161dc9ec31631e967773b8e042836180200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987 +f4390900000000001600145230c895305a232ef2a4feb0a91e7d99e22fd515d20bd20000000000160014b6fbbd9053e47891fae7f3db7dd3966062b2 +513c0247304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f +3dc900a4d0ab5cc3c97d5be85f1801210255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b0000000001000000000101 +4c701c32d3b0ce9408d8ec96d80934dbc6cb42df616e6e751dae82afdb46214e0200000000ffffffff03881300000000000017a9144c0e4893237f85 +479f489b32c8ff0faf3ee2e1c987c02709000000000016001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a8a07a00600000000160014062d20a6 +692350b7a39397c50857a7f725788da002483045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0 +bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e012102e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672 +a69cddc60000000001000000000101c3f609b5166e32bd0c29168767621bfc56411a3ff9e84932fc2c612407566a370200000000ffffffff03881300 +000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141437b91493d1929b1b42a80e83229c347c28f937 +eaaf90060000000016001491ad2cce99e8e4455de5f44559816b98213f3503024830450221008f8eee212f209ba2a179197bd4ba35a35ad7a3045990 +25ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b012103f870bcd3a46e80e4b1236302e6 +2318b412cc97ef096fc976a89deb569dc11ef1000000000100000001ba4f0ae59d7cb81f0c7edd63796387fde6825a3536953c2824d7d945c192bd20 +020000006b483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab70 +6870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60ffffffff03881300 +000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001413afc3a26c010dddaf2410a9d97b5054a4e8d309 +04e05938000000001600145228ee46a95383b314396dda75e707b8bed830340000000001000000015d71dc04e56bc08b61180a2b9531a0747f56615f +f27ec51cbc72ac7a800cc27a020000006a473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc +8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae +4ef0b5ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141c397ba7ea8410dbd5 +56c51f8184a14ab015114324b6a10600000000160014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe0000000001000000000102142a200a7460ad +f754232337f48bf7c38ba411eed0cd2f98900ffd9f2adc1ed20100000000ffffffff99d67c2a69b04831bd229ea6022c91c68999c38d0ba92211b56f +15c5f8222bf50100000000ffffffff03e803000000000000220020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f00 +00000000000000226a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454db972100000000001600145e41d2fb8de1c2 +50410416d8dd153c685d3f9c7b024730440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992 +fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f012103c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b7646635 +0247304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616 +386b86b419197b360c871fca012102c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab280000000001000000000102913a +5817e0b3bddb0bc6869e12200b5115be11522cf76125229d47e184da48460100000000ffffffff684d66517d21106d1bcbba96964e31a532fec33c09 +8cd621e2500c702340b6780100000000ffffffff03e803000000000000220020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc6 +1e1c72310000000000000000226a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730f2962100000000001600145e41 +d2fb8de1c250410416d8dd153c685d3f9c7b02483045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070 +cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada547243012102016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745 +e650f82bd1230248304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d1394 +5e012d0031246d4574e92f97e3701ca729b6140121029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b03100000000 diff --git a/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-1.json b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-1.json new file mode 100644 index 0000000000..7016fad55b --- /dev/null +++ b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-1.json @@ -0,0 +1,33 @@ +{ + "hash": "015f37a20d517645a11a6cdd316049f41bc77b4a4057b2dd092114b78147f42c", + "confirmations": 12, + "strippedsize": 2270, + "size": 3178, + "weight": 9988, + "height": 139, + "version": 536870912, + "versionHex": "20000000", + "merkleroot": "6137e975fe5c3d1ad153a43941fe812252ab8cda60283e6e93937cfc9a0369be", + "tx": [ + "09bbfd286d0399b57ac7c85956fccbe7e080cedd7a01723d8d68038f0cf57159", + "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", + "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", + "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", + "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", + "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", + "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", + "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", + "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", + "719606705c6832f7180ab9db5e1ce6a51bad80a5cbf3b57b806dd10f3d7d5124", + "16cd3283068d2965f26045e15a285e39a762af32ec388ae8c28fb6cb2c468768" + ], + "time": 1605510591, + "mediantime": 1589548514, + "nonce": 1, + "bits": "207fffff", + "difficulty": 4.656542373906925E-10, + "chainwork": "0000000000000000000000000000000000000000000000000000000000000118", + "nTx": 11, + "previousblockhash": "1a477356be8211dd4b3a4162521331a6af9b5999401aad8d147a56a1f28c3da3", + "nextblockhash": "7d8267341a57f1f626b450eb22b2dbf208f13ec176e1cc015aa4d2b2ea55016d" +} diff --git a/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-2.json b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-2.json new file mode 100644 index 0000000000..5947c1c902 --- /dev/null +++ b/core/src/test/resources/bisq/core/dao/node/full/rpc/getblock-result-verbosity-2.json @@ -0,0 +1,698 @@ +{ + "hash": "015f37a20d517645a11a6cdd316049f41bc77b4a4057b2dd092114b78147f42c", + "confirmations": 12, + "strippedsize": 2270, + "size": 3178, + "weight": 9988, + "height": 139, + "version": 536870912, + "versionHex": "20000000", + "merkleroot": "6137e975fe5c3d1ad153a43941fe812252ab8cda60283e6e93937cfc9a0369be", + "tx": [ + { + "txid": "09bbfd286d0399b57ac7c85956fccbe7e080cedd7a01723d8d68038f0cf57159", + "hash": "ed1620cdd028c99d57ee0709f963976bb75cac93f5171f5a2f5dd9976e5c56bc", + "version": 2, + "size": 171, + "vsize": 144, + "weight": 576, + "locktime": 0, + "vin": [ + { + "coinbase": "028b000101", + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 50.00026953, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 f2d479a78b981e4a2b05be5f89ef7c468ac48e78 OP_EQUAL", + "hex": "a914f2d479a78b981e4a2b05be5f89ef7c468ac48e7887", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2NFPC3k5RjZ4GAQgLqJdiVdJ6gqWqEBryXq" + ] + } + }, + { + "value": 0.0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c", + "hex": "6a24aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c", + "type": "nulldata" + } + } + ], + "hex": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff05028b000101ffffffff02495b062a0100000017a914f2d479a78b981e4a2b05be5f89ef7c468ac48e78870000000000000000266a24aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c0120000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "txid": "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", + "hash": "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", + "version": 1, + "size": 252, + "vsize": 252, + "weight": 1008, + "locktime": 0, + "vin": [ + { + "txid": "c3782ddecd85760380ef71329a94b26a7a0f668701b8121773eb14e874714c27", + "vout": 2, + "scriptSig": { + "asm": "3045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5[ALL] 03dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416", + "hex": "483045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.00608194, + "n": 1, + "scriptPubKey": { + "asm": "0 007128282856f8e8f3c75909f9f1474b55cb1f16", + "hex": "0014007128282856f8e8f3c75909f9f1474b55cb1f16", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qqpcjs2pg2muw3u78tyylnu28fd2uk8ckr30ezc" + ] + } + }, + { + "value": 6.30257669, + "n": 2, + "scriptPubKey": { + "asm": "0 9bc809698674ec7c01d35d438e9d0de1aa87b6c8", + "hex": "00149bc809698674ec7c01d35d438e9d0de1aa87b6c8", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qn0yqj6vxwnk8cqwnt4pca8gdux4g0dkghc9rur" + ] + } + } + ], + "hex": "0100000001274c7174e814eb731712b80187660f7a6ab2949a3271ef80037685cdde2d78c3020000006b483045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c247090000000000160014007128282856f8e8f3c75909f9f1474b55cb1f1605f89025000000001600149bc809698674ec7c01d35d438e9d0de1aa87b6c800000000" + }, + { + "txid": "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", + "hash": "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", + "version": 1, + "size": 301, + "vsize": 301, + "weight": 1204, + "locktime": 138, + "vin": [ + { + "txid": "91435b1e8a138fce39acdb6efbc6187e76ad10db84baf9ed4c987af48cccfa14", + "vout": 0, + "scriptSig": { + "asm": "0 304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b3576[ALL] 3045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e[ALL] 52210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152ae", + "hex": "0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152ae" + }, + "sequence": 4294967294 + } + ], + "vout": [ + { + "value": 0.0219, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + } + ], + "hex": "010000000114facc8cf47a984cedf9ba84db10ad767e18c6fb6edbac39ce8f138a1e5b439100000000da0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152aefeffffff01b06a21000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c9878a000000" + }, + { + "txid": "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", + "hash": "a1cb920222f6470dae2b90af42ec4da2e541bc4de9cec11441ca2dd0032bca2c", + "version": 1, + "size": 254, + "vsize": 173, + "weight": 689, + "locktime": 0, + "vin": [ + { + "txid": "e941f2963a9b34caba2663a936b5f029ebd51bb306c4ff2ba0674549880dba33", + "vout": 2, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "304402200d4f214756753861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac650047193001", + "02772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c32" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.01605126, + "n": 1, + "scriptPubKey": { + "asm": "0 f6da24d081a1b63dcacf4a5762a8ed91fd472c68", + "hex": "0014f6da24d081a1b63dcacf4a5762a8ed91fd472c68", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1q7mdzf5yp5xmrmjk0fftk928dj875wtrgkwqr5x" + ] + } + }, + { + "value": 0.12153947, + "n": 2, + "scriptPubKey": { + "asm": "0 4755561caa18d651bf59912545764811d0ab96f6", + "hex": "00144755561caa18d651bf59912545764811d0ab96f6", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qga24v892rrt9r06ejyj52ajgz8g2h9hkwquzg4" + ] + } + } + ], + "hex": "0100000000010133ba0d88494567a02bffc406b31bd5eb29f0b536a96326baca349b3a96f241e90200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987067e180000000000160014f6da24d081a1b63dcacf4a5762a8ed91fd472c685b74b900000000001600144755561caa18d651bf59912545764811d0ab96f60247304402200d4f214756753861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac6500471930012102772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c3200000000" + }, + { + "txid": "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", + "hash": "16e1ae45d2c6ddaaefeed97a822a95a2933096bf377db147a63d9b730cc30d20", + "version": 1, + "size": 254, + "vsize": 173, + "weight": 689, + "locktime": 0, + "vin": [ + { + "txid": "183628048e3b7767e93116c39edc6191311ff3e4666cef10dbda038854c46ee0", + "vout": 2, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f3dc900a4d0ab5cc3c97d5be85f1801", + "0255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.0060466, + "n": 1, + "scriptPubKey": { + "asm": "0 5230c895305a232ef2a4feb0a91e7d99e22fd515", + "hex": "00145230c895305a232ef2a4feb0a91e7d99e22fd515", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1q2gcv39fstg3jau4yl6c2j8nan83zl4g4w5ch6j" + ] + } + }, + { + "value": 0.13765586, + "n": 2, + "scriptPubKey": { + "asm": "0 b6fbbd9053e47891fae7f3db7dd3966062b2513c", + "hex": "0014b6fbbd9053e47891fae7f3db7dd3966062b2513c", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qkmammyznu3ufr7h870dhm5ukvp3ty5fudkl63e" + ] + } + } + ], + "hex": "01000000000101e06ec4548803dadb10ef6c66e4f31f319161dc9ec31631e967773b8e042836180200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987f4390900000000001600145230c895305a232ef2a4feb0a91e7d99e22fd515d20bd20000000000160014b6fbbd9053e47891fae7f3db7dd3966062b2513c0247304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f3dc900a4d0ab5cc3c97d5be85f1801210255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b00000000" + }, + { + "txid": "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", + "hash": "bae1180680abb989f5b89b5f49729d3eedcf757a12fd0b7649c944a4ba37eaee", + "version": 1, + "size": 255, + "vsize": 173, + "weight": 690, + "locktime": 0, + "vin": [ + { + "txid": "4e2146dbaf82ae1d756e6e61df42cbc6db3409d896ecd80894ceb0d3321c704c", + "vout": 2, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "3045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e01", + "02e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672a69cddc6" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.006, + "n": 1, + "scriptPubKey": { + "asm": "0 89c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a", + "hex": "001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1q38rehsrz35kck8xershdpe6akylx70e63azh0p" + ] + } + }, + { + "value": 1.11150986, + "n": 2, + "scriptPubKey": { + "asm": "0 062d20a6692350b7a39397c50857a7f725788da0", + "hex": "0014062d20a6692350b7a39397c50857a7f725788da0", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qqckjpfnfydgt0gunjlzss4a87ujh3rdq7u4yrr" + ] + } + } + ], + "hex": "010000000001014c701c32d3b0ce9408d8ec96d80934dbc6cb42df616e6e751dae82afdb46214e0200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a8a07a00600000000160014062d20a6692350b7a39397c50857a7f725788da002483045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e012102e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672a69cddc600000000" + }, + { + "txid": "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", + "hash": "f072bb4fec0ae128ed678d24475cf719c0dd3ba0b44bf14f7300df60408ee365", + "version": 1, + "size": 255, + "vsize": 173, + "weight": 690, + "locktime": 0, + "vin": [ + { + "txid": "376a560724612cfc3249e8f93f1a4156fc1b62678716290cbd326e16b509f6c3", + "vout": 2, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "30450221008f8eee212f209ba2a179197bd4ba35a35ad7a304599025ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b01", + "03f870bcd3a46e80e4b1236302e62318b412cc97ef096fc976a89deb569dc11ef1" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.016, + "n": 1, + "scriptPubKey": { + "asm": "0 1437b91493d1929b1b42a80e83229c347c28f937", + "hex": "00141437b91493d1929b1b42a80e83229c347c28f937", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qzsmmj9yn6xffkx6z4q8gxg5ux37z37fhr598r9" + ] + } + }, + { + "value": 1.10145514, + "n": 2, + "scriptPubKey": { + "asm": "0 91ad2cce99e8e4455de5f44559816b98213f3503", + "hex": "001491ad2cce99e8e4455de5f44559816b98213f3503", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qjxkjen5earjy2h0973z4nqttnqsn7dgrnsgy27" + ] + } + } + ], + "hex": "01000000000101c3f609b5166e32bd0c29168767621bfc56411a3ff9e84932fc2c612407566a370200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141437b91493d1929b1b42a80e83229c347c28f937eaaf90060000000016001491ad2cce99e8e4455de5f44559816b98213f3503024830450221008f8eee212f209ba2a179197bd4ba35a35ad7a304599025ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b012103f870bcd3a46e80e4b1236302e62318b412cc97ef096fc976a89deb569dc11ef100000000" + }, + { + "txid": "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", + "hash": "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", + "version": 1, + "size": 252, + "vsize": 252, + "weight": 1008, + "locktime": 0, + "vin": [ + { + "txid": "20bd92c145d9d724283c9536355a82e6fd87637963dd7e0c1fb87c9de50a4fba", + "vout": 2, + "scriptSig": { + "asm": "3045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a534[ALL] 039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60", + "hex": "483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.006, + "n": 1, + "scriptPubKey": { + "asm": "0 13afc3a26c010dddaf2410a9d97b5054a4e8d309", + "hex": "001413afc3a26c010dddaf2410a9d97b5054a4e8d309", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qzwhu8gnvqyxamteyzz5aj76s2jjw35cfctjvy8" + ] + } + }, + { + "value": 9.45414148, + "n": 2, + "scriptPubKey": { + "asm": "0 5228ee46a95383b314396dda75e707b8bed83034", + "hex": "00145228ee46a95383b314396dda75e707b8bed83034", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1q2g5wu34f2wpmx9pedhd8tec8hzldsvp56hljn8" + ] + } + } + ], + "hex": "0100000001ba4f0ae59d7cb81f0c7edd63796387fde6825a3536953c2824d7d945c192bd20020000006b483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001413afc3a26c010dddaf2410a9d97b5054a4e8d30904e05938000000001600145228ee46a95383b314396dda75e707b8bed8303400000000" + }, + { + "txid": "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", + "hash": "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", + "version": 1, + "size": 251, + "vsize": 251, + "weight": 1004, + "locktime": 0, + "vin": [ + { + "txid": "7ac20c807aac72bc1cc57ef25f61567f74a031952b0a18618bc06be504dc715d", + "vout": 2, + "scriptSig": { + "asm": "3044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc540152[ALL] 0374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5", + "hex": "473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 5.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", + "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" + ] + } + }, + { + "value": 0.016, + "n": 1, + "scriptPubKey": { + "asm": "0 1c397ba7ea8410dbd556c51f8184a14ab0151143", + "hex": "00141c397ba7ea8410dbd556c51f8184a14ab0151143", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qrsuhhfl2ssgdh42kc50crp9pf2cp2y2ruplr8l" + ] + } + }, + { + "value": 1.1126122, + "n": 2, + "scriptPubKey": { + "asm": "0 c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe", + "hex": "0014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qcwxrmzgvg90jcylkjfwp45w55l957l07mtsydu" + ] + } + } + ], + "hex": "01000000015d71dc04e56bc08b61180a2b9531a0747f56615ff27ec51cbc72ac7a800cc27a020000006a473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141c397ba7ea8410dbd556c51f8184a14ab015114324b6a10600000000160014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe00000000" + }, + { + "txid": "719606705c6832f7180ab9db5e1ce6a51bad80a5cbf3b57b806dd10f3d7d5124", + "hash": "c670f473daee2ad2fabc2b8cc9e99d499c19ef9f2d3331345674a0d3b1b96639", + "version": 1, + "size": 425, + "vsize": 263, + "weight": 1052, + "locktime": 0, + "vin": [ + { + "txid": "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", + "vout": 1, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "30440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f01", + "03c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b7646635" + ], + "sequence": 4294967295 + }, + { + "txid": "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", + "vout": 1, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616386b86b419197b360c871fca01", + "02c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab28" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 1.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "0 223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f", + "hex": "0020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f", + "reqSigs": 1, + "type": "witness_v0_scripthash", + "addresses": [ + "bcrt1qyg7e0qrnsqhhnehvm36erewuru82wqcdv3n0w0rtjqu3h3ew3phs27rfy6" + ] + } + }, + { + "value": 0.0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN 758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454", + "hex": "6a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454", + "type": "nulldata" + } + }, + { + "value": 0.02201563, + "n": 2, + "scriptPubKey": { + "asm": "0 5e41d2fb8de1c250410416d8dd153c685d3f9c7b", + "hex": "00145e41d2fb8de1c250410416d8dd153c685d3f9c7b", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qteqa97udu8p9qsgyzmvd69fudpwnl8rmt8putj" + ] + } + } + ], + "hex": "01000000000102142a200a7460adf754232337f48bf7c38ba411eed0cd2f98900ffd9f2adc1ed20100000000ffffffff99d67c2a69b04831bd229ea6022c91c68999c38d0ba92211b56f15c5f8222bf50100000000ffffffff03e803000000000000220020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f0000000000000000226a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454db972100000000001600145e41d2fb8de1c250410416d8dd153c685d3f9c7b024730440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f012103c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b76466350247304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616386b86b419197b360c871fca012102c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab2800000000" + }, + { + "txid": "16cd3283068d2965f26045e15a285e39a762af32ec388ae8c28fb6cb2c468768", + "hash": "a854736ce90b603599bf21be28bea909c3ffafaed57d5eacea1f1730b68db0d8", + "version": 1, + "size": 427, + "vsize": 264, + "weight": 1054, + "locktime": 0, + "vin": [ + { + "txid": "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", + "vout": 1, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "3045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada54724301", + "02016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745e650f82bd123" + ], + "sequence": 4294967295 + }, + { + "txid": "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", + "vout": 1, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d13945e012d0031246d4574e92f97e3701ca729b61401", + "029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b031" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 1.0E-5, + "n": 0, + "scriptPubKey": { + "asm": "0 e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c7231", + "hex": "0020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c7231", + "reqSigs": 1, + "type": "witness_v0_scripthash", + "addresses": [ + "bcrt1qu05pq3hajevm2ujhxmh6gp9urj8ektlk7zhhed7a4nwvv8suwgcstjh7k7" + ] + } + }, + { + "value": 0.0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730", + "hex": "6a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730", + "type": "nulldata" + } + }, + { + "value": 0.0220133, + "n": 2, + "scriptPubKey": { + "asm": "0 5e41d2fb8de1c250410416d8dd153c685d3f9c7b", + "hex": "00145e41d2fb8de1c250410416d8dd153c685d3f9c7b", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bcrt1qteqa97udu8p9qsgyzmvd69fudpwnl8rmt8putj" + ] + } + } + ], + "hex": "01000000000102913a5817e0b3bddb0bc6869e12200b5115be11522cf76125229d47e184da48460100000000ffffffff684d66517d21106d1bcbba96964e31a532fec33c098cd621e2500c702340b6780100000000ffffffff03e803000000000000220020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c72310000000000000000226a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730f2962100000000001600145e41d2fb8de1c250410416d8dd153c685d3f9c7b02483045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada547243012102016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745e650f82bd1230248304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d13945e012d0031246d4574e92f97e3701ca729b6140121029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b03100000000" + } + ], + "time": 1605510591, + "mediantime": 1589548514, + "nonce": 1, + "bits": "207fffff", + "difficulty": 4.656542373906925E-10, + "chainwork": "0000000000000000000000000000000000000000000000000000000000000118", + "nTx": 11, + "previousblockhash": "1a477356be8211dd4b3a4162521331a6af9b5999401aad8d147a56a1f28c3da3", + "nextblockhash": "7d8267341a57f1f626b450eb22b2dbf208f13ec176e1cc015aa4d2b2ea55016d" +} diff --git a/core/src/test/resources/bisq/core/dao/node/full/rpc/getnetworkinfo-result.json b/core/src/test/resources/bisq/core/dao/node/full/rpc/getnetworkinfo-result.json new file mode 100644 index 0000000000..08e347328f --- /dev/null +++ b/core/src/test/resources/bisq/core/dao/node/full/rpc/getnetworkinfo-result.json @@ -0,0 +1,52 @@ +{ + "version": 210000, + "subversion": "/Satoshi:0.21.0/", + "protocolversion": 70016, + "localservices": "000000000100040d", + "localservicesnames": [ + "NETWORK", + "BLOOM", + "WITNESS", + "NETWORK_LIMITED", + "MY_CUSTOM_SERVICE" + ], + "localrelay": true, + "timeoffset": -2, + "networkactive": true, + "connections": 9, + "connections_in": 0, + "connections_out": 9, + "networks": [ + { + "name": "ipv4", + "limited": false, + "reachable": true, + "proxy": "", + "proxy_randomize_credentials": false + }, + { + "name": "ipv6", + "limited": false, + "reachable": true, + "proxy": "", + "proxy_randomize_credentials": false + }, + { + "name": "onion", + "limited": true, + "reachable": false, + "proxy": "", + "proxy_randomize_credentials": false + } + ], + "relayfee": 1.0E-5, + "incrementalfee": 1.0E-5, + "localaddresses": [ + { + "address": "2001:0:2851:782c:2c65:3676:fde6:18f8", + "port": 8333, + "score": 26 + } + ], + "warnings": "" +} diff --git a/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json new file mode 100644 index 0000000000..74e094fd09 --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json @@ -0,0 +1,10 @@ +{ + "37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da": "hUWPf,37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da,19910000,200,0,668994, tx_missing_from_blockchain_for_4_days", + "b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b": "ebdttmzh,b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b,4000000,5000,1,669137, tx_missing_from_blockchain_for_2_days", + "10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de": "kmbyoexc,10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de,33000000,332,0,668954, tx_not_found", + "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "nlaIlAvE,cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188,5000000,546,1,669262, invalid_missing_fee_address", + "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "feescammer,fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74,1000000,546,1,669442, invalid_missing_fee_address", + "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "PBFICEAS,72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813,2000000,546,1,672969, dust_fee_scammer", + "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "feescammer,1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e,2000000,546,1,669227, dust_fee_scammer", + "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "feescammer,17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96,2000000,546,1,669340, dust_fee_scammer" +} diff --git a/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json b/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json new file mode 100644 index 0000000000..7b5b315529 --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json @@ -0,0 +1,22 @@ +{ + "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195, underpaid but accepted due to use of different DAO parameter", + "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "FQ0A7G,ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e,15970000,92,0,640438, underpaid but accepted due to use of different DAO parameter", + "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "046698,051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b,15970000,92,0,667927, bsq fee underpaid using 5.75 rate for some weird reason", + "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "7213472,e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9,200000000,200000,1,578672, unknown_fee_receiver_1PUXU1MQ", + "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "aAPLmh98,44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18,140000000,140000,1,578629, unknown_fee_receiver_1PUXU1MQ", + "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "pwdbdku,654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d,24980000,238000,1,554947, unknown_fee_receiver_18GzH11", + "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390", + "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "89284,2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1,900000,9,0,666473", + "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "EHGVHSL,a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba,1000000,5000,1,665825", + "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "M2CNGNN,ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e,600000,6,0,669043", + "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "qHBsg,cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348,25840000,258,0,611324", + "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "87822,aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25,1000000,10,0,668839", + "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "9134295,9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6,30000000,30000,1,666606", + "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "5D4EQC,768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d,10000000,101,0,668001", + "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "23608,9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523,5000000,5000,1,668593", + "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "I3WzjuF,02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592,1000000,10,0,666563", + "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "WlvThoI,995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8,1000000,5000,1,669231", + "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "ffhpgz0z,ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d,2000000,20,0,667351", + "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "jgtwzsn,b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e,1000000,10,0,666372", + "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "AZhkSO,dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7,200000000,200000,1,668526" +} diff --git a/core/src/test/resources/bisq/core/provider/mempool/txInfo.json b/core/src/test/resources/bisq/core/provider/mempool/txInfo.json new file mode 100644 index 0000000000..9c72bbc177 --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/txInfo.json @@ -0,0 +1,27 @@ +{ + "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "{\"txid\":\"44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":147186800}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":140000},{\"scriptpubkey_address\":\"1HwN7DhxNQdFKzMbrQq5vRHzY4xXGTRcne\",\"value\":147000000}],\"size\":226,\"weight\":904,\"fee\":46800,\"status\":{\"confirmed\":true,\"block_height\":578630}}", + "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "{\"txid\":\"2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1393}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":10872788}}],\"vout\":[{\"scriptpubkey_address\":\"1NsTgbTUKhveanGCmsawJKLf6asQhJP4p2\",\"value\":1384},{\"scriptpubkey_address\":\"bc1qlw44hxyqfwcmcuuvtktduhth5ah4djl63sc4eq\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1qyty4urzh25j5qypqu7v9mzhwt3p0zvaxeehpxp\",\"value\":9967337}],\"size\":552,\"weight\":1557,\"fee\":5460,\"status\":{\"confirmed\":true,\"block_height\":666479}}", + "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}", + "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "{\"txid\":\"a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":798055}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qy69ekanm2twzqqr7vz9qcxypyta29wdm2t0ay8\",\"value\":600000},{\"scriptpubkey_address\":\"bc1qp6q2urrntp8tq67lhymftsq0dpqvqmpnus7hym\",\"value\":184830}],\"size\":254,\"weight\":689,\"fee\":8225,\"status\":{\"confirmed\":true,\"block_height\":665826}}", + "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "{\"txid\":\"ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4165}},{\"vout\":2,\"prevout\":{\"value\":593157}},{\"vout\":2,\"prevout\":{\"value\":595850}}],\"vout\":[{\"scriptpubkey_address\":\"16Y1WqYEbWygHz6kuhJxWXos3bw46JNNoZ\",\"value\":4159},{\"scriptpubkey_address\":\"bc1qkxjvjp2hyegjpw5jtlju7fcr7pv9en3u7cg7q7\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q9x95y8ktsxg9jucky66da3v2s2har56cy3nkkg\",\"value\":575363}],\"size\":555,\"weight\":1563,\"fee\":13650,\"status\":{\"confirmed\":true,\"block_height\":669045}}", + "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "{\"txid\":\"cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4898}},{\"vout\":2,\"prevout\":{\"value\":15977562}}],\"vout\":[{\"scriptpubkey_address\":\"16SCUfnCLddxgoAYLUcsJcoE4VxBRzgTSz\",\"value\":4640},{\"scriptpubkey_address\":\"1N9Pb6DTJXh96QjzYLDFTZuBvFXgFPi18N\",\"value\":1292000},{\"scriptpubkey_address\":\"1C7tg4KT9wQvLR5xfPDqD4U35Ncwk3UQxm\",\"value\":14681720}],\"size\":406,\"weight\":1624,\"fee\":4100,\"status\":{\"confirmed\":true,\"block_height\":611325}}", + "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "{\"txid\":\"aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":3512}},{\"vout\":2,\"prevout\":{\"value\":1481349}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"14rNP2aC23hr6u8ALmksm3RgJys7CAD3No\",\"value\":3502},{\"scriptpubkey_address\":\"bc1qvctcjcrhznptmydv4hxwc4wd2km76shkl3jj29\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qsdzpvr6sehypswcwjsmmjzctjhy5hkwqvf2vh8\",\"value\":476289}],\"size\":555,\"weight\":1563,\"fee\":5070,\"status\":{\"confirmed\":true,\"block_height\":668841}}", + "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "{\"txid\":\"9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4533500}}],\"vout\":[{\"scriptpubkey_address\":\"1EKXx73oUhHaUh8JBimtiPGgHfwNmxYKAj\",\"value\":30000},{\"scriptpubkey_address\":\"bc1qde7asrvrnkn5st5q8u038fxt9tlrgyaxwju6hn\",\"value\":4500000}],\"size\":226,\"weight\":574,\"fee\":3500,\"status\":{\"confirmed\":true,\"block_height\":666607}}", + "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "{\"txid\":\"768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":249699}},{\"vout\":1,\"prevout\":{\"value\":4023781}}],\"vout\":[{\"scriptpubkey_address\":\"1J8wtmJuurfBSrRb27urtoHzuQey1ipXPX\",\"value\":249598},{\"scriptpubkey_address\":\"bc1qfmyw7pwaqucprcsauqr6gvez9wep290r4amd3y\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1q2lx0fymd3mmk4pzjq2k8hn7mk3hnctnjtu497t\",\"value\":2517382}],\"size\":405,\"weight\":1287,\"fee\":6500,\"status\":{\"confirmed\":true,\"block_height\":668002}}", + "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "{\"txid\":\"02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":33860}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":39304}},{\"vout\":3,\"prevout\":{\"value\":5000000}}],\"vout\":[{\"scriptpubkey_address\":\"1Le1auzXSpEnyMc6S9KNentnye3gTPLnuA\",\"value\":33850},{\"scriptpubkey_address\":\"bc1qs73jfmjzclsx9466pvpslfuqc2kkv5uc8u928a\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q85zlv50mddyuerze7heve0vcv4f80qsw2szv34\",\"value\":4031088}],\"size\":701,\"weight\":1829,\"fee\":8226,\"status\":{\"confirmed\":true,\"block_height\":666564}}", + "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "{\"txid\":\"9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":4466600}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qrl0dvwp6hpqlcj65qfhl70lz67yjvhlc8z73a4\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qg55gnkhgg4zltdh76sdef33xzr7h95g3xsxesg\",\"value\":3709675}],\"size\":254,\"weight\":689,\"fee\":1925,\"status\":{\"confirmed\":true,\"block_height\":668843}}", + "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "{\"txid\":\"995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":36006164}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qefxyxsq9tskaw0qarxf0hdxusxe64l8zsmsgrz\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q6fky0fxcg3zrz5t0xdyq5sh90h7m5sya0wf9gx\",\"value\":34390664}],\"size\":256,\"weight\":697,\"fee\":10500,\"status\":{\"confirmed\":true,\"block_height\":669233}}", + "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "{\"txid\":\"fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":49144}},{\"vout\":0,\"prevout\":{\"value\":750000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qgsx9y62ajme3gg8v9n9jfps2694uy9r6f9unj0\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q6lqf0jehmaadwmdhap98rulflft27z00g0qphn\",\"value\":187144}],\"size\":372,\"weight\":834,\"fee\":12000,\"status\":{\"confirmed\":true,\"block_height\":669442}}", + "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "{\"txid\":\"ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":10237}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":900000}},{\"vout\":2,\"prevout\":{\"value\":8858290}}],\"vout\":[{\"scriptpubkey_address\":\"1DQXP1AXR1qkXficdkfXHHy2JkbtRGFQ1b\",\"value\":10217},{\"scriptpubkey_address\":\"bc1q9jfjvhvr42smvwylrlqcrefcdxagdzf52aquzm\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1qc6qraj5h8qxvluh2um4rvunqn68fltc9kjfrk9\",\"value\":7753730}],\"size\":702,\"weight\":1833,\"fee\":4580,\"status\":{\"confirmed\":true,\"block_height\":667352}}", + "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}", + "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "{\"txid\":\"b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1432}},{\"vout\":2,\"prevout\":{\"value\":30302311}}],\"vout\":[{\"scriptpubkey_address\":\"16MK64AGvKVF7xu9Xfjh8o7Xo4e1HMhUqq\",\"value\":1422},{\"scriptpubkey_address\":\"bc1qjp535w2zl3cxg02xgdx8yewtvn6twcnj86t73c\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qa58rfr0wumczmau0qehjwcsdkcgs5dmkg7url5\",\"value\":28698421}],\"size\":405,\"weight\":1287,\"fee\":3900,\"status\":{\"confirmed\":true,\"block_height\":666373}}", + "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "{\"txid\":\"dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":230000000}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":200000},{\"scriptpubkey_address\":\"bc1qaq3v7gjqaeyx7yzkcu59l8f47apkfutr927xa8\",\"value\":30000000},{\"scriptpubkey_address\":\"bc1qx9avgdnkal2jfcfjqdsdu7ly60awl4wcfgk6m0\",\"value\":199793875}],\"size\":255,\"weight\":690,\"fee\":6125,\"status\":{\"confirmed\":true,\"block_height\":668527}}", + "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "{\"txid\":\"ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":61000}},{\"vout\":0,\"prevout\":{\"value\":6415500}}],\"vout\":[{\"scriptpubkey_address\":\"164hNDe95nNsQYVSVbeypn36HqT5uD5AoT\",\"value\":60908},{\"scriptpubkey_address\":\"1MEsN2jLyrcWBMjggSPs88xAnj6D38sQL3\",\"value\":2395500},{\"scriptpubkey_address\":\"1A3pYPW1zQcMpHUnSfPCxYWgCrUW93t2yV\",\"value\":3973352}],\"size\":408,\"weight\":1632,\"fee\":46740,\"status\":{\"confirmed\":true,\"block_height\":640441}}", + "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "{\"txid\":\"654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":26000000}}],\"vout\":[{\"scriptpubkey_address\":\"18GzH11T5h2fpvUoBJDub7MgNJVw3FfqQ8\",\"value\":238000},{\"scriptpubkey_address\":\"1JYZ4cba5pjPxXqm5MDGUVvj2k3cZezRaR\",\"value\":3000000},{\"scriptpubkey_address\":\"12DNP86oaEXfEBkow4Kpkw2tNaqoECYhtc\",\"value\":22756800}],\"size\":260,\"weight\":1040,\"fee\":5200,\"status\":{\"confirmed\":true,\"block_height\":554950}}", + "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "{\"txid\":\"1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":563209}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":214153}},{\"vout\":2,\"prevout\":{\"value\":116517}},{\"vout\":2,\"prevout\":{\"value\":135306}},{\"vout\":2,\"prevout\":{\"value\":261906}},{\"vout\":2,\"prevout\":{\"value\":598038}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600932}},{\"vout\":1,\"prevout\":{\"value\":600944}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":546},{\"scriptpubkey_address\":\"bc1qwcwu3mx0nmf290y8t0jlukhxujaul0fc4jxe44\",\"value\":4743164},{\"scriptpubkey_address\":\"bc1qduw8nd2sscyezk02xj3a3ks5adh8wmctqaew6g\",\"value\":145131}],\"size\":1746,\"weight\":3417,\"fee\":2164,\"status\":{\"confirmed\":true,\"block_height\":669227}}", + "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "{\"txid\":\"e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":4250960}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":200000},{\"scriptpubkey_address\":\"1MSkjSzF1dTKR121scX64Brvs4zhExVE8Q\",\"value\":4000000}],\"size\":225,\"weight\":900,\"fee\":50960,\"status\":{\"confirmed\":true,\"block_height\":578733}}", + "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "{\"txid\":\"051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23985}},{\"vout\":1,\"prevout\":{\"value\":6271500}},{\"vout\":0,\"prevout\":{\"value\":2397000}},{\"vout\":2,\"prevout\":{\"value\":41281331}}],\"vout\":[{\"scriptpubkey_address\":\"16pULNutwpJ5E6EaxopQQDAVaFJXt8B18Z\",\"value\":23893},{\"scriptpubkey_address\":\"bc1q6hkhftt9v5kkcj9wr66ycqy23dqyle3h3wnv50\",\"value\":18365500},{\"scriptpubkey_address\":\"bc1q3ffqm4e4wxdg8jgcw0wlpw4vg9hgwnql3y9zn0\",\"value\":31546169}],\"size\":703,\"weight\":2476,\"fee\":38254,\"status\":{\"confirmed\":true,\"block_height\":667928}}", + "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "{\"txid\":\"72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":12000000}}],\"vout\":[{\"scriptpubkey_address\":\"3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU\",\"value\":546},{\"scriptpubkey_address\":\"bc1q6xthjqca0p83mua54e9t0sapxkvc7n3dvwssxc\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1q3uaew9e6uqm6pth8nq7wh3wcwzxwh2q25fggcg\",\"value\":9388079}],\"size\":254,\"weight\":689,\"fee\":11375,\"status\":{\"confirmed\":true,\"block_height\":672972}}", + "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "{\"txid\":\"17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":2810563}}],\"vout\":[{\"scriptpubkey_address\":\"13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR\",\"value\":546},{\"scriptpubkey_address\":\"bc1qklv4zsl598ujy2ntl5g3wqjxasu2f74egw0tlm\",\"value\":1603262},{\"scriptpubkey_address\":\"bc1qclesyfupj309620thesxmj4vcdscjfykdqz4np\",\"value\":1205124}],\"size\":256,\"weight\":697,\"fee\":1631,\"status\":{\"confirmed\":true,\"block_height\":669340}}", + "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "{\"txid\":\"cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":200584}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1q7sd0k2a6p942848y5nsk9cqwdguhd7c04t2t3w\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qrcez45uf02sg6zvk3mqmtlc9vnrvn50jcywlk5\",\"value\":49144}],\"size\":371,\"weight\":833,\"fee\":1440,\"status\":{\"confirmed\":true,\"block_height\":669442}}" +} \ No newline at end of file diff --git a/core/src/test/resources/mainnet.seednodes b/core/src/test/resources/mainnet.seednodes new file mode 100644 index 0000000000..5a4b52ad07 --- /dev/null +++ b/core/src/test/resources/mainnet.seednodes @@ -0,0 +1,9 @@ +# nodeaddress.onion:port [(@owner,@backup)] +5quyxpxheyvzmb2d.onion:8000 (@miker) +s67qglwhkgkyvr74.onion:8000 (@emzy) +ef5qnzx6znifo3df.onion:8000 (@alexej996) +jhgcy2won7xnslrb.onion:8000 (@wiz,@nicolasdorier) +3f3cu2yw7u457ztq.onion:8000 (@devinbileck,@ripcurlx) +723ljisnynbtdohi.onion:8000 (@emzy) +rm7b56wbrcczpjvl.onion:8000 (@miker) +fl3mmribyxgrv63c.onion:8000 (@devinbileck,@ripcurlx) diff --git a/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..9cd68a02bd --- /dev/null +++ b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline # enable mocking final classes in mockito diff --git a/core/src/test/resources/regtest.seednodes b/core/src/test/resources/regtest.seednodes new file mode 100644 index 0000000000..58f66ba9cc --- /dev/null +++ b/core/src/test/resources/regtest.seednodes @@ -0,0 +1,10 @@ +# For development you need to change that to your local onion addresses +# 1. Run a seed node with prog args: --bitcoinNetwork=regtest --nodePort=8002 --appName=bisq_seed_node_rxdkppp3vicnbgqt.onion_8002 +# 2. Find your local onion address in bisq_seed_node_rxdkppp3vicnbgqt.onion_8002/regtest/tor/hiddenservice/hostname +# 3. Shut down the seed node +# 4. Rename the directory with your local onion address +# 5. Edit here your found onion address (new NodeAddress("YOUR_ONION.onion:8002") + +# nodeaddress.onion:port [(@owner)] +rxdkppp3vicnbgqt.onion:8002 +4ie52dse64kaarxw.onion:8002 diff --git a/core/src/test/resources/testnet.seednodes b/core/src/test/resources/testnet.seednodes new file mode 100644 index 0000000000..77b6af07e6 --- /dev/null +++ b/core/src/test/resources/testnet.seednodes @@ -0,0 +1,7 @@ +# nodeaddress.onion:port [(@owner)] +snenz4mea65wigen.onion:8001 +fjr5w4eckjghqtnu.onion:8001 +3d56s6acbi3vk52v.onion:8001 +74w2sttlo4qk6go3.onion:8001 +gtif46mfxirv533z.onion:8001 +jmc5ajqvtnzqaggm.onion:8001 diff --git a/core/update_translations.sh b/core/update_translations.sh new file mode 100755 index 0000000000..0ea9070bdb --- /dev/null +++ b/core/update_translations.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +cd $(dirname $0) +tx pull -l de,es,ja,pt,ru,zh_CN,zh_TW,vi,th_TH,fa,fr,pt_BR,it,cs + +translations="translations/bisq-desktop.displaystringsproperties" +i18n="src/main/resources/i18n" + +mv "$translations/de.properties" "$i18n/displayStrings_de.properties" +mv "$translations/es.properties" "$i18n/displayStrings_es.properties" +mv "$translations/ja.properties" "$i18n/displayStrings_ja.properties" +mv "$translations/pt.properties" "$i18n/displayStrings_pt.properties" +mv "$translations/ru.properties" "$i18n/displayStrings_ru.properties" +mv "$translations/zh_CN.properties" "$i18n/displayStrings_zh-hans.properties" +mv "$translations/zh_TW.properties" "$i18n/displayStrings_zh-hant.properties" +mv "$translations/vi.properties" "$i18n/displayStrings_vi.properties" +mv "$translations/th_TH.properties" "$i18n/displayStrings_th.properties" +mv "$translations/fa.properties" "$i18n/displayStrings_fa.properties" +mv "$translations/fr.properties" "$i18n/displayStrings_fr.properties" +mv "$translations/pt_BR.properties" "$i18n/displayStrings_pt-br.properties" +mv "$translations/it.properties" "$i18n/displayStrings_it.properties" +mv "$translations/cs.properties" "$i18n/displayStrings_cs.properties" + +rm -rf $translations diff --git a/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java b/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java new file mode 100644 index 0000000000..f813f84d3e --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java @@ -0,0 +1,23 @@ +/* + * 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 bisq.daemon.app; + +import bisq.core.app.BisqHeadlessApp; + +public class BisqDaemon extends BisqHeadlessApp { +} diff --git a/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java new file mode 100644 index 0000000000..a3bd88e1a0 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java @@ -0,0 +1,115 @@ +/* + * 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 bisq.daemon.app; + +import bisq.core.app.BisqHeadlessAppMain; +import bisq.core.app.BisqSetup; +import bisq.core.app.CoreModule; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.handlers.ResultHandler; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.daemon.grpc.GrpcServer; + +@Slf4j +public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.BisqSetupListener { + + private GrpcServer grpcServer; + + public static void main(String[] args) { + new BisqDaemonMain().execute(args); + } + + ///////////////////////////////////////////////////////////////////////////////////// + // First synchronous execution tasks + ///////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void configUserThread() { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(this.getClass().getSimpleName()) + .setDaemon(true) + .build(); + UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); + } + + @Override + protected void launchApplication() { + headlessApp = new BisqDaemon(); + + UserThread.execute(this::onApplicationLaunched); + } + + @Override + protected void onApplicationLaunched() { + super.onApplicationLaunched(); + headlessApp.setGracefulShutDownHandler(this); + } + + + ///////////////////////////////////////////////////////////////////////////////////// + // We continue with a series of synchronous execution tasks + ///////////////////////////////////////////////////////////////////////////////////// + + @Override + protected AppModule getModule() { + return new CoreModule(config); + } + + @Override + protected void applyInjector() { + super.applyInjector(); + + headlessApp.setInjector(injector); + } + + @Override + protected void startApplication() { + // We need to be in user thread! We mapped at launchApplication already... + headlessApp.startApplication(); + + // In headless mode we don't have an async behaviour so we trigger the setup by + // calling onApplicationStarted. + onApplicationStarted(); + } + + @Override + protected void onApplicationStarted() { + super.onApplicationStarted(); + + grpcServer = injector.getInstance(GrpcServer.class); + grpcServer.start(); + } + + @Override + public void gracefulShutDown(ResultHandler resultHandler) { + super.gracefulShutDown(resultHandler); + + grpcServer.shutdown(); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java new file mode 100644 index 0000000000..3fae432993 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java @@ -0,0 +1,69 @@ +package bisq.daemon.grpc; + +import bisq.core.api.CoreApi; + +import bisq.proto.grpc.RegisterDisputeAgentReply; +import bisq.proto.grpc.RegisterDisputeAgentRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.DisputeAgentsGrpc.DisputeAgentsImplBase; +import static bisq.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcDisputeAgentsService extends DisputeAgentsImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcDisputeAgentsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void registerDisputeAgent(RegisterDisputeAgentRequest req, + StreamObserver responseObserver) { + try { + coreApi.registerDisputeAgent(req.getDisputeAgentType(), req.getRegistrationKey()); + var reply = RegisterDisputeAgentReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + // Do not limit devs' ability to test agent registration + // and call validation in regtest arbitration daemons. + put(getRegisterDisputeAgentMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java new file mode 100644 index 0000000000..4c139e1709 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java @@ -0,0 +1,143 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.common.handlers.ErrorMessageHandler; + +import bisq.proto.grpc.AvailabilityResultWithDescription; +import bisq.proto.grpc.TakeOfferReply; + +import protobuf.AvailabilityResult; + +import io.grpc.stub.StreamObserver; + +import org.slf4j.Logger; + +import lombok.Getter; + +import static bisq.proto.grpc.TradesGrpc.getTakeOfferMethod; +import static java.lang.String.format; +import static java.util.Arrays.stream; + +/** + * An implementation of bisq.common.handlers.ErrorMessageHandler that avoids + * an exception loop with the UI's bisq.common.taskrunner framework. + * + * The legacy ErrorMessageHandler is for reporting error messages only to the UI, but + * some core api tasks (takeoffer) require one. This implementation works around + * the problem of Task ErrorMessageHandlers not throwing exceptions to the gRPC client. + * + * Extra care is needed because exceptions thrown by an ErrorMessageHandler inside + * a Task may be thrown back to the GrpcService object, and if a gRPC ErrorMessageHandler + * responded by throwing another exception, the loop may only stop after the gRPC + * stream is closed. + * + * A unique instance should be used for a single gRPC call. + */ +public class GrpcErrorMessageHandler implements ErrorMessageHandler { + + @Getter + private boolean isErrorHandled = false; + + private final String fullMethodName; + private final StreamObserver responseObserver; + private final GrpcExceptionHandler exceptionHandler; + private final Logger log; + + public GrpcErrorMessageHandler(String fullMethodName, + StreamObserver responseObserver, + GrpcExceptionHandler exceptionHandler, + Logger log) { + this.fullMethodName = fullMethodName; + this.exceptionHandler = exceptionHandler; + this.responseObserver = responseObserver; + this.log = log; + } + + @Override + public void handleErrorMessage(String errorMessage) { + // A task runner may call handleErrorMessage(String) more than once. + // Throw only one exception if that happens, to avoid looping until the + // grpc stream is closed + if (!isErrorHandled) { + this.isErrorHandled = true; + log.error(errorMessage); + + if (takeOfferWasCalled()) { + handleTakeOfferError(errorMessage); + } else { + exceptionHandler.handleErrorMessage(log, + errorMessage, + responseObserver); + } + } + } + + private void handleTakeOfferError(String errorMessage) { + // If the errorMessage originated from a UI purposed TaskRunner, it should + // contain an AvailabilityResult enum name. If it does, derive the + // AvailabilityResult enum from the errorMessage, wrap it in a new + // AvailabilityResultWithDescription enum, then send the + // AvailabilityResultWithDescription to the client instead of throwing + // an exception. The client should use the grpc reply object's + // AvailabilityResultWithDescription field if reply.hasTrade = false, and the + // client can decide to throw an exception with the client friendly error + // description, or take some other action based on the AvailabilityResult enum. + // (Some offer availability problems are not fatal, and retries are appropriate.) + try { + var failureReason = getAvailabilityResultWithDescription(errorMessage); + var reply = TakeOfferReply.newBuilder() + .setFailureReason(failureReason) + .build(); + @SuppressWarnings("unchecked") + var takeOfferResponseObserver = (StreamObserver) responseObserver; + takeOfferResponseObserver.onNext(reply); + takeOfferResponseObserver.onCompleted(); + } catch (IllegalArgumentException ex) { + log.error("", ex); + exceptionHandler.handleErrorMessage(log, + errorMessage, + responseObserver); + } + } + + private AvailabilityResultWithDescription getAvailabilityResultWithDescription(String errorMessage) { + AvailabilityResult proto = getAvailabilityResult(errorMessage); + String description = getAvailabilityResultDescription(proto); + return AvailabilityResultWithDescription.newBuilder() + .setAvailabilityResult(proto) + .setDescription(description) + .build(); + } + + private AvailabilityResult getAvailabilityResult(String errorMessage) { + return stream(AvailabilityResult.values()) + .filter((e) -> errorMessage.toUpperCase().contains(e.name())) + .findFirst().orElseThrow(() -> + new IllegalArgumentException( + format("Could not find an AvailabilityResult in error message:%n%s", errorMessage))); + } + + private String getAvailabilityResultDescription(AvailabilityResult proto) { + return bisq.core.offer.AvailabilityResult.fromProto(proto).description(); + } + + private boolean takeOfferWasCalled() { + return fullMethodName.equals(getTakeOfferMethod().getFullMethodName()); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java new file mode 100644 index 0000000000..bfa950978f --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java @@ -0,0 +1,124 @@ +/* + * 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 bisq.daemon.grpc; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.function.Function; +import java.util.function.Predicate; + +import org.slf4j.Logger; + +import static io.grpc.Status.INVALID_ARGUMENT; +import static io.grpc.Status.UNKNOWN; + +/** + * The singleton instance of this class handles any expected core api Throwable by + * wrapping its message in a gRPC StatusRuntimeException and sending it to the client. + * An unexpected Throwable's message will be replaced with an 'unexpected' error message. + */ +@Singleton +class GrpcExceptionHandler { + + private final Predicate isExpectedException = (t) -> + t instanceof IllegalStateException || t instanceof IllegalArgumentException; + + @Inject + public GrpcExceptionHandler() { + } + + public void handleException(Logger log, + Throwable t, + StreamObserver responseObserver) { + // Log the core api error (this is last chance to do that), wrap it in a new + // gRPC StatusRuntimeException, then send it to the client in the gRPC response. + log.error("", t); + var grpcStatusRuntimeException = wrapException(t); + responseObserver.onError(grpcStatusRuntimeException); + throw grpcStatusRuntimeException; + } + + public void handleExceptionAsWarning(Logger log, + String calledMethod, + Throwable t, + StreamObserver responseObserver) { + // Just log a warning instead of an error with full stack trace. + log.warn(calledMethod + " -> " + t.getMessage()); + var grpcStatusRuntimeException = wrapException(t); + responseObserver.onError(grpcStatusRuntimeException); + throw grpcStatusRuntimeException; + } + + public void handleErrorMessage(Logger log, + String errorMessage, + StreamObserver responseObserver) { + // This is used to wrap Task errors from the ErrorMessageHandler + // interface, an interface that is not allowed to throw exceptions. + log.error(errorMessage); + var grpcStatusRuntimeException = new StatusRuntimeException( + UNKNOWN.withDescription(cliStyleErrorMessage.apply(errorMessage))); + responseObserver.onError(grpcStatusRuntimeException); + throw grpcStatusRuntimeException; + } + + private StatusRuntimeException wrapException(Throwable t) { + // We want to be careful about what kinds of exception messages we send to the + // client. Expected core exceptions should be wrapped in an IllegalStateException + // or IllegalArgumentException, with a consistently styled and worded error + // message. But only a small number of the expected error types are currently + // handled this way; there is much work to do to handle the variety of errors + // that can occur in the api. In the meantime, we take care to not pass full, + // unexpected error messages to the client. If the exception type is unexpected, + // we omit details from the gRPC exception sent to the client. + if (isExpectedException.test(t)) { + if (t.getCause() != null) + return new StatusRuntimeException(mapGrpcErrorStatus(t.getCause(), t.getCause().getMessage())); + else + return new StatusRuntimeException(mapGrpcErrorStatus(t, t.getMessage())); + } else { + return new StatusRuntimeException(mapGrpcErrorStatus(t, "unexpected error on server")); + } + } + + private final Function cliStyleErrorMessage = (e) -> { + String[] line = e.split("\\r?\\n"); + int lastLine = line.length; + return line[lastLine - 1].toLowerCase(); + }; + + private Status mapGrpcErrorStatus(Throwable t, String description) { + // We default to the UNKNOWN status, except were the mapping of a core api + // exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy + // to support RESTful clients, we will need to have more specific mappings + // to support correct HTTP 1.1. status codes. + //noinspection SwitchStatementWithTooFewBranches + switch (t.getClass().getSimpleName()) { + // We go ahead and use a switch statement instead of if, in anticipation + // of more, specific exception mappings. + case "IllegalArgumentException": + return INVALID_ARGUMENT.withDescription(description); + default: + return UNKNOWN.withDescription(description); + } + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java new file mode 100644 index 0000000000..553ff1b30f --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java @@ -0,0 +1,72 @@ +package bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.trade.statistics.TradeStatistics3; + +import bisq.proto.grpc.GetTradeStatisticsReply; +import bisq.proto.grpc.GetTradeStatisticsRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.GetTradeStatisticsGrpc.GetTradeStatisticsImplBase; +import static bisq.proto.grpc.GetTradeStatisticsGrpc.getGetTradeStatisticsMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcGetTradeStatisticsService extends GetTradeStatisticsImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcGetTradeStatisticsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getTradeStatistics(GetTradeStatisticsRequest req, + StreamObserver responseObserver) { + try { + var tradeStatistics = coreApi.getTradeStatistics().stream() + .map(TradeStatistics3::toProtoTradeStatistics3) + .collect(Collectors.toList()); + + var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetTradeStatisticsMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcHelpService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcHelpService.java new file mode 100644 index 0000000000..2721420b77 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcHelpService.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; + +import bisq.proto.grpc.GetMethodHelpReply; +import bisq.proto.grpc.GetMethodHelpRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.HelpGrpc.HelpImplBase; +import static bisq.proto.grpc.HelpGrpc.getGetMethodHelpMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcHelpService extends HelpImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcHelpService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getMethodHelp(GetMethodHelpRequest req, + StreamObserver responseObserver) { + try { + String helpText = coreApi.getMethodHelp(req.getMethodName()); + var reply = GetMethodHelpReply.newBuilder().setMethodHelp(helpText).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetMethodHelpMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java new file mode 100644 index 0000000000..54658e4c9a --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -0,0 +1,205 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.api.model.OfferInfo; +import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; + +import bisq.proto.grpc.CancelOfferReply; +import bisq.proto.grpc.CancelOfferRequest; +import bisq.proto.grpc.CreateOfferReply; +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.GetMyOfferReply; +import bisq.proto.grpc.GetMyOfferRequest; +import bisq.proto.grpc.GetMyOffersReply; +import bisq.proto.grpc.GetMyOffersRequest; +import bisq.proto.grpc.GetOfferReply; +import bisq.proto.grpc.GetOfferRequest; +import bisq.proto.grpc.GetOffersReply; +import bisq.proto.grpc.GetOffersRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.api.model.OfferInfo.toOfferInfo; +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.OffersGrpc.*; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcOffersService extends OffersImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcOffersService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getOffer(GetOfferRequest req, + StreamObserver responseObserver) { + try { + Offer offer = coreApi.getOffer(req.getId()); + var reply = GetOfferReply.newBuilder() + .setOffer(toOfferInfo(offer).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getMyOffer(GetMyOfferRequest req, + StreamObserver responseObserver) { + try { + Offer offer = coreApi.getMyOffer(req.getId()); + OpenOffer openOffer = coreApi.getMyOpenOffer(req.getId()); + var reply = GetMyOfferReply.newBuilder() + .setOffer(toOfferInfo(offer, openOffer.getTriggerPrice()).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getOffers(GetOffersRequest req, + StreamObserver responseObserver) { + try { + List result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode()) + .stream().map(OfferInfo::toOfferInfo) + .collect(Collectors.toList()); + var reply = GetOffersReply.newBuilder() + .addAllOffers(result.stream() + .map(OfferInfo::toProtoMessage) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getMyOffers(GetMyOffersRequest req, + StreamObserver responseObserver) { + try { + List result = coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode()) + .stream().map(OfferInfo::toOfferInfo) + .collect(Collectors.toList()); + var reply = GetMyOffersReply.newBuilder() + .addAllOffers(result.stream() + .map(OfferInfo::toProtoMessage) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void createOffer(CreateOfferRequest req, + StreamObserver responseObserver) { + try { + coreApi.createAnPlaceOffer( + req.getCurrencyCode(), + req.getDirection(), + req.getPrice(), + req.getUseMarketBasedPrice(), + req.getMarketPriceMargin(), + req.getAmount(), + req.getMinAmount(), + req.getBuyerSecurityDeposit(), + req.getTriggerPrice(), + req.getPaymentAccountId(), + req.getMakerFeeCurrencyCode(), + offer -> { + // This result handling consumer's accept operation will return + // the new offer to the gRPC client after async placement is done. + OfferInfo offerInfo = toOfferInfo(offer); + CreateOfferReply reply = CreateOfferReply.newBuilder() + .setOffer(offerInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void cancelOffer(CancelOfferRequest req, + StreamObserver responseObserver) { + try { + coreApi.cancelOffer(req.getId()); + var reply = CancelOfferReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java new file mode 100644 index 0000000000..9ac400d100 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -0,0 +1,184 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; + +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountReply; +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; +import bisq.proto.grpc.CreatePaymentAccountReply; +import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; +import bisq.proto.grpc.GetPaymentAccountFormReply; +import bisq.proto.grpc.GetPaymentAccountFormRequest; +import bisq.proto.grpc.GetPaymentAccountsReply; +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsReply; +import bisq.proto.grpc.GetPaymentMethodsRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.PaymentAccountsGrpc.*; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcPaymentAccountsService extends PaymentAccountsImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcPaymentAccountsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void createPaymentAccount(CreatePaymentAccountRequest req, + StreamObserver responseObserver) { + try { + PaymentAccount paymentAccount = coreApi.createPaymentAccount(req.getPaymentAccountForm()); + var reply = CreatePaymentAccountReply.newBuilder() + .setPaymentAccount(paymentAccount.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getPaymentAccounts(GetPaymentAccountsRequest req, + StreamObserver responseObserver) { + try { + var paymentAccounts = coreApi.getPaymentAccounts().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentAccountsReply.newBuilder() + .addAllPaymentAccounts(paymentAccounts).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getPaymentMethods(GetPaymentMethodsRequest req, + StreamObserver responseObserver) { + try { + var paymentMethods = coreApi.getFiatPaymentMethods().stream() + .map(PaymentMethod::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentMethodsReply.newBuilder() + .addAllPaymentMethods(paymentMethods).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getPaymentAccountForm(GetPaymentAccountFormRequest req, + StreamObserver responseObserver) { + try { + var paymentAccountFormJson = coreApi.getPaymentAccountForm(req.getPaymentMethodId()); + var reply = GetPaymentAccountFormReply.newBuilder() + .setPaymentAccountFormJson(paymentAccountFormJson) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void createCryptoCurrencyPaymentAccount(CreateCryptoCurrencyPaymentAccountRequest req, + StreamObserver responseObserver) { + try { + PaymentAccount paymentAccount = coreApi.createCryptoCurrencyPaymentAccount(req.getAccountName(), + req.getCurrencyCode(), + req.getAddress(), + req.getTradeInstant()); + var reply = CreateCryptoCurrencyPaymentAccountReply.newBuilder() + .setPaymentAccount(paymentAccount.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, + StreamObserver responseObserver) { + try { + var paymentMethods = coreApi.getCryptoCurrencyPaymentMethods().stream() + .map(PaymentMethod::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetCryptoCurrencyPaymentMethodsReply.newBuilder() + .addAllPaymentMethods(paymentMethods).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getCreatePaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getCreateCryptoCurrencyPaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getGetPaymentAccountsMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetPaymentMethodsMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetPaymentAccountFormMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java new file mode 100644 index 0000000000..bec21b9c5b --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; + +import bisq.proto.grpc.MarketPriceReply; +import bisq.proto.grpc.MarketPriceRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.PriceGrpc.PriceImplBase; +import static bisq.proto.grpc.PriceGrpc.getGetMarketPriceMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcPriceService extends PriceImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcPriceService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getMarketPrice(MarketPriceRequest req, + StreamObserver responseObserver) { + try { + coreApi.getMarketPrice(req.getCurrencyCode(), + price -> { + var reply = MarketPriceReply.newBuilder().setPrice(price).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetMarketPriceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java new file mode 100644 index 0000000000..f031539e06 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -0,0 +1,93 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreContext; + +import bisq.common.UserThread; +import bisq.common.config.Config; + +import io.grpc.Server; +import io.grpc.ServerBuilder; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import lombok.extern.slf4j.Slf4j; + +import static io.grpc.ServerInterceptors.interceptForward; + + + +import bisq.daemon.grpc.interceptor.PasswordAuthInterceptor; + +@Singleton +@Slf4j +public class GrpcServer { + + private final Server server; + + @Inject + public GrpcServer(CoreContext coreContext, + Config config, + PasswordAuthInterceptor passwordAuthInterceptor, + GrpcDisputeAgentsService disputeAgentsService, + GrpcHelpService helpService, + GrpcOffersService offersService, + GrpcPaymentAccountsService paymentAccountsService, + GrpcPriceService priceService, + GrpcShutdownService shutdownService, + GrpcVersionService versionService, + GrpcGetTradeStatisticsService tradeStatisticsService, + GrpcTradesService tradesService, + GrpcWalletsService walletsService) { + this.server = ServerBuilder.forPort(config.apiPort) + .executor(UserThread.getExecutor()) + .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) + .addService(interceptForward(helpService, helpService.interceptors())) + .addService(interceptForward(offersService, offersService.interceptors())) + .addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors())) + .addService(interceptForward(priceService, priceService.interceptors())) + .addService(shutdownService) + .addService(interceptForward(tradeStatisticsService, tradeStatisticsService.interceptors())) + .addService(interceptForward(tradesService, tradesService.interceptors())) + .addService(interceptForward(versionService, versionService.interceptors())) + .addService(interceptForward(walletsService, walletsService.interceptors())) + .intercept(passwordAuthInterceptor) + .build(); + coreContext.setApiUser(true); + } + + public void start() { + try { + server.start(); + log.info("listening on port {}", server.getPort()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public void shutdown() { + log.info("Server shutdown started"); + server.shutdown(); + log.info("Server shutdown complete"); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java new file mode 100644 index 0000000000..f7bf00de7e --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java @@ -0,0 +1,59 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.app.BisqHeadlessApp; + +import bisq.common.UserThread; + +import bisq.proto.grpc.ShutdownServerGrpc; +import bisq.proto.grpc.StopReply; +import bisq.proto.grpc.StopRequest; + +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@Slf4j +class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase { + + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcShutdownService(GrpcExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + @Override + public void stop(StopRequest req, + StreamObserver responseObserver) { + try { + log.info("Shutdown request received."); + var reply = StopReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + UserThread.runAfter(BisqHeadlessApp.getShutDownHandler(), 500, MILLISECONDS); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java new file mode 100644 index 0000000000..8cfc74f746 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -0,0 +1,186 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.api.model.TradeInfo; +import bisq.core.trade.Trade; + +import bisq.proto.grpc.ConfirmPaymentReceivedReply; +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; +import bisq.proto.grpc.ConfirmPaymentStartedReply; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.GetTradeReply; +import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.KeepFundsReply; +import bisq.proto.grpc.KeepFundsRequest; +import bisq.proto.grpc.TakeOfferReply; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.WithdrawFundsReply; +import bisq.proto.grpc.WithdrawFundsRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.api.model.TradeInfo.toTradeInfo; +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.TradesGrpc.*; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcTradesService extends TradesImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcTradesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getTrade(GetTradeRequest req, + StreamObserver responseObserver) { + try { + Trade trade = coreApi.getTrade(req.getTradeId()); + String role = coreApi.getTradeRole(req.getTradeId()); + var reply = GetTradeReply.newBuilder() + .setTrade(toTradeInfo(trade, role).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalArgumentException cause) { + // Offer makers may call 'gettrade' many times before a trade exists. + // Log a 'trade not found' warning instead of a full stack trace. + exceptionHandler.handleExceptionAsWarning(log, "getTrade", cause, responseObserver); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void takeOffer(TakeOfferRequest req, + StreamObserver responseObserver) { + GrpcErrorMessageHandler errorMessageHandler = + new GrpcErrorMessageHandler(getTakeOfferMethod().getFullMethodName(), + responseObserver, + exceptionHandler, + log); + coreApi.takeOffer(req.getOfferId(), + req.getPaymentAccountId(), + req.getTakerFeeCurrencyCode(), + trade -> { + TradeInfo tradeInfo = toTradeInfo(trade); + var reply = TakeOfferReply.newBuilder() + .setTrade(tradeInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }, + errorMessage -> { + if (!errorMessageHandler.isErrorHandled()) + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + + @Override + public void confirmPaymentStarted(ConfirmPaymentStartedRequest req, + StreamObserver responseObserver) { + try { + coreApi.confirmPaymentStarted(req.getTradeId()); + var reply = ConfirmPaymentStartedReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void confirmPaymentReceived(ConfirmPaymentReceivedRequest req, + StreamObserver responseObserver) { + try { + coreApi.confirmPaymentReceived(req.getTradeId()); + var reply = ConfirmPaymentReceivedReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void keepFunds(KeepFundsRequest req, + StreamObserver responseObserver) { + try { + coreApi.keepFunds(req.getTradeId()); + var reply = KeepFundsReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void withdrawFunds(WithdrawFundsRequest req, + StreamObserver responseObserver) { + try { + coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), req.getMemo()); + var reply = WithdrawFundsReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java new file mode 100644 index 0000000000..ed04be4a59 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java @@ -0,0 +1,85 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; + +import bisq.proto.grpc.GetVersionReply; +import bisq.proto.grpc.GetVersionRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.GetVersionGrpc.GetVersionImplBase; +import static bisq.proto.grpc.GetVersionGrpc.getGetVersionMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@VisibleForTesting +@Slf4j +public class GrpcVersionService extends GetVersionImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getVersion(GetVersionRequest req, StreamObserver responseObserver) { + try { + var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetVersionMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java new file mode 100644 index 0000000000..d40c9319e6 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -0,0 +1,393 @@ +/* + * 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 bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.TxFeeRateInfo; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcaster; + +import bisq.proto.grpc.GetAddressBalanceReply; +import bisq.proto.grpc.GetAddressBalanceRequest; +import bisq.proto.grpc.GetBalancesReply; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetFundingAddressesReply; +import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetTransactionReply; +import bisq.proto.grpc.GetTransactionRequest; +import bisq.proto.grpc.GetTxFeeRateReply; +import bisq.proto.grpc.GetTxFeeRateRequest; +import bisq.proto.grpc.GetUnusedBsqAddressReply; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; +import bisq.proto.grpc.LockWalletReply; +import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.RemoveWalletPasswordReply; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqReply; +import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcReply; +import bisq.proto.grpc.SendBtcRequest; +import bisq.proto.grpc.SetTxFeeRatePreferenceReply; +import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.SetWalletPasswordReply; +import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.UnlockWalletReply; +import bisq.proto.grpc.UnlockWalletRequest; +import bisq.proto.grpc.UnsetTxFeeRatePreferenceReply; +import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.VerifyBsqSentToAddressReply; +import bisq.proto.grpc.VerifyBsqSentToAddressRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import com.google.common.util.concurrent.FutureCallback; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static bisq.core.api.model.TxInfo.toTxInfo; +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.WalletsGrpc.*; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +class GrpcWalletsService extends WalletsImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcWalletsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { + try { + var balances = coreApi.getBalances(req.getCurrencyCode()); + var reply = GetBalancesReply.newBuilder() + .setBalances(balances.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getAddressBalance(GetAddressBalanceRequest req, + StreamObserver responseObserver) { + try { + AddressBalanceInfo balanceInfo = coreApi.getAddressBalanceInfo(req.getAddress()); + var reply = GetAddressBalanceReply.newBuilder() + .setAddressBalanceInfo(balanceInfo.toProtoMessage()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getFundingAddresses(GetFundingAddressesRequest req, + StreamObserver responseObserver) { + try { + List balanceInfo = coreApi.getFundingAddresses(); + var reply = GetFundingAddressesReply.newBuilder() + .addAllAddressBalanceInfo( + balanceInfo.stream() + .map(AddressBalanceInfo::toProtoMessage) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req, + StreamObserver responseObserver) { + try { + String address = coreApi.getUnusedBsqAddress(); + var reply = GetUnusedBsqAddressReply.newBuilder() + .setAddress(address) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void sendBsq(SendBsqRequest req, + StreamObserver responseObserver) { + try { + coreApi.sendBsq(req.getAddress(), + req.getAmount(), + req.getTxFeeRate(), + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction tx) { + log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes", + tx.getTxId().toString(), + tx.getOutputSum(), + tx.getFee(), + tx.getMessageSize()); + var reply = SendBsqReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void onFailure(TxBroadcastException ex) { + throw new IllegalStateException(ex); + } + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void sendBtc(SendBtcRequest req, + StreamObserver responseObserver) { + try { + coreApi.sendBtc(req.getAddress(), + req.getAmount(), + req.getTxFeeRate(), + req.getMemo(), + new FutureCallback<>() { + @Override + public void onSuccess(Transaction tx) { + if (tx != null) { + log.info("Successfully published BTC tx: id {}, output sum {} sats, fee {} sats, size {} bytes", + tx.getTxId().toString(), + tx.getOutputSum(), + tx.getFee(), + tx.getMessageSize()); + var reply = SendBtcReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } else { + throw new IllegalStateException("btc transaction is null"); + } + } + + @Override + public void onFailure(@NotNull Throwable t) { + log.error("", t); + throw new IllegalStateException(t); + } + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void verifyBsqSentToAddress(VerifyBsqSentToAddressRequest req, + StreamObserver responseObserver) { + try { + boolean isAmountReceived = coreApi.verifyBsqSentToAddress(req.getAddress(), req.getAmount()); + var reply = VerifyBsqSentToAddressReply.newBuilder() + .setIsAmountReceived(isAmountReceived) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getTxFeeRate(GetTxFeeRateRequest req, + StreamObserver responseObserver) { + try { + coreApi.getTxFeeRate(() -> { + TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo(); + var reply = GetTxFeeRateReply.newBuilder() + .setTxFeeRateInfo(txFeeRateInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void setTxFeeRatePreference(SetTxFeeRatePreferenceRequest req, + StreamObserver responseObserver) { + try { + coreApi.setTxFeeRatePreference(req.getTxFeeRatePreference(), () -> { + TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo(); + var reply = SetTxFeeRatePreferenceReply.newBuilder() + .setTxFeeRateInfo(txFeeRateInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void unsetTxFeeRatePreference(UnsetTxFeeRatePreferenceRequest req, + StreamObserver responseObserver) { + try { + coreApi.unsetTxFeeRatePreference(() -> { + TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo(); + var reply = UnsetTxFeeRatePreferenceReply.newBuilder() + .setTxFeeRateInfo(txFeeRateInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getTransaction(GetTransactionRequest req, + StreamObserver responseObserver) { + try { + Transaction tx = coreApi.getTransaction(req.getTxId()); + var reply = GetTransactionReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void setWalletPassword(SetWalletPasswordRequest req, + StreamObserver responseObserver) { + try { + coreApi.setWalletPassword(req.getPassword(), req.getNewPassword()); + var reply = SetWalletPasswordReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void removeWalletPassword(RemoveWalletPasswordRequest req, + StreamObserver responseObserver) { + try { + coreApi.removeWalletPassword(req.getPassword()); + var reply = RemoveWalletPasswordReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void lockWallet(LockWalletRequest req, + StreamObserver responseObserver) { + try { + coreApi.lockWallet(); + var reply = LockWalletReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void unlockWallet(UnlockWalletRequest req, + StreamObserver responseObserver) { + try { + coreApi.unlockWallet(req.getPassword(), req.getTimeout()); + var reply = UnlockWalletReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetUnusedBsqAddressMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getSendBsqMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getGetTxFeeRateMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getSetTxFeeRatePreferenceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getUnsetTxFeeRatePreferenceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetTransactionMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + + // Trying to set or remove a wallet password several times before the 1st attempt has time to + // persist the change to disk may corrupt the wallet, so allow only 1 attempt per 5 seconds. + put(getSetWalletPasswordMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS, 5)); + put(getRemoveWalletPasswordMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS, 5)); + + put(getLockWalletMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getUnlockWalletMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/interceptor/CallRateMeteringInterceptor.java b/daemon/src/main/java/bisq/daemon/grpc/interceptor/CallRateMeteringInterceptor.java new file mode 100644 index 0000000000..5f1dfd5c13 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/interceptor/CallRateMeteringInterceptor.java @@ -0,0 +1,136 @@ +/* + * 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 bisq.daemon.grpc.interceptor; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.StatusRuntimeException; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static io.grpc.Status.PERMISSION_DENIED; +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; + +@Slf4j +public final class CallRateMeteringInterceptor implements ServerInterceptor { + + // Maps the gRPC server method names to rate meters. This allows one interceptor + // instance to handle rate metering for any or all the methods in a Grpc*Service. + protected final Map serviceCallRateMeters; + + public CallRateMeteringInterceptor(Map serviceCallRateMeters) { + this.serviceCallRateMeters = serviceCallRateMeters; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, + Metadata headers, + ServerCallHandler serverCallHandler) { + Optional> rateMeterKV = getRateMeterKV(serverCall); + rateMeterKV.ifPresentOrElse( + (kv) -> checkRateMeterAndMaybeCloseCall(kv, serverCall), + () -> handleMissingRateMeterConfiguration(serverCall)); + + // We leave it to the gRPC framework to clean up if the server call was closed + // above. But we still have to invoke startCall here because the method must + // return a ServerCall.Listener. + return serverCallHandler.startCall(serverCall, headers); + } + + private void checkRateMeterAndMaybeCloseCall(Map.Entry rateMeterKV, + ServerCall serverCall) { + String methodName = rateMeterKV.getKey(); + GrpcCallRateMeter rateMeter = rateMeterKV.getValue(); + + if (!rateMeter.checkAndIncrement()) + handlePermissionDeniedWarningAndCloseCall(methodName, rateMeter, serverCall); + else + log.info(rateMeter.getCallsCountProgress(methodName)); + } + + private void handleMissingRateMeterConfiguration(ServerCall serverCall) + throws StatusRuntimeException { + log.debug("The gRPC service's call rate metering interceptor does not" + + " meter the {} method.", + getRateMeterKey(serverCall)); + } + + private void handlePermissionDeniedWarningAndCloseCall(String methodName, + GrpcCallRateMeter rateMeter, + ServerCall serverCall) + throws StatusRuntimeException { + String msg = getDefaultRateExceededError(methodName, rateMeter); + log.warn(msg + "."); + serverCall.close(PERMISSION_DENIED.withDescription(msg.toLowerCase()), new Metadata()); + } + + private String getDefaultRateExceededError(String methodName, + GrpcCallRateMeter rateMeter) { + // The derived method name may not be an exact match to CLI's method name. + String timeUnitName = StringUtils.chop(rateMeter.getTimeUnit().name().toLowerCase()); + // Just print 'getversion', not the grpc method descriptor's + // full-method-name: 'io.bisq.protobuffer.getversion/getversion'. + String loggedMethodName = methodName.split("/")[1]; + return format("The maximum allowed number of %s calls (%d/%s) has been exceeded", + loggedMethodName, + rateMeter.getAllowedCallsPerTimeWindow(), + timeUnitName); + } + + private Optional> getRateMeterKV(ServerCall serverCall) { + String rateMeterKey = getRateMeterKey(serverCall); + return serviceCallRateMeters.entrySet().stream() + .filter((e) -> e.getKey().equals(rateMeterKey)).findFirst(); + } + + private String getRateMeterKey(ServerCall serverCall) { + // Get the rate meter map key from the server call method descriptor. The + // returned String (e.g., 'io.bisq.protobuffer.Offers/CreateOffer') will match + // a map entry key in the 'serviceCallRateMeters' constructor argument, if it + // was defined in the Grpc*Service class' rateMeteringInterceptor method. + return serverCall.getMethodDescriptor().getFullMethodName(); + } + + @Override + public String toString() { + String rateMetersString = + serviceCallRateMeters.entrySet() + .stream() + .map(Object::toString) + .collect(joining("\n\t\t")); + return "CallRateMeteringInterceptor {" + "\n\t" + + "serviceCallRateMeters {" + "\n\t\t" + + rateMetersString + "\n\t" + "}" + "\n" + + "}"; + } + + public static CallRateMeteringInterceptor valueOf(Map rateMeters) { + return new CallRateMeteringInterceptor(new HashMap<>() {{ + putAll(rateMeters); + }}); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java new file mode 100644 index 0000000000..73096a0336 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java @@ -0,0 +1,95 @@ +package bisq.daemon.grpc.interceptor; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayDeque; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; + +@Slf4j +public class GrpcCallRateMeter { + + @Getter + private final int allowedCallsPerTimeWindow; + @Getter + private final TimeUnit timeUnit; + @Getter + private final int numTimeUnits; + + @Getter + private transient final long timeUnitIntervalInMilliseconds; + + private transient final ArrayDeque callTimestamps; + + public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit) { + this(allowedCallsPerTimeWindow, timeUnit, 1); + } + + public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit, int numTimeUnits) { + this.allowedCallsPerTimeWindow = allowedCallsPerTimeWindow; + this.timeUnit = timeUnit; + this.numTimeUnits = numTimeUnits; + this.timeUnitIntervalInMilliseconds = timeUnit.toMillis(1) * numTimeUnits; + this.callTimestamps = new ArrayDeque<>(); + } + + public boolean checkAndIncrement() { + if (getCallsCount() < allowedCallsPerTimeWindow) { + incrementCallsCount(); + return true; + } else { + return false; + } + } + + public int getCallsCount() { + removeStaleCallTimestamps(); + return callTimestamps.size(); + } + + public String getCallsCountProgress(String calledMethodName) { + String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase()); + // Just print 'GetVersion has been called N times...', + // not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...' + String loggedMethodName = calledMethodName.split("/")[1]; + return format("%s has been called %d time%s in the last %s, rate limit is %d/%s", + loggedMethodName, + callTimestamps.size(), + callTimestamps.size() == 1 ? "" : "s", + shortTimeUnitName, + allowedCallsPerTimeWindow, + shortTimeUnitName); + } + + private void incrementCallsCount() { + callTimestamps.add(currentTimeMillis()); + } + + private void removeStaleCallTimestamps() { + while (!callTimestamps.isEmpty() && isStale.test(callTimestamps.peek())) { + callTimestamps.remove(); + } + } + + private final Predicate isStale = (t) -> { + long stale = currentTimeMillis() - this.getTimeUnitIntervalInMilliseconds(); + // Is the given timestamp before the current time minus 1 timeUnit in millis? + return t < stale; + }; + + @Override + public String toString() { + return "GrpcCallRateMeter{" + + "allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow + + ", timeUnit=" + timeUnit.name() + + ", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds + + ", callsCount=" + callTimestamps.size() + + '}'; + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfig.java b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfig.java new file mode 100644 index 0000000000..b01ac17655 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfig.java @@ -0,0 +1,288 @@ +/* + * 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 bisq.daemon.grpc.interceptor; + +import io.grpc.ServerInterceptor; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import com.google.common.annotations.VisibleForTesting; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.file.FileUtil.deleteFileIfExists; +import static bisq.common.file.FileUtil.renameFile; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readAllBytes; + +@VisibleForTesting +@Slf4j +public class GrpcServiceRateMeteringConfig { + + public static final String RATE_METERS_CONFIG_FILENAME = "ratemeters.json"; + + private static final String KEY_GRPC_SERVICE_CLASS_NAME = "grpcServiceClassName"; + private static final String KEY_METHOD_RATE_METERS = "methodRateMeters"; + private static final String KEY_ALLOWED_CALL_PER_TIME_WINDOW = "allowedCallsPerTimeWindow"; + private static final String KEY_TIME_UNIT = "timeUnit"; + private static final String KEY_NUM_TIME_UNITS = "numTimeUnits"; + + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + private final List> methodRateMeters; + private final String grpcServiceClassName; + + public GrpcServiceRateMeteringConfig(String grpcServiceClassName) { + this(grpcServiceClassName, new ArrayList<>()); + } + + public GrpcServiceRateMeteringConfig(String grpcServiceClassName, + List> methodRateMeters) { + this.grpcServiceClassName = grpcServiceClassName; + this.methodRateMeters = methodRateMeters; + } + + @SuppressWarnings("unused") + public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName, + int maxCalls, + TimeUnit timeUnit) { + return addMethodCallRateMeter(methodName, maxCalls, timeUnit, 1); + } + + public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName, + int maxCalls, + TimeUnit timeUnit, + int numTimeUnits) { + methodRateMeters.add(new LinkedHashMap<>() {{ + put(methodName, new GrpcCallRateMeter(maxCalls, timeUnit, numTimeUnits)); + }}); + return this; + } + + public boolean isConfigForGrpcService(Class clazz) { + return isConfigForGrpcService(clazz.getSimpleName()); + } + + public boolean isConfigForGrpcService(String grpcServiceClassSimpleName) { + return this.grpcServiceClassName.equals(grpcServiceClassSimpleName); + } + + @Override + public String toString() { + return "GrpcServiceRateMeteringConfig{" + "\n" + + " grpcServiceClassName='" + grpcServiceClassName + '\'' + "\n" + + ", methodRateMeters=" + methodRateMeters + "\n" + + '}'; + } + + public static Optional getCustomRateMeteringInterceptor(File installationDir, + Class grpcServiceClass) { + File configFile = new File(installationDir, RATE_METERS_CONFIG_FILENAME); + return configFile.exists() + ? toServerInterceptor(configFile, grpcServiceClass) + : Optional.empty(); + } + + public static Optional toServerInterceptor(File configFile, Class grpcServiceClass) { + // From a global rate metering config file, create a specific gRPC service + // interceptor configuration in the form of an interceptor constructor argument, + // a map. + // Transforming json into the List> is a bit + // convoluted due to Gson's loss of generic type information during deserialization. + Optional grpcServiceConfig = getAllDeserializedConfigs(configFile) + .stream().filter(x -> x.isConfigForGrpcService(grpcServiceClass)).findFirst(); + if (grpcServiceConfig.isPresent()) { + Map serviceCallRateMeters = new HashMap<>(); + for (Map methodToRateMeterMap : grpcServiceConfig.get().methodRateMeters) { + Map.Entry entry = methodToRateMeterMap.entrySet().stream().findFirst().orElseThrow(() + -> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map.")); + serviceCallRateMeters.put(entry.getKey(), entry.getValue()); + } + return Optional.of(new CallRateMeteringInterceptor(serviceCallRateMeters)); + } else { + return Optional.empty(); + } + } + + @SuppressWarnings("unchecked") + private static List> getMethodRateMetersMap(Map gsonMap) { + List> rateMeters = new ArrayList<>(); + // Each gsonMap is a Map with a single entry: + // {getVersion={allowedCallsPerTimeUnit=8.0, timeUnit=SECONDS, callsCount=0.0, isRunning=false}} + // Convert it to a multiple entry Map, where the key + // is a method name. + for (Map singleEntryRateMeterMap : (List>) gsonMap.get(KEY_METHOD_RATE_METERS)) { + log.debug("Gson's single entry {} {} = {}", + gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME), + singleEntryRateMeterMap.getClass().getSimpleName(), + singleEntryRateMeterMap); + Map.Entry entry = singleEntryRateMeterMap.entrySet().stream().findFirst().orElseThrow(() + -> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map.")); + String methodName = entry.getKey(); + GrpcCallRateMeter rateMeter = getGrpcCallRateMeter(entry); + rateMeters.add(new LinkedHashMap<>() {{ + put(methodName, rateMeter); + }}); + } + return rateMeters; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static List deserialize(File configFile) { + verifyConfigFile(configFile); + List serviceMethodConfigurations = new ArrayList<>(); + // Gson cannot deserialize a json string to List + // so easily for us, so we do it here before returning the list of configurations. + List rawConfigList = gson.fromJson(toJson(configFile), ArrayList.class); + // Gson gave us a list of maps with keys grpcServiceClassName, methodRateMeters: + // String grpcServiceClassName + // List methodRateMeters + for (Object rawConfig : rawConfigList) { + Map gsonMap = (Map) rawConfig; + String grpcServiceClassName = (String) gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME); + List> rateMeters = getMethodRateMetersMap(gsonMap); + serviceMethodConfigurations.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName, rateMeters)); + } + return serviceMethodConfigurations; + } + + @SuppressWarnings("unchecked") + private static GrpcCallRateMeter getGrpcCallRateMeter(Map.Entry gsonEntry) { + Map valueMap = (Map) gsonEntry.getValue(); + int allowedCallsPerTimeWindow = ((Number) valueMap.get(KEY_ALLOWED_CALL_PER_TIME_WINDOW)).intValue(); + TimeUnit timeUnit = TimeUnit.valueOf((String) valueMap.get(KEY_TIME_UNIT)); + int numTimeUnits = ((Number) valueMap.get(KEY_NUM_TIME_UNITS)).intValue(); + return new GrpcCallRateMeter(allowedCallsPerTimeWindow, timeUnit, numTimeUnits); + } + + private static void verifyConfigFile(File configFile) { + if (configFile == null) + throw new IllegalStateException("Cannot read null json config file."); + + if (!configFile.exists()) + throw new IllegalStateException(format("cannot find json config file %s", configFile.getAbsolutePath())); + } + + private static String toJson(File configFile) { + try { + return new String(readAllBytes(Paths.get(configFile.getAbsolutePath()))); + } catch (IOException ex) { + throw new IllegalStateException(format("Cannot read json string from file %s.", + configFile.getAbsolutePath())); + } + } + + private static List allDeserializedConfigs; + + private static List getAllDeserializedConfigs(File configFile) { + // We deserialize once, not for each gRPC service wanting an interceptor. + if (allDeserializedConfigs == null) + allDeserializedConfigs = deserialize(configFile); + + return allDeserializedConfigs; + } + + @VisibleForTesting + public static class Builder { + private final List rateMeterConfigs = new ArrayList<>(); + + public void addCallRateMeter(String grpcServiceClassName, + String methodName, + int maxCalls, + TimeUnit timeUnit) { + addCallRateMeter(grpcServiceClassName, + methodName, + maxCalls, + timeUnit, + 1); + } + + public void addCallRateMeter(String grpcServiceClassName, + String methodName, + int maxCalls, + TimeUnit timeUnit, + int numTimeUnits) { + log.info("Adding call rate metering definition {}.{} ({}/{}ms).", + grpcServiceClassName, + methodName, + maxCalls, + timeUnit.toMillis(1) * numTimeUnits); + rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName)) + .findFirst().ifPresentOrElse( + (config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits), + () -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName) + .addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits))); + } + + public File build() { + File tmpFile = serializeRateMeterDefinitions(); + File configFile = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toFile(); + try { + deleteFileIfExists(configFile); + renameFile(tmpFile, configFile); + } catch (IOException ex) { + throw new IllegalStateException(format("Could not create config file %s.", + configFile.getAbsolutePath()), ex); + } + return configFile; + } + + private File serializeRateMeterDefinitions() { + String json = gson.toJson(rateMeterConfigs); + File file = createTmpFile(); + try (OutputStreamWriter outputStreamWriter = + new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { + outputStreamWriter.write(json); + } catch (Exception ex) { + throw new IllegalStateException(format("Cannot write file for json string %s.", json), ex); + } + return file; + } + + private File createTmpFile() { + File file; + try { + file = File.createTempFile("ratemeters_", + ".tmp", + Paths.get(getProperty("java.io.tmpdir")).toFile()); + } catch (IOException ex) { + throw new IllegalStateException("Cannot create tmp ratemeters json file.", ex); + } + return file; + } + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/interceptor/PasswordAuthInterceptor.java b/daemon/src/main/java/bisq/daemon/grpc/interceptor/PasswordAuthInterceptor.java new file mode 100644 index 0000000000..4ed1af4f3d --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/interceptor/PasswordAuthInterceptor.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.daemon.grpc.interceptor; + +import bisq.common.config.Config; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.StatusRuntimeException; + +import javax.inject.Inject; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Metadata.Key; +import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.String.format; + +/** + * Authorizes rpc server calls by comparing the value of the caller's + * {@value PASSWORD_KEY} header to an expected value set at server startup time. + * + * @see bisq.common.config.Config#apiPassword + */ +public class PasswordAuthInterceptor implements ServerInterceptor { + + private static final String PASSWORD_KEY = "password"; + + private final String expectedPasswordValue; + + @Inject + public PasswordAuthInterceptor(Config config) { + this.expectedPasswordValue = config.apiPassword; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, + Metadata headers, + ServerCallHandler serverCallHandler) { + var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER)); + + if (actualPasswordValue == null) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( + format("missing '%s' rpc header value", PASSWORD_KEY))); + + if (!actualPasswordValue.equals(expectedPasswordValue)) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( + format("incorrect '%s' rpc header value", PASSWORD_KEY))); + + return serverCallHandler.startCall(serverCall, headers); + } +} diff --git a/daemon/src/main/resources/logback.xml b/daemon/src/main/resources/logback.xml new file mode 100644 index 0000000000..bc8edf0221 --- /dev/null +++ b/daemon/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + + diff --git a/daemon/src/test/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfigTest.java b/daemon/src/test/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfigTest.java new file mode 100644 index 0000000000..2b1044e4df --- /dev/null +++ b/daemon/src/test/java/bisq/daemon/grpc/interceptor/GrpcServiceRateMeteringConfigTest.java @@ -0,0 +1,190 @@ +/* + * 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 bisq.daemon.grpc.interceptor; + +import io.grpc.ServerInterceptor; + +import java.nio.file.Paths; + +import java.io.File; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static java.lang.System.getProperty; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + + +import bisq.daemon.grpc.GrpcVersionService; + +@Slf4j +public class GrpcServiceRateMeteringConfigTest { + + private static final GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder(); + private static File configFile; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static Optional versionServiceInterceptor; + + @BeforeClass + public static void setup() { + // This is the tested rate meter, it allows 3 calls every 2 seconds. + builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), + "getVersion", + 3, + SECONDS, + 2); + builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), + "badMethodNameDoesNotBreakAnything", + 100, + DAYS); + // The other Grpc*Service classes are not @VisibleForTesting, so we hardcode + // the simple class name. + builder.addCallRateMeter("GrpcOffersService", + "createOffer", + 5, + MINUTES); + builder.addCallRateMeter("GrpcTradesService", + "takeOffer", + 10, + DAYS); + builder.addCallRateMeter("GrpcWalletsService", + "sendBtc", + 3, + HOURS); + } + + @Before + public void buildConfigFile() { + if (configFile == null) + configFile = builder.build(); + } + + @Test + public void testConfigFileBuild() { + assertNotNull(configFile); + assertTrue(configFile.exists()); + assertTrue(configFile.length() > 0); + String expectedConfigFilePath = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toString(); + assertEquals(expectedConfigFilePath, configFile.getAbsolutePath()); + } + + @Test + public void testGetVersionCallRateMeter() { + // Check the interceptor has 2 rate meters, for getVersion and badMethodNameDoesNotBreakAnything. + CallRateMeteringInterceptor versionServiceInterceptor = buildInterceptor(); + assertEquals(2, versionServiceInterceptor.serviceCallRateMeters.size()); + + // Check the rate meter config. + GrpcCallRateMeter rateMeter = versionServiceInterceptor.serviceCallRateMeters.get("getVersion"); + assertEquals(3, rateMeter.getAllowedCallsPerTimeWindow()); + assertEquals(SECONDS, rateMeter.getTimeUnit()); + assertEquals(2, rateMeter.getNumTimeUnits()); + assertEquals(2 * 1000, rateMeter.getTimeUnitIntervalInMilliseconds()); + + // Do as many calls as allowed within rateMeter.getTimeUnitIntervalInMilliseconds(). + doMaxIsAllowedChecks(true, + rateMeter.getAllowedCallsPerTimeWindow(), + rateMeter); + + // The next 3 calls are blocked because we've exceeded the 3calls/2s limit. + doMaxIsAllowedChecks(false, + rateMeter.getAllowedCallsPerTimeWindow(), + rateMeter); + + // Let all of the rate meter's cached call timestamps become stale by waiting for + // 2001 ms, then we can call getversion another 'allowedCallsPerTimeUnit' times. + rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds()); + // All the stale call timestamps are gone and the call count is back to zero. + assertEquals(0, rateMeter.getCallsCount()); + + doMaxIsAllowedChecks(true, + rateMeter.getAllowedCallsPerTimeWindow(), + rateMeter); + // We've exceeded the call/second limit. + assertFalse(rateMeter.checkAndIncrement()); + + // Let all of the call timestamps go stale again by waiting for 2001 ms. + rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds()); + + // Call twice, resting 0.5s after each call. + for (int i = 0; i < 2; i++) { + assertTrue(rateMeter.checkAndIncrement()); + rest(500); + } + // Call the 3rd time, then let one of the rate meter's timestamps go stale. + assertTrue(rateMeter.checkAndIncrement()); + rest(1001); + + // The call count was decremented by one because one timestamp went stale. + assertEquals(2, rateMeter.getCallsCount()); + assertTrue(rateMeter.checkAndIncrement()); + assertEquals(rateMeter.getAllowedCallsPerTimeWindow(), rateMeter.getCallsCount()); + + // We've exceeded the call limit again. + assertFalse(rateMeter.checkAndIncrement()); + } + + private void doMaxIsAllowedChecks(boolean expectedIsAllowed, + int expectedCallsCount, + GrpcCallRateMeter rateMeter) { + for (int i = 1; i <= rateMeter.getAllowedCallsPerTimeWindow(); i++) { + assertEquals(expectedIsAllowed, rateMeter.checkAndIncrement()); + } + assertEquals(expectedCallsCount, rateMeter.getCallsCount()); + } + + @AfterClass + public static void teardown() { + if (configFile != null) + configFile.deleteOnExit(); + } + + private void rest(long milliseconds) { + try { + TimeUnit.MILLISECONDS.sleep(milliseconds); + } catch (InterruptedException ignored) { + } + } + + private CallRateMeteringInterceptor buildInterceptor() { + //noinspection OptionalAssignedToNull + if (versionServiceInterceptor == null) { + versionServiceInterceptor = getCustomRateMeteringInterceptor( + configFile.getParentFile(), + GrpcVersionService.class); + } + assertTrue(versionServiceInterceptor.isPresent()); + return (CallRateMeteringInterceptor) versionServiceInterceptor.get(); + } +} diff --git a/desktop/package/29CDFD3B.asc b/desktop/package/29CDFD3B.asc new file mode 100644 index 0000000000..18f37a913f --- /dev/null +++ b/desktop/package/29CDFD3B.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFl5pBEBEACmse+BgUYi+WLTHR4xDwFE5LyEIT3a5t+lGolO3cVkfw5RI+7g +FEpxXzWontiLxDdDi34nr1zXOIEjSgQ7HzdtnFiTRN4tIENCBul4YiCOiyBi5ofN +ejAHqmeiO0KsDBQZBdyiK1iWi6yNbpG/rARwHu/Rx5ouT1YX1hV92Qh1bnU+4j4O +FcePQRNl+4q/SrtKdm047Ikr/LBvy/WYBYe9BcQGhbHI4DrUOSnIuI/Zq7xLF8QS +U/an/d0ftbSBZNX3anDiZjzSmR16seRQtvRO6mehWFNlgLMOGgFeJzPkByTd1TlV +K/KaHKQ71FNkRiP87pwkHZI5zJPAQfve+KmYPwOyETUaX43XOuixqutUV6Lrd0ng +bKe6q4nZDOWi5a4I3+hkrfzaGOKm9TlZoEmpJHh6pa5ULoDnEpKCg5Dgn3NGwokw +57sDAC2mtf41/uSkR20ALN1q4iOLXiHn+T6Z+Uq7aL3OcKGcBu4xC6Jfofmmdfdd +QxEEaeHvAI9ETlKy3tsMhEs5XD6m90rCKLnb97Y8eT/xJL4/oDsxI0o7qICz1nFS +2IhV8xULZ2533vNQPMEbSLoTzgz1OEPYwI1b+YJDFlp1y0XRiEtDZiAFfgsJY7UE +DizfuUFsK5LOkw2+NVmLphDVrDW1MXbhX1xspZDmBG9giE08sPtHj/EZHwARAQAB +tDNDaHJpc3RvcGggQXR0ZW5lZGVyIDxjaHJpc3RvcGguYXR0ZW5lZGVyQGdtYWls +LmNvbT6JAj0EEwEKACcFAll5pBECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQAC +HgECF4AACgkQzV3BxSnN/Ts46g/+KJL3M6Bfr9K9muNjEeA6yBS5HmZ2tBJI3PiF +pJC5MBO5H+3ukI8nyzYil3JhEqCcTUspBGbbkqbwbSQuS19VYBHHxhSAn7B/MHFC +FnlbKEzS3cHyp95lGPLJ/y5FXXnSxdlC1EXFcuSjHWR27cCUGuH+1diuyh2eZoT5 +fN27g5B5ej0ExXoCp8J4MtMhcHXjGy7Kiv0CbcY8vYEYbqd5GsMvk7UZIa+vWTMz +JE1fp6msFfUFzHXYRhO/TKi8iRtVaUUcaOHz7kb226ckVnzIO3CjsGg7y19BYaWf +C6Rw0XqPfCf7PoJjhRxbC/9ZWujy/pkaOtOBoq+IZECkiHsKUcZgNdU7xMyCE0a5 +jOvJrzKna6MELPczTyeWqZvL0dKNhllw5WJIhzf5mcFqOb1OlNjWxC1BnOeNk51f ++FDtjxOyp6P7uL0dPy7j4TA7aHgQNKy2Uvx3+Eu9EHKL2T35xXPvma1ZVybQlMBK +z7rbjTIiKTf5LqTtFyE4Kx6IS29rygyJPxz81r4pbjoGUIxLnhxL+6LwxCPwmbkI +fFRD+gk8ODmhgY947D6VBPPrrH4U9YiUJZ718b3tCJoubLPrGUfbFlKaGBloK+Ld +0ulJGZrQWxiK3y1KO1AF8k1ge9utJowLAq8rZOUdSPb/cjo3OsspqJR9OQQXNO0n +6WL3Y/a5Ag0EWXmkEQEQAMt06beoYe/vmAWR91y5BUIu1zNmQP2NNAZ1Jh1K3q7a +AVEamyVmdF4i2JVF7fTnRGWDiKgjF2f9KJA2mC9v6EK6l7KK/7oQfFgympku8hSL +jtp/TWIZZ1D9z16GdqmWaRGdMkqmjf7Wpy26A5TCsUbGvn1tm9P8PxqNfgCv3Cap +FhPciK4o/e4gXY7tUbYMC65Dmq3OoJWWzAGqeDmbH4U5BcoZBk+SFyknF/5NWGuz +E0yl6TRkgEhzneyBcaV1bmSVcWBpNozoyZC49JggrwFJExd5QQE06iWbx+OkWHYt +ObJSKQd3liC1EcAFzI0BoZQ5ZE8VoTXpVQXQcsYtbWKj5BReiEIovi3/+CmjxUFS +M7fjeelRwVWeh0/FnD7KxF5LshUDlrc/JIRxI9RYZcbhoXB1UMc/5SX5AT0+a86p +Gay7yE0JQGtap1Hi5yf1yDMJr1i89u1LfKXbHb2jMOzyiDYR2kaPO0IDpDJ6kjPc +fFAcNt/FpJw5U3mBKy8tHlIMoFd/5hTFBf9Pnrj3bmXx2dSd1Y3l6sQjhceSIALQ +I95QfXY57a04mHURO/CCxwzLlKeI1Qp7zT9TiV7oBx85uY2VtrxPdPmPHF0y9Fnh +K1Pq2VAN53WHGK9MEuyIV/VxebN7w2tDhVi9SI2UmdGuDdrLlCBhT0UeCYt2jFxF +ABEBAAGJAiUEGAEKAA8FAll5pBECGwwFCQeGH4AACgkQzV3BxSnN/TsbkQ//dsg1 +fvzYZDv989U/dcvZHWdQHhjRz1+Y2oSmRzsab+lbCMd9nbtHa4CNjc5UxFrZst83 +7fBvUPrldNFCA94UOEEORRUJntLdcHhNnPK+pBkLzLcQbtww9nD94B6hqdLND5iW +hnKuI7BXFg8uzH3fRrEhxNByfXv1Uyq9aolsbvRjfFsL7n/+02aKuBzIO5VbFedN +0aZ52mA1aooDKD69kppBWXs+sxPkHkpCexJUkr3ekjsH8jk10Over8DNj8QN4ii2 +I3/xsRCCvrvcKNfm4LR49KJ+5YUUkOo1xWSwOzWHV9lpn2abMEqqIqnubvENclNi +qIbE6vkAaILyubilgxTVbc6SntUarUO/59j2a0c+pDnHgLB799bnh0mAmXXCVO3Z +14GpaH15iaUCgRgxx9uP+lQIj6LtrPOsc5b5J6VLgdxQlDXejKe9PaM6+agtIBmb +I24t36ljmRrha2QH90MhyDPrJ/U6ch/ilgTTNRWbfTsALRxzNmnHvj0Y55IsdYg3 +bm71QT99x9kNuozo7I4MrGElS+9Pwy31lcY17OSy/K1wqpLCW1exc4SwJRsAImNW +QLNcwMx1fIBhPiyuhRVsjoCEda5rO+NYF8U8u/UrXixNXsHGBgaynWO/rI9KFg0f +NYeOG8Xnm4CxuWqUu0FDMv6BhkMCTz2X4xcnbtI= +=9LRS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/desktop/package/5BC5ED73.asc b/desktop/package/5BC5ED73.asc new file mode 100644 index 0000000000..d1f70e1e0c --- /dev/null +++ b/desktop/package/5BC5ED73.asc @@ -0,0 +1,50 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFLubUkBEAC9dIbgokeCmvyELlpIW56AIgRPsqm5WqxXQyaoKGc2jwWsuHY2 +10ekprWficlPS2AC/lV0Mj5rtEgintRYh0Do0gKVaiCL31/L2lPh9WVuLeYQ2Oyv +4p5u7BFHLOu+j3VynLI9MKlr7rT1gDuFLGp8eTfaYnIgFmZ1uTB48YoYw9AAnOpT +qtxIYZ81jS7lPkQeeViGEqdJdTDZZUKeKaTnJL+yaq6kSFhUW9I4HPxS/oZGRuFn +qefqmDyWypc5bl4CsxLHhhNGI4QrCEHZcQEGwx4Fn8qFXW+47e4KVBZrh0QxIjNJ +Rg41DF/oBBsTMXJogVawKIlyQalE+WcKVQtKcUcCBw3cLaHzn/QMYrfQTMhB/3Sk +kuN4TCx7HOyM9rFt7y+lz5buPdHlocqbISk6QtbiMCKyb5XwXVcE/MAas/LGE2il +zxf7el9Sfey8Yd0t71SAJXrItdygz+iAxoTtnXbjIB/3YzkfSPD4nCAbbHmzx+C6 +oV1Xw07usdXLBLQf5jPvKKzjO+xAMHyS7Sf6JJod2ACdJXBEuA2YhK9GNqojfJjI +/w0GpV96tAHq3tb30QXZe5NxxIdiw4h5q+VGgIHwpRtNeqx2ngpxY8qHBm5UBYk0 +KKX8msoDIwjnVtfcBFkuPiJlxQ48JRmh80vW4ZEZ3Rm2zRv1lsWpx/QhRwARAQAB +tBxDaHJpcyBCZWFtcyA8Y2hyaXNAYmVhbXMuaW8+iQI3BBMBAgAiBQJS7m1JAhsD +BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRA9IU+PW8XtcxXHD/dAAY9mw9AT +5LeDFvtkZlK2hec0MPWnoUMBjxTCsaoH+fBCT2Bt53hwkHw4tm0mkxpq3E+zoy2B +1tOHSI7a6AxBREBXEh2OFDU2gDj/H8nR65gw+OAJSCzAH2s1+v30YcajsRZUEbhu +wxXWg+e01wKsJZjjcef47Q3N6H6h/H5HrtCz2+s/E3JmDNlD8zR1ueqfG0LjvmD9 +MJfjI8sHhRUBoQeLxUirn4oD0++jf3M4JIClZeq1ZHJBxvk+fyty4CTn7ekhqJvl +p9z+AF3MgpmHfbvzDUeqSKFVuLUd3KijI4I9vgbv5/EZuXP+punbXKIOFqjCyLpP +zToDrjupNIkddwvhNTaapHyxlR5fIRRRgGB2fVDkfsfNz9rIoEM6k7saAxgJrccz +Ry193nic4IuyX/LVFVxpX8rSYvVNmbaLPSOre6o4pc+26Etj5uddsLQAxPOdk4m3 +rd8lVNtKEHbQ/6IFC2wdH52v4kIc5NNIa3YnmjXzaQ3W0dPaS9VDruQm20+zHubs +LIU0kh1O9gSiTsPK3IHAu0Y/usdYES/IwxdyUR+Lue0XTS/NaKvt3BqZ5wnIQRKo +X1ka5iUwpmJ6OlI4eqc+3noHQfgNfYrhCR8g9A0FypHctE0pO2UTqCnaCmHuX4Gw ++I3Q7IWvpF/mqeRp6eerByt6H3iwvA93uQINBFLubUkBEADRMq7zeNj6dXGCY7JC +Uy2YqRvL5N5+AMF2iC4WZ/Lci8iALGcPcsSI8CwdTqGl9SOV/5qqBR3osz50tDoK +H+NUjd0sN86kefTVhk9a2TlTKTUmFocqc4sJi2uLl8gBySoyBwucMD1JULvxmdOp +i40n/YcIZ/NsUr5MZsLAxNRNbc9SiNhG6Ccq8mURbuwVx+S+qQEqgKAjMAeKeWDa ++kFAzfBRi+CoN0yvOF1hDmcXe0zQuShPZU1/KbbSWc0nUcO78b05xK1da5+/iTaU +4GepVYO8o11YiYEV4DgVTTBilFST27vaAe8Re1VBlKlQdSM6tuJAc8IG7FbGyu33 +mCzMNfj0niIErZIcFAsrwAeT3ea/d9ckp/xBK51hgRctaNl4Tw9GVudfrVspREGf +oUBwOICUhpv51gbuvNWdyUvThYdIGWPGO7NMMCfWFkiJi/UKd5PDcnif1DXnsw4M +FnV67AqWDr0neIxz46RjGvPBOERu7uFSrey70V5HA50rTETofr59dblnICDyS7Jn +yVM1pLzrKgm+R1LXilrH9+1dmEU/oJlmbY6ikX3IQTUZLnLsP3I/u0V8YbAa3Q4p +EqifZscPzw0A65FB1ihAjfj9Ar10LbPIOSbj8rLB2/hCA3TtkXvYxaq7jwOf68Gm +6M8Uh6h0EbVg/MkrAQhlPhtb1QARAQABiQIfBBgBAgAJBQJS7m1JAhsMAAoJED0h +T49bxe1zZdoP/0bMLMiOQFg1/64QeI0n8OcNbcVsWh+1NWi7LtTFX3pKuiWhTOiS +UJslD9Kwtbe9tqiOXxXoXO/XOPOZfa2hv6D7q9xyv5aGClFY5NXc7pNP3I6CqCh0 +6VOy99X2m9H2rYE9RCg4CRt1rIT1Uzespx+kdQgJNBSmwFFT/DvpbPQ+LZBu3izp +MK2qZXd2yoe4xv1Oo0dodU/OVgjkgQk38flphDUxOkkOy1meU42Oh6iY4BvuhelD +a9eJgtXovWqCGoZErbfQZMgzpZVeHjvLEsOUye0nZlo/hpTjiHYhUJrjZN3Muik5 +7BhHLm0MRu1o0kgAhE2Vd3qjKgMjQDnZGmn7bi3pSwdE6qob6B4A6dsN8R589tEN +haxPnmjjyM+F4dw/O//Hb2dwOv0386Kv8lNINdY/1S6HRNeh+c4eh6MAd7nf+vWU +JZjF6aPmr6Sa0VXVrMdsLo/7RBZxHtRBc8glQPM13hSYeU86a5Qn9AyHwS3fVgcc +pKOk2kLJ9XMRuzD70qWItebghB5Yrtp1sL0LMhNYBkAMv73QxoW11fI/6T3fBqAS +1xGI0yMF/tFTIP1TRwJ0uEgK9vOYlS01OM4ajLGfcV/ZWelQDCM2cJXshq/extL1 +C3Ba3TvZjzPPWR//c0wkF/4gg/V2A/9Jjam7BVS4JWd/bFRwZ5aR3qux +=AWz+ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/desktop/package/F379A1C6.asc b/desktop/package/F379A1C6.asc new file mode 100644 index 0000000000..19799d246b --- /dev/null +++ b/desktop/package/F379A1C6.asc @@ -0,0 +1,156 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFWK4uEBEADjSnRHU294auU1BPH+50OvsWnIvMb6kzqRdY3xlxecRAMsC/Dh +XyKVvY+wtC2a/1R+Cj5VO/geEDt0WBbwqj/zAi+x8ttrzZDn5CxmWvU6ulFCFKAr +cmB/eZmBMQSJ/JSZw1DeD090/tafuYUDjfhcqE1ajh7WxSIbMudaAm5yd/AuHB3c ++mlr5fjBwtBN1nyjfi9N3f7XJS8GrdJFC43/1FWHS3Z+GHydLkIcLS1keT5fYJbe +VZGC/RzUJBxqN6UFxIRJhPIplyBFfQBpWIFFxZNr6VZWeQlGnFjX1v3//hmD7mnT +3aGqqkUFcI5q7De3nNm2wfVnV50bzqj+FiSZWUUpWvgD01uzxWxzCVERn8s1jana +jLt3hfS8ly5kx311oZTyhXDR5z5LsrOjJv7U+hwhtDHAI0yyD7LPWCYFK2jwljYV +Tli8KHchMOlV0Yxm62ebmO/orju4Rq+T4id2nfwJGimRY/DX+k7/1qSHdyjnoYn1 +qqpVWD0UhjNLf337PThr20nA/FD3hjwnmIT5becHzrPGbRnr3Y2s77LFUe+nfGE3 +wvQmmpSNccFIz/146lynxJHWMfSqOJMgJZWpSPFKd39BhxxP9g5Sou6wEnM+YWYT +eOI1dGPejA4EHZec7s3j7hcx33rejydmsjW8yJjkRaFxYJk4jaoT7LgGiQARAQAB +tCVNYW5mcmVkIEthcnJlciA8bWFuZnJlZEBiaXRzcXVhcmUuaW8+iQI9BBMBCgAn +BQJViuLhAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEPW4RDbz +eaHGLFIQAM/w8BBoZ7+K4hxZmjkt/lXDXddHg9jvfU3jgIR+CkThcHy7n1e+QvsG +k0FozYsFyCaLwOiGR0/eUcUu+aegwRnx+eh/scElcAN40RYIr2nCU5vNGqmKBrP2 +ShQu0z5CcqFoccIHZ7VSe42SYb3GJ8g2mtC1Um+ryytZtF0g7nJxGWe//4YmqavC +TV4KU5akJGfFVPDW84qJJo34gwg80oshxnOQnXfoa+xlmSDQMaOTYH07cR9N64JW +TUad4aTl7niFZPizzg9r4ltRwUzvyXD5CHQoKqGWvZO0pHvRRq5SHp5nDoKh5hZs +lb/QJu6X8bTlLwhpOLsXPBPqoXQibRiICfAdVPBYnHMtvJ7RcuZyazvpYYjlgYsK +kol+jUude4zAIEky1A/77wl1pBXURw8vk8CRPIqAAOniaTySk6b24fseYsMcmP1Z +VLt4HxV0njBRAe51DV8AiT/gscTdg8/GsJRjzKedCs0SZIjBg5/1iULXRtQZrUd7 +vkOZZCSRzMAf4iHGK0qFuUoEkoZacv+bfOfwse62F89ngM1iB9RdyPPZIuh70i8O +Ebzzs6TBptq9WtV5LEXtkkHyfCugoIKegdKZmZBxnNT+XZMQQ68E8jeTiUA8MaPR +hYJp2FL+DLsTcLHt+ffHmcYJ1+6/v8UMIx6wC2k862+h4Y1aBme9iQEiBBABCgAM +BQJVwoK3BYMHhh+AAAoJEAxMpdRX1mvaFmYH/jx1ayv+6zNlYsFaL3idIBCmWQb7 +Lu4qE8bhSGN557jc7HoYT1DAYubm4zV65KxMVs+AsrNoy9Q4mXxpzIsi3X/J/3qF +L7hF+ZiZf8ms0FNScFW4rrWJpWaZ03zf97hx7D08oBxMtn++hA0/Ur7YN6fLtOe4 +sv19D8U6UvjT1LsDIpDXDUuLNTAcpq5liOGL9PHa3przvekuVIROgosGbdfY34KO +v9PfyL2H7Q6np7awjBE3GsbIMcEp+JsFgE8M3GJzke/absuqeNHpCgpIWPMoWCXb +guKOsVipIBuRNhaRG4hRFAQAUYRe2UL9ZH6BBKfAZoOkgYP+Kv01XGhADXGJAiIE +EAEKAAwFAlXCgr8FgweGH4AACgkQQBJQlmprLEaYSw//ccfvWZGxvi4R0EHM61pD ++Jp0iTdMb+8L1lK6+pzVaQvPf06UWD9qjN79cWDI0HmVFVgFPE0qRbsIi33s7ltF +Gc3Dw7ql2R7q1XS06pkT8ihesdYgauNA0802js5/RJK3joZEujNAQkz33O4daECf +MWEVia0JrFZktUlwVTjKOzKyoBlUiV/Rg/ivnTRVXyfDIp7qCUHcIhz19U4zK0kh +NKkgVxddKIeyivmUghzQbYEkAZlvBfLRXvnnK1TdouOgLOvHetf7LQDKpgHxJmre +o8XjHrF08/mDfKRvqh8Vi/j5zj8Kyy7LIjF3rHzCJDjwDp4tgSDEekMgEzYLoiyn +/y6lCxS78m+/EkCdE2Hncb81n6fgldQTSCChpfUbvqQAuewb9wonQp3gtqIdEwg1 +WS4T78m1qNfFP9I3UYKWRdSplifJAhr2NAyaf9fSNVSGRZk5sDcaXRrraPj5DHR6 +kgDITkv1ph6sB+4cu/6XmoZ8ZWAmmQFz4/EejlBu4khZHVPCojtGmbyOAAmV8M+a +4zWPXxdtOlKUCZpa7jOPkto4V804K5FsOn3qascdLdd/VYtjPWE/qoWs96/J81w5 +SJIXZ0s3j/EWwRKtcxZO22x0IXDIA5oPY7gQC1JDT/dt1blbGQ9nCLIPL5QoxxAF +noxMOtoVHlC98rjnPgCtuACJASIEEAEKAAwFAlXCgsYFgweGH4AACgkQeiIM+js9 +3FtGugf+JPu6S1RNVbgv8n1cX9Krt3JnXi0ybzhlCILxe8lRqzk9OXsVzY1Zvnor +0L35jYa2Y8GSEgivKEZXcdJroCXmBJRWs3ck3SSevxmqDm1+nQ96TBFtno6m8hf4 +3UoT0YnuryGffV0XEyO/m7ujIj5iF8UvWC6d+ve/oQw815IJROZNBjgRn6bhpgnq +sWVWioSQg9Jzqs/h8rjFWrscbln/mBCxyn6PsjIO6N1ArBcB9s95iCxiz6MXiMrl +vGbd6KIsaaG6H9IXfCFcOXkN+1pfr6439LRZMxC1hqHEqLWPV8iCwPyFJkHJUeyg +8hYuIFeEIvG4Z9ukEKrd27sh7eCgN4kCHAQQAQgABgUCVulMPQAKCRA97MEF9d0j +ghURD/4p+kGPeqQZq4sq4v7wxYPLnihTIdD1rZXOtWa3wnVOf5o03MGpXaaQIyez +LRgF1FSgkAV0v1kOJcUVOwZNXfivFAz5b5dV5cX8X/8AFc798gOQ2BpKDs8Gh4Vh +V+aV9Zslac0QLKA8LKmJOlVCb+GpQKmwPZ8IFfr+NtMhRW/h1WSualLHYpmmfH0A +GCnDM00w1pgGavtcTjIrvihA0uw4ySFT6QzI1z+1zmZlPsdpZEbpAeTyrGecDIRj +FAOTsmbe2YOk6kzj3xhwL8hMjtfX4EZl9KW1bGR6/fy8fVaM8lHi0Pa4BgIeca+M +ir+kHw7G1FgHrjiqOUuvCuK9uln1We0DttIi7RB1lYmX+Ds7XfSKj/OcrHWwxtJk +phKofIyGt9b12tqjJKdyS+81FBjgsiUSGQJdThm2vefVKKMqc91OlGPg8q3804x2 +3BlOHg98pz3TjOmrARSzhpGLz1KfS8o3YQYQ1HqymS1zyjuim5V+pf1s7bFg6RE9 +d0ipnTwEXmXIuU8fu09DAm6Z4o9XP2RP49cCieDOQ0dp2YKIzae6RkZjCjUPujiI +pZeN8rCageX6G0iuwKMOLfNn/g5ItHOm72U28aV6hMzTmHvdB+eaKl304N1fzjya ++FSncOcO9SazYGEKjs46ec48k389lXZ7nMuZDMwPZgsDyzZWn4kCHAQSAQoABgUC +WDhcJAAKCRDAwHYTL/p2lSfUD/sGJhNJiIrGYf9qw8qBQJyaQDoNBFHLvl2tUpOf ++TVojDywJ+51askcL3L0hldbUg4wziPi2FV8AtRyerDKNcMUJYn6SQ3Rhx/7eFP/ +vnUqJ7f8ZJEk7LzGYDZGQpnSe/eyXNARVjoPUFhjl6mTLtKPZWfaprs2e+yvQimy +2hgsiWOvc19ifsRg6KVSSTBqUS+FCSw0VRR1wt5cmrFRkuRfGoCHHd8mXkI1qSit +xfFQxyURxHWxLkWnwN9y0G8cYvSOI/hgmfY/MYY6NRWKbmzXIve5n7qFKNFBR3n4 +NA9oJwI2Mzop8GuwSU54QlmiG6N0Elqt7c+aU1bOGt3dWJS8e5J7VBZqoFrIzPAC +DObdSkU3Y04ZQ5LcnAn0n6dpZRTX2Fv0Tcxv7MCEfQCDCeBs9xDrXIcEZLNyrBQE +kcXbL4EUBjsq80fLV5/a5iyhS67pJc10mS5T8pkFd7hA6eTesRRbP2Do1ndiZCPw +E2gugDmz6hjTAUwG6iLu2rwJ2aOOm3V3PmYZ/JM45zGTjKFb2sEzkuOG1YdHIt30 +FXqEswItLMWQl5xTwuHId9mPvgKLz9h5ZYt8ML5G+QXFEVnKiU0pFWabDgpb81cS +0aAYQcSOUG5ObyBjHsZXwQKNpe9oEFN6xrbE7dp4FxtZpZXExLE+PUffxjOyGw0W +Lj+WYokCPQQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCWU795AUJ +BaVOgwAKCRD1uEQ283mhxrjVD/41bwb1y38w1K3dlOavw6t2RwwvmBgNDlhLFrM1 +ZK3Kjk+p9s2/8GoeGdPiVgrqv3okI+Ztme+R+jtWRPSozczfZyIdgR2/jdhS8P0L +IUIbQlkn7cvvDb4Wf8lAUhnGF/a+Gmnpn+Ju65KcTxFBGSSt5q2iQVbsW+krhyoy +nuD6C/2QLDKH+YPOahihmrTpLQkJ4IwdK+0LfoWqcgNB5JiRKd4fcgXEYTxxBMSc +5QwlRkU638PTkjGaBbb7I+RxWrk3Y75SyyFbD/svJm4JxQGFQCvPOiesSTQrQuCV +opoZj0YKfZzUpgiVYQFm1MCLLhWs9nDxJ0d2lxropUTm+8BYuuy/pSki60GGbKv6 +MnWUhExmde01U3wjxkHeXX9u2qswL/spORVtqtxDvWQeUyZyhIs1Slled+7RLOww +bJNamKGVdBcN3XZwaxNeuBX1nppjKKeleS56C0BFuTVEptEsdRj62FVJIli1MH43 +IxAN0iJUsO2XSljhmixQu37jfkLW4HlCLiZwYLCJDoXFtHZZ5nwURGeBGSeGyWzC +tx7DJvXDEx/GWMJzU500X4iTc5gcvLLsTm5bxKthOITITHgvXXAMmc0YpLmmueZA +UNMShQsxkzm3QOBCVgjW532OQHM66Plsact4hCYJ+p0GSQGcUgKmNGcfQKmLNhvR +oT2NlYkCIgQTAQoADAUCWF7kZAWDB4YfgAAKCRBAElCWamssRtBxD/0RK9Rk9eCg +jj61Vk8rA/Uvmz7ZEwHlunL6pucvy2RZL+ztMNSlLPYcvtByvSvUo8Q9G/YnjR6l +EGMi5DERN5Euc2nMIlg82EWQyd3MEAcBxcqriLuKrybizce1o8pUExV84DJYchr/ +A9ei93GiHbNodMxv5zt+4pu/e540DxATf9ME6EpJtbzJcUwsGUrPOtC9Xp13t1sL +4sHL1z3TVPOzOQ8HSlfMOUdYNoJg89MjTnX//rOpfSIq3FcEVURipOgLKDhiQEmQ +tpeknv7uZDZuRLxK/J7IebmnbnmV3wq3l5LVCLRTzpCXtucTwnKoRYBcbT676F70 +GbM7QPwdiyiq17lx3+YHElHm0KGfxn6iUSPGtEJv6RcO8glU/VIN6/N8HxH5Y9HE +28+eC094GnKD3xQtMzSrzTkp7q5NGZPBS4wT3mGwem4pfjuYbkWea3+71jnr6SHW +ciGWo4kXuVp9Va1ZFuNxk8o0G8YdHV30oEXJwTyyxXFMdePraz95B4fjtJdpgsnq +JLsFIIgdpECBeiyqy83pOCv5aoRiqz8xAYrkZWYKtw1xdMZ25LoyI/OgdmCIjrr1 +q+VGPXi2CcB2Y4XmJCUCYUh1W7eGoXlOEWA3upONa+RkrOZ0bEQ/VuHqvWeVol7i +uWqdxi9IrjPK2hhrMPMKeNphpjLLitfWM4kBIgQTAQoADAUCWF7kdQWDB4YfgAAK +CRAMTKXUV9Zr2pLpB/9K4hu0ELE9D5ZvsFlD6lQUNvonuiZGX/xBsejYL/rEPWSv +2dJpb815rPkJtoqUZ5cem2sJhOKc+ZIKHZy0hiobqbePZXArT7dh5aIfMYfFfvYE +YOZYUD9dbMjzjqHrpfUvht6IxekYfkm+XKYL1PGdxGZ4AiK9ZoRVnM/eK2p6+qcG +VEPkHJXJKNRMvPJTniKsW3vryqM0J0Z6wDqH9IEWIhus1GeEcm/j7Hw0OO5gku2/ +uI8SndS6dtCjruGcVLpG0quMdCFCsp/jtH1y1opFDT3cW7g7q3RQxw0dNzflraaz +vQm+caO8QtZGVS6vegaOf2a4hfACKtt9EzVVLq93iQIiBBMBCgAMBQJYXuR9BYMH +hh+AAAoJEMM+Vu/rhndFAXwP/2bAkgdM2CZ/WRXRAecBCCIz483+bS4yXaraSeJz +bWl8Cp3aVMqRyLGcDo0UhYRTzDfgcX2YLlK0pBwAnvNd2hi8cqQC1RWOdYrgUMzN +/StI9NOImov0n8kWqK+kqdxJIwz2Rs3crlwpDR5bosTzG/HwwxtNGB1h1T72RrGf +ISOrqtRgHnAod9DBluONJaUv1QN95zHCVQqh0deAZLYtOlPhUbXYgfZQGlufUBpt +SJAsA2W5yuN+Qiav7TetqQDN/zapPIxGSkfuN+t22ek65OyAbEHQP7Z1ltI/+PEM +DaqU+Pzb9UJtZEpcHDCTB4YbI9+H6k/WAp6w2Cm3nNEJiaYAr8XocxxTMnjlVtnr +OE/4++wuQE8EvcuvnxXkhyLbZnhAdpv1lUgzll3pAhe9w4RCpX4tz5JHB5EoQXAP +LHIQRzWc8TzE4H+aUdSwrewQL3qxrgadJDST2Em/DFIj8EftwbJCLpm8jtp9fw7U +gP4iv8hYhUBCRH24n5h8YwbwCOOGadLNfoUh6DHHa7ZdktOADQfcmOhA840FoL+v +t85+sVCgGaBsoPkgS/ZvE5KRbtugiMuCO5OS3lIpzDjaw+2nH1Wq2uuuBya3owyJ +GIxKtLNxg/pZhwnSsVwi3sDmmXLq6WDTCAfeR+8+PKgC6IgDUAYalpoxEq7grVi9 +e/yiiQEiBBMBCgAMBQJYXuSFBYMHhh+AAAoJEHoiDPo7PdxbfaEH/AshsMxZBRt3 +f5f4JrTQtk8veAqwKzHNUMbl9sQzBjEvg9V7SaoGp1cKPb1e9Sy1VDD496dBYgxc +rUOG7Wp3XD/Gccht2a/EL+CJaJHyVzfskLTtFtnrMItLCR7uuqQEvK/Q2IxJpaMt +wNKEpzDCgKzFXUjet/gB2j90dMWqPVDh/GRiktrcDVXaX3roMBenWmzeBpT8oUA2 +Tw8gUr696Y7d2RgDHncTlm/qS5w1QyLT5CIXd1Os6+eA8MIjFUSKaDxNM9zCzGvv +hkj7bBfi1xxmPonogGKd56tgBFZ7EGeFD/TGLjCtJLYz8pPP/F2az557xFJ41aVt +Cq0wTtHSrsGJAiIEEwEKAAwFAlhe/hQFgweGH4AACgkQNmQEJH0guzLtoRAAvA1V +ZPyE/RoTjTkZ0468R7txGSNiQBMHeKQzSj5vrFvFjuQOx1pKvPbBa//pfddmXsyN +4+fXkm+i3jhiww85VmfP+jaPZE7ha3gI1sLIXywBUEQXGtN+JrGdIfx4fm9Xj+Km +a41o77XxnYxg/puqtoxXuFQfF+KcX3SgCzaGnhn+p2YfUtIgqaVkQl6H6vhKley8 +pZcB58O9Eu0RbpGg/FWovOWY/Jg8DdbOpQmrp4tXD116rt8m0jEJcWk/DPexehHn +Znt4Xi/oogBiccRDd/ebUeyjUkkrPk+IQjdYYOuN0i0nMUL9KsWLJwUHNa2IWv1e +xgVg9dWuPk13K2hJFzdvGa19IVsBOEEXgfIyC2ZSqz0zFhAQQ/2saRDvITgQS10W +duL55lv78YevjqeETEHW2DeXkzUiRwe64BUuu/9LFsSLuwCwLrvz3Yyh0T21MAAA +/5sHsai4hRhxAhVoWfelKShzmZdh7bdqrxDrivutdcOn9Evdw3IQ9rsDtgyDrvmm +Mok1eSYvZF61yhHvdVU6wQOET7u3T7eSFoAW7EknuAd4rSIZ2AqBchARGEbz3m7w +aidB1KmedzlGNk0DEWcXiqpdgQdvalzxfJJSIOsJic7FH+p2xBnFYBVdS/ftgrC2 +kuPY4dpfVviNxGLrRDd8fYfdVDolMW1pOWo7oQq5Ag0EVYri4QEQAOtygi1rXfDl +/H18Evad7dz96ZFDGSQNoD9eC4UCGD5F2AqEil7pTNapIDqcGaz1MZl5k4B9CjH7 +mutQukLXcHtdrc5eXYjMQZ/jVFjjv5j3fPgwWrz6LfxYD/jxw7uTgDHlgEo/Dv8D +WMeE3wcycKhlG9KT/qdx+1b36ds7ecYeooYIxHSCAbQl+4mKjn4HNIhAGTcNe7i9 +79rGApBNJgpSYnaqK7i3CFvIeMRWLQKk41s4sBrwZI+hEFnlZoJ3Le7Mh/0emcfs +ZCk4YNwdfGiZWoic8ZMudx0JUkso/ELRxzx/bgNls+vpQb3SQ1zuFZ8xnOunEmaf +DYbg/hJguAT3fnvGqqeO0305+OVflxcoUyxXDxLtY+4t6SEj2v3L9t+ZpbQg12+d +lr3Eel+NltXibv2yVhwP0NpQq+CJ+nPDQWsCcK/FelP2ik1EZqasQPZZFBORKXNV +JCmWXm+8GNbwN9wvVR9rmwh0h8v9RAbh7Q4inYnxiVVKIH14ZGQp8i3NW/k5sOuk +RqM203tEV4LGCP+bwswcwPCmvfid3L8oQmPA7ezL6rmlehe7ctP4iHEX/xxGbRzD +ZWNdNZrTdq0h1WR6ce7Ya52VNN1dBoqkbZmzQxD+NC/3dv/yl8MfnEeJDdvQCGQ2 +0zCbGfXWc2T9ov4BK/a05cDBlXaIpH/fABEBAAGJAiUEGAEKAA8FAlWK4uECGwwF +CQeGH4AACgkQ9bhENvN5ocZGxA/+I+GLTTFaHRy6ZNmAr6uEPQ59yXOE5k2ZrML7 +F2nnIR0FJFydhnLSsxCt89zXxmxk4kA4h+M5jmyB4HiIGp0u0lC/zpklJwJ8+EKj +KpSaL9zdo1hwojybGar78mF4qsQ2EZP0TIq41gOZ/qx7dVaDSu75cuQvgGakEQcx +89B5RGaZRKLlE68Mo2QXktNENnPFkkOPBoil8KX34DHIWJafncwu0vObcE31ifIZ +j9j3FoeupnIW4HXEbsZBWkM0k/Fzx3wdvvYuEwR0JvihSJ4YEncB33weZ+u1+XTa +cAWt98oubYMoR+M2d4+EAmOJVjz0oGXNvs/BBwSCem3c/oSt43R3lc7zMU8shZf8 +bKS+TGYnV/kRWcNc2l0BTiRRUwFZ0/XvAcNXJsB1CyrvbWvrZiDIm6tA3xOJzFGY +wLNTM1BqfNfrPbzov67vkkbxxRlTRx1x6LTFPV0H1FTZ5CSQgahjm9SwANb0jyU7 +xR9hL3zBvKr7quR7mM1zzjnoGkNMdVsM02fBrmqfhABychMFMVVOWhyLLQO47YZB +ghu/JigFHreRBbTOPLcCSfkH24EL91nDnfLp6KHLcz2DfU2W1lajwRfDm2rpbKx+ +6iAnmNBJV49ZaM7lFqPaJz942mVySd+4rygkuF1olWxN1EbzK0/bKRuzljIj5U+r +vUTpzlk= +=ZIr3 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/desktop/package/linux/Dockerfile b/desktop/package/linux/Dockerfile new file mode 100644 index 0000000000..14e02228fb --- /dev/null +++ b/desktop/package/linux/Dockerfile @@ -0,0 +1,20 @@ +### +# +# Quick dockerfile meant to help building. +# Missing: +# - crypto fixes to JDK +# - various paths in the build script +### + +# pull base image +FROM openjdk:8-jdk +ENV version 1.6.2-SNAPSHOT + +RUN apt-get update && apt-get install -y --no-install-recommends openjfx && rm -rf /var/lib/apt/lists/* && +apt-get install -y vim fakeroot + + +COPY 64bitBuild.sh /root +COPY bisq-$version.jar /root +# cd to the Dex directory and execute the jar. +#CMD cd ~/Dex && java -jar Dex.jar diff --git a/desktop/package/linux/icon.png b/desktop/package/linux/icon.png new file mode 100644 index 0000000000..84b00ccbea Binary files /dev/null and b/desktop/package/linux/icon.png differ diff --git a/desktop/package/macosx/Bisq-background.tiff b/desktop/package/macosx/Bisq-background.tiff new file mode 100644 index 0000000000..9fb7dde469 Binary files /dev/null and b/desktop/package/macosx/Bisq-background.tiff differ diff --git a/desktop/package/macosx/Bisq-dmg-setup.scpt b/desktop/package/macosx/Bisq-dmg-setup.scpt new file mode 100644 index 0000000000..d07ed4b0b7 --- /dev/null +++ b/desktop/package/macosx/Bisq-dmg-setup.scpt @@ -0,0 +1,41 @@ +tell application "Finder" + tell disk "Bisq" + open + set current view of container window to icon view + set toolbar visible of container window to false + set statusbar visible of container window to false + set pathbar visible of container window to false + + -- size of window should match size of background (1034x641) + set the bounds of container window to {400, 100, 1434, 741} + + set theViewOptions to the icon view options of container window + set arrangement of theViewOptions to not arranged + set icon size of theViewOptions to 128 + set background picture of theViewOptions to file ".background:background.tiff" + + -- Create alias for install location + make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"} + + set allTheFiles to the name of every item of container window + repeat with theFile in allTheFiles + set theFilePath to POSIX Path of theFile + if theFilePath is "/Bisq.app" + -- Position application location + set position of item theFile of container window to {345, 343} + else if theFilePath is "/Applications" + -- Position install location + set position of item theFile of container window to {677, 343} + else + -- Move all other files far enough to be not visible if user has "show hidden files" option set + set position of item theFile of container window to {1000, 0} + end + end repeat + + close + open + update without registering applications + delay 5 + end tell +end tell + diff --git a/desktop/package/macosx/Bisq-volume.icns b/desktop/package/macosx/Bisq-volume.icns new file mode 100644 index 0000000000..d60546972a Binary files /dev/null and b/desktop/package/macosx/Bisq-volume.icns differ diff --git a/desktop/package/macosx/Bisq.icns b/desktop/package/macosx/Bisq.icns new file mode 100644 index 0000000000..d60546972a Binary files /dev/null and b/desktop/package/macosx/Bisq.icns differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_128x128.png b/desktop/package/macosx/Bisq.iconset/icon_128x128.png new file mode 100644 index 0000000000..8ce4f1a692 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_128x128.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_128x128@2x.png b/desktop/package/macosx/Bisq.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..14aa898df9 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_128x128@2x.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_16x16.png b/desktop/package/macosx/Bisq.iconset/icon_16x16.png new file mode 100644 index 0000000000..ee1e2496ea Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_16x16.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_16x16@2x.png b/desktop/package/macosx/Bisq.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..efb686497e Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_16x16@2x.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_256x256.png b/desktop/package/macosx/Bisq.iconset/icon_256x256.png new file mode 100644 index 0000000000..0dfd20d60c Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_256x256.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_256x256@2x.png b/desktop/package/macosx/Bisq.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..5f18f50176 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_256x256@2x.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_32x32.png b/desktop/package/macosx/Bisq.iconset/icon_32x32.png new file mode 100644 index 0000000000..a0f19e09f9 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_32x32.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_32x32@2x.png b/desktop/package/macosx/Bisq.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..581433e968 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_32x32@2x.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_512x512.png b/desktop/package/macosx/Bisq.iconset/icon_512x512.png new file mode 100644 index 0000000000..eb91e3623c Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_512x512.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_512x512@2x.png b/desktop/package/macosx/Bisq.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..9a97499742 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_512x512@2x.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_64x64.png b/desktop/package/macosx/Bisq.iconset/icon_64x64.png new file mode 100644 index 0000000000..8f84e615f7 Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_64x64.png differ diff --git a/desktop/package/macosx/Bisq.iconset/icon_64x64@2x.png b/desktop/package/macosx/Bisq.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..082f7c633d Binary files /dev/null and b/desktop/package/macosx/Bisq.iconset/icon_64x64@2x.png differ diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist new file mode 100644 index 0000000000..e7ac7c34c9 --- /dev/null +++ b/desktop/package/macosx/Info.plist @@ -0,0 +1,58 @@ + + + + + + + CFBundleVersion + 1.6.2 + + CFBundleShortVersionString + 1.6.2 + + CFBundleExecutable + Bisq + + CFBundleName + Bisq + + CFBundleIdentifier + io.bisq.CAT + + CFBundleIconFile + Bisq.icns + + LSApplicationCategoryType + public.app-category.finance + + NSHumanReadableCopyright + Copyright © 2013-2021 - The Bisq developers + + + LSAppNapIsDisabled + + + + NSSupportsAutomaticGraphicsSwitching + + + NSHighResolutionCapable + true + + CFBundleAllowMixedLocalizations + + + LSMinimumSystemVersion + 10.7.4 + + CFBundleDevelopmentRegion + English + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + APPL + + diff --git a/desktop/package/macosx/copy_dbs.sh b/desktop/package/macosx/copy_dbs.sh new file mode 100755 index 0000000000..f1e49afb02 --- /dev/null +++ b/desktop/package/macosx/copy_dbs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +cd $(dirname $0)/../../../ + +version="1.6.2" + +# Set BISQ_DIR as environment var to the path of your locally synced Bisq data directory e.g. BISQ_DIR=~/Library/Application\ Support/Bisq + +dbDir=$BISQ_DIR/btc_mainnet/db +resDir=p2p/src/main/resources + +# Only commit new TradeStatistics3Store if you plan to add it to +# https://github.com/bisq-network/bisq/blob/0345c795e2c227d827a1f239a323dda1250f4e69/common/src/main/java/bisq/common/app/Version.java#L40 as well. +cp "$dbDir/TradeStatistics3Store" "$resDir/TradeStatistics3Store_${version}_BTC_MAINNET" +cp "$dbDir/AccountAgeWitnessStore" "$resDir/AccountAgeWitnessStore_${version}_BTC_MAINNET" +cp "$dbDir/DaoStateStore" "$resDir/DaoStateStore_BTC_MAINNET" +cp "$dbDir/SignedWitnessStore" "$resDir/SignedWitnessStore_BTC_MAINNET" diff --git a/desktop/package/macosx/finalize.sh b/desktop/package/macosx/finalize.sh new file mode 100755 index 0000000000..077117cf8d --- /dev/null +++ b/desktop/package/macosx/finalize.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +cd ../../ + +version="1.6.2-SNAPSHOT" + +target_dir="releases/$version" + +# Set BISQ_GPG_USER as environment var to the email address used for gpg signing. e.g. BISQ_GPG_USER=manfred@bitsquare.io +# Set BISQ_VM_PATH as environment var to the directory where your shared folders for virtual box are residing + +vmPath=$BISQ_VM_PATH +linux64=$vmPath/vm_shared_ubuntu +win64=$vmPath/vm_shared_windows +macos=$vmPath/vm_shared_macosx + +deployDir=deploy + +rm -r $target_dir + +mkdir -p $target_dir + +# sig key mkarrer +cp "$target_dir/../../package/F379A1C6.asc" "$target_dir/" +# sig key cbeams +cp "$target_dir/../../package/5BC5ED73.asc" "$target_dir/" +# sig key Christoph Atteneder +cp "$target_dir/../../package/29CDFD3B.asc" "$target_dir/" +# signing key +cp "$target_dir/../../package/signingkey.asc" "$target_dir/" + +dmg="Bisq-$version.dmg" +cp "$macos/$dmg" "$target_dir/" + +deb="bisq_$version-1_amd64.deb" +deb64="Bisq-64bit-$version.deb" +cp "$linux64/$deb" "$target_dir/$deb64" + +rpm="bisq-$version-1.x86_64.rpm" +rpm64="Bisq-64bit-$version.rpm" +cp "$linux64/$rpm" "$target_dir/$rpm64" + +exe="Bisq-$version.exe" +exe64="Bisq-64bit-$version.exe" +cp "$win64/$exe" "$target_dir/$exe64" + +rpi="jar-lib-for-raspberry-pi-$version.zip" +cp "$deployDir/$rpi" "$target_dir/" + +cd "$target_dir" + +echo Create signatures +gpg --digest-algo SHA256 --local-user $BISQ_GPG_USER --output $dmg.asc --detach-sig --armor $dmg +gpg --digest-algo SHA256 --local-user $BISQ_GPG_USER --output $deb64.asc --detach-sig --armor $deb64 +gpg --digest-algo SHA256 --local-user $BISQ_GPG_USER --output $rpm64.asc --detach-sig --armor $rpm64 +gpg --digest-algo SHA256 --local-user $BISQ_GPG_USER --output $exe64.asc --detach-sig --armor $exe64 +gpg --digest-algo SHA256 --local-user $BISQ_GPG_USER --output $rpi.asc --detach-sig --armor $rpi + +echo Verify signatures +gpg --digest-algo SHA256 --verify $dmg{.asc*,} +gpg --digest-algo SHA256 --verify $deb64{.asc*,} +gpg --digest-algo SHA256 --verify $rpm64{.asc*,} +gpg --digest-algo SHA256 --verify $exe64{.asc*,} +gpg --digest-algo SHA256 --verify $rpi{.asc*,} + +mkdir $win64/$version +cp -r . $win64/$version + +open "." diff --git a/desktop/package/macosx/insert_snapshot_version.sh b/desktop/package/macosx/insert_snapshot_version.sh new file mode 100755 index 0000000000..1147e7c997 --- /dev/null +++ b/desktop/package/macosx/insert_snapshot_version.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +cd $(dirname $0)/../../../ + +version=1.6.2 + +find . -type f \( -name "finalize.sh" \ +-o -name "create_app.sh" \ +-o -name "build.gradle" \ +-o -name "release.bat" \ +-o -name "package.bat" \ +-o -name "release.sh" \ +-o -name "package.sh" \ +-o -name "version.txt" \ +-o -name "Dockerfile" \ +\) -exec sed -i '' s/$version/"$version-SNAPSHOT"/ {} + diff --git a/desktop/package/macosx/macos.entitlements b/desktop/package/macosx/macos.entitlements new file mode 100644 index 0000000000..0e0df6c762 --- /dev/null +++ b/desktop/package/macosx/macos.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/desktop/package/macosx/replace_version_number.sh b/desktop/package/macosx/replace_version_number.sh new file mode 100755 index 0000000000..1ba86fa450 --- /dev/null +++ b/desktop/package/macosx/replace_version_number.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +cd $(dirname $0)/../../../. + +oldVersion=1.6.1 +newVersion=1.6.2 + +find . -type f \( -name "finalize.sh" \ +-o -name "create_app.sh" \ +-o -name "build.gradle" \ +-o -name "release.bat" \ +-o -name "package.bat" \ +-o -name "release.sh" \ +-o -name "package.sh" \ +-o -name "version.txt" \ +-o -name "Dockerfile" \ +\) -exec sed -i '' s/"$oldVersion-SNAPSHOT"/$newVersion/ {} + + +find . -type f \( -name "Info.plist" \ +-o -name "SeedNodeMain.java" \ +-o -name "Version.java" \ +-o -name "copy_dbs.sh" \ +\) -exec sed -i '' s/$oldVersion/$newVersion/ {} + + diff --git a/desktop/package/package.gradle b/desktop/package/package.gradle new file mode 100644 index 0000000000..eeab2e6c6b --- /dev/null +++ b/desktop/package/package.gradle @@ -0,0 +1,518 @@ +import java.time.LocalDateTime +import org.apache.tools.ant.taskdefs.condition.Os + +import static groovy.io.FileType.* + +task jpackageSanityChecks { + description 'Interactive sanity checks on the version of the code that will be packaged' + + doLast { + executeCmd("git --no-pager log -5 --oneline") + ant.input(message: "Above you see the current HEAD and its recent history.\n" + + "Is this the right commit for packaging? (y=continue, n=abort)", + addproperty: "sanity-check-1", + validargs: "y,n") + if (ant.properties['sanity-check-1'] == 'n') { + ant.fail('Aborting') + } + + executeCmd("git status --short --branch") + ant.input(message: "Above you see any local changes that are not in the remote branch.\n" + + "If you have any local changes, please abort, get them merged, get the latest branch and try again.\n" + + "Continue with packaging? (y=continue, n=abort)", + addproperty: "sanity-check-2", + validargs: "y,n") + if (ant.properties['sanity-check-2'] == 'n') { + ant.fail('Aborting') + } + + // TODO Evtl check programmatically in gradle (i.e. fail if below v11) + executeCmd("java --version") + ant.input(message: "Above you see the installed java version, which will be used to compile and build Bisq.\n" + + "Is this java version ok for that? (y=continue, n=abort)", + addproperty: "sanity-check-3", + validargs: "y,n") + if (ant.properties['sanity-check-3'] == 'n') { + ant.fail('Aborting') + } + } +} + +task getJavaBinariesDownloadURLs { + description 'Find out which JDK will be used for jpackage and prepare to download it' + dependsOn 'jpackageSanityChecks' + + doLast { + // The build directory will be deleted next time the clean task runs + // Therefore, we can use it to store any temp files (separate JDK for jpackage, etc) and resulting build artefacts + // We create a temp folder in the build directory which holds all jpackage-related artefacts (not just the final installers) + String tempRootDirName = 'temp-' + LocalDateTime.now().format('yyyy.MM.dd-HHmmssSSS') + File tempRootDir = new File(project.buildDir, tempRootDirName) + tempRootDir.mkdirs() + ext.tempRootDir = tempRootDir + println "Created temp root folder " + tempRootDir + + File binariesFolderPath = new File(tempRootDir, "binaries") + binariesFolderPath.mkdirs() + ext.binariesFolderPath = binariesFolderPath + + // TODO Extend script logic to alternatively allow a local (separate, v14+) JDK for jpackage + // TODO Another option is to use the local JDK for everything: build jars and use jpackage (but then it has to be v14+) + + // Define the download URLs (and associated binary hashes) for the JDK used to package the installers + // These JDKs are independent of what is installed on the building system + // + // If these specific versions are not hosted by AdoptOpenJDK anymore, or if different versions are desired, + // simply update the links and associated hashes below + // + // See https://adoptopenjdk.net/releases.html?variant=openjdk15&jvmVariant=hotspot for latest download URLs + // On the download page linked above, filter as follows to get the binary URL + associated SHA256: + // - architecture: x64 + // - operating system: + // -- linux ( -> use the tar.gz JDK link) + // -- macOS ( -> use the tar.gz JDK link) + // -- windows ( -> use the .zip JDK link) + Map jdk15Binaries = [ + 'linux' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.2%2B7/OpenJDK15U-jdk_x64_linux_hotspot_15.0.2_7.tar.gz', + 'linux-sha256' : '94f20ca8ea97773571492e622563883b8869438a015d02df6028180dd9acc24d', + 'mac' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.2%2B7/OpenJDK15U-jdk_x64_mac_hotspot_15.0.2_7.tar.gz', + 'mac-sha256' : 'd358a7ff03905282348c6c80562a4da2e04eb377b60ad2152be4c90f8d580b7f', + 'windows' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.2%2B7/OpenJDK15U-jdk_x64_windows_hotspot_15.0.2_7.zip', + 'windows-sha256': 'b80dde2b7f8374eff0f1726c1cbdb48fb095fdde21489046d92f7144baff5741' + + // TODO For some reason, using "--runtime-image jdk-11" does NOT work with a v15 jpackage, but works with v14 + ] + + String osKey + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + osKey = 'windows' + } else if (Os.isFamily(Os.FAMILY_MAC)) { + osKey = 'mac' + } else { + osKey = 'linux' + } + + ext.jdk15Binary_DownloadURL = jdk15Binaries[osKey] + ext.jdk15Binary_SHA256Hash = jdk15Binaries[osKey + '-sha256'] + } +} + +task retrieveAndExtractJavaBinaries { + description 'Retrieve necessary Java binaries and extract them' + dependsOn 'getJavaBinariesDownloadURLs' + + doLast { + File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") as File + + // Folder where the jpackage JDK archive will be downloaded and extracted + String jdkForJpackageDirName = "jdk-jpackage" + File jdkForJpackageDir = new File(tempRootDir, jdkForJpackageDirName) + jdkForJpackageDir.mkdirs() + + String jdkForJpackageArchiveURL = getJavaBinariesDownloadURLs.property('jdk15Binary_DownloadURL') + String jdkForJpackageArchiveHash = getJavaBinariesDownloadURLs.property('jdk15Binary_SHA256Hash') + String jdkForJpackageArchiveFileName = jdkForJpackageArchiveURL.tokenize('/').last() + File jdkForJpackageFile = new File(jdkForJpackageDir, jdkForJpackageArchiveFileName) + + // Download necessary JDK binaries + verify hash + ext.downloadAndVerifyArchive(jdkForJpackageArchiveURL, jdkForJpackageArchiveHash, jdkForJpackageFile) + + // Extract them + String jpackageBinaryFileName + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + ext.extractArchiveZip(jdkForJpackageFile, jdkForJpackageDir) + jpackageBinaryFileName = 'jpackage.exe' + } else { + ext.extractArchiveTarGz(jdkForJpackageFile, jdkForJpackageDir) + jpackageBinaryFileName = 'jpackage' + } + + // Find jpackage in the newly extracted JDK + // Don't rely on hardcoded paths to reach it, because the path depends on the version and platform + jdkForJpackageDir.traverse(type: FILES, nameFilter: jpackageBinaryFileName) { + println 'Using jpackage binary from ' + it + ext.jpackageFilePath = it.path + } + } + + ext.downloadAndVerifyArchive = { String archiveURL, String archiveSHA256, File destinationArchiveFile -> + println "Downloading ${archiveURL}" + ant.get(src: archiveURL, dest: destinationArchiveFile) + println 'Download saved to ' + destinationArchiveFile + + println 'Verifying checksum for downloaded binary ...' + ant.jdkHash = archiveSHA256 + ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${jdkHash}', verifyProperty: 'hashMatches') + if (ant.properties['hashMatches'] != 'true') { + ant.fail('Checksum mismatch: Downloaded JDK binary has a different checksum than expected') + } + println 'Checksum verified' + } + + ext.extractArchiveTarGz = { File tarGzFile, File destinationDir -> + println "Extracting tar.gz ${tarGzFile}" + // Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly) + copy { + from tarTree(resources.gzip(tarGzFile)) + into destinationDir + } + println "Extracted to ${destinationDir}" + } + + ext.extractArchiveZip = { File zipFile, File destinationDir -> + println "Extracting zip ${zipFile}..." + ant.unzip(src: zipFile, dest: destinationDir) + println "Extracted to ${destinationDir}" + } +} + +task packageInstallers { + description 'Call jpackage to prepare platform-specific binaries for this platform' + dependsOn 'retrieveAndExtractJavaBinaries' + // Clean all previous artefacts and create a fresh shadowJar for the installers + dependsOn rootProject.clean + dependsOn ':desktop:shadowJar' + + doLast { + String jPackageFilePath = retrieveAndExtractJavaBinaries.property('jpackageFilePath') + File binariesFolderPath = file(getJavaBinariesDownloadURLs.property('binariesFolderPath')) + + File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") as File + // The jpackageTempDir stores temp files used by jpackage for building the installers + // It can be inspected in order to troubleshoot the packaging process + File jpackageTempDir = new File(tempRootDir, "jpackage-temp") + jpackageTempDir.mkdirs() + + // ALL contents of this folder will be included in the resulting installers + // However, the fat jar is the only one we need + // Therefore, this location should point to a folder that ONLY contains the fat jar + // If later we will need to include other non-jar resources, we can do that by adding --resource-dir to the jpackage opts + String fatJarFolderPath = "${project(':desktop').buildDir}/libs/fatJar" + String mainJarName = shadowJar.getArchiveFileName().get() + + delete(fatJarFolderPath) + mkdir(fatJarFolderPath) + copy { + from "${project(':desktop').buildDir}/libs/${mainJarName}" + into fatJarFolderPath + } + + // We convert the fat jar into a deterministic one by stripping out comments with date, etc. + // jar file created from https://github.com/ManfredKarrer/tools + executeCmd("java -jar \"${project(':desktop').projectDir}/package/tools-1.0.jar\" ${fatJarFolderPath}/${mainJarName}") + + // Store deterministic jar SHA-256 + ant.checksum(file: "${fatJarFolderPath}/${mainJarName}", algorithm: 'SHA-256') + copy { + from "${fatJarFolderPath}/${mainJarName}.SHA-256" + into binariesFolderPath + } + + // TODO For non-modular applications: use jlink to create a custom runtime containing only the modules required + + // See jpackager argument documentation: + // https://docs.oracle.com/en/java/javase/15/docs/specs/man/jpackage.html + + // Remove the -SNAPSHOT suffix from the version string (originally defined in build.gradle) + // Having it in would have resulted in an invalid version property for several platforms (mac, linux/rpm) + String appVersion = version.replaceAll("-SNAPSHOT", "") + println "Packaging Bisq version ${appVersion}" + + // zip jar lib for Raspberry Pi only on macOS as there are path issues on Windows and it is only needed once + // for the release + if (Os.isFamily(Os.FAMILY_MAC)) { + println "Zipping jar lib for raspberry pi" + ant.zip(basedir: "${project(':desktop').buildDir}/app/lib", + destfile: "${binariesFolderPath}/jar-lib-for-raspberry-pi-${appVersion}.zip") + } + + String appDescription = 'A decentralized bitcoin exchange network.' + String appCopyright = '© 2021 Bisq' + String appNameAndVendor = 'Bisq' + + String commonOpts = new String( + // Generic options + " --dest \"${binariesFolderPath}\"" + + " --name ${appNameAndVendor}" + + " --description \"${appDescription}\"" + + " --app-version ${appVersion}" + + " --copyright \"${appCopyright}\"" + + " --vendor ${appNameAndVendor}" + + " --temp \"${jpackageTempDir}\"" + + + // Options for creating the application image + " --input ${fatJarFolderPath}" + + + // Options for creating the application launcher + " --main-jar ${mainJarName}" + + " --main-class bisq.desktop.app.BisqAppMain" + + " --java-options -Xss1280k" + + " --java-options -XX:MaxRAM=4g" + + " --java-options -Djava.net.preferIPv4Stack=true" + // Warning: this will cause guice reflection exceptions and lead to issues with the guice internal cache + // resulting in the UI not loading +// " --java-options -Djdk.module.illegalAccess=deny" + + ) + + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // TODO Found no benefit in using --resource-dir "..package/windows", it has the same outcome as opts below + String windowsOpts = new String( + " --icon \"${project(':desktop').projectDir}/package/windows/Bisq.ico\"" + + " --resource-dir \"${project(':desktop').projectDir}/package/windows\"" + + " --win-dir-chooser" + + " --win-per-user-install" + + " --win-menu" + + " --win-shortcut" + ) + + executeCmd(jPackageFilePath + commonOpts + windowsOpts + " --type exe") + + // Set the necessary permissions before calling signtool + executeCmd("\"attrib -R \"${binariesFolderPath}/Bisq-${appVersion}.exe\"\"") + + // In addition to the groovy quotes around the string, the entire Windows command must also be surrounded + // by quotes, plus each path inside the command has to be quoted as well + // Reason for this is that the path to the called executable contains spaces + // See https://stackoverflow.com/questions/6376113/how-do-i-use-spaces-in-the-command-prompt/6378038#6378038 + executeCmd("\"\"C:\\Program Files (x86)\\Windows Kits\\10\\App Certification Kit\\signtool.exe\" sign /v /fd SHA256 /a \"${binariesFolderPath}/Bisq-${appVersion}.exe\"\"") + } else if (Os.isFamily(Os.FAMILY_MAC)) { + // See https://docs.oracle.com/en/java/javase/14/jpackage/override-jpackage-resources.html + // for details of "--resource-dir" + + String macOpts = new String( + " --resource-dir \"${project(':desktop').projectDir}/package/macosx\"" + ) + + // Env variable can be set by calling "export BISQ_PACKAGE_SIGNING_IDENTITY='Some value'" + // See "man codesign" for details about the expected signing identity + String envVariableSigningID = "$System.env.BISQ_PACKAGE_SIGNING_IDENTITY" + println "Environment variable BISQ_PACKAGE_SIGNING_IDENTITY is: ${envVariableSigningID}" + ant.input(message: "Sign the app using the above signing identity? (y=yes, n=no)", + addproperty: "macos-sign-check", + validargs: "y,n") + if (ant.properties['macos-sign-check'] == 'y') { + // Create a temp folder to extract the macos-specific dylibs that need to be signed + File tempDylibFolderPath = new File(tempRootDir, "dylibs-to-sign") + tempDylibFolderPath.mkdirs() + + // Dylibs relevant for signing (paths relative to the tempDylibFolderPath) + String dylibsToSign = new String( + " libjavafx_iio.dylib" + + " libglass.dylib" + + " libjavafx_font.dylib" + + " libprism_common.dylib" + + " libprism_es2.dylib" + + " libdecora_sse.dylib" + + " libprism_sw.dylib" + + " META-INF/native/libio_grpc_netty_shaded_netty_tcnative_osx_x86_64.jnilib" + ) + + // macOS step 1: Sign dylibs and replace them in the shadow jar + // Extract dylibss for signing + executeCmd("cd ${tempDylibFolderPath} &&" + + " jar xf ${fatJarFolderPath}/${mainJarName}" + + dylibsToSign) + // Sign them + executeCmd("cd ${tempDylibFolderPath} &&" + + " codesign -vvv --options runtime --deep --force --sign \"${envVariableSigningID}\"" + + dylibsToSign) + + // Verify signature + executeCmd("cd ${tempDylibFolderPath} &&" + + " codesign -vvv --deep --strict " + dylibsToSign) + + // Replace unsigned files in jar file + executeCmd("cd ${tempDylibFolderPath} &&" + + " jar uf ${fatJarFolderPath}/${mainJarName}" + + dylibsToSign) + + // macOS step 2: Build app-image using the shadow jar above (containing signed dylibs) + // NOTE: licensing file cannot be added at this point only when creating the dmg later + executeCmd(jPackageFilePath + + commonOpts + + macOpts + + " --type app-image") + + // macOS step 3: Sign app (hardended runtime) + File bisqAppImageFullPath = new File(binariesFolderPath, "Bisq.app") + executeCmd("codesign" + + " --sign \"${envVariableSigningID}\"" + + " --options runtime" + + " --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" + + " --force" + + " --verbose" + + " ${bisqAppImageFullPath}/Contents/runtime/Contents/MacOS/libjli.dylib") + executeCmd("codesign" + + " --sign \"${envVariableSigningID}\"" + + " --options runtime" + + " --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" + + " --force" + + " --verbose" + + " ${bisqAppImageFullPath}/Contents/MacOS/Bisq") + executeCmd("codesign" + + " --sign \"${envVariableSigningID}\"" + + " --options runtime" + + " --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" + + " --force" + + " --verbose" + + " ${bisqAppImageFullPath}") + + // macOS step 4: Package the app-image into a dmg bundle + executeCmd(jPackageFilePath + + " --dest \"${binariesFolderPath}\"" + + " --name ${appNameAndVendor}" + + " --description \"${appDescription}\"" + + " --app-version ${appVersion}" + + " --copyright \"${appCopyright}\"" + + " --vendor ${appNameAndVendor}" + + " --temp \"${jpackageTempDir}\"" + + " --app-image ${bisqAppImageFullPath}" + + " --mac-sign" + + macOpts + + " --type dmg") + + // macOS step 5: Delete unused app image + delete(bisqAppImageFullPath) + + // macOS step 6: Sign dmg bundle + executeCmd("codesign" + + " --sign \"${envVariableSigningID}\"" + + " --options runtime" + + " --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" + + " -vvvv" + + " --deep" + + " '${binariesFolderPath}/Bisq-${appVersion}.dmg'") + + // macOS step 7: Upload for notarization + // See https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3087734 + String envVariableAcUsername = "$System.env.BISQ_PACKAGE_NOTARIZATION_AC_USERNAME" + String envVariableAscProvider = "$System.env.BISQ_PACKAGE_NOTARIZATION_ASC_PROVIDER" + // e.g. network.bisq.CAT is used when binaries are built by @ripcurlx + String envVariablePrimaryBundleId = "$System.env.BISQ_PRIMARY_BUNDLE_ID" + def uploadForNotarizationOutput = executeCmd("xcrun altool --notarize-app" + + " --primary-bundle-id '${envVariablePrimaryBundleId}'" + + " --username '${envVariableAcUsername}'" + + " --password '@keychain:AC_PASSWORD'" + + " --asc-provider '${envVariableAscProvider}'" + + " --file '${binariesFolderPath}/Bisq-${appVersion}.dmg'") + // Response: + // No errors uploading '[PATH_TO_BISQ_REPO]/bisq/desktop/build/temp-620637000/binaries/Bisq-1.1.1.dmg'. + // RequestUUID = ea8bba77-97b7-4c15-a53f-8bbccf627190 + def requestUUID = uploadForNotarizationOutput.split('RequestUUID = ')[1].trim() + println "Extracted RequestUUID: " + requestUUID + + // Every 1 minute, check the status + def notarizationEndedInSuccess = false + def notarizationEndedInFailure = false + while (!(notarizationEndedInSuccess || notarizationEndedInFailure)) { + println "Current time is:" + executeCmd('date') + println "Waiting for 1 minute..." + sleep(1 * 60 * 1000) + + println "Checking notarization status" + + def checkNotarizationStatusOutput = executeCmd("xcrun altool --notarization-info" + + " '${requestUUID}'" + + " --username '${envVariableAcUsername}'" + + " --password '@keychain:AC_PASSWORD'") + + notarizationEndedInSuccess = checkNotarizationStatusOutput.contains('success') + notarizationEndedInFailure = checkNotarizationStatusOutput.contains('invalid') + } + + if (notarizationEndedInFailure) { + ant.fail('Notarization failed, aborting') + } + + if (notarizationEndedInSuccess) { + println "Notarization was successful" + + // macOS step 8: Staple ticket on dmg + executeCmd("xcrun stapler staple" + + " '${binariesFolderPath}/Bisq-${appVersion}.dmg'") + } + + } else { + // If user didn't confirm the optional signing step, then generate a plain non-signed dmg + executeCmd(jPackageFilePath + commonOpts + macOpts + " --type dmg") + } + } else { + String linuxOpts = new String( + " --icon ${project(':desktop').projectDir}/package/linux/icon.png" + + + // This defines the first part of the resulting packages (the application name) + // deb requires lowercase letters, therefore the application name is written in lowercase + " --linux-package-name bisq" + + + // This represents the linux package version (revision) + // By convention, this is part of the deb/rpm package names, in addition to the software version + " --linux-app-release 1" + + + " --linux-menu-group Network" + + " --linux-shortcut" + ) + + // Package deb + executeCmd(jPackageFilePath + commonOpts + linuxOpts + + " --linux-deb-maintainer noreply@bisq.network" + + " --type deb") + + // Clean jpackage temp folder, needs to be empty for the next packaging step (rpm) + jpackageTempDir.deleteDir() + jpackageTempDir.mkdirs() + + // Package rpm + executeCmd(jPackageFilePath + commonOpts + linuxOpts + + " --linux-rpm-license-type AGPLv3" + // https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses + " --type rpm") + } + + // Env variable can be set by calling "export BISQ_SHARED_FOLDER='Some value'" + // This is to copy the final binary/ies to a shared folder for further processing if a VM is used. + String envVariableSharedFolder = "$System.env.BISQ_SHARED_FOLDER" + println "Environment variable BISQ_SHARED_FOLDER is: ${envVariableSharedFolder}" + ant.input(message: "Copy the created binary to a shared folder? (y=yes, n=no)", + addproperty: "copy-to-shared-folder", + validargs: "y,n") + if (ant.properties['copy-to-shared-folder'] == 'y') { + copy { + from binariesFolderPath + into envVariableSharedFolder + } + executeCmd("open " + envVariableSharedFolder) + } + + println "The binaries are ready:" + binariesFolderPath.traverse { + println it.path + } + } +} + +def executeCmd(String cmd) { + String shell + String shellArg + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + shell = 'cmd' + shellArg = '/c' + } else { + shell = 'bash' + shellArg = '-c' + } + + println "Executing command:\n${cmd}\n" + // See "Executing External Processes" section of + // http://docs.groovy-lang.org/next/html/documentation/ + def commands = [shell, shellArg, cmd] + def process = commands.execute(null, project.rootDir) + def result + if (process.waitFor() == 0) { + result = process.text + println "Command output (stdout):\n${result}" + } else { + result = process.err.text + println "Command output (stderr):\n${result}" + } + return result +} diff --git a/desktop/package/signingkey.asc b/desktop/package/signingkey.asc new file mode 100644 index 0000000000..0e9920d475 --- /dev/null +++ b/desktop/package/signingkey.asc @@ -0,0 +1 @@ +29CDFD3B \ No newline at end of file diff --git a/desktop/package/tools-1.0.jar b/desktop/package/tools-1.0.jar new file mode 100644 index 0000000000..69abf5498a Binary files /dev/null and b/desktop/package/tools-1.0.jar differ diff --git a/desktop/package/windows/Bisq.ico b/desktop/package/windows/Bisq.ico new file mode 100644 index 0000000000..4b6dda4951 Binary files /dev/null and b/desktop/package/windows/Bisq.ico differ diff --git a/desktop/package/windows/images/WixUIBannerBmp.bmp b/desktop/package/windows/images/WixUIBannerBmp.bmp new file mode 100644 index 0000000000..8af4e9b770 Binary files /dev/null and b/desktop/package/windows/images/WixUIBannerBmp.bmp differ diff --git a/desktop/package/windows/images/WixUIDialogBmp.bmp b/desktop/package/windows/images/WixUIDialogBmp.bmp new file mode 100644 index 0000000000..0f3e140bb2 Binary files /dev/null and b/desktop/package/windows/images/WixUIDialogBmp.bmp differ diff --git a/desktop/package/windows/main.wxs b/desktop/package/windows/main.wxs new file mode 100644 index 0000000000..9c04430f2a --- /dev/null +++ b/desktop/package/windows/main.wxs @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    + + 1 + + + 1 + + + !(loc.message.install.dir.exist) + + + + + + + + 1 + INSTALLDIR_VALID="0" + INSTALLDIR_VALID="1" + + + + 1 + 1 + + + + + + + + + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + + + + + + + + + + + Not Installed + + JP_UPGRADABLE_FOUND + + + JP_DOWNGRADABLE_FOUND + + + + + + diff --git a/desktop/package/windows/overrides.wxi b/desktop/package/windows/overrides.wxi new file mode 100644 index 0000000000..7ff18a3af8 --- /dev/null +++ b/desktop/package/windows/overrides.wxi @@ -0,0 +1,33 @@ + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/CandleStickChart.css b/desktop/src/main/java/bisq/desktop/CandleStickChart.css new file mode 100644 index 0000000000..376e0db666 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/CandleStickChart.css @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2008, 2013 Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* ====== CANDLE STICK CHART =========================================================== */ + +.candlestick-tooltip-label { + -fx-font-size: 0.75em; + -fx-font-weight: bold; + -fx-text-fill: #666666; + -fx-padding: 2 5 2 0; +} + +.candlestick-average-line { + -fx-stroke: -bs-candle-stick-average-line; + -fx-stroke-width: 1px; +} + +.candlestick-line { + -stick-line-fill: -bs-sell; + -fx-stroke: -stick-line-fill; + -fx-stroke-width: 1px; +} + +.candlestick-line.close-above-open { + -stick-line-fill: -bs-candle-stick-won; +} + +.candlestick-line.open-above-close { + -stick-line-fill: -bs-candle-stick-loss; +} + +.candlestick-bar { + -fx-padding: 5; + -demo-bar-fill: -bs-sell; + -fx-background-color: -demo-bar-fill; + -fx-background-insets: 0; +} + +.candlestick-bar.close-above-open { + -demo-bar-fill: -bs-candle-stick-won; +} + +.candlestick-bar.open-above-close { + -demo-bar-fill: -bs-candle-stick-loss; +} + +.candlestick-bar.empty { + -demo-bar-fill: #cccccc; +} + +.volume-bar { + -fx-padding: 5; + -fx-background-color: -bs-volume-transparent; + -fx-background-insets: 0; +} + +.chart-alternative-row-fill { + -fx-fill: transparent; + -fx-stroke: transparent; + -fx-stroke-width: 0; +} + +.chart-plot-background { + -fx-background-color: -bs-background-color; +} diff --git a/desktop/src/main/java/bisq/desktop/DesktopModule.java b/desktop/src/main/java/bisq/desktop/DesktopModule.java new file mode 100644 index 0000000000..c688c866bf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/DesktopModule.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.desktop; + +import bisq.desktop.common.fxml.FxmlViewLoader; +import bisq.desktop.common.view.ViewFactory; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.guice.InjectorViewFactory; + +import bisq.core.locale.Res; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +import com.google.inject.Singleton; +import com.google.inject.name.Names; + +import java.util.ResourceBundle; + +import static bisq.common.config.Config.APP_NAME; + +public class DesktopModule extends AppModule { + + public DesktopModule(Config config) { + super(config); + } + + @Override + protected void configure() { + bind(ViewFactory.class).to(InjectorViewFactory.class); + + bind(ResourceBundle.class).toInstance(Res.getResourceBundle()); + bind(ViewLoader.class).to(FxmlViewLoader.class).in(Singleton.class); + + bindConstant().annotatedWith(Names.named(APP_NAME)).to(config.appName); + } +} diff --git a/desktop/src/main/java/bisq/desktop/Navigation.java b/desktop/src/main/java/bisq/desktop/Navigation.java new file mode 100644 index 0000000000..b59bc8f867 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/Navigation.java @@ -0,0 +1,174 @@ +/* + * 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 bisq.desktop; + +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.main.MainView; +import bisq.desktop.main.market.MarketView; + +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.NavigationPath; +import bisq.common.proto.persistable.PersistedDataHost; + +import com.google.inject.Inject; + +import javax.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public final class Navigation implements PersistedDataHost { + private static final ViewPath DEFAULT_VIEW_PATH = ViewPath.to(MainView.class, MarketView.class); + + public interface Listener { + void onNavigationRequested(ViewPath path, @Nullable Object data); + } + + // New listeners can be added during iteration so we use CopyOnWriteArrayList to + // prevent invalid array modification + private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); + private final PersistenceManager persistenceManager; + private ViewPath currentPath; + // Used for returning to the last important view. After setup is done we want to + // return to the last opened view (e.g. sell/buy) + private ViewPath returnPath; + // this string is updated just before saving to disk so it reflects the latest currentPath situation. + private final NavigationPath navigationPath = new NavigationPath(); + + // Persisted fields + @Getter + @Setter + private ViewPath previousPath = DEFAULT_VIEW_PATH; + + + @Inject + public Navigation(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + + persistenceManager.initialize(navigationPath, PersistenceManager.Source.PRIVATE_LOW_PRIO); + } + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + List> viewClasses = persisted.getPath().stream() + .map(className -> { + try { + return (Class) Class.forName(className).asSubclass(View.class); + } catch (ClassNotFoundException e) { + log.warn("Could not find the viewPath class {}; exception: {}", className, e); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!viewClasses.isEmpty()) { + previousPath = new ViewPath(viewClasses); + } + completeHandler.run(); + }, + completeHandler); + } + + @SafeVarargs + public final void navigateTo(Class... viewClasses) { + navigateTo(ViewPath.to(viewClasses), null); + } + + @SafeVarargs + public final void navigateToWithData(Object data, Class... viewClasses) { + navigateTo(ViewPath.to(viewClasses), data); + } + + public void navigateTo(ViewPath newPath, @Nullable Object data) { + if (newPath == null) + return; + + ArrayList> temp = new ArrayList<>(); + for (int i = 0; i < newPath.size(); i++) { + Class viewClass = newPath.get(i); + temp.add(viewClass); + if (currentPath == null || + (currentPath.size() > i && + viewClass != currentPath.get(i) && + i != newPath.size() - 1)) { + ArrayList> temp2 = new ArrayList<>(temp); + for (int n = i + 1; n < newPath.size(); n++) { + //noinspection unchecked + Class[] newTemp = new Class[i + 1]; + currentPath = ViewPath.to(temp2.toArray(newTemp)); + navigateTo(currentPath, data); + viewClass = newPath.get(n); + temp2.add(viewClass); + } + } + } + + currentPath = newPath; + previousPath = currentPath; + listeners.forEach((e) -> e.onNavigationRequested(currentPath, data)); + requestPersistence(); + } + + private void requestPersistence() { + if (currentPath.tip() != null) { + navigationPath.setPath(currentPath.stream().map(Class::getName).collect(Collectors.toUnmodifiableList())); + } + persistenceManager.requestPersistence(); + } + + public void navigateToPreviousVisitedView() { + if (previousPath == null || previousPath.size() == 0) + previousPath = DEFAULT_VIEW_PATH; + + navigateTo(previousPath, null); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public ViewPath getReturnPath() { + return returnPath; + } + + public ViewPath getCurrentPath() { + return currentPath; + } + + public void setReturnPath(ViewPath returnPath) { + this.returnPath = returnPath; + } +} diff --git a/desktop/src/main/java/bisq/desktop/app/BisqApp.java b/desktop/src/main/java/bisq/desktop/app/BisqApp.java new file mode 100644 index 0000000000..31419d525a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/app/BisqApp.java @@ -0,0 +1,397 @@ +/* + * 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 bisq.desktop.app; + +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.main.MainView; +import bisq.desktop.main.debug.DebugView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.BsqEmptyWalletWindow; +import bisq.desktop.main.overlays.windows.BtcEmptyWalletWindow; +import bisq.desktop.main.overlays.windows.FilterWindow; +import bisq.desktop.main.overlays.windows.ManualPayoutTxWindow; +import bisq.desktop.main.overlays.windows.SendAlertMessageWindow; +import bisq.desktop.main.overlays.windows.ShowWalletDataWindow; +import bisq.desktop.util.CssTheme; +import bisq.desktop.util.ImageUtil; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.user.Cookie; +import bisq.core.user.CookieKey; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.app.DevEnv; +import bisq.common.app.Log; +import bisq.common.config.Config; +import bisq.common.setup.GracefulShutDownHandler; +import bisq.common.setup.UncaughtExceptionHandler; +import bisq.common.util.Utilities; + +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.name.Names; + +import com.google.common.base.Joiner; + +import javafx.application.Application; + +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.StackPane; + +import javafx.geometry.Rectangle2D; +import javafx.geometry.BoundingBox; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; +import static bisq.desktop.util.Layout.INITIAL_WINDOW_WIDTH; +import static bisq.desktop.util.Layout.MIN_WINDOW_HEIGHT; +import static bisq.desktop.util.Layout.MIN_WINDOW_WIDTH; + +@Slf4j +public class BisqApp extends Application implements UncaughtExceptionHandler { + @Setter + private static Consumer appLaunchedHandler; + @Getter + private static Runnable shutDownHandler; + + @Setter + private Injector injector; + @Setter + private GracefulShutDownHandler gracefulShutDownHandler; + private Stage stage; + private boolean popupOpened; + private Scene scene; + private boolean shutDownRequested; + private MainView mainView; + + public BisqApp() { + shutDownHandler = this::stop; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // JavaFx Application implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + // NOTE: This method is not called on the JavaFX Application Thread. + @Override + public void init() { + } + + @Override + public void start(Stage stage) { + this.stage = stage; + + appLaunchedHandler.accept(this); + } + + public void startApplication(Runnable onApplicationStartedHandler) { + try { + mainView = loadMainView(injector); + mainView.setOnApplicationStartedHandler(onApplicationStartedHandler); + scene = createAndConfigScene(mainView, injector); + setupStage(scene); + } catch (Throwable throwable) { + log.error("Error during app init", throwable); + handleUncaughtException(throwable, false); + } + } + + @Override + public void stop() { + if (!shutDownRequested) { + new Popup().headLine(Res.get("popup.shutDownInProgress.headline")) + .backgroundInfo(Res.get("popup.shutDownInProgress.msg")) + .hideCloseButton() + .useAnimation(false) + .show(); + new Thread(() -> { + gracefulShutDownHandler.gracefulShutDown(() -> { + log.debug("App shutdown complete"); + }); + }).start(); + shutDownRequested = true; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UncaughtExceptionHandler implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + if (!shutDownRequested) { + if (scene == null) { + log.warn("Scene not available yet, we create a new scene. The bug might be caused by an exception in a constructor or by a circular dependency in Guice. throwable=" + throwable.toString()); + scene = new Scene(new StackPane(), 1000, 650); + CssTheme.loadSceneStyles(scene, CssTheme.CSS_THEME_LIGHT, false); + stage.setScene(scene); + stage.show(); + } + try { + try { + if (!popupOpened) { + popupOpened = true; + new Popup().error(Objects.requireNonNullElse(throwable.getMessage(), throwable.toString())) + .onClose(() -> popupOpened = false) + .show(); + } + } catch (Throwable throwable3) { + log.error("Error at displaying Throwable."); + throwable3.printStackTrace(); + } + if (doShutDown) + stop(); + } catch (Throwable throwable2) { + // If printStackTrace cause a further exception we don't pass the throwable to the Popup. + log.error(throwable2.toString()); + if (doShutDown) + stop(); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private Scene createAndConfigScene(MainView mainView, Injector injector) { + //Rectangle maxWindowBounds = new Rectangle(); + Rectangle2D maxWindowBounds = new Rectangle2D(0, 0, 0, 0); + try { + maxWindowBounds = Screen.getPrimary().getBounds(); + } catch (IllegalArgumentException e) { + // Multi-screen environments may encounter IllegalArgumentException (Window must not be zero) + // Just ignore the exception and continue, which means the window will use the minimum window size below + // since we are unable to determine if we can use a larger size + } + Scene scene = new Scene(mainView.getRoot(), + maxWindowBounds.getWidth() < INITIAL_WINDOW_WIDTH ? + Math.max(maxWindowBounds.getWidth(), MIN_WINDOW_WIDTH) : + INITIAL_WINDOW_WIDTH, + maxWindowBounds.getHeight() < INITIAL_WINDOW_HEIGHT ? + Math.max(maxWindowBounds.getHeight(), MIN_WINDOW_HEIGHT) : + INITIAL_WINDOW_HEIGHT); + + addSceneKeyEventHandler(scene, injector); + + Preferences preferences = injector.getInstance(Preferences.class); + var config = injector.getInstance(Config.class); + preferences.getCssThemeProperty().addListener((ov) -> { + CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); + }); + CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); + + return scene; + } + + private void setupStage(Scene scene) { + stage.setOnCloseRequest(event -> { + event.consume(); + shutDownByUser(); + }); + + // configure the primary stage + String appName = injector.getInstance(Key.get(String.class, Names.named(Config.APP_NAME))); + List postFixes = new ArrayList<>(); + if (!Config.baseCurrencyNetwork().isMainnet()) { + postFixes.add(Config.baseCurrencyNetwork().name()); + } + if (injector.getInstance(Config.class).useLocalhostForP2P) { + postFixes.add("LOCALHOST"); + } + if (injector.getInstance(Config.class).useDevMode) { + postFixes.add("DEV MODE"); + } + if (!postFixes.isEmpty()) { + appName += " [" + Joiner.on(", ").join(postFixes) + " ]"; + } + + stage.setTitle(appName); + stage.setScene(scene); + stage.setMinWidth(MIN_WINDOW_WIDTH); + stage.setMinHeight(MIN_WINDOW_HEIGHT); + stage.getIcons().add(ImageUtil.getApplicationIconImage()); + + User user = injector.getInstance(User.class); + layoutStageFromPersistedData(stage, user); + addStageLayoutListeners(stage, user); + + // make the UI visible + stage.show(); + } + + private void layoutStageFromPersistedData(Stage stage, User user) { + Cookie cookie = user.getCookie(); + cookie.getAsOptionalDouble(CookieKey.STAGE_X).flatMap(x -> + cookie.getAsOptionalDouble(CookieKey.STAGE_Y).flatMap(y -> + cookie.getAsOptionalDouble(CookieKey.STAGE_W).flatMap(w -> + cookie.getAsOptionalDouble(CookieKey.STAGE_H).map(h -> new BoundingBox(x, y, w, h))))) + .ifPresent(stageBoundingBox -> { + stage.setX(stageBoundingBox.getMinX()); + stage.setY(stageBoundingBox.getMinY()); + stage.setWidth(stageBoundingBox.getWidth()); + stage.setHeight(stageBoundingBox.getHeight()); + }); + } + + private void addStageLayoutListeners(Stage stage, User user) { + stage.widthProperty().addListener((observable, oldValue, newValue) -> { + user.getCookie().putAsDouble(CookieKey.STAGE_W, (double) newValue); + user.requestPersistence(); + }); + stage.heightProperty().addListener((observable, oldValue, newValue) -> { + user.getCookie().putAsDouble(CookieKey.STAGE_H, (double) newValue); + user.requestPersistence(); + }); + stage.xProperty().addListener((observable, oldValue, newValue) -> { + user.getCookie().putAsDouble(CookieKey.STAGE_X, (double) newValue); + user.requestPersistence(); + }); + stage.yProperty().addListener((observable, oldValue, newValue) -> { + user.getCookie().putAsDouble(CookieKey.STAGE_Y, (double) newValue); + user.requestPersistence(); + }); + } + + private MainView loadMainView(Injector injector) { + CachingViewLoader viewLoader = injector.getInstance(CachingViewLoader.class); + return (MainView) viewLoader.load(MainView.class); + } + + private void addSceneKeyEventHandler(Scene scene, Injector injector) { + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { + if (Utilities.isCtrlPressed(KeyCode.W, keyEvent) || + Utilities.isCtrlPressed(KeyCode.Q, keyEvent)) { + shutDownByUser(); + } else { + if (Utilities.isAltOrCtrlPressed(KeyCode.E, keyEvent)) { + injector.getInstance(BtcEmptyWalletWindow.class).show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.B, keyEvent)) { + injector.getInstance(BsqEmptyWalletWindow.class).show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) { + injector.getInstance(SendAlertMessageWindow.class).show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) { + injector.getInstance(FilterWindow.class).show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.H, keyEvent)) { + log.warn("We re-published all proposalPayloads and blindVotePayloads to the P2P network."); + injector.getInstance(MissingDataRequestService.class).reRepublishAllGovernanceData(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.T, keyEvent)) { + // Toggle between show tor logs and only show warnings. Helpful in case of connection problems + String pattern = "org.berndpruenster.netlayer"; + Level logLevel = ((Logger) LoggerFactory.getLogger(pattern)).getLevel(); + if (logLevel != Level.DEBUG) { + log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG"); + Log.setCustomLogLevel(pattern, Level.DEBUG); + } else { + log.info("Set log level for org.berndpruenster.netlayer classes to WARN"); + Log.setCustomLogLevel(pattern, Level.WARN); + } + } else if (Utilities.isAltOrCtrlPressed(KeyCode.J, keyEvent)) { + WalletsManager walletsManager = injector.getInstance(WalletsManager.class); + if (walletsManager.areWalletsAvailable()) + new ShowWalletDataWindow(walletsManager).show(); + else + new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.G, keyEvent)) { + if (injector.getInstance(BtcWalletService.class).isWalletReady()) + injector.getInstance(ManualPayoutTxWindow.class).show(); + else + new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show(); + } else if (DevEnv.isDevMode()) { + if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent)) + showDebugWindow(scene, injector); + } + } + }); + } + + private void shutDownByUser() { + boolean hasOpenOffers = false; + for (OpenOffer openOffer : injector.getInstance(OpenOfferManager.class).getObservableList()) { + if (openOffer.getState().equals(OpenOffer.State.AVAILABLE)) { + hasOpenOffers = true; + break; + } + } + if (!hasOpenOffers) { + // No open offers, so no need to show the popup. + stop(); + return; + } + + // We show a popup to inform user that open offers will be removed if Bisq is not running. + String key = "showOpenOfferWarnPopupAtShutDown"; + if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { + new Popup().information(Res.get("popup.info.shutDownWithOpenOffers")) + .dontShowAgainId(key) + .useShutDownButton() + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + stop(); + } + } + + // Used for debugging trade process + private void showDebugWindow(Scene scene, Injector injector) { + ViewLoader viewLoader = injector.getInstance(ViewLoader.class); + View debugView = viewLoader.load(DebugView.class); + Parent parent = (Parent) debugView.getRoot(); + Stage stage = new Stage(); + stage.setScene(new Scene(parent)); + stage.setTitle("Debug window"); // Don't translate, just for dev + stage.initModality(Modality.NONE); + stage.initStyle(StageStyle.UTILITY); + stage.initOwner(scene.getWindow()); + stage.setX(this.stage.getX() + this.stage.getWidth() + 10); + stage.setY(this.stage.getY()); + stage.show(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java new file mode 100644 index 0000000000..d2577194db --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -0,0 +1,142 @@ +/* + * 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 bisq.desktop.app; + +import bisq.desktop.common.UITimer; +import bisq.desktop.common.view.guice.InjectorViewFactory; +import bisq.desktop.setup.DesktopPersistedDataHost; + +import bisq.core.app.AvoidStandbyModeService; +import bisq.core.app.BisqExecutable; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.app.Version; + +import javafx.application.Application; +import javafx.application.Platform; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BisqAppMain extends BisqExecutable { + + public static final String DEFAULT_APP_NAME = "Bisq"; + + private BisqApp application; + + public BisqAppMain() { + super("Bisq Desktop", "bisq-desktop", DEFAULT_APP_NAME, Version.VERSION); + } + + public static void main(String[] args) { + // For some reason the JavaFX launch process results in us losing the thread + // context class loader: reset it. In order to work around a bug in JavaFX 8u25 + // and below, you must include the following code as the first line of your + // realMain method: + Thread.currentThread().setContextClassLoader(BisqAppMain.class.getClassLoader()); + + new BisqAppMain().execute(args); + } + + @Override + public void onSetupComplete() { + log.debug("onSetupComplete"); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // First synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void configUserThread() { + UserThread.setExecutor(Platform::runLater); + UserThread.setTimerClass(UITimer.class); + } + + @Override + protected void launchApplication() { + BisqApp.setAppLaunchedHandler(application -> { + BisqAppMain.this.application = (BisqApp) application; + // Map to user thread! + UserThread.execute(this::onApplicationLaunched); + }); + + Application.launch(BisqApp.class); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // As application is a JavaFX application we need to wait for onApplicationLaunched + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onApplicationLaunched() { + super.onApplicationLaunched(); + application.setGracefulShutDownHandler(this); + } + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + application.handleUncaughtException(throwable, doShutDown); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // We continue with a series of synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected AppModule getModule() { + return new BisqAppModule(config); + } + + @Override + protected void applyInjector() { + super.applyInjector(); + + application.setInjector(injector); + injector.getInstance(InjectorViewFactory.class).setInjector(injector); + } + + @Override + protected void readAllPersisted(Runnable completeHandler) { + super.readAllPersisted(DesktopPersistedDataHost.getPersistedDataHosts(injector), completeHandler); + } + + @Override + protected void setupAvoidStandbyMode() { + injector.getInstance(AvoidStandbyModeService.class).init(); + } + + @Override + protected void startApplication() { + // We need to be in user thread! We mapped at launchApplication already. Once + // the UI is ready we get onApplicationStarted called and start the setup there. + application.startApplication(this::onApplicationStarted); + } + + @Override + protected void onApplicationStarted() { + super.onApplicationStarted(); + + // Relevant to have this in the logs, for support cases + // This can only be called after JavaFX is initialized, otherwise the version logged will be null + // Therefore, calling this as part of onApplicationStarted() + log.info("Using JavaFX {}", System.getProperty("javafx.version")); + } +} diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java b/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java new file mode 100644 index 0000000000..1720cd7695 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.desktop.app; + +import bisq.desktop.DesktopModule; + +import bisq.core.app.CoreModule; + +import bisq.common.app.AppModule; +import bisq.common.config.Config; + +public class BisqAppModule extends AppModule { + + public BisqAppModule(Config config) { + super(config); + } + + @Override + protected void configure() { + install(new CoreModule(config)); + install(new DesktopModule(config)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css new file mode 100644 index 0000000000..dc97dfc84b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -0,0 +1,2232 @@ +@font-face { + src: url("/fonts/IBMPlexSans-Regular.ttf"); +} +@font-face { + src: url("/fonts/IBMPlexSans-Bold.ttf"); +} +@font-face { + src: url("/fonts/IBMPlexSans-Medium.ttf"); +} +@font-face { + src: url("/fonts/IBMPlexSans-Light.ttf"); +} +@font-face { + src: url("/fonts/IBMPlexMono-Regular.ttf"); +} +.root { + -fx-font-size: 13; +} + +.root:dir(ltr){ + -fx-font-family: "IBM Plex Sans"; +} + +/******************************************************************************************************************** + * * + * General * + * * + ********************************************************************************************************************/ +/* Text */ +.error-text { + -fx-text-fill: -bs-rd-error-red; +} + +.error { + -fx-accent: -bs-rd-error-red; +} + +.success-text { + -fx-text-fill: -bs-color-primary; +} + +.highlight, .highlight-static { + -fx-text-fill: -fx-accent; + -fx-fill: -fx-accent; +} + +.highlight:hover { + -fx-text-fill: -bs-text-color; + -fx-fill: -bs-text-color; +} + +.info { + -fx-text-fill: -bs-color-primary; + -fx-fill: -bs-color-primary; +} + +.info:hover { + -fx-text-fill: -bs-color-gray-6; + -fx-fill: -bs-color-gray-6; +} + +.sub-info { + -fx-text-fill: -bs-color-gray-4; + -fx-fill: -bs-color-gray-4; +} + +.headline-label { + -fx-font-weight: bold; + -fx-font-size: 1.692em; +} + +.warning-box { + -fx-background-color: -bs-yellow-light; + -fx-spacing: 6; + -fx-alignment: center; +} + +.warning { + -fx-text-fill: -bs-yellow; + -fx-fill: -bs-yellow; +} + +.warning:hover { + -fx-text-fill: -bs-color-gray-6; + -fx-fill: -bs-color-gray-6; +} + +.zero-decimals { + -fx-text-fill: -bs-color-gray-3; +} + +.confirmation-label { + -fx-font-size: 1.077em; + -fx-text-fill: -bs-rd-font-confirmation-label; +} + +.confirmation-value { + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Mono"; + -fx-text-fill: -bs-rd-font-dark-gray; +} + +.confirmation-text-field-as-label:readonly { + -fx-background-color: transparent !important; + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Mono"; + -fx-padding: 0 !important; +} + +/* Other UI Elements */ +.separator *.line { + -fx-border-style: solid; + -fx-border-width: 0 0 1 0; + -fx-border-color: -bs-rd-separator-dark; +} + +.separator:vertical *.line { + -fx-border-width: 0 1 0 0; +} + +.jfx-progress-bar > .bar, +.jfx-progress-bar:indeterminate > .bar { + -fx-background-color: -bs-color-primary; +} + +.jfx-progress-bar.error > .bar, +.jfx-progress-bar:indeterminate.error > .bar { + -fx-background-color: -bs-rd-error-red; +} + +.jfx-progress-bar > .track { + -fx-background-color: -bs-progress-bar-track; +} + +.jfx-spinner { + -jfx-radius: 10; +} + +.jfx-spinner:indeterminate .arc, +.jfx-spinner:determinate .arc { + -fx-stroke: -bs-color-primary; +} + +.busyanimation .text.percentage { + -fx-fill: null; +} + +.jfx-button, .action-button { + -fx-background-color: -bs-color-gray-bbb; + -fx-text-fill: -bs-rd-font-dark-gray; + -fx-font-size: 0.923em; + -fx-font-weight: normal; + -fx-background-radius: 2px; + -fx-pref-height: 32; + -fx-min-height: -fx-pref-height; + -fx-padding: 0 40 0 40; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 2, 0, 0, 0, 1); + -fx-cursor: hand; +} + +.jfx-button:hover, .jfx-button:focused { + -fx-background-color: derive(-bs-color-gray-2, -10%); +} + +.action-button:hover, .action-button:focused { + -fx-background-color: derive(-bs-color-primary-dark, -10%); +} + +.action-button { + -fx-background-color: -bs-color-primary-dark; + -fx-text-fill: -bs-background-color; +} + +.compact-button, .table-cell .jfx-button, .action-button.compact-button { + -fx-padding: 0 10 0 10; +} + +.text-button { + -fx-background-color: transparent; + -fx-underline: true; + -fx-padding: 0 10 0 10; + -fx-pref-height: 28; + -fx-min-height: -fx-pref-height; +} + +.text-button:hover { + -fx-text-fill: -bs-text-color; + -fx-background-color: transparent; + -fx-underline: false; +} + +.jfx-checkbox { + -jfx-checked-color: -bs-color-primary; + -fx-font-size: 0.692em; +} + +.jfx-check-box .box, +.jfx-check-box:indeterminate .box, +.jfx-check-box:indeterminate:selected .box { + -fx-border-radius: 0; + -fx-border-width: 1; + -fx-pref-width: 12; + -fx-pref-height: 12; +} + +.jfx-check-box .mark, +.jfx-check-box .indeterminate-mark { + -fx-border-radius: 0; + -fx-border-width: 1; +} + +.jfx-combo-box { + -jfx-focus-color: -bs-color-primary; + -jfx-unfocus-color: -bs-color-gray-line; + -fx-background-color: -bs-background-color; +} + +.jfx-combo-box > .list-cell { + -fx-text-fill: -bs-text-color; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.jfx-combo-box > .arrow-button > .arrow { + -fx-background-color: null; + -fx-border-color: -jfx-unfocus-color; + -fx-shape: "M 0 0 l 3.5 4 l 3.5 -4"; +} + +.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { + -fx-background: -fx-selection-bar; + -fx-background-color: -fx-selection-bar; +} + +.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover, +.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover { + -fx-background: -fx-accent; + -fx-background-color: -fx-selection-bar; +} + + +/* list view */ +.list-view .list-cell { + -fx-background-color: -bs-background-color; +} +.list-view .list-cell:odd { + -fx-background-color: derive(-bs-background-color, -5%); +} +.list-view .list-cell:even { + -fx-background-color: derive(-bs-background-color, 5%); +} +.list-view .list-cell:hover, +.list-view .list-cell:selected, +.table-view .table-cell:hover, +.table-view .table-cell:selected { + -fx-background: -fx-accent; + -fx-background-color: -fx-selection-bar; + -fx-border-color: -fx-selection-bar; +} + +.number-column.table-cell { + -fx-background-color: -bs-background-color; +} + +.list-view:focused, +.tree-view:focused, +.table-view:focused, +.tree-table-view:focused, +.table-view:focused, +tree-table-view:focused { + -fx-background-insets: 0; +} +.list-view:focused { + -fx-background-color: -bs-color-primary; + -fx-background-insets: 0; +} + +/* Selected rows */ +.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected, +.tree-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-cell:filled:selected, +.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected, +.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected, +.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell .table-cell:selected, +.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell .tree-table-cell:selected { + -fx-background-insets: 0; +} + +/* Selected when control is not focused */ +.list-cell:filled:selected, +.tree-cell:filled:selected, +.table-row-cell:filled:selected, +.tree-table-row-cell:filled:selected, +.table-row-cell:filled > .table-cell:selected, +.tree-table-row-cell:filled > .tree-table-cell:selected { + -fx-background-insets: 0; +} + +.jfx-text-field { + -jfx-focus-color: -bs-color-primary; + -fx-background-color: -bs-background-color; + -fx-background-radius: 3 3 0 0; + -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +} + +.jfx-text-field > .input-line { + -fx-translate-x: -0.333333em; +} + +.jfx-text-field > .input-focused-line { + -fx-translate-x: -0.333333em; +} + +.jfx-text-field-top-label { + -fx-text-fill: -bs-color-gray-dim; +} + +.jfx-text-field:readonly, .hyperlink-with-icon { + -fx-background-color: -bs-color-gray-1; + -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +} + +.jfx-text-field:readonly > .input-line { + -fx-background-color: transparent; +} + +.jfx-text-field:readonly > .input-focused-line { + -fx-background-color: transparent; +} + +.jfx-text-field:disabled > .input-line { + -fx-background-color: transparent; + -fx-border-width: 0; +} + +.jfx-text-field:disabled > .input-focused-line { + -fx-background-color: transparent; + -fx-border-width: 0; +} + +#info-field { + -fx-prompt-text-fill: -bs-text-color; +} + +.jfx-password-field { + -fx-background-color: -bs-background-color; + -fx-background-radius: 3 3 0 0; + -jfx-focus-color: -bs-color-primary; + -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +} + +.jfx-password-field > .input-line { + -fx-translate-x: -0.333333em; +} + +.jfx-password-field > .input-focused-line { + -fx-translate-x: -0.333333em; +} + +.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error { + -jfx-focus-color: -bs-rd-error-red; + -jfx-unfocus-color: -bs-rd-error-red; +} + +.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label { + -fx-text-fill: -bs-rd-error-red; + -fx-font-size: 0.692em; + -fx-padding: -0.5em 0 0 0; +} + +.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon { + -fx-text-fill: -bs-rd-error-red; + -fx-font-size: 1em; +} + +.input-with-border { + -fx-background-color: -bs-background-color; + -fx-border-width: 1; + -fx-border-color: -bs-background-gray; + -fx-border-radius: 3; + -fx-pref-height: 43; + -fx-pref-width: 310; + -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); +} + +.input-with-border .text-field { + -fx-alignment: center-right; + -fx-pref-height: 43; + -fx-font-size: 1.385em; +} + +.input-with-border > .input-label { + -fx-font-size: 0.692em; + -fx-min-width: 60; + -fx-padding: 16; + -fx-alignment: center; +} + +.input-with-border .icon { + -fx-padding: 10; +} + +.input-with-border-readonly { + -fx-background-color: -bs-color-gray-1; + -fx-border-width: 0; + -fx-pref-width: 300; +} + +.input-with-border-readonly .text-field { + -fx-alignment: center-right; + -fx-font-size: 1em; + -fx-background-color: -bs-color-gray-1; +} + +.input-with-border-readonly .text-field > .input-line { + -fx-background-color: transparent; +} + +.input-with-border-readonly > .input-label { + -fx-font-size: 0.692em; + -fx-min-width: 30; + -fx-padding: 8; + -fx-alignment: center; +} + +.input-with-border-readonly .icon { + -fx-padding: 2; +} + +.jfx-badge .badge-pane { + -fx-background-color: -bs-red; + -fx-background-radius: 15; + -fx-pref-width: 15; + -fx-pref-height: 15; +} + +.jfx-badge.new .badge-pane { + -fx-pref-width: 30; +} + +.jfx-badge.auto-conf .badge-pane { + -fx-background-color: -xmr-orange; + -fx-pref-width: -1; + -fx-padding: -1 10 0 10; +} + +.jfx-badge .badge-pane .label { + -fx-font-weight: bold; + -fx-font-size: 0.692em; + -fx-text-fill: -bs-background-color; +} + +.jfx-badge { + -fx-padding: -3 0 0 0; +} + +.jfx-toggle-button, +.jfx-toggle-button:armed, +.jfx-toggle-button:hover, +.jfx-toggle-button:focused, +.jfx-toggle-button:selected, +.jfx-toggle-button:focused:selected { + -jfx-toggle-color: -bs-color-primary-dark; + -jfx-size: 8; +} + +.jfx-text-area { + -jfx-focus-color: -bs-color-primary; + -jfx-unfocus-color: -bs-color-gray-line; + -fx-background-color: -bs-background-color; + -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +} + +.jfx-text-area:readonly { + -fx-background-color: transparent; +} + +.jfx-text-area > .input-line { + -fx-translate-x: -0.333333em; +} + +.jfx-text-area > .input-focused-line { + -fx-translate-x: -0.333333em; +} + +.wallet-seed-words { + -fx-font-family: "IBM Plex Mono"; +} + +.wallet-seed-words .content, +.wallet-seed-words:focused .content { + -fx-padding: 12 12 0 12; +} + +.jfx-date-picker { + -jfx-default-color: -bs-color-primary; +} + +.jfx-date-picker .jfx-text-field { + -fx-padding: 0.333333em 0em 0.333333em 0em; +} + +.jfx-date-picker .jfx-text-field > .input-line { + -fx-translate-x: 0em; +} + +.jfx-date-picker .jfx-text-field > .input-focused-line { + -fx-translate-x: 0em; +} + +.jfx-date-picker > .arrow-button > .arrow { + -fx-shape: "M320 384h128v128h-128zM512 384h128v128h-128zM704 384h128v128h-128zM128 768h128v128h-128zM320 768h128v128h-128zM512 768h128v128h-128zM320 576h128v128h-128zM512 576h128v128h-128zM704 576h128v128h-128zM128 576h128v128h-128zM832 0v64h-128v-64h-448v64h-128v-64h-128v1024h960v-1024h-128zM896 960h-832v-704h832v704z"; + -fx-background-color: -jfx-default-color; + -fx-background-insets: 0; + -fx-padding: 10; +} + +.jfx-date-picker > .arrow-button > .jfx-svg-glyph { + -fx-background-color: -jfx-default-color; +} + +.date-picker-popup .month-year-pane { + -fx-background-color: -bs-color-primary-dark; +} + +.scroll-bar { + -fx-background-color: -bs-background-color; + -fx-background-radius: 0; +} + +.scroll-bar:horizontal .track, +.scroll-bar:vertical .track { + -fx-background-color: -bs-background-color; + -fx-border-color: -bs-background-color; + -fx-background-radius: 0; +} + +.scroll-bar:vertical .track-background, +.scroll-bar:horizontal .track-background { + -fx-background-color: -bs-background-color; + -fx-background-insets: 0; + -fx-background-radius: 0; +} + +.scroll-bar:horizontal .thumb { + -fx-background-color: -bs-color-gray-2; + -fx-background-insets: 2 0 2 0; + -fx-background-radius: 3; +} + +.scroll-bar:vertical .thumb { + -fx-background-color: -bs-color-gray-2; + -fx-background-insets: 0 2 0 2; + -fx-background-radius: 3; +} + +.scroll-bar:horizontal .thumb:hover, +.scroll-bar:vertical .thumb:hover { + -fx-background-color: -bs-color-gray-ccc; +} + +.scroll-bar:horizontal .thumb:pressed, +.scroll-bar:vertical .thumb:pressed { + -fx-background-color: -bs-color-gray-ddd; +} + +.scroll-bar:vertical .increment-button, +.scroll-bar:vertical .decrement-button, +.scroll-bar:horizontal .increment-button, +.scroll-bar:horizontal .decrement-button { + -fx-background-color: -bs-background-color; + -fx-padding: 1; +} + +.scroll-bar:horizontal .increment-arrow, +.scroll-bar:vertical .increment-arrow, +.scroll-bar:horizontal .decrement-arrow, +.scroll-bar:vertical .decrement-arrow { + -fx-shape: null; + -fx-background-color: -bs-background-color; +} + +.scroll-bar:vertical:focused, +.scroll-bar:horizontal:focused { + -fx-background-color: -bs-background-color, -bs-color-gray-ccc, -bs-color-gray-ddd; +} + +/* Behavior */ +.show-hand { + -fx-cursor: hand; +} + +.hide-focus { + -fx-focus-color: transparent; +} + +/* Font */ +.very-small-text { + -fx-font-size: 0.692em; +} + +.small-text { + -fx-font-size: 0.769em; +} + +.normal-text { + -fx-font-size: 0.923em; +} + +.default-text { + -fx-font-size: 13; +} + +/* Splash */ +#splash { + -fx-background-color: -bs-background-color; +} + +/* Main UI */ +#base-content-container { + -fx-background-color: -bs-background-gray; +} + +.content-pane { + -fx-background-color: linear-gradient(-bs-content-pane-bg-top 0%, -bs-content-pane-bg-bottom 100%); +} + +#footer-pane { + -fx-background-color: -bs-footer-pane-background; + -fx-font-size: 0.923em; + -fx-text-fill: -bs-footer-pane-text; +} + +#footer-pane-line { + -fx-background: -bs-footer-pane-line; +} + +#headline-label { + -fx-font-weight: bold; + -fx-font-size: 1.385em; +} + +/* Main navigation */ +.top-navigation { + -fx-background-color: -bs-rd-nav-background; + -fx-border-width: 0 0 1 0; + -fx-border-color: -bs-rd-nav-primary-border; + -fx-pref-height: 57; + -fx-padding: 0 11 0 0; +} + +.top-navigation .separator:vertical .line { + -fx-border-color: transparent transparent transparent -bs-rd-nav-border-color; + -fx-border-width: 1; + -fx-border-insets: 0 0 0 1; +} + +.nav-primary { + -fx-background-color: -bs-rd-nav-primary-background; + -fx-padding: 0 11 0 11; + -fx-border-width: 0 1 0 0; + -fx-border-color: -bs-rd-nav-primary-border; + -fx-min-width: 410; +} + +.nav-secondary { + -fx-padding: 0 11 0 11; + -fx-min-width: 296; +} + +.nav-price-balance { + -fx-background-color: -bs-color-gray-background; + -fx-background-radius: 3; + -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); + -fx-pref-height: 41; + -fx-padding: 0 10 0 0; +} + +.nav-price-balance .separator:vertical .line { + -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; + -fx-border-width: 1; + -fx-border-insets: 0 0 0 1; +} + +.nav-price-balance .jfx-combo-box > .input-line { + -fx-pref-height: 0px; +} + +.jfx-badge > .nav-button { + -fx-translate-y: 1; +} + +.nav-button { + -fx-cursor: hand; + -fx-background-color: transparent; + -fx-padding: 11; +} + +.nav-button .text { + -fx-font-size: 0.769em; + -fx-font-weight: bold; + -fx-fill: -bs-rd-nav-deselected; +} + +.nav-button-japanese .text { + -fx-font-size: 1em; +} + +.nav-button:selected { + -fx-background-color: -bs-background-color; + -fx-border-radius: 4; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); +} + +.nav-button:selected .text { + -fx-fill: -bs-rd-nav-selected; +} + +.nav-balance-display { + -fx-alignment: center-left; + -fx-text-fill: -bs-rd-font-balance; +} + +.nav-balance-label { + -fx-font-size: 0.769em; + -fx-alignment: center-left; + -fx-text-fill: -bs-rd-font-balance-label; +} + +#nav-alert-label { + -fx-font-weight: bold; + -fx-alignment: center; + -fx-font-size: 0.846em; + -fx-text-fill: -bs-background-color; +} + +.text-field { + -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); +} + +.text-area { + -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); +} + +#label-url { + -fx-cursor: hand; + -fx-text-fill: -bs-color-blue-0; + -fx-underline: true; +} + +/** Confirmation Indicator */ +.progress-indicator > .determinate-indicator > .indicator { + -fx-background-color: -fx-control-inner-background; + -fx-border-color: -fx-box-border; + -fx-border-width: 1; + -fx-padding: 0.166667em; /* 2px */ +} + +/******************************************************************************* + * * + * Icons * + * * + ******************************************************************************/ +.icon, .icon:hover { + -fx-cursor: hand; +} + +.hidden-icon-button { + -fx-background-color: transparent; + -fx-padding: 0; + -fx-cursor: hand; +} + +#icon-button { + -fx-cursor: hand; + -fx-background-color: transparent; +} + +.copy-icon-disputes { + -fx-text-fill: -bs-background-color; +} + +.copy-icon:hover { + -fx-text-fill: -bs-text-color; +} + +.received-funds-icon { + -fx-text-fill: -bs-green-soft; +} + +.sent-funds-icon { + -fx-text-fill: -bs-red-soft; +} + +.version { + -fx-text-fill: -bs-text-color; + -fx-underline: false; + -fx-cursor: null; +} + +.version-new { + -fx-text-fill: -bs-rd-error-red; + -fx-underline: true; + -fx-cursor: hand; +} + +.alert { + -fx-text-fill: -bs-rd-error-red; +} + +.opaque-icon { + -fx-fill: -bs-color-gray-bbb; + -fx-opacity: 1; +} + +.opaque-icon-character { + -fx-font-size: 3em; + -fx-text-fill: -bs-color-gray-bbb; + -fx-padding: 24 2 0 2; +} + +.opaque-icon-character.small { + -fx-font-size: 1em; + -fx-padding: 27 2 0 2; +} + +.alert-icon { + -fx-fill: -bs-rd-error-red; + -fx-cursor: hand; +} + +.close-icon { + -fx-fill: -bs-text-color; +} + +.close-icon:hover { + -fx-fill: -fx-accent; +} + +.tooltip-icon { + -fx-fill: -bs-text-color; +} + +/******************************************************************************* + * * + * Tooltip * + * * + ******************************************************************************/ +.tooltip { + -fx-background: -bs-background-color; + -fx-text-fill: -bs-text-color; + -fx-background-color: -bs-background-color; + -fx-background-radius: 6px; + -fx-background-insets: 0; + -fx-padding: 0.667em 0.75em 0.667em 0.75em; /* 10px */ + -fx-effect: dropshadow(three-pass-box, -bs-text-color-transparent, 10, 0, 0, 3); + -fx-font-size: 0.85em; +} + +/* Same style like non editable textfield. But textfield spans a whole column in a grid, so we use generally +textfield */ +#label-with-background { + -fx-background-color: -bs-color-gray-fafa; + -fx-border-radius: 4; + -fx-padding: 4 4 4 4; +} + +#funds-confidence { + -fx-progress-color: -bs-color-gray-dim; + -fx-max-width: 20; + -fx-max-height: 20; +} + +#xmr-confidence { + -fx-progress-color: -xmr-orange; + -fx-max-width: 20; + -fx-max-height: 20; +} + +.hyperlink, +.hyperlink.force-underline .text, +.hyperlink:hover, +.hyperlink:visited, +.hyperlink:hover:visited, +.hyperlink:focused { + -fx-border-style: none; + -fx-border-width: 0px; + -fx-underline: true; + -fx-text-fill: -bs-rd-font-dark; + -fx-fill: -bs-rd-font-dark; +} + +.hyperlink.no-underline { + -fx-underline: false; +} + +.hyperlink:hover { + -fx-text-fill: -bs-text-color; + -fx-fill: -bs-text-color; +} + +.hyperlink:hover, +.hyperlink:visited, +.hyperlink:hover:visited { + -fx-underline: false; +} + +.hyperlink.highlight, +.hyperlink.highlight .text.hyperlink.no-underline { + -fx-text-fill: -fx-accent; + -fx-fill: -fx-accent; +} + +.hyperlink.error { + -fx-text-fill: -bs-rd-error-red; + -fx-fill: -bs-rd-error-red; +} + +/******************************************************************************* + * * + * Table * + * * + ******************************************************************************/ +.table-view .table-row-cell:even .table-cell { + -fx-background-color: derive(-bs-background-color, 5%); + -fx-border-color: derive(-bs-background-color,5%); +} +.table-view .table-row-cell:odd .table-cell { + -fx-background-color: derive(-bs-background-color,-5%); + -fx-border-color: derive(-bs-background-color,-5%); +} +.table-view .table-row-cell:hover .table-cell, +.table-view .table-row-cell:selected .table-cell { + -fx-background: -fx-accent; + -fx-background-color: -fx-selection-bar; + -fx-border-color: -fx-selection-bar; +} +.table-row-cell { + -fx-border-color: -bs-background-color; +} +.table-row-cell:empty, .table-row-cell:empty:even, .table-row-cell:empty:odd { + -fx-background-color: -bs-background-color; + -fx-min-height: 36; +} +.offer-table .table-row-cell { + -fx-background: -fx-accent; + -fx-background-color: -bs-color-gray-6; +} + +.table-view .table-cell { + -fx-alignment: center-left; + -fx-padding: 2 0 2 0; + /*-fx-padding: 3 0 2 0;*/ +} + +.table-view .table-cell.last-column { + -fx-alignment: center-right; + -fx-padding: 2 10 2 0; +} + +.table-view .table-cell.last-column.avatar-column { + -fx-alignment: center; + -fx-padding: 2 0 2 0; +} + +.table-view .column-header.last-column { + -fx-padding: 0 10 0 0; +} + +.table-view .column-header.last-column .label { + -fx-alignment: center-right; +} + +.table-view .column-header.last-column.avatar-column { + -fx-padding: 0; +} + +.table-view .column-header.last-column.avatar-column .label { + -fx-alignment: center; +} + +.table-view .table-cell.first-column { + -fx-padding: 2 0 2 10; +} + +.table-view .column-header.first-column { + -fx-padding: 0 0 0 10; +} + +.number-column.table-cell { + -fx-font-size: 1em; + -fx-padding: 0 0 0 0; +} + +.table-view .filler { + -fx-background-color: -bs-color-gray-0; +} + +.table-view { + -fx-control-inner-background-alt: -fx-control-inner-background; +} + +.table-view .column-header .label { + -fx-alignment: center-left; + -fx-font-weight: normal; + -fx-font-size: 0.923em; + -fx-padding: 0; +} + +.table-view .column-header { + -fx-background-color: -bs-color-gray-0; + -fx-padding: 0; +} + +.table-view .focus { + -fx-alignment: center-left; +} + +.table-view .text { + -fx-fill: -bs-text-color; +} + +/* horizontal scrollbars are never needed and are flickering at scaling so lets turn them off */ +.table-view > .scroll-bar:horizontal { + -fx-opacity: 0; +} + +.table-view:focused { + -fx-background-color: -fx-box-border, -fx-control-inner-background; + -fx-background-insets: 0, 1; + -fx-padding: 1; +} + +.table-view:focused .table-row-cell:focused { + -fx-background-color: -fx-table-cell-border-color, -fx-background; + -fx-background-insets: 0, 0 0 1 0; +} + +.offer-table .table-row-cell { + -fx-border-color: -bs-background-color; + -fx-table-cell-border-color: -bs-background-color; +} + +.table-row-cell { + -fx-border-width: 0 0 1 0; + -fx-border-color: -bs-color-gray-0; + -fx-table-cell-border-color: -bs-background-color; +} + +.table-row-cell:selected { + -fx-border-width: 0 0 1 0; + -fx-table-cell-border-color: transparent; +} + +.table-row-cell:empty { + -fx-border-width: 0; + -fx-background-color: -bs-background-color; + -fx-table-cell-border-color: transparent; +} + +.table-view .table-row-cell:selected .table-row-cell:row-selection .table-row-cell:cell-selection .text { + -fx-fill: -bs-text-color; +} + +.table-view .table-row-cell:selected .button .text { + -fx-fill: -bs-text-color; +} + +.table-view .table-row-cell .copy-icon .text, +.table-view .table-row-cell .copy-icon .text:hover { + -fx-fill: -fx-accent; +} + +.table-view .table-row-cell:selected .copy-icon .text { + -fx-fill: -bs-text-color; +} + +.table-view .table-row-cell:selected .copy-icon .text:hover { + -fx-fill: -bs-text-color; +} + +.table-view .table-row-cell:selected .hyperlink .text { + -fx-fill: -bs-text-color; + -fx-border-style: none; + -fx-border-width: 0px; +} + +.table-view .table-row-cell .hyperlink .text { + -fx-fill: -bs-rd-font-dark; + -fx-border-style: none; + -fx-border-width: 0px; +} + +.table-view .table-row-cell .hyperlink .text:hover, +.table-view .table-row-cell:selected .hyperlink .text:hover { + -fx-fill: -bs-text-color; + -fx-border-style: none; + -fx-border-width: 0px; +} + +.table-view .table-row-cell .hyperlink:hover, +.table-view .table-row-cell .hyperlink:visited, +.table-view .table-row-cell .hyperlink:hover:visited { + -fx-underline: false; + -fx-border-style: none; + -fx-border-width: 0px; +} + +.table-view .table-row-cell .hyperlink:focused { + -fx-border-style: none; + -fx-border-width: 0px; +} + +.table-view.large-rows .table-row-cell { + -fx-cell-size: 47px; +} + +/******************************************************************************* + * * + * Icons * + * * + ******************************************************************************/ + +#non-clickable-icon { + -fx-text-fill: -bs-color-gray-4; +} + +.delete-icon { + -fx-fill: -bs-red; +} + +.delete { + -fx-text-fill: -bs-rd-error-red; + -fx-fill: -bs-rd-error-red; +} + +.delete:hover { + -fx-text-fill: -bs-text-color; + -fx-fill: -bs-text-color; +} + +.warn-icon { + -fx-text-fill: -bs-yellow; + -fx-fill: -bs-yellow; +} + +.warn-icon:hover { + -fx-text-fill: -bs-yellow; + -fx-fill: -bs-yellow; +} + +.error-icon { + -fx-text-fill: -bs-rd-error-red; + -fx-fill: -bs-rd-error-red; +} + +.error-icon:hover { + -fx-text-fill: -bs-rd-error-red; + -fx-fill: -bs-rd-error-red; +} + + +/******************************************************************************* + * * + * Images * + * * + ******************************************************************************/ +.qr-code { + -fx-cursor: hand; +} + +/******************************************************************************* + * * + * Textarea * + * * + ******************************************************************************/ +.text-area { + -fx-border-color: -bs-background-gray; +} + +/******************************************************************************* + * * + * Tab pane * + * * + ******************************************************************************/ +.jfx-tab-pane { + -fx-padding: 0; + -jfx-disable-animation: true; +} + +.jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { + -fx-background-color: transparent; + -fx-pref-width: 20; + -fx-pref-height: 20; + -fx-min-width: -fx-pref-width; + -fx-max-width: -fx-pref-width; + -fx-min-height: -fx-pref-height; + -fx-max-height: -fx-pref-height; +} + +.jfx-tab-pane .headers-region .tab .tab-container .tab-close-button .jfx-rippler { + -jfx-rippler-fill: -fx-accent; +} + +.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; + -fx-background-color: -bs-rd-font-light; +} + +.jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { + -fx-padding: 0 0 2 0; +} + +.jfx-tab-pane .headers-region .tab:selected .tab-container .tab-close-button > .jfx-svg-glyph { + -fx-background-color: -fx-accent; +} + +.jfx-tab-pane .tab-header-background { + -fx-background-color: -bs-color-gray-background; + -fx-border-width: 0 0 1 0; + -fx-border-color: -bs-rd-tab-border; +} + +.jfx-tab-pane .headers-region .tab-selected-line { + -fx-background-color: -fx-accent; + -fx-pref-height: 1; +} + +.jfx-tab-pane .headers-region .tab .tab-container .tab-label { + -fx-text-fill: -bs-rd-font-light; + -fx-padding: 14; + -fx-font-size: 0.769em; + -fx-font-weight: normal; +} + +.jfx-tab-pane .depth-container { + -fx-effect: none; +} + +.jfx-tab-pane .headers-region .tab:selected .tab-container .tab-label { + -fx-text-fill: -fx-accent; +} + +.jfx-tab-pane .headers-region > .tab > .jfx-rippler { + -jfx-rippler-fill: -fx-accent; +} + +.jfx-tab-pane .headers-region .tab:closable { + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-border-style: none; + -fx-border-insets: 0; + -fx-padding: 9; +} + +.jfx-tab-pane .headers-region .tab:closable .tab-container .tab-label { + -fx-padding: 5; +} + +#form-header-text { + -fx-font-weight: bold; + -fx-font-size: 1.077em; +} + +#form-title { + -fx-font-weight: bold; +} + +/* scroll-pane */ +.scroll-pane { + -fx-background-insets: 0; + -fx-padding: 0; +} + +.scroll-pane:focused { + -fx-background-insets: 0; +} + +.scroll-pane .corner { + -fx-background-insets: 0; +} + +/* validation */ +.validation-error { + -fx-text-fill: -bs-red; +} + +/* Account */ +#content-pane-top { + -fx-background-color: -bs-color-gray-2, + linear-gradient(-bs-color-gray-2 0%, -bs-color-gray-3 100%), + linear-gradient(-bs-color-gray-2 0%, -bs-background-gray 100%); + -fx-background-insets: 0 0 0 0, 0, 1; +} + +#info-icon-label { + -fx-font-size: 1.231em; + -fx-text-fill: -bs-color-gray-13; +} + +/* OfferPayload book */ +#num-offers { + -fx-font-size: 0.923em; +} + +/* Create offer */ +#direction-icon-label { + -fx-font-weight: bold; + -fx-font-size: 1.231em; + -fx-text-fill: -bs-color-gray-6; +} + +#input-description-label { + -fx-font-size: 0.846em; + -fx-alignment: center-left; +} + +#create-offer-calc-label { + -fx-font-weight: bold; + -fx-font-size: 1.538em; + -fx-padding: 15 5 0 5; +} + +#toggle-price-left { + -fx-border-radius: 4 0 0 4; + -fx-padding: 4 4 4 4; + -fx-border-color: -bs-color-gray-4; + -fx-border-style: solid none solid solid; + -fx-border-insets: 0 -2 0 0; + -fx-background-insets: 0 -2 0 0; + -fx-background-radius: 4 0 0 4; +} + +#toggle-price-right { + -fx-border-radius: 0 4 4 0; + -fx-padding: 4 4 4 4; + -fx-border-color: -bs-color-gray-4; + -fx-border-style: solid solid solid none; + -fx-border-insets: 0 0 0 -2; + -fx-background-insets: 0 0 0 -2; + -fx-background-radius: 0 4 4 0; +} + +#totals-separator { + -fx-background: -bs-color-gray-4; +} + +#payment-info { + -fx-background-color: -bs-content-background-gray; +} + +.toggle-button-active { + -fx-background-color: -bs-blue-transparent; +} + +.toggle-button-inactive { + -fx-background-color: -bs-color-gray-1; +} + +#trade-fee-textfield { + -fx-font-size: 0.9em; + -fx-alignment: center-right; +} + +/* Open Offer */ +.offer-disabled .label { + -fx-text-fill: -bs-color-gray-3; +} + +/* OfferBook */ +.table-title { + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans Medium"; + -fx-alignment: center-left; +} + +.combo-box-editor-bold { + -fx-font-weight: bold; + -fx-padding: 5 8 5 8 !important; + -fx-text-fill: -bs-text-color; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.currency-label-small { + -fx-font-size: 0.692em; + -fx-text-fill: -bs-rd-font-lighter; + -fx-alignment: center; + -fx-pref-height: 35px; + -fx-pref-width: 45px; +} + +.offer-label-small { + -fx-font-size: 0.692em; + -fx-alignment: center-right; + -fx-text-fill: -bs-text-color; +} + +.currency-label-selected { + -fx-text-fill: -bs-text-color; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.currency-label { + -fx-font-size: 1.077em; + -fx-text-fill: -bs-rd-font-dark-gray; + -fx-alignment: center-left; + -fx-pref-height: 35px; +} + +/* Offer */ +.percentage-label { + -fx-alignment: center; +} + +.offer-separator { + -fx-background: -bs-color-gray-6; +} + +#address-text-field { + -fx-cursor: hand; + -fx-text-fill: -fx-accent; + -fx-prompt-text-fill: -bs-text-color; +} + +#address-text-field:hover { + -fx-text-fill: -bs-text-color; +} + +#address-text-field-error { + -fx-cursor: hand; + -fx-text-fill: -bs-rd-error-red; + -fx-prompt-text-fill: -bs-text-color; +} + +/* Account setup */ +#wizard-item-background-deactivated { + -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-aaa); + -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +#wizard-item-background-active { + -fx-body-color: linear-gradient(to bottom, -bs-bg-gray-5, -bs-color-gray-6); + -fx-outer-border: linear-gradient(to bottom, -bs-color-blue-1, -bs-color-blue-2); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +#wizard-item-background-completed { + -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-aaa); + -fx-outer-border: linear-gradient(to bottom, -bs-bg-green, -bs-color-green-2); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +/* Account settings */ +#account-settings-item-background-disabled { + -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-1); + -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +#account-settings-item-background-active { + -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-1); + -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +#account-settings-item-background-selected { + -fx-body-color: linear-gradient(to bottom, -bs-color-gray-5, -bs-color-gray-1); + -fx-outer-border: linear-gradient(to bottom, -bs-color-blue-1, -bs-color-blue-2); + -fx-background-color: -fx-shadow-highlight-color, + -fx-outer-border, + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +/* Pending trades */ +#trade-wizard-item-background-disabled { + -fx-text-fill: -bs-rd-font-light; +} + +#trade-wizard-item-background-active { + -fx-text-fill: -bs-text-color; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.trade-step-label { + -fx-text-fill: -bs-background-color; +} + +.trade-step-disabled-bg { + -fx-fill: -bs-color-gray-ccc; +} + +.trade-step-active-bg { + -fx-fill: -bs-color-primary-dark; +} + +.trade-msg-state-undefined { + -fx-text-fill: -bs-yellow; +} + +.trade-msg-state-sent { + -fx-text-fill: -bs-yellow-light; +} + +.trade-msg-state-arrived { + -fx-text-fill: -bs-turquoise; +} + +.trade-msg-state-stored { + -fx-text-fill: -bs-color-blue-4; +} + +.trade-msg-state-acknowledged { + -fx-text-fill: -bs-color-primary; +} + +.trade-msg-state-failed { + -fx-text-fill: -bs-rd-error-red; +} + +#open-support-button { + -fx-font-weight: bold; + -fx-font-size: 1.077em; + -fx-background-color: -bs-warning; +} + +#open-dispute-button { + -fx-font-weight: bold; + -fx-text-fill: -bs-background-color; + -fx-font-size: 1.077em; + -fx-background-color: -bs-rd-error-red; +} + +/* TitledGroupBg */ +.titled-group-bg-label, .titled-group-bg-label-active { + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans Medium"; + -fx-text-fill: -bs-text-color; + -fx-background-color: transparent; +} + +.titled-group-bg, .titled-group-bg-active { + -fx-body-color: -bs-color-gray-background; + -fx-border-color: -bs-rd-separator; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-background-insets: 0; +} + +.titled-group-bg.last, .titled-group-bg-active.last { + -fx-border-width: 0; +} + +/* TableGroupHeadline */ +#table-group-headline { + -fx-background-color: -bs-content-background-gray; + -fx-background-insets: 10 0 -1 0, 0, 1, 2; + -fx-background-radius: 3px, 3px, 2px, 1px; +} + +/* copied form modena.css text-input */ +#flow-pane-checkboxes-bg { + -fx-text-fill: -fx-text-inner-color; + -fx-highlight-fill: derive(-fx-control-inner-background, -20%); + -fx-highlight-text-fill: -fx-text-inner-color; + -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); + -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), + linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background); + -fx-background-insets: 0, 1; + -fx-background-radius: 3, 2; + -fx-padding: 0.333333em 0.583em 0.333333em 0.583em; /* 4 7 4 7 */ +} + +#flow-pane-checkboxes-non-editable-bg { + -fx-text-fill: -fx-text-inner-color; + -fx-highlight-fill: derive(-fx-control-inner-background, -20%); + -fx-highlight-text-fill: -fx-text-inner-color; + -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); + -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), + linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -bs-color-gray-1); + -fx-background-insets: 0, 1; + -fx-background-radius: 3, 2; + -fx-padding: 0.333333em 0.583em 0.333333em 0.583em; /* 4 7 4 7 */ +} + +/* message-list-view*/ +#message-list-view.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { + -fx-background-color: -bs-background-color; +} + +#message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell { + -fx-background-color: -bs-background-color; +} + +#message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled { + -fx-background-color: -bs-background-color; +} + +#message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { + -fx-background-color: -bs-background-color; +} + +#message-list-view.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell { + -fx-background-color: -bs-background-color; +} + +#message-list-view.list-cell { + -fx-padding: 0.25em 0.583em 0.25em 0.583em; +} + +#message-list-view.list-view { + -fx-background-color: -fx-box-border, -fx-control-inner-background; + -fx-background-insets: 0, 1; + -fx-padding: 1; +} + +#message-list-view.list-view:focused { + -fx-background-color: -fx-box-border, -fx-control-inner-background; + -fx-background-insets: 0, 1; + -fx-padding: 1; +} + +/* bubble */ +#message-bubble-green { + -fx-background-color: -bs-color-primary; + -fx-background-radius: 10 10 10 10; +} + +#message-bubble-blue { + -fx-background-color: -bs-rd-message-bubble; + -fx-background-radius: 10 10 10 10; +} + +#message-bubble-grey { + -fx-background-color: -bs-color-gray-3; + -fx-background-radius: 10 10 10 10; +} + +.attachment-icon { + -fx-text-fill: -bs-background-color; + -fx-cursor: hand; +} + +.attachment-icon-black { + -fx-text-fill: -bs-text-color; + -fx-cursor: hand; +} + +/******************************************************************************* + * * + * Grid pane * + * * + ******************************************************************************/ +.grid-pane { + -fx-background-color: -bs-content-background-gray; + -fx-background-radius: 5; + -fx-effect: null; + -fx-effect: dropshadow(gaussian, -bs-color-gray-10, 10, 0, 0, 0); + -fx-background-insets: 10; +} + +/******************************************************************************************************************** + * * + * Market overview * + * * + ********************************************************************************************************************/ +.chart-pane { + -fx-background-color: -bs-background-color; +} + +#charts .chart-legend, #charts-dao .chart-legend { + -fx-font-size: 1.077em; + -fx-alignment: center; +} + +#charts .axis, #price-chart .axis, #volume-chart .axis, #charts-dao .axis { + -fx-tick-label-fill: -bs-rd-font-lighter; + -fx-tick-label-font-size: 0.769em; + -fx-font-size: 0.880em; +} + +#price-chart .axis-tick-mark-text-node, +#volume-chart .axis-tick-mark-text-node, +#charts-dao .axis-tick-mark-text-node { + -fx-text-alignment: center; +} + +#charts .chart-plot-background, #charts-dao .chart-plot-background { + -fx-background-color: -bs-background-color; +} + +#charts .default-color0.chart-area-symbol { + -fx-background-color: -bs-sell, -bs-background-color; +} + +#charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { + -fx-background-color: -bs-buy, -bs-background-color; +} + +#charts .default-color0.chart-series-area-line { + -fx-stroke: -bs-sell; +} + +#charts .default-color1.chart-series-area-line, #charts-dao .default-color0.chart-series-area-line { + -fx-stroke: -bs-buy; +} + +/* The .chart-line-symbol rules change the color of the legend symbol */ +#charts-dao .default-color0.chart-series-line { -fx-stroke: -bs-chart-dao-line1; } +#charts-dao .default-color0.chart-line-symbol { -fx-background-color: -bs-chart-dao-line1, -bs-chart-dao-line1; } + +#charts-dao .default-color1.chart-series-line { -fx-stroke: -bs-chart-dao-line2; } +#charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-chart-dao-line2; } + +#charts-dao .default-color2.chart-series-line { -fx-stroke: -bs-chart-dao-line3; } +#charts-dao .default-color2.chart-line-symbol { -fx-background-color: -bs-chart-dao-line3, -bs-chart-dao-line3; } + +#charts-dao .default-color3.chart-series-line { -fx-stroke: -bs-chart-dao-line4; } +#charts-dao .default-color3.chart-line-symbol { -fx-background-color: -bs-chart-dao-line4, -bs-chart-dao-line4; } + +#charts-dao .default-color4.chart-series-line { -fx-stroke: -bs-chart-dao-line5; } +#charts-dao .default-color4.chart-line-symbol { -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; } + +#charts-dao .default-color5.chart-series-line { -fx-stroke: -bs-chart-dao-line6; } +#charts-dao .default-color5.chart-line-symbol { -fx-background-color: -bs-chart-dao-line6, -bs-chart-dao-line6; } + +#charts-legend-toggle0 { + -jfx-toggle-color: -bs-chart-dao-line1 +} +#charts-legend-toggle1 { + -jfx-toggle-color: -bs-chart-dao-line2; +} +#charts-legend-toggle2 { + -jfx-toggle-color: -bs-chart-dao-line3; +} +#charts-legend-toggle3 { + -jfx-toggle-color: -bs-chart-dao-line4; +} +#charts-legend-toggle4 { + -jfx-toggle-color: -bs-chart-dao-line5; +} +#charts-legend-toggle5 { + -jfx-toggle-color: -bs-chart-dao-line6; +} + +#charts-dao .chart-series-line { + -fx-stroke-width: 2px; +} + +#charts .default-color0.chart-series-area-fill { + -fx-fill: -bs-sell-transparent; +} + +#charts .default-color1.chart-series-area-fill, #charts-dao .default-color0.chart-series-area-fill { + -fx-fill: -bs-buy-transparent; +} +.chart-vertical-grid-lines { + -fx-stroke: transparent; +} + +#charts .axis-label { + -fx-font-size: 0.769em; + -fx-alignment: center-left; +} + +#charts .axisy .axis-label { + -fx-alignment: center; +} + +#chart-navigation-label { + -fx-text-fill: -bs-rd-font-lighter; + -fx-font-size: 0.769em; + -fx-alignment: center; +} + +#chart-navigation-center-pane { + -fx-background-color: -bs-progress-bar-track; +} + +/******************************************************************************************************************** + * * + * Highlight buttons * + * * + ********************************************************************************************************************/ +#buy-button-big { + -fx-font-size: 1em; + -fx-background-color: -bs-buy; + -fx-text-fill: -bs-white; +} + +#buy-button { + -fx-background-color: -bs-buy; + -fx-text-fill: -bs-white; +} + +#buy-button-big:hover, #buy-button:hover, +#buy-button-big:focused, #buy-button:focused { + -fx-background-color: derive(-bs-buy, -10%); +} + +#sell-button-big { + -fx-background-color: -bs-sell; + -fx-text-fill: -bs-white; + -fx-font-size: 1em; +} + +#sell-button { + -fx-background-color: -bs-sell; + -fx-text-fill: -bs-white; +} + +#sell-button-big:hover, #sell-button:hover, +#sell-button-big:focused, #sell-button:focused { + -fx-background-color: derive(-bs-sell, -10%); +} + +#sell-button-big.grey-style, #buy-button-big.grey-style, +#sell-button.grey-style, #buy-button.grey-style { + -fx-background-color: -bs-color-gray-bbb; + -fx-text-fill: -bs-rd-font-dark-gray; +} + +.action-button:disabled, #sell-button:disabled, #buy-button:disabled { + -fx-background-color: -bs-color-gray-0; + -fx-text-fill: -bs-rd-font-dark-gray; +} + +/******************************************************************************************************************** + * * + * Popups * + * * + ********************************************************************************************************************/ +.popup-headline { + -fx-font-size: 1.538em; + -fx-text-fill: -bs-rd-font-dark; +} + +.popup-headline-information { + -fx-font-size: 1.538em; + -fx-text-fill: -bs-color-primary; +} + +.popup-headline-warning { + -fx-font-size: 1.538em; + -fx-text-fill: -bs-rd-error-red; +} + +.popup-icon-information { + -fx-text-fill: -bs-color-primary; +} + +.popup-icon-warning { + -fx-text-fill: -bs-rd-error-red; +} + +.popup-bg, .notification-popup-bg, .peer-info-popup-bg { + -fx-font-size: 1.077em; + -fx-text-fill: -bs-rd-font-dark; + -fx-background-color: -bs-background-color; + -fx-background-radius: 0; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); +} + +.popup-bg-top { + -fx-font-size: 1.077em; + -fx-text-fill: -bs-rd-font-dark; + -fx-background-color: -bs-background-color; + -fx-background-radius: 0; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); +} + +.notification-popup-headline, peer-info-popup-headline { + -fx-font-size: 1.077em; + /*-fx-font-weight: bold;*/ + -fx-text-fill: -bs-color-primary; +} + +.notification-popup-bg { + -fx-font-size: 0.846em; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); +} + +.peer-info-popup-bg { + -fx-font-size: 0.846em; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); +} + +#price-feed-combo { + -fx-background-color: none; +} + +#price-feed-combo > .list-cell { + -fx-text-fill: -bs-text-color; + -fx-font-family: "IBM Plex Sans"; +} + +#invert-market-price { + -fx-text-fill: -bs-color-gray-11; +} + +#popup-qr-code-info { + -fx-font-size: 0.846em; +} + +#ident-num-label { + -fx-font-weight: bold; + -fx-alignment: center; + -fx-font-size: 0.769em; + -fx-text-fill: -bs-background-color; +} + +#toggle-left { + -fx-border-radius: 4 0 0 4; + -fx-border-color: -bs-rd-separator-dark; + -fx-border-style: solid; + -fx-border-width: 0 1 0 0; + -fx-background-radius: 4 0 0 4; + -fx-background-color: -bs-background-color; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); +} + +#toggle-center { + -fx-border-radius: 0; + -fx-border-color: -bs-rd-separator-dark; + -fx-border-style: solid; + -fx-border-width: 0 1 0 0; + -fx-border-insets: 0; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-background-color: -bs-background-color; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); +} + +#toggle-center:selected, #toggle-left:selected, #toggle-right:selected { + -fx-text-fill: -bs-background-color; + -fx-background-color: -bs-toggle-selected; +} + +#toggle-right { + -fx-border-radius: 0 4 4 0; + -fx-border-width: 0; + -fx-background-radius: 0 4 4 0; + -fx-background-color: -bs-background-color; + -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); +} + +#toggle-left:hover, #toggle-right:hover, #toggle-center:hover { + -fx-background-color: -bs-toggle-selected; +} + +/******************************************************************************************************************** + * * + * Arbitration * + * * + ********************************************************************************************************************/ +.message { + -fx-text-fill: -bs-text-color; +} + +.my-message { + -fx-text-fill: -bs-background-color; +} + +.message-header { + -fx-text-fill: -bs-color-gray-3; + -fx-font-size: 0.846em; +} + +.my-message-header { + -fx-text-fill: -bs-rd-message-bubble; + -fx-fill: -bs-rd-message-bubble; + -fx-font-size: 0.846em; +} + +.dispute-chat-border { + -fx-background-color: -bs-color-blue-5; +} + +/******************************************************************************************************************** + * * + * DAO * + * * + ********************************************************************************************************************/ +.dao-tx-type-trade-fee-icon, +.dao-tx-type-trade-fee-icon:hover { + -fx-text-fill: -bs-color-green-2; +} + +.dao-tx-type-unverified-icon, +.dao-tx-type-unverified-icon:hover { + -fx-text-fill: -bs-yellow; +} + +.dao-tx-type-invalid-icon, +.dao-tx-type-invalid-icon:hover { + -fx-text-fill: -bs-red-soft; +} + +.dao-tx-type-self-icon, +.dao-tx-type-self-icon:hover { + -fx-text-fill: -bs-color-gray-2; +} + +.dao-tx-type-proposal-fee-icon, +.dao-tx-type-proposal-fee-icon:hover { + -fx-text-fill: -bs-color-green-4; +} + +.dao-tx-type-genesis-icon, +.dao-tx-type-genesis-icon:hover { + -fx-text-fill: -fx-accent; +} + +.dao-tx-type-received-funds-icon, +.dao-tx-type-received-funds-icon:hover { + -fx-text-fill: -bs-green-soft; +} + +.dao-tx-type-sent-funds-icon, +.dao-tx-type-sent-funds-icon:hover { + -fx-text-fill: -bs-red-soft; +} + +.dao-tx-type-vote-icon, +.dao-tx-type-vote-icon:hover { + -fx-text-fill: -bs-color-blue-5; +} + +.dao-tx-type-vote-reveal-icon, +.dao-tx-type-vote-reveal-icon:hover { + -fx-text-fill: -bs-color-blue-4; +} + +.dao-tx-type-issuance-icon, +.dao-tx-type-issuance-icon:hover { + -fx-text-fill: -bs-color-green-3; +} + +.dao-tx-type-lockup-icon, +.dao-tx-type-lockup-icon:hover { + -fx-text-fill: -bs-color-blue-5; +} + +.dao-tx-type-unlock-icon, +.dao-tx-type-unlock-icon:hover { + -fx-text-fill: -bs-color-green-3; +} + +.dao-accepted-icon { + -fx-text-fill: -bs-color-primary; +} + +.dao-rejected-icon { + -fx-text-fill: -bs-rd-error-red; +} + +.dao-ignored-icon { + -fx-text-fill: -bs-color-gray-4; +} + +.compensation-root { + -fx-background-insets: 0, 0 0 0 0; +} + +.info-icon { + -fx-text-fill: -fx-accent; +} + +.info-icon-button { + -fx-cursor: hand; + -fx-background-color: transparent; +} + +.dao-remove-proposal-icon { + -fx-text-fill: -fx-accent; +} + +.dao-news-titled-group .titled-group-bg-label-active { + -fx-font-size: 0.923em; +} + +.dao-news-teaser { + -fx-font-size: 1.538em; + -fx-font-family: "IBM Plex Sans Light"; +} + +.dao-news-section-header { + -fx-font-size: 1.923em; + -fx-text-fill: -bs-rd-green-dark; + -fx-font-family: "IBM Plex Sans Light"; +} + +.dao-news-section-content, .dao-news-content, .dao-news-section-link { + -fx-font-size: 0.923em; +} + +.dao-news-section-content { + -fx-text-fill: -bs-rd-font-dark; +} + +.dao-news-content, .dao-news-section-link, .dao-news-section-link .hyperlink, .dao-launch-version { + -fx-text-fill: -bs-rd-font-light; + -fx-fill: -bs-rd-font-light; +} + +.dao-news-link { + -fx-text-fill: -fx-accent; + -fx-padding: 16 0 0 0; +} + +.dao-news-link .hyperlink { + -fx-fill: -fx-accent; +} + +.dao-inSync { + -fx-text-fill: -bs-rd-green; +} + +.dao-inConflict { + -fx-text-fill: -bs-rd-error-red; +} + +.dao-kpi-big { + -fx-font-size: 1.923em; + -fx-text-fill: -bs-rd-font-dark; + -fx-font-family: "IBM Plex Sans Light"; +} + +.dao-kpi-subtext { + -fx-text-fill: -bs-rd-font-light; + -fx-font-size: 0.923em; +} + +.price-trend-up { + -fx-text-fill: -bs-color-primary; + -fx-padding: 2 0 0 0; +} + +.price-trend-down { + -fx-text-fill: -bs-red; + -fx-padding: 2 0 0 0; +} + +/******************************************************************************************************************** + * * + * News * + * * + ********************************************************************************************************************/ + +.news-version { + -fx-alignment: center-left; + -fx-font-size: 1em; +} + +.news-feature-headline { + -fx-font-size: 1.077em; + -fx-text-fill: -bs-rd-font-dark-gray; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.news-feature-description { + -fx-font-size: 1em; + -fx-text-fill: -bs-rd-font-dark-gray; +} + +.news-feature-image { + -fx-border-style: solid; + -fx-border-width: 1; + -fx-border-color: -bs-rd-separator-dark; +} +/******************************************************************************************************************** + * * + * Notifications * + * * + ********************************************************************************************************************/ +#notification-erase-button { + -fx-background-color: -bs-red-soft; + -fx-text-fill: -bs-background-color; +} + +.status-icon { + -fx-text-fill: -fx-faint-focus-color; +} + +/******************************************************************************************************************** + * * + * Popover * + * * + ********************************************************************************************************************/ +.popover > .content { + -fx-padding: 10; +} + +.popover > .content .default-text { + -fx-text-fill: -bs-text-color; +} + +.popover > .border { + -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-transparent-dark) !important; + -fx-fill: -bs-background-color !important; +} diff --git a/desktop/src/main/java/bisq/desktop/common/UITimer.java b/desktop/src/main/java/bisq/desktop/common/UITimer.java new file mode 100644 index 0000000000..e8e92901e9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/UITimer.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.desktop.common; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.reactfx.FxTimer; + +import javafx.application.Platform; + +import java.time.Duration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UITimer implements Timer { + private final Logger log = LoggerFactory.getLogger(UITimer.class); + private bisq.common.reactfx.Timer timer; + + public UITimer() { + } + + @Override + public Timer runLater(Duration delay, Runnable runnable) { + executeDirectlyIfPossible(() -> { + if (timer == null) { + timer = FxTimer.create(delay, runnable); + timer.restart(); + } else { + log.warn("runLater called on an already running timer."); + } + }); + return this; + } + + @Override + public Timer runPeriodically(Duration interval, Runnable runnable) { + executeDirectlyIfPossible(() -> { + if (timer == null) { + timer = FxTimer.createPeriodic(interval, runnable); + timer.restart(); + } else { + log.warn("runPeriodically called on an already running timer."); + } + }); + return this; + } + + @Override + public void stop() { + executeDirectlyIfPossible(() -> { + if (timer != null) { + timer.stop(); + timer = null; + } + }); + } + + private void executeDirectlyIfPossible(Runnable runnable) { + if (Platform.isFxApplicationThread()) { + runnable.run(); + } else { + UserThread.execute(runnable); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/ViewfxException.java b/desktop/src/main/java/bisq/desktop/common/ViewfxException.java new file mode 100644 index 0000000000..a2012f1bf4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/ViewfxException.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.desktop.common; + +import static java.lang.String.format; + +public class ViewfxException extends RuntimeException { + public ViewfxException(Throwable cause, String format, Object... args) { + super(format(format, args), cause); + } + + public ViewfxException(String format, Object... args) { + super(format(format, args)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/fxml/FxmlViewLoader.java b/desktop/src/main/java/bisq/desktop/common/fxml/FxmlViewLoader.java new file mode 100644 index 0000000000..95135a0561 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/fxml/FxmlViewLoader.java @@ -0,0 +1,147 @@ +/* + * 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 bisq.desktop.common.fxml; + +import bisq.desktop.common.ViewfxException; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewFactory; +import bisq.desktop.common.view.ViewLoader; + +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.google.common.base.Joiner; + +import javafx.fxml.FXMLLoader; + +import java.net.URL; + +import java.io.IOException; + +import java.util.ResourceBundle; + +import java.lang.annotation.Annotation; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public class FxmlViewLoader implements ViewLoader { + + private final ViewFactory viewFactory; + private final ResourceBundle resourceBundle; + + @Inject + public FxmlViewLoader(ViewFactory viewFactory, ResourceBundle resourceBundle) { + this.viewFactory = viewFactory; + this.resourceBundle = resourceBundle; + } + + @SuppressWarnings("unchecked") + public View load(Class viewClass) { + FxmlView fxmlView = viewClass.getAnnotation(FxmlView.class); + + final Class convention; + final Class defaultConvention = + (Class) getDefaultValue(FxmlView.class, "convention"); + + final String specifiedLocation; + final String defaultLocation = (String) getDefaultValue(FxmlView.class, "location"); + + if (fxmlView == null) { + convention = defaultConvention; + specifiedLocation = defaultLocation; + } else { + convention = fxmlView.convention(); + specifiedLocation = fxmlView.location(); + } + + if (convention == null || specifiedLocation == null) + throw new IllegalStateException("Convention and location should never be null."); + + + try { + final String resolvedLocation; + if (specifiedLocation.equals(defaultLocation)) + resolvedLocation = convention.newInstance().apply(viewClass); + else + resolvedLocation = specifiedLocation; + + URL fxmlUrl = viewClass.getClassLoader().getResource(resolvedLocation); + if (fxmlUrl == null) + throw new ViewfxException( + "Failed to load view class [%s] because FXML file at [%s] could not be loaded " + + "as a classpath resource. Does it exist?", viewClass, specifiedLocation); + + return loadFromFxml(fxmlUrl); + } catch (InstantiationException | IllegalAccessException ex) { + throw new ViewfxException(ex, "Failed to load view from class %s", viewClass); + } + } + + private View loadFromFxml(URL fxmlUrl) { + checkNotNull(fxmlUrl, "FXML URL must not be null"); + try { + FXMLLoader loader = new FXMLLoader(fxmlUrl, resourceBundle); + loader.setControllerFactory(viewFactory); + loader.load(); + Object controller = loader.getController(); + if (controller == null) + throw new ViewfxException("Failed to load view from FXML file at [%s]. " + + "Does it declare an fx:controller attribute?", fxmlUrl); + if (!(controller instanceof View)) + throw new ViewfxException("Controller of type [%s] loaded from FXML file at [%s] " + + "does not implement [%s] as expected.", controller.getClass(), fxmlUrl, View.class); + return (View) controller; + } catch (IOException ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + cause.printStackTrace(); + log.error(cause.toString()); + // We want to show stackTrace in error popup + String stackTrace = Utilities.toTruncatedString(Joiner.on("\n").join(cause.getStackTrace()), 800, false); + throw new ViewfxException(cause, "%s at loading view class\nStack trace:\n%s", + cause.getClass().getSimpleName(), stackTrace); + } else { + throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", fxmlUrl); + } + } + } + + /** + * Copied and adapted from Spring Framework v4.3.6's AnnotationUtils#defaultValue + * method in order to make it possible to drop Bisq's dependency on Spring altogether. + */ + @SuppressWarnings("SameParameterValue") + private static Object getDefaultValue(Class annotationType, String attributeName) { + if (annotationType == null || attributeName == null || attributeName.length() == 0) { + return null; + } + try { + return annotationType.getDeclaredMethod(attributeName).getDefaultValue(); + } catch (Exception ex) { + return null; + } + } +} + diff --git a/desktop/src/main/java/bisq/desktop/common/model/Activatable.java b/desktop/src/main/java/bisq/desktop/common/model/Activatable.java new file mode 100644 index 0000000000..69ff8fca27 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/Activatable.java @@ -0,0 +1,25 @@ +/* + * 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 bisq.desktop.common.model; + +public interface Activatable { + + void _activate(); + + void _deactivate(); +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/ActivatableDataModel.java b/desktop/src/main/java/bisq/desktop/common/model/ActivatableDataModel.java new file mode 100644 index 0000000000..726a81ac32 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/ActivatableDataModel.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.desktop.common.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ActivatableDataModel implements Activatable, DataModel { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public final void _activate() { + this.activate(); + } + + protected void activate() { + } + + @Override + public final void _deactivate() { + this.deactivate(); + } + + protected void deactivate() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/ActivatableViewModel.java b/desktop/src/main/java/bisq/desktop/common/model/ActivatableViewModel.java new file mode 100644 index 0000000000..86356c9330 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/ActivatableViewModel.java @@ -0,0 +1,41 @@ +/* + * 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 bisq.desktop.common.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ActivatableViewModel implements Activatable, ViewModel { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public final void _activate() { + this.activate(); + } + + protected void activate() { + } + + @Override + public final void _deactivate() { + this.deactivate(); + } + + protected void deactivate() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/ActivatableWithDataModel.java b/desktop/src/main/java/bisq/desktop/common/model/ActivatableWithDataModel.java new file mode 100644 index 0000000000..e6c9353c26 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/ActivatableWithDataModel.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 bisq.desktop.common.model; + +public class ActivatableWithDataModel extends WithDataModel implements Activatable { + + public ActivatableWithDataModel(D dataModel) { + super(dataModel); + } + + @Override + public final void _activate() { + dataModel._activate(); + this.activate(); + } + + protected void activate() { + } + + @Override + public final void _deactivate() { + dataModel._deactivate(); + this.deactivate(); + } + + protected void deactivate() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/DataModel.java b/desktop/src/main/java/bisq/desktop/common/model/DataModel.java new file mode 100644 index 0000000000..d1a0ae352f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/DataModel.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.desktop.common.model; + +public interface DataModel extends Model { +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/Model.java b/desktop/src/main/java/bisq/desktop/common/model/Model.java new file mode 100644 index 0000000000..99f62a0d26 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/Model.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.desktop.common.model; + +public interface Model { +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/ViewModel.java b/desktop/src/main/java/bisq/desktop/common/model/ViewModel.java new file mode 100644 index 0000000000..c27cbf2ae9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/ViewModel.java @@ -0,0 +1,21 @@ +/* + * 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 bisq.desktop.common.model; + +public interface ViewModel extends Model { +} diff --git a/desktop/src/main/java/bisq/desktop/common/model/WithDataModel.java b/desktop/src/main/java/bisq/desktop/common/model/WithDataModel.java new file mode 100644 index 0000000000..652de90bb6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/model/WithDataModel.java @@ -0,0 +1,33 @@ +/* + * 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 bisq.desktop.common.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class WithDataModel { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + public final D dataModel; + + protected WithDataModel(D dataModel) { + this.dataModel = checkNotNull(dataModel, "Delegate object must not be null"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/AbstractView.java b/desktop/src/main/java/bisq/desktop/common/view/AbstractView.java new file mode 100644 index 0000000000..265a229a0e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/AbstractView.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.desktop.common.view; + +import javafx.fxml.FXML; + +import javafx.scene.Node; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractView implements View { + + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + protected + @FXML + R root; + protected final M model; + + public AbstractView(M model) { + this.model = model; + } + + public AbstractView() { + this(null); + } + + public R getRoot() { + return root; + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/ActivatableView.java b/desktop/src/main/java/bisq/desktop/common/view/ActivatableView.java new file mode 100644 index 0000000000..a8ebe57bfa --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/ActivatableView.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.desktop.common.view; + +import javafx.scene.Node; + +public abstract class ActivatableView extends InitializableView { + + public ActivatableView(M model) { + super(model); + } + + public ActivatableView() { + this(null); + } + + @Override + protected void prepareInitialize() { + if (root != null) { + root.sceneProperty().addListener((ov, oldValue, newValue) -> { + if (oldValue == null && newValue != null) + activate(); + else if (oldValue != null && newValue == null) + deactivate(); + }); + } + } + + protected void activate() { + } + + protected void deactivate() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/ActivatableViewAndModel.java b/desktop/src/main/java/bisq/desktop/common/view/ActivatableViewAndModel.java new file mode 100644 index 0000000000..433a19d136 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/ActivatableViewAndModel.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.desktop.common.view; + +import bisq.desktop.common.model.Activatable; + +import javafx.scene.Node; + +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class ActivatableViewAndModel extends ActivatableView { + + public ActivatableViewAndModel(M model) { + super(checkNotNull(model, "Model must not be null")); + } + + @Override + protected void prepareInitialize() { + if (root != null) { + root.sceneProperty().addListener((ov, oldValue, newValue) -> { + if (oldValue == null && newValue != null) { + model._activate(); + activate(); + } else if (oldValue != null && newValue == null) { + model._deactivate(); + deactivate(); + } + }); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/CachingViewLoader.java b/desktop/src/main/java/bisq/desktop/common/view/CachingViewLoader.java new file mode 100644 index 0000000000..06e47daace --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/CachingViewLoader.java @@ -0,0 +1,50 @@ +/* + * 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 bisq.desktop.common.view; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.HashMap; +import java.util.Map; + +@Singleton +public class CachingViewLoader implements ViewLoader { + + private final Map, View> cache = new HashMap<>(); + private final ViewLoader viewLoader; + + @Inject + public CachingViewLoader(ViewLoader viewLoader) { + this.viewLoader = viewLoader; + } + + @Override + public View load(Class viewClass) { + if (cache.containsKey(viewClass)) + return cache.get(viewClass); + + View view = viewLoader.load(viewClass); + cache.put(viewClass, view); + return view; + } + + public void removeFromCache(Class viewClass) { + cache.remove(viewClass); + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/DefaultPathConvention.java b/desktop/src/main/java/bisq/desktop/common/view/DefaultPathConvention.java new file mode 100644 index 0000000000..5c74828f5e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/DefaultPathConvention.java @@ -0,0 +1,30 @@ +/* + * 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 bisq.desktop.common.view; + +public class DefaultPathConvention implements FxmlView.PathConvention { + + /** + * Convert a '.'-based fully-qualified name of {@code viewClass} to a '/'-based + * resource path suffixed with ".fxml". + */ + @Override + public String apply(Class viewClass) { + return viewClass.getName().replace('.', '/').concat(".fxml"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/FxmlView.java b/desktop/src/main/java/bisq/desktop/common/view/FxmlView.java new file mode 100644 index 0000000000..534991f1ac --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/FxmlView.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.desktop.common.view; + +import java.util.function.Function; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FxmlView { + + /** + * The location of the FXML file associated with annotated {@link View} class. By default the location will be + * determined by {@link #convention()}. + */ + String location() default ""; + + /** + * The function used to determine the location of the FXML file associated with the annotated {@link View} class. + * By default it is the fully-qualified view class name, converted to a resource path, replacing the + * {@code .class} suffix replaced with {@code .fxml}. + */ + Class convention() default DefaultPathConvention.class; + + interface PathConvention extends Function, String> { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/InitializableView.java b/desktop/src/main/java/bisq/desktop/common/view/InitializableView.java new file mode 100644 index 0000000000..3a3336396b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/InitializableView.java @@ -0,0 +1,49 @@ +/* + * 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 bisq.desktop.common.view; + +import javafx.fxml.Initializable; + +import javafx.scene.Node; + +import java.net.URL; + +import java.util.ResourceBundle; + +public abstract class InitializableView extends AbstractView implements Initializable { + + public InitializableView(M model) { + super(model); + } + + public InitializableView() { + this(null); + } + + @Override + public final void initialize(URL location, ResourceBundle resources) { + prepareInitialize(); + initialize(); + } + + protected void prepareInitialize() { + } + + protected void initialize() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/View.java b/desktop/src/main/java/bisq/desktop/common/view/View.java new file mode 100644 index 0000000000..a7d35ee90b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/View.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.desktop.common.view; + +import javafx.scene.Node; + +public interface View { + Node getRoot(); +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/ViewFactory.java b/desktop/src/main/java/bisq/desktop/common/view/ViewFactory.java new file mode 100644 index 0000000000..454c76b112 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/ViewFactory.java @@ -0,0 +1,23 @@ +/* + * 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 bisq.desktop.common.view; + +import javafx.util.Callback; + +public interface ViewFactory extends Callback, Object> { +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/ViewLoader.java b/desktop/src/main/java/bisq/desktop/common/view/ViewLoader.java new file mode 100644 index 0000000000..1775889526 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/ViewLoader.java @@ -0,0 +1,22 @@ +/* + * 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 bisq.desktop.common.view; + +public interface ViewLoader { + View load(Class viewClass); +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/ViewPath.java b/desktop/src/main/java/bisq/desktop/common/view/ViewPath.java new file mode 100644 index 0000000000..8b4a80146a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/ViewPath.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.desktop.common.view; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +public final class ViewPath extends ArrayList> { + private ViewPath() { + } + + public ViewPath(Collection> c) { + super(c); + } + + @SafeVarargs + public static ViewPath to(Class... elements) { + ViewPath path = new ViewPath(); + List> list = Arrays.asList(elements); + path.addAll(list); + return path; + } + + public static ViewPath from(ViewPath original) { + ViewPath path = new ViewPath(); + path.addAll(original); + return path; + } + + public Class tip() { + if (size() == 0) + return null; + + return get(size() - 1); + } +} diff --git a/desktop/src/main/java/bisq/desktop/common/view/guice/InjectorViewFactory.java b/desktop/src/main/java/bisq/desktop/common/view/guice/InjectorViewFactory.java new file mode 100644 index 0000000000..b85de8fb75 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/common/view/guice/InjectorViewFactory.java @@ -0,0 +1,42 @@ +/* + * 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 bisq.desktop.common.view.guice; + +import bisq.desktop.common.view.ViewFactory; + +import com.google.inject.Injector; + +import javax.inject.Singleton; + +import com.google.common.base.Preconditions; + +@Singleton +public class InjectorViewFactory implements ViewFactory { + + private Injector injector; + + public void setInjector(Injector injector) { + this.injector = injector; + } + + @Override + public Object call(Class aClass) { + Preconditions.checkNotNull(injector, "Injector has not yet been provided"); + return injector.getInstance(aClass); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AddressTextField.java b/desktop/src/main/java/bisq/desktop/components/AddressTextField.java new file mode 100644 index 0000000000..f9ca98a113 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AddressTextField.java @@ -0,0 +1,169 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; + +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.net.URI; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddressTextField extends AnchorPane { + private static final Logger log = LoggerFactory.getLogger(AddressTextField.class); + + private final StringProperty address = new SimpleStringProperty(); + private final StringProperty paymentLabel = new SimpleStringProperty(); + private final ObjectProperty amountAsCoin = new SimpleObjectProperty<>(Coin.ZERO); + private boolean wasPrimaryButtonDown; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public AddressTextField(String label) { + JFXTextField textField = new BisqTextField(); + textField.setId("address-text-field"); + textField.setEditable(false); + textField.setLabelFloat(true); + textField.setPromptText(label); + + textField.textProperty().bind(address); + String tooltipText = Res.get("addressTextField.openWallet"); + textField.setTooltip(new Tooltip(tooltipText)); + + textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown()); + textField.setOnMouseReleased(event -> { + if (wasPrimaryButtonDown) + GUIUtil.showFeeInfoBeforeExecute(AddressTextField.this::openWallet); + + wasPrimaryButtonDown = false; + }); + + textField.focusTraversableProperty().set(focusTraversableProperty().get()); + Label extWalletIcon = new Label(); + extWalletIcon.setLayoutY(3); + extWalletIcon.getStyleClass().addAll("icon", "highlight"); + extWalletIcon.setTooltip(new Tooltip(tooltipText)); + AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN); + extWalletIcon.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); + + Label copyIcon = new Label(); + copyIcon.setLayoutY(3); + copyIcon.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + copyIcon.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(() -> { + if (address.get() != null && address.get().length() > 0) + Utilities.copyToClipboard(address.get()); + })); + + AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(extWalletIcon, 5.0); + AnchorPane.setRightAnchor(textField, 55.0); + AnchorPane.setLeftAnchor(textField, 0.0); + + getChildren().addAll(textField, copyIcon, extWalletIcon); + } + + private void openWallet() { + try { + Utilities.openURI(URI.create(getBitcoinURI())); + } catch (Exception e) { + log.warn(e.getMessage()); + new Popup().warning(Res.get("addressTextField.openWallet.failed")).show(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setAddress(String address) { + this.address.set(address); + } + + public String getAddress() { + return address.get(); + } + + public StringProperty addressProperty() { + return address; + } + + public Coin getAmountAsCoin() { + return amountAsCoin.get(); + } + + public ObjectProperty amountAsCoinProperty() { + return amountAsCoin; + } + + public void setAmountAsCoin(Coin amountAsCoin) { + this.amountAsCoin.set(amountAsCoin); + } + + public String getPaymentLabel() { + return paymentLabel.get(); + } + + public StringProperty paymentLabelProperty() { + return paymentLabel; + } + + public void setPaymentLabel(String paymentLabel) { + this.paymentLabel.set(paymentLabel); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getBitcoinURI() { + if (amountAsCoin.get().isNegative()) { + log.warn("Amount must not be negative"); + setAmountAsCoin(Coin.ZERO); + } + return GUIUtil.getBitcoinURI(address.get(), amountAsCoin.get(), + paymentLabel.get()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AddressWithIconAndDirection.java b/desktop/src/main/java/bisq/desktop/components/AddressWithIconAndDirection.java new file mode 100644 index 0000000000..6f9c0defc4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AddressWithIconAndDirection.java @@ -0,0 +1,75 @@ +/* + * 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 bisq.desktop.components; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddressWithIconAndDirection extends HBox { + private static final Logger log = LoggerFactory.getLogger(AddressWithIconAndDirection.class); + private final Hyperlink hyperlink; + + public AddressWithIconAndDirection(String text, String address, boolean received) { + Label directionIcon = new Label(); + directionIcon.getStyleClass().add("icon"); + directionIcon.getStyleClass().add(received ? "received-funds-icon" : "sent-funds-icon"); + AwesomeDude.setIcon(directionIcon, received ? AwesomeIcon.SIGNIN : AwesomeIcon.SIGNOUT); + if (received) + directionIcon.setRotate(180); + directionIcon.setMouseTransparent(true); + + setAlignment(Pos.CENTER_LEFT); + Label label = new AutoTooltipLabel(text); + label.setMouseTransparent(true); + HBox.setMargin(directionIcon, new Insets(0, 3, 0, 0)); + HBox.setHgrow(label, Priority.ALWAYS); + + hyperlink = new ExternalHyperlink(address); + HBox.setMargin(hyperlink, new Insets(0)); + HBox.setHgrow(hyperlink, Priority.SOMETIMES); + // You need to set max width to Double.MAX_VALUE to make HBox.setHgrow working like expected! + // also pref width needs to be not default (-1) + hyperlink.setMaxWidth(Double.MAX_VALUE); + hyperlink.setPrefWidth(0); + + getChildren().addAll(directionIcon, label, hyperlink); + } + + public void setOnAction(EventHandler handler) { + hyperlink.setOnAction(handler); + } + + public void setTooltip(Tooltip tooltip) { + hyperlink.setTooltip(tooltip); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipButton.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipButton.java new file mode 100644 index 0000000000..ea084195df --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipButton.java @@ -0,0 +1,62 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.skins.JFXButtonSkin; + +import javafx.scene.Node; +import javafx.scene.control.Skin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipButton extends JFXButton { + + public AutoTooltipButton() { + super(); + } + + public AutoTooltipButton(String text) { + super(text.toUpperCase()); + } + + public AutoTooltipButton(String text, Node graphic) { + super(text.toUpperCase(), graphic); + } + + public void updateText(String text) { + setText(text.toUpperCase()); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipButtonSkin(this); + } + + private class AutoTooltipButtonSkin extends JFXButtonSkin { + public AutoTooltipButtonSkin(JFXButton button) { + super(button); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipCheckBox.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipCheckBox.java new file mode 100644 index 0000000000..799c712306 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipCheckBox.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.skins.JFXCheckBoxSkin; + +import javafx.scene.control.Skin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipCheckBox extends JFXCheckBox { + + public AutoTooltipCheckBox() { + super(); + } + + public AutoTooltipCheckBox(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipCheckBoxSkin(this); + } + + private class AutoTooltipCheckBoxSkin extends JFXCheckBoxSkin { + public AutoTooltipCheckBoxSkin(JFXCheckBox checkBox) { + super(checkBox); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipLabel.java new file mode 100644 index 0000000000..96681d4aa6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipLabel.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.desktop.components; + +import javafx.scene.control.Label; +import javafx.scene.control.Skin; +import javafx.scene.control.skin.LabelSkin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipLabel extends Label { + + public AutoTooltipLabel() { + super(); + } + + public AutoTooltipLabel(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipLabelSkin(this); + } + + private class AutoTooltipLabelSkin extends LabelSkin { + + public AutoTooltipLabelSkin(Label label) { + super(label); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipRadioButton.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipRadioButton.java new file mode 100644 index 0000000000..b329ec5186 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipRadioButton.java @@ -0,0 +1,52 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXRadioButton; + +import javafx.scene.control.Skin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipRadioButton extends JFXRadioButton { + + public AutoTooltipRadioButton() { + super(); + } + + public AutoTooltipRadioButton(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipRadioButtonSkin(this); + } + + private class AutoTooltipRadioButtonSkin extends JFXRadioButtonSkinBisqStyle { + public AutoTooltipRadioButtonSkin(JFXRadioButton radioButton) { + super(radioButton); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipSlideToggleButton.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipSlideToggleButton.java new file mode 100644 index 0000000000..4c98e594fd --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipSlideToggleButton.java @@ -0,0 +1,31 @@ +package bisq.desktop.components; + +import com.jfoenix.controls.JFXToggleButton; +import com.jfoenix.skins.JFXToggleButtonSkin; + +import javafx.scene.control.Skin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipSlideToggleButton extends JFXToggleButton { + public AutoTooltipSlideToggleButton() { + super(); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipSlideToggleButton.AutoTooltipSlideToggleButtonSkin(this); + } + + private class AutoTooltipSlideToggleButtonSkin extends JFXToggleButtonSkin { + public AutoTooltipSlideToggleButtonSkin(JFXToggleButton toggleButton) { + super(toggleButton); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java new file mode 100644 index 0000000000..aa6cb17af2 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java @@ -0,0 +1,83 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.layout.HBox; + +public class AutoTooltipTableColumn extends TableColumn { + + private Label helpIcon; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); + + public AutoTooltipTableColumn(String text) { + super(); + + setTitle(text); + } + + public AutoTooltipTableColumn(String text, String help) { + + setTitleWithHelpText(text, help); + } + + public void setTitle(String title) { + setGraphic(new AutoTooltipLabel(title)); + } + + public void setTitleWithHelpText(String title, String help) { + helpIcon = new Label(); + AwesomeDude.setIcon(helpIcon, AwesomeIcon.QUESTION_SIGN, "1em"); + helpIcon.setOpacity(0.4); + helpIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(help))); + helpIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); + + final AutoTooltipLabel label = new AutoTooltipLabel(title); + final HBox hBox = new HBox(label, helpIcon); + hBox.setStyle("-fx-alignment: center-left"); + hBox.setSpacing(4); + setGraphic(hBox); + } + + private PopOver createInfoPopOver(String help) { + Label helpLabel = new Label(help); + helpLabel.setMaxWidth(300); + helpLabel.setWrapText(true); + return createInfoPopOver(helpLabel); + } + + private PopOver createInfoPopOver(Node node) { + node.getStyleClass().add("default-text"); + + PopOver infoPopover = new PopOver(node); + if (helpIcon.getScene() != null) { + infoPopover.setDetachable(false); + infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); + + infoPopover.show(helpIcon, -10); + } + return infoPopover; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipToggleButton.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipToggleButton.java new file mode 100644 index 0000000000..a530599c8f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipToggleButton.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.desktop.components; + +import javafx.scene.Node; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.skin.ToggleButtonSkin; + +import static bisq.desktop.components.TooltipUtil.showTooltipIfTruncated; + +public class AutoTooltipToggleButton extends ToggleButton { + + public AutoTooltipToggleButton() { + super(); + } + + public AutoTooltipToggleButton(String text) { + super(text); + } + + public AutoTooltipToggleButton(String text, Node graphic) { + super(text, graphic); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipToggleButtonSkin(this); + } + + private class AutoTooltipToggleButtonSkin extends ToggleButtonSkin { + public AutoTooltipToggleButtonSkin(ToggleButton toggleButton) { + super(toggleButton); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + showTooltipIfTruncated(this, getSkinnable()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java new file mode 100644 index 0000000000..716f81dc0c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java @@ -0,0 +1,197 @@ +/* + * 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 bisq.desktop.components; + +import bisq.common.UserThread; + +import org.apache.commons.lang3.StringUtils; + +import com.jfoenix.controls.JFXComboBox; +import com.jfoenix.skins.JFXComboBoxListViewSkin; + +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; + +import javafx.event.Event; +import javafx.event.EventHandler; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implements searchable dropdown (an autocomplete like experience). + * + * Clients must use setAutocompleteItems() instead of setItems(). + * + * @param type of the ComboBox item; in the simplest case this can be a String + */ +public class AutocompleteComboBox extends JFXComboBox { + private ArrayList completeList; + private ArrayList matchingList; + private JFXComboBoxListViewSkin comboBoxListViewSkin; + + public AutocompleteComboBox() { + this(FXCollections.observableArrayList()); + } + + private AutocompleteComboBox(ObservableList items) { + super(items); + setEditable(true); + clearOnFocus(); + setEmptySkinToGetMoreControlOverListView(); + fixSpaceKey(); + setAutocompleteItems(items); + reactToQueryChanges(); + } + + /** + * Set the complete list of ComboBox items. Use this instead of setItems(). + */ + public void setAutocompleteItems(List items) { + completeList = new ArrayList<>(items); + matchingList = new ArrayList<>(completeList); + setValue(null); + getSelectionModel().clearSelection(); + setItems(FXCollections.observableList(matchingList)); + getEditor().setText(""); + } + + /** + * Triggered when value change is *confirmed*. In practical terms + * this is when user clicks item on the dropdown or hits [ENTER] + * while typing in the text. + * + * This is in contrast to onAction event that is triggered + * on every (unconfirmed) value change. The onAction is not really + * suitable for the search enabled ComboBox. + */ + public final void setOnChangeConfirmed(EventHandler eh) { + setOnHidden(e -> { + var inputText = getEditor().getText(); + + // Case 1: fire if input text selects (matches) an item + var selectedItem = getSelectionModel().getSelectedItem(); + var inputTextItem = getConverter().fromString(inputText); + if (selectedItem != null && selectedItem.equals(inputTextItem)) { + eh.handle(e); + getParent().requestFocus(); + return; + } + + // Case 2: fire if the text is empty to support special "show all" case + if (inputText.isEmpty()) { + eh.handle(e); + getParent().requestFocus(); + } + }); + } + + // Clear selection and query when ComboBox gets new focus. This is usually what user + // wants - to have a blank slate for a new search. The primary motivation though + // was to work around UX glitches related to (starting) editing text when combobox + // had specific item selected. + private void clearOnFocus() { + getEditor().focusedProperty().addListener((observableValue, hadFocus, hasFocus) -> { + if (!hadFocus && hasFocus) { + removeFilter(); + forceRedraw(); + } + }); + } + + // The ComboBox API does not provide enough control over the underlying + // ListView that is used as a dropdown. The only way to get this control + // is to set custom ListViewSkin. The default skin is null and so useless. + private void setEmptySkinToGetMoreControlOverListView() { + comboBoxListViewSkin = new JFXComboBoxListViewSkin<>(this); + setSkin(comboBoxListViewSkin); + } + + // By default pressing [SPACE] caused editor text to reset. The solution + // is to suppress relevant event on the underlying ListViewSkin. + private void fixSpaceKey() { + comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) -> { + if (event.getCode() == KeyCode.SPACE) + event.consume(); + }); + } + + private void filterBy(String query) { + ArrayList newMatchingList = new ArrayList<>(); + for (T item : completeList) + if (StringUtils.containsIgnoreCase(asString(item), query)) + newMatchingList.add(item); + matchingList = newMatchingList; + setValue(null); + getSelectionModel().clearSelection(); + setItems(FXCollections.observableList(matchingList)); + int pos = getEditor().getCaretPosition(); + if (pos > query.length()) pos = query.length(); + getEditor().setText(query); + getEditor().positionCaret(pos); + } + + private void reactToQueryChanges() { + getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { + UserThread.execute(() -> { + String query = getEditor().getText(); + var exactMatch = completeList.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); + if (!exactMatch) { + if (query.isEmpty()) + removeFilter(); + else + filterBy(query); + forceRedraw(); + } + }); + }); + } + + private void removeFilter() { + matchingList = new ArrayList<>(completeList); + setValue(null); + getSelectionModel().clearSelection(); + setItems(FXCollections.observableList(matchingList)); + getEditor().setText(""); + } + + private void forceRedraw() { + adjustVisibleRowCount(); + if (matchingListSize() > 0) { + comboBoxListViewSkin.getPopupContent().autosize(); + show(); + } else { + hide(); + } + } + + private void adjustVisibleRowCount() { + setVisibleRowCount(Math.min(10, matchingListSize())); + } + + private String asString(T item) { + return getConverter().toString(item); + } + + private int matchingListSize() { + return matchingList.size(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/BalanceTextField.java b/desktop/src/main/java/bisq/desktop/components/BalanceTextField.java new file mode 100644 index 0000000000..5ab785428f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/BalanceTextField.java @@ -0,0 +1,98 @@ +/* + * 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 bisq.desktop.components; + +import bisq.core.util.coin.CoinFormatter; + +import org.bitcoinj.core.Coin; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.effect.BlurType; +import javafx.scene.effect.DropShadow; +import javafx.scene.effect.Effect; +import javafx.scene.layout.AnchorPane; +import javafx.scene.paint.Color; + +import javax.annotation.Nullable; + +public class BalanceTextField extends AnchorPane { + + private Coin targetAmount; + private final JFXTextField textField; + private final Effect fundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.GREEN, 4, 0.0, 0, 0); + private final Effect notFundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.ORANGERED, 4, 0.0, 0, 0); + private CoinFormatter formatter; + @Nullable + private Coin balance; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BalanceTextField(String label) { + textField = new BisqTextField(); + textField.setLabelFloat(true); + textField.setPromptText(label); + textField.setFocusTraversable(false); + textField.setEditable(false); + textField.setId("info-field"); + + AnchorPane.setRightAnchor(textField, 0.0); + AnchorPane.setLeftAnchor(textField, 0.0); + + getChildren().addAll(textField); + } + + public void setFormatter(CoinFormatter formatter) { + this.formatter = formatter; + } + + public void setBalance(Coin balance) { + this.balance = balance; + + updateBalance(balance); + } + + public void setTargetAmount(Coin targetAmount) { + this.targetAmount = targetAmount; + + if (this.balance != null) + updateBalance(balance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateBalance(Coin balance) { + if (formatter != null) + textField.setText(formatter.formatCoinWithCode(balance)); + + //TODO: replace with new validation logic +// if (targetAmount != null) { +// if (balance.compareTo(targetAmount) >= 0) +// textField.setEffect(fundedEffect); +// else +// textField.setEffect(notFundedEffect); +// } else { +// textField.setEffect(null); +// } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/BisqTextArea.java b/desktop/src/main/java/bisq/desktop/components/BisqTextArea.java new file mode 100644 index 0000000000..856f5e791f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/BisqTextArea.java @@ -0,0 +1,12 @@ +package bisq.desktop.components; + +import com.jfoenix.controls.JFXTextArea; + +import javafx.scene.control.Skin; + +public class BisqTextArea extends JFXTextArea { + @Override + protected Skin createDefaultSkin() { + return new JFXTextAreaSkinBisqStyle(this); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/BisqTextField.java b/desktop/src/main/java/bisq/desktop/components/BisqTextField.java new file mode 100644 index 0000000000..a9cd242a74 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/BisqTextField.java @@ -0,0 +1,21 @@ +package bisq.desktop.components; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Skin; + +public class BisqTextField extends JFXTextField { + + public BisqTextField(String value) { + super(value); + } + + public BisqTextField() { + super(); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXTextFieldSkinBisqStyle<>(this, 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/BsqAddressTextField.java b/desktop/src/main/java/bisq/desktop/components/BsqAddressTextField.java new file mode 100644 index 0000000000..a74a4ae2e3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/BsqAddressTextField.java @@ -0,0 +1,135 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.main.overlays.notifications.Notification; +import bisq.desktop.util.GUIUtil; + +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class BsqAddressTextField extends AnchorPane { + private final StringProperty address = new SimpleStringProperty(); + private final StringProperty paymentLabel = new SimpleStringProperty(); + private final ObjectProperty amountAsCoin = new SimpleObjectProperty<>(Coin.ZERO); + private boolean wasPrimaryButtonDown; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BsqAddressTextField() { + TextField textField = new BisqTextField(); + textField.setId("address-text-field"); + textField.setEditable(false); + textField.textProperty().bind(address); + String tooltipText = Res.get("addressTextField.copyToClipboard"); + textField.setTooltip(new Tooltip(tooltipText)); + + textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown()); + textField.setOnMouseReleased(event -> { + if (wasPrimaryButtonDown && address.get() != null && address.get().length() > 0) { + Utilities.copyToClipboard(address.get()); + Notification walletFundedNotification = new Notification() + .notification(Res.get("addressTextField.addressCopiedToClipboard")) + .hideCloseButton() + .autoClose(); + + walletFundedNotification.show(); + } + + wasPrimaryButtonDown = false; + }); + + textField.focusTraversableProperty().set(focusTraversableProperty().get()); + + Label copyIcon = new Label(); + copyIcon.setLayoutY(3); + copyIcon.getStyleClass().addAll("icon", "highlight"); + copyIcon.setTooltip(new Tooltip(Res.get("addressTextField.copyToClipboard"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + copyIcon.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(() -> { + if (address.get() != null && address.get().length() > 0) + Utilities.copyToClipboard(address.get()); + })); + + AnchorPane.setRightAnchor(copyIcon, 5.0); + AnchorPane.setRightAnchor(textField, 30.0); + AnchorPane.setLeftAnchor(textField, 0.0); + + getChildren().addAll(textField, copyIcon); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setAddress(String address) { + this.address.set(address); + } + + public String getAddress() { + return address.get(); + } + + public StringProperty addressProperty() { + return address; + } + + public Coin getAmountAsCoin() { + return amountAsCoin.get(); + } + + public ObjectProperty amountAsCoinProperty() { + return amountAsCoin; + } + + public void setAmountAsCoin(Coin amountAsCoin) { + this.amountAsCoin.set(amountAsCoin); + } + + public String getPaymentLabel() { + return paymentLabel.get(); + } + + public StringProperty paymentLabelProperty() { + return paymentLabel; + } + + public void setPaymentLabel(String paymentLabel) { + this.paymentLabel.set(paymentLabel); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/BusyAnimation.java b/desktop/src/main/java/bisq/desktop/components/BusyAnimation.java new file mode 100644 index 0000000000..4a6cb7a2ca --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/BusyAnimation.java @@ -0,0 +1,65 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXSpinner; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +public class BusyAnimation extends JFXSpinner { + + private final BooleanProperty isRunningProperty = new SimpleBooleanProperty(); + + public BusyAnimation() { + this(true); + } + + public BusyAnimation(boolean isRunning) { + getStyleClass().add("busyanimation"); + isRunningProperty.set(isRunning); + + updateVisibility(); + } + + public void play() { + isRunningProperty.set(true); + + setProgress(-1); + updateVisibility(); + } + + public void stop() { + isRunningProperty.set(false); + setProgress(0); + updateVisibility(); + } + + public boolean isRunning() { + return isRunningProperty.get(); + } + + public BooleanProperty isRunningProperty() { + return isRunningProperty; + } + + private void updateVisibility() { + setVisible(isRunning()); + setManaged(isRunning()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/ColoredDecimalPlacesWithZerosText.java b/desktop/src/main/java/bisq/desktop/components/ColoredDecimalPlacesWithZerosText.java new file mode 100644 index 0000000000..4622d74b94 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/ColoredDecimalPlacesWithZerosText.java @@ -0,0 +1,61 @@ +/* + * 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 bisq.desktop.components; + +import bisq.core.util.FormattingUtils; + +import bisq.common.util.Tuple2; + +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; + +import javafx.geometry.Pos; + +public class ColoredDecimalPlacesWithZerosText extends HBox { + + public ColoredDecimalPlacesWithZerosText(String number, int numberOfZerosToColorize) { + super(); + + if (numberOfZerosToColorize <= 0) { + getChildren().addAll(new Label(number)); + } else if (number.contains(FormattingUtils.RANGE_SEPARATOR)) { + String[] splitNumber = number.split(FormattingUtils.RANGE_SEPARATOR); + Tuple2 numbers = getSplittedNumberNodes(splitNumber[0], numberOfZerosToColorize); + getChildren().addAll(numbers.first, numbers.second); + + getChildren().add(new Label(FormattingUtils.RANGE_SEPARATOR)); + + numbers = getSplittedNumberNodes(splitNumber[1], numberOfZerosToColorize); + getChildren().addAll(numbers.first, numbers.second); + } else { + Tuple2 numbers = getSplittedNumberNodes(number, numberOfZerosToColorize); + getChildren().addAll(numbers.first, numbers.second); + } + setAlignment(Pos.CENTER_LEFT); + } + + private Tuple2 getSplittedNumberNodes(String number, int numberOfZeros) { + String placesBeforeZero = number.split("0{1," + Integer.toString(numberOfZeros) + "}$")[0]; + String zeroDecimalPlaces = number.substring(placesBeforeZero.length()); + Label first = new AutoTooltipLabel(placesBeforeZero); + Label last = new Label(zeroDecimalPlaces); + last.getStyleClass().add("zero-decimals"); + + return new Tuple2<>(first, last); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java new file mode 100644 index 0000000000..62f7cf1210 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java @@ -0,0 +1,31 @@ +/* + * 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 bisq.desktop.components; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +public class ExternalHyperlink extends HyperlinkWithIcon { + + public ExternalHyperlink(String text) { + super(text, MaterialDesignIcon.LINK); + } + + public ExternalHyperlink(String text, String style) { + super(text, MaterialDesignIcon.LINK, style); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/FundsTextField.java b/desktop/src/main/java/bisq/desktop/components/FundsTextField.java new file mode 100644 index 0000000000..45b45f3b64 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/FundsTextField.java @@ -0,0 +1,86 @@ +/* + * 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 bisq.desktop.components; + +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static bisq.desktop.util.FormBuilder.getIcon; + +public class FundsTextField extends InfoTextField { + public static final Logger log = LoggerFactory.getLogger(FundsTextField.class); + + private final StringProperty fundsStructure = new SimpleStringProperty(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + + public FundsTextField() { + super(); + textField.textProperty().unbind(); + textField.textProperty().bind(Bindings.concat(textProperty(), " ", fundsStructure)); + + Label copyIcon = getIcon(AwesomeIcon.COPY); + copyIcon.setLayoutY(5); + copyIcon.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyIcon, new Tooltip(Res.get("shared.copyToClipboard"))); + copyIcon.setOnMouseClicked(e -> { + String text = getText(); + if (text != null && text.length() > 0) { + String copyText; + String[] strings = text.split(" "); + if (strings.length > 1) + copyText = strings[0]; // exclude the BTC postfix + else + copyText = text; + + Utilities.copyToClipboard(copyText); + } + }); + + AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(infoIcon, 62.0); + AnchorPane.setRightAnchor(textField, 55.0); + + getChildren().add(copyIcon); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setFundsStructure(String fundsStructure) { + this.fundsStructure.set(fundsStructure); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java new file mode 100644 index 0000000000..b4a24825e0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java @@ -0,0 +1,95 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.util.FormBuilder; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.GlyphIcons; + +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.text.Text; + +import javafx.geometry.Insets; + +import lombok.Getter; + +public class HyperlinkWithIcon extends Hyperlink { + @Getter + private Node icon; + + public HyperlinkWithIcon(String text) { + this(text, AwesomeIcon.INFO_SIGN); + } + + public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { + super(text); + + Label icon = new Label(); + AwesomeDude.setIcon(icon, awesomeIcon); + icon.setMinWidth(20); + icon.setOpacity(0.7); + icon.getStyleClass().addAll("hyperlink", "no-underline"); + setPadding(new Insets(0)); + icon.setPadding(new Insets(0)); + + setIcon(icon); + } + + public HyperlinkWithIcon(String text, GlyphIcons icon) { + this(text, icon, null); + } + + public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { + super(text); + + Text textIcon = FormBuilder.getIcon(icon); + textIcon.setOpacity(0.7); + textIcon.getStyleClass().addAll("hyperlink", "no-underline"); + + if (style != null) { + textIcon.getStyleClass().add(style); + getStyleClass().add(style); + } + + setPadding(new Insets(0)); + + setIcon(textIcon); + } + + public void hideIcon() { + setGraphic(null); + } + + public void setIcon(Node icon) { + this.icon = icon; + setGraphic(icon); + + setContentDisplay(ContentDisplay.RIGHT); + setGraphicTextGap(7.0); + } + + public void clear() { + setText(""); + setGraphic(null); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java new file mode 100644 index 0000000000..add5cf9dbf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; + +import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.GlyphIcons; + +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; + +import javafx.geometry.Insets; + +import static bisq.desktop.util.FormBuilder.getIcon; + +public class InfoAutoTooltipLabel extends AutoTooltipLabel { + + public static final int DEFAULT_WIDTH = 300; + private Node textIcon; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); + private ContentDisplay contentDisplay; + + public InfoAutoTooltipLabel(String text, GlyphIcons icon, ContentDisplay contentDisplay, String info) { + this(text, contentDisplay); + + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); + } + + public InfoAutoTooltipLabel(String text, AwesomeIcon icon, ContentDisplay contentDisplay, String info, double width) { + super(text); + + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, width); + } + + public InfoAutoTooltipLabel(String text, ContentDisplay contentDisplay) { + super(text); + this.contentDisplay = contentDisplay; + } + + public void setIcon(GlyphIcons icon) { + textIcon = getIcon(icon); + } + + public void setIcon(GlyphIcons icon, String info) { + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); + } + + public void setIcon(AwesomeIcon icon) { + textIcon = getIcon(icon); + } + + public void hideIcon() { + textIcon = null; + setGraphic(textIcon); + } + + private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) { + textIcon.setOpacity(0.4); + textIcon.getStyleClass().add("tooltip-icon"); + textIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(info, width))); + textIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); + + setGraphic(textIcon); + setContentDisplay(contentDisplay); + } + + private PopOver createInfoPopOver(String info, double width) { + Label helpLabel = new Label(info); + helpLabel.setMaxWidth(width); + helpLabel.setWrapText(true); + helpLabel.setPadding(new Insets(10)); + return createInfoPopOver(helpLabel); + } + + private PopOver createInfoPopOver(Node node) { + node.getStyleClass().add("default-text"); + + PopOver infoPopover = new PopOver(node); + if (textIcon.getScene() != null) { + infoPopover.setDetachable(false); + infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); + + infoPopover.show(textIcon, -10); + } + return infoPopover; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/InfoDisplay.java b/desktop/src/main/java/bisq/desktop/components/InfoDisplay.java new file mode 100644 index 0000000000..329b385717 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/InfoDisplay.java @@ -0,0 +1,244 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.util.FormBuilder; + +import bisq.core.locale.Res; + +import bisq.common.UserThread; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.Parent; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; +import javafx.scene.layout.GridPane; +import javafx.scene.text.TextFlow; + +import javafx.geometry.Insets; +import javafx.geometry.VPos; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; + +/** + * Convenience Component for info icon, info text and link display in a GridPane. + * Only the properties needed are supported. + * We need to extend from Parent so we can use it in FXML, but the InfoDisplay is not used as node, + * but add the children nodes to the gridPane. + */ +public class InfoDisplay extends Parent { + + private final StringProperty text = new SimpleStringProperty(); + private final IntegerProperty rowIndex = new SimpleIntegerProperty(0); + private final IntegerProperty columnIndex = new SimpleIntegerProperty(0); + private final ObjectProperty> onAction = new SimpleObjectProperty<>(); + private final ObjectProperty gridPane = new SimpleObjectProperty<>(); + + private boolean useReadMore; + + private final Label icon = FormBuilder.getIcon(AwesomeIcon.INFO_SIGN); + private final TextFlow textFlow; + private final Label label; + private final Hyperlink link; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InfoDisplay() { + icon.setId("non-clickable-icon"); + icon.visibleProperty().bind(visibleProperty()); + + GridPane.setValignment(icon, VPos.TOP); + GridPane.setMargin(icon, new Insets(-2, 0, 0, 0)); + GridPane.setRowSpan(icon, 2); + + label = new AutoTooltipLabel(); + label.textProperty().bind(text); + label.setTextOverrun(OverrunStyle.WORD_ELLIPSIS); + // width is set a frame later so we hide it first + label.setVisible(false); + + link = new Hyperlink(Res.get("shared.readMore")); + link.setPadding(new Insets(0, 0, 0, -2)); + + // We need that to know if we have a wrapping or not. + // Did not find a way to get that from the API. + Label testLabel = new AutoTooltipLabel(); + testLabel.textProperty().bind(text); + + textFlow = new TextFlow(); + textFlow.visibleProperty().bind(visibleProperty()); + textFlow.getChildren().addAll(testLabel); + + testLabel.widthProperty().addListener((ov, o, n) -> { + useReadMore = (double) n > textFlow.getWidth(); + link.setText(Res.get(useReadMore ? "shared.readMore" : "shared.openHelp")); + UserThread.execute(() -> textFlow.getChildren().setAll(label, link)); + }); + + // update the width when the window gets resized + ChangeListener listener = (ov2, oldValue2, windowWidth) -> { + if (label.prefWidthProperty().isBound()) + label.prefWidthProperty().unbind(); + label.setPrefWidth((double) windowWidth - localToScene(0, 0).getX() - 35); + }; + + + // when clicking "Read more..." we expand and change the link to the Help + link.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent actionEvent) { + if (useReadMore) { + + label.setWrapText(true); + link.setText(Res.get("shared.openHelp")); + getScene().getWindow().widthProperty().removeListener(listener); + if (label.prefWidthProperty().isBound()) + label.prefWidthProperty().unbind(); + label.prefWidthProperty().bind(textFlow.widthProperty()); + link.setVisited(false); + // focus border is a bit confusing here so we remove it + link.getStyleClass().add("hide-focus"); + link.setOnAction(onAction.get()); + getParent().layout(); + } else { + onAction.get().handle(actionEvent); + } + } + }); + + sceneProperty().addListener((ov, oldValue, newValue) -> { + if (oldValue == null && newValue != null && newValue.getWindow() != null) { + newValue.getWindow().widthProperty().addListener(listener); + // localToScene does deliver 0 instead of the correct x position when scene property gets set, + // so we delay for 1 render cycle + UserThread.execute(() -> { + label.setVisible(true); + label.prefWidthProperty().unbind(); + label.setPrefWidth(newValue.getWindow().getWidth() - localToScene(0, 0).getX() - 35); + }); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setText(String text) { + this.text.set(text); + UserThread.execute(() -> { + if (getScene() != null) { + label.setVisible(true); + label.prefWidthProperty().unbind(); + label.setPrefWidth(getScene().getWindow().getWidth() - localToScene(0, 0).getX() - 35); + } + }); + } + + public void setGridPane(GridPane gridPane) { + this.gridPane.set(gridPane); + + gridPane.getChildren().addAll(icon, textFlow); + + GridPane.setColumnIndex(icon, columnIndex.get()); + GridPane.setColumnIndex(textFlow, columnIndex.get() + 1); + + GridPane.setRowIndex(icon, rowIndex.get()); + GridPane.setRowIndex(textFlow, rowIndex.get()); + } + + public void setRowIndex(int rowIndex) { + this.rowIndex.set(rowIndex); + + GridPane.setRowIndex(icon, rowIndex); + GridPane.setRowIndex(textFlow, rowIndex); + } + + public void setColumnIndex(int columnIndex) { + this.columnIndex.set(columnIndex); + + GridPane.setColumnIndex(icon, columnIndex); + GridPane.setColumnIndex(textFlow, columnIndex + 1); + + } + + public final void setOnAction(EventHandler eventHandler) { + onAction.set(eventHandler); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public int getColumnIndex() { + return columnIndex.get(); + } + + public IntegerProperty columnIndexProperty() { + return columnIndex; + } + + public int getRowIndex() { + return rowIndex.get(); + } + + public IntegerProperty rowIndexProperty() { + return rowIndex; + } + + public EventHandler getOnAction() { + return onAction.get(); + } + + public ObjectProperty> onActionProperty() { + return onAction; + } + + public GridPane getGridPane() { + return gridPane.get(); + } + + public ObjectProperty gridPaneProperty() { + return gridPane; + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java new file mode 100644 index 0000000000..6d625c956f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java @@ -0,0 +1,157 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import lombok.Getter; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class InfoInputTextField extends AnchorPane { + private final StringProperty text = new SimpleStringProperty(); + @Getter + private final InputTextField inputTextField; + private final Label icon; + private final PopOverWrapper popoverWrapper = new PopOverWrapper(); + @Nullable + private Node node; + + public InfoInputTextField() { + this(0); + } + + public InfoInputTextField(double inputLineExtension) { + super(); + + inputTextField = new InputTextField(inputLineExtension); + AnchorPane.setRightAnchor(inputTextField, 0.0); + AnchorPane.setLeftAnchor(inputTextField, 0.0); + + icon = new Label(); + icon.setLayoutY(3); + AnchorPane.setLeftAnchor(icon, 7.0); + icon.setOnMouseEntered(e -> { + if (node != null) { + popoverWrapper.showPopOver(() -> checkNotNull(createPopOver())); + } + }); + icon.setOnMouseExited(e -> { + if (node != null) { + popoverWrapper.hidePopOver(); + } + }); + + hideIcon(); + + getChildren().addAll(inputTextField, icon); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setContentForInfoPopOver(Node node) { + setContentForPopOver(node, AwesomeIcon.INFO_SIGN); + } + + public void setContentForWarningPopOver(Node node) { + setContentForPopOver(node, AwesomeIcon.WARNING_SIGN, "warning"); + } + + public void setContentForPrivacyPopOver(Node node) { + setContentForPopOver(node, AwesomeIcon.EYE_CLOSE); + } + + public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) { + setContentForPopOver(node, awesomeIcon, null); + } + + public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) { + this.node = node; + AwesomeDude.setIcon(icon, awesomeIcon); + icon.getStyleClass().addAll("icon", style == null ? "info" : style); + icon.setManaged(true); + icon.setVisible(true); + } + + public void hideIcon() { + icon.setManaged(false); + icon.setVisible(false); + } + + public void setIconsRightAligned() { + AnchorPane.clearConstraints(icon); + AnchorPane.clearConstraints(inputTextField); + + AnchorPane.setRightAnchor(icon, 7.0); + AnchorPane.setLeftAnchor(inputTextField, 0.0); + AnchorPane.setRightAnchor(inputTextField, 0.0); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setText(String text) { + this.text.set(text); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private PopOver createPopOver() { + if (node == null) { + return null; + } + + node.getStyleClass().add("default-text"); + PopOver popover = new PopOver(node); + if (icon.getScene() != null) { + popover.setDetachable(false); + popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP); + popover.setArrowIndent(5); + popover.show(icon, -17); + } + return popover; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/InfoTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java new file mode 100644 index 0000000000..fe253b2f2b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java @@ -0,0 +1,157 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; + +import bisq.common.UserThread; + +import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.text.Text; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; + +import static bisq.desktop.util.FormBuilder.getIcon; +import static bisq.desktop.util.FormBuilder.getRegularIconForLabel; + +public class InfoTextField extends AnchorPane { + public static final Logger log = LoggerFactory.getLogger(InfoTextField.class); + + @Getter + protected final JFXTextField textField; + + private final StringProperty text = new SimpleStringProperty(); + protected final Label infoIcon; + private Label currentIcon; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); + private PopOver.ArrowLocation arrowLocation; + + public InfoTextField() { + + arrowLocation = PopOver.ArrowLocation.RIGHT_TOP; + textField = new BisqTextField(); + textField.setLabelFloat(true); + textField.setEditable(false); + textField.textProperty().bind(text); + textField.setFocusTraversable(false); + textField.setId("info-field"); + + infoIcon = getIcon(AwesomeIcon.INFO_SIGN); + infoIcon.setLayoutY(5); + infoIcon.getStyleClass().addAll("icon", "info"); + + AnchorPane.setRightAnchor(infoIcon, 7.0); + AnchorPane.setRightAnchor(textField, 0.0); + AnchorPane.setLeftAnchor(textField, 0.0); + + hideIcons(); + + getChildren().addAll(textField, infoIcon); + } + + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setContentForInfoPopOver(Node node) { + + currentIcon = infoIcon; + + hideIcons(); + setActionHandlers(node); + } + + public void setContent(MaterialDesignIcon icon, String info, String style, double opacity) { + hideIcons(); + + currentIcon = new Label(); + Text textIcon = getRegularIconForLabel(icon, currentIcon); + + setActionHandlers(new Label(info)); + + currentIcon.setLayoutY(5); + textIcon.getStyleClass().addAll("icon", style); + currentIcon.setOpacity(opacity); + AnchorPane.setRightAnchor(currentIcon, 7.0); + + getChildren().add(currentIcon); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + private void hideIcons() { + infoIcon.setManaged(false); + infoIcon.setVisible(false); + } + + private void setActionHandlers(Node node) { + + currentIcon.setManaged(true); + currentIcon.setVisible(true); + + // As we don't use binding here we need to recreate it on mouse over to reflect the current state + currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node))); + currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); + } + + private PopOver createPopOver(Node node) { + node.getStyleClass().add("default-text"); + + PopOver popover = new PopOver(node); + if (currentIcon.getScene() != null) { + popover.setDetachable(false); + popover.setArrowLocation(arrowLocation); + popover.setArrowIndent(5); + + popover.show(currentIcon, -17); + } + return popover; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setText(String text) { + this.text.set(text); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/InputTextField.java b/desktop/src/main/java/bisq/desktop/components/InputTextField.java new file mode 100644 index 0000000000..536813a1a0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/InputTextField.java @@ -0,0 +1,147 @@ +/* + * 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 bisq.desktop.components; + + +import bisq.desktop.util.validation.JFXInputValidator; + +import bisq.core.util.validation.InputValidator; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Skin; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * TextField 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 field. + * The errorMessageDisplay gets closed when the ValidatingTextField 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 textField 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 InputTextField extends JFXTextField { + + private final ObjectProperty validationResult = new SimpleObjectProperty<> + (new InputValidator.ValidationResult(true)); + + private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); + private double inputLineExtension = 0; + + 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 InputTextField() { + 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 InputTextField(double inputLineExtension) { + this(); + this.inputLineExtension = inputLineExtension; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // 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())); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObjectProperty validationResultProperty() { + return validationResult; + } + + protected Skin createDefaultSkin() { + return new JFXTextFieldSkinBisqStyle<>(this, inputLineExtension); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/JFXRadioButtonSkinBisqStyle.java b/desktop/src/main/java/bisq/desktop/components/JFXRadioButtonSkinBisqStyle.java new file mode 100644 index 0000000000..3ab8cbd587 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/JFXRadioButtonSkinBisqStyle.java @@ -0,0 +1,285 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXRadioButton; +import com.jfoenix.controls.JFXRippler; +import com.jfoenix.transitions.JFXAnimationTimer; +import com.jfoenix.transitions.JFXKeyFrame; +import com.jfoenix.transitions.JFXKeyValue; + +import javafx.animation.Interpolator; + +import javafx.scene.Node; +import javafx.scene.control.RadioButton; +import javafx.scene.control.skin.RadioButtonSkin; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.StrokeType; +import javafx.scene.text.Text; + +import javafx.geometry.HPos; +import javafx.geometry.VPos; + +import javafx.util.Duration; + +/** + * Code copied and adapted from com.jfoenix.skins.JFXRadioButtonSkin + */ + +public class JFXRadioButtonSkinBisqStyle extends RadioButtonSkin { + private final JFXRippler rippler; + private double padding = 12; + + private Circle radio, dot; + private final StackPane container; + + private JFXAnimationTimer timer; + + public JFXRadioButtonSkinBisqStyle(JFXRadioButton control) { + super(control); + + final double radioRadius = 7; + radio = new Circle(radioRadius); + radio.getStyleClass().setAll("radio"); + radio.setStrokeWidth(1); + radio.setStrokeType(StrokeType.INSIDE); + radio.setFill(Color.TRANSPARENT); + radio.setSmooth(true); + + dot = new Circle(radioRadius); + dot.getStyleClass().setAll("dot"); + dot.fillProperty().bind(control.selectedColorProperty()); + dot.setScaleX(0); + dot.setScaleY(0); + dot.setSmooth(true); + + container = new StackPane(radio, dot); + container.getStyleClass().add("radio-container"); + + rippler = new JFXRippler(container, JFXRippler.RipplerMask.CIRCLE) { + @Override + protected double computeRippleRadius() { + double width = ripplerPane.getWidth(); + double width2 = width * width; + return Math.min(Math.sqrt(width2 + width2), RIPPLE_MAX_RADIUS) * 1.1 + 5; + } + + @Override + protected void setOverLayBounds(Rectangle overlay) { + overlay.setWidth(ripplerPane.getWidth()); + overlay.setHeight(ripplerPane.getHeight()); + } + + protected void initControlListeners() { + // if the control got resized the overlay rect must be rest + control.layoutBoundsProperty().addListener(observable -> resetRippler()); + if (getChildren().contains(control)) { + control.boundsInParentProperty().addListener(observable -> resetRippler()); + } + control.addEventHandler(MouseEvent.MOUSE_PRESSED, + (event) -> createRipple(event.getX() + padding, event.getY() + padding)); + // create fade out transition for the ripple + control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple()); + } + + @Override + protected Node getMask() { + double radius = ripplerPane.getWidth() / 2; + return new Circle(radius, radius, radius); + } + + @Override + protected void positionControl(Node control) { + + } + }; + + updateChildren(); + + // show focused state + control.focusedProperty().addListener((o, oldVal, newVal) -> { + if (!control.disableVisualFocusProperty().get()) { + if (newVal) { + if (!getSkinnable().isPressed()) { + rippler.setOverlayVisible(true); + } + } else { + rippler.setOverlayVisible(false); + } + } + }); + + control.pressedProperty().addListener((o, oldVal, newVal) -> rippler.setOverlayVisible(false)); + + timer = new JFXAnimationTimer( + new JFXKeyFrame(Duration.millis(200), + JFXKeyValue.builder() + .setTarget(dot.scaleXProperty()) + .setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0) + .setInterpolator(Interpolator.EASE_BOTH) + .build(), + JFXKeyValue.builder() + .setTarget(dot.scaleYProperty()) + .setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0) + .setInterpolator(Interpolator.EASE_BOTH) + .build(), + JFXKeyValue.builder() + .setTarget(radio.strokeProperty()) + .setEndValueSupplier(() -> getSkinnable().isSelected() ? ((JFXRadioButton) getSkinnable()).getSelectedColor() : ((JFXRadioButton) getSkinnable()).getUnSelectedColor()) + .setInterpolator(Interpolator.EASE_BOTH) + .build() + )); + + + registerChangeListener(control.selectedColorProperty(), obs -> updateColors()); + registerChangeListener(control.unSelectedColorProperty(), obs -> updateColors()); + registerChangeListener(control.selectedProperty(), obs -> { + boolean isSelected = getSkinnable().isSelected(); + Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor(); + Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor(); + rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor); + if (((JFXRadioButton) getSkinnable()).isDisableAnimation()) { + // apply end values + timer.applyEndValues(); + } else { + // play selection animation + timer.reverseAndContinue(); + } + }); + + updateColors(); + timer.applyEndValues(); + } + + @Override + protected void updateChildren() { + super.updateChildren(); + if (radio != null) { + removeRadio(); + getChildren().addAll(container, rippler); + } + } + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + final RadioButton radioButton = getSkinnable(); + final double contWidth = snapSizeX(container.prefWidth(-1)); + final double contHeight = snapSizeY(container.prefHeight(-1)); + final double computeWidth = Math.max(radioButton.prefWidth(-1), radioButton.minWidth(-1)); + final double width = snapSizeX(contWidth); + final double height = snapSizeY(contHeight); + + final double labelWidth = Math.min(computeWidth - contWidth, w - width); + final double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h); + final double maxHeight = Math.max(contHeight, labelHeight); + final double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x; + + final double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x + 5; + + layoutLabelInArea(xOffset + contWidth + padding / 3, yOffset, labelWidth, maxHeight, radioButton.getAlignment()); + ((Text) getChildren().get((getChildren().get(0) instanceof Text) ? 0 : 1)). + textProperty().set(getSkinnable().textProperty().get()); + + container.resize(width, height); + positionInArea(container, + xOffset, + yOffset, + contWidth, + maxHeight, + 0, + radioButton.getAlignment().getHpos(), + radioButton.getAlignment().getVpos()); + + final double ripplerWidth = width + 2 * padding; + final double ripplerHeight = height + 2 * padding; + rippler.resizeRelocate((width / 2 + xOffset) - ripplerWidth / 2, + (height / 2 + yOffset) + 2 - ripplerHeight / 2, + ripplerWidth, ripplerHeight); + } + + private void removeRadio() { + // TODO: replace with removeIf + for (int i = 0; i < getChildren().size(); i++) { + if ("radio".equals(getChildren().get(i).getStyleClass().get(0))) { + getChildren().remove(i); + } + } + } + + private void updateColors() { + boolean isSelected = getSkinnable().isSelected(); + Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor(); + Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor(); + rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor); + radio.setStroke(isSelected ? selectedColor : unSelectedColor); + } + + @Override + protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return super.computeMinWidth(height, + topInset, + rightInset, + bottomInset, + leftInset) + snapSize(radio.minWidth(-1)) + padding / 3; + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return super.computePrefWidth(height, + topInset, + rightInset, + bottomInset, + leftInset) + snapSizeX(radio.prefWidth(-1)) + padding / 3; + } + + private static double computeXOffset(double width, double contentWidth, HPos hpos) { + switch (hpos) { + case LEFT: + return 0; + case CENTER: + return (width - contentWidth) / 2; + case RIGHT: + return width - contentWidth; + } + return 0; + } + + private static double computeYOffset(double height, double contentHeight, VPos vpos) { + switch (vpos) { + case TOP: + return 0; + case CENTER: + return (height - contentHeight) / 2; + case BOTTOM: + return height - contentHeight; + default: + return 0; + } + } + + @Override + public void dispose() { + super.dispose(); + timer.dispose(); + timer = null; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/JFXTextAreaSkinBisqStyle.java b/desktop/src/main/java/bisq/desktop/components/JFXTextAreaSkinBisqStyle.java new file mode 100644 index 0000000000..07a4f6d391 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/JFXTextAreaSkinBisqStyle.java @@ -0,0 +1,119 @@ +package bisq.desktop.components; + +import com.jfoenix.adapters.ReflectionHelper; +import com.jfoenix.controls.JFXTextArea; +import com.jfoenix.skins.PromptLinesWrapper; +import com.jfoenix.skins.ValidationPane; + +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.skin.TextAreaSkin; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; + +import javafx.geometry.Insets; + +import java.util.Arrays; + +import java.lang.reflect.Field; + +/** + * Code copied and adapted from com.jfoenix.skins.JFXTextAreaSkin + */ + +public class JFXTextAreaSkinBisqStyle extends TextAreaSkin { + + private boolean invalid = true; + + private ScrollPane scrollPane; + private Text promptText; + + private ValidationPane errorContainer; + private PromptLinesWrapper linesWrapper; + + public JFXTextAreaSkinBisqStyle(JFXTextArea textArea) { + super(textArea); + // init text area properties + scrollPane = (ScrollPane) getChildren().get(0); + textArea.setWrapText(true); + + linesWrapper = new PromptLinesWrapper<>( + textArea, + promptTextFillProperty(), + textArea.textProperty(), + textArea.promptTextProperty(), + () -> promptText); + + linesWrapper.init(() -> createPromptNode(), scrollPane); + errorContainer = new ValidationPane<>(textArea); + getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); + + registerChangeListener(textArea.disableProperty(), obs -> linesWrapper.updateDisabled()); + registerChangeListener(textArea.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); + registerChangeListener(textArea.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); + registerChangeListener(textArea.disableAnimationProperty(), obs -> errorContainer.updateClip()); + + } + + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + super.layoutChildren(x, y, w, h); + + final double height = getSkinnable().getHeight(); + final double width = getSkinnable().getWidth(); + linesWrapper.layoutLines(x - 2, y - 2, width, h, height, promptText == null ? 0 : promptText.getLayoutBounds().getHeight() + 3); + errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), width, h); + linesWrapper.updateLabelFloatLayout(); + + if (invalid) { + invalid = false; + // set the default background of text area viewport to white + Region viewPort = (Region) scrollPane.getChildrenUnmodifiable().get(0); + viewPort.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, + CornerRadii.EMPTY, + Insets.EMPTY))); + // reapply css of scroll pane in case set by the user + viewPort.applyCss(); + errorContainer.invalid(w); + // focus + linesWrapper.invalid(); + } + } + + private void createPromptNode() { + if (promptText != null || !linesWrapper.usePromptText.get()) { + return; + } + promptText = new Text(); + promptText.setManaged(false); + promptText.getStyleClass().add("text"); + promptText.visibleProperty().bind(linesWrapper.usePromptText); + promptText.fontProperty().bind(getSkinnable().fontProperty()); + promptText.textProperty().bind(getSkinnable().promptTextProperty()); + promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); + promptText.getTransforms().add(linesWrapper.promptTextScale); + linesWrapper.promptContainer.getChildren().add(promptText); + if (getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) { + promptText.setTranslateY(-Math.floor(scrollPane.getHeight())); + linesWrapper.promptTextScale.setX(0.85); + linesWrapper.promptTextScale.setY(0.85); + } + + try { + Field field = ReflectionHelper.getField(TextAreaSkin.class, "promptNode"); + Object oldValue = field.get(this); + if (oldValue != null) { + removeHighlight(Arrays.asList(((Node) oldValue))); + } + field.set(this, promptText); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + diff --git a/desktop/src/main/java/bisq/desktop/components/JFXTextFieldSkinBisqStyle.java b/desktop/src/main/java/bisq/desktop/components/JFXTextFieldSkinBisqStyle.java new file mode 100644 index 0000000000..4aca7692e5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/JFXTextFieldSkinBisqStyle.java @@ -0,0 +1,147 @@ +package bisq.desktop.components; + +import com.jfoenix.adapters.ReflectionHelper; +import com.jfoenix.controls.base.IFXLabelFloatControl; +import com.jfoenix.skins.PromptLinesWrapper; +import com.jfoenix.skins.ValidationPane; + +import javafx.scene.Node; +import javafx.scene.control.TextField; +import javafx.scene.control.skin.TextFieldSkin; +import javafx.scene.layout.Pane; +import javafx.scene.text.Text; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.value.ObservableDoubleValue; + +import java.lang.reflect.Field; + +/** + * Code copied and adapted from com.jfoenix.skins.JFXTextFieldSkin + */ + +public class JFXTextFieldSkinBisqStyle extends TextFieldSkin { + + private double inputLineExtension; + private boolean invalid = true; + + private Text promptText; + private Pane textPane; + private Node textNode; + private ObservableDoubleValue textRight; + private DoubleProperty textTranslateX; + + private ValidationPane errorContainer; + private PromptLinesWrapper linesWrapper; + + public JFXTextFieldSkinBisqStyle(T textField, double inputLineExtension) { + super(textField); + textPane = (Pane) this.getChildren().get(0); + this.inputLineExtension = inputLineExtension; + + // get parent fields + textNode = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textNode"); + textTranslateX = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textTranslateX"); + textRight = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textRight"); + + linesWrapper = new PromptLinesWrapper( + textField, + promptTextFillProperty(), + textField.textProperty(), + textField.promptTextProperty(), + () -> promptText); + + linesWrapper.init(() -> createPromptNode(), textPane); + + ReflectionHelper.setFieldContent(TextFieldSkin.class, this, "usePromptText", linesWrapper.usePromptText); + + errorContainer = new ValidationPane<>(textField); + + getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); + + registerChangeListener(textField.disableProperty(), obs -> linesWrapper.updateDisabled()); + registerChangeListener(textField.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); + registerChangeListener(textField.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); + registerChangeListener(textField.disableAnimationProperty(), obs -> errorContainer.updateClip()); + } + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + super.layoutChildren(x, y, w, h); + + final double height = getSkinnable().getHeight(); + final double width = getSkinnable().getWidth() + inputLineExtension; + final double paddingLeft = getSkinnable().getPadding().getLeft(); + linesWrapper.layoutLines(x, y, width, h, height, Math.floor(h)); + errorContainer.layoutPane(x - paddingLeft, height + linesWrapper.focusedLine.getHeight(), width, h); + + if (getSkinnable().getWidth() > 0) { + updateTextPos(); + } + + linesWrapper.updateLabelFloatLayout(); + + if (invalid) { + invalid = false; + // update validation container + errorContainer.invalid(w); + // focus + linesWrapper.invalid(); + } + } + + + private void updateTextPos() { + double textWidth = textNode.getLayoutBounds().getWidth(); + final double promptWidth = promptText == null ? 0 : promptText.getLayoutBounds().getWidth(); + switch (getSkinnable().getAlignment().getHpos()) { + case CENTER: + linesWrapper.promptTextScale.setPivotX(promptWidth / 2); + double midPoint = textRight.get() / 2; + double newX = midPoint - textWidth / 2; + if (newX + textWidth <= textRight.get()) { + textTranslateX.set(newX); + } + break; + case LEFT: + linesWrapper.promptTextScale.setPivotX(0); + break; + case RIGHT: + linesWrapper.promptTextScale.setPivotX(promptWidth); + break; + } + + } + + private void createPromptNode() { + if (promptText != null || !linesWrapper.usePromptText.get()) { + return; + } + promptText = new Text(); + promptText.setManaged(false); + promptText.getStyleClass().add("text"); + promptText.visibleProperty().bind(linesWrapper.usePromptText); + promptText.fontProperty().bind(getSkinnable().fontProperty()); + promptText.textProperty().bind(getSkinnable().promptTextProperty()); + promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); + promptText.setLayoutX(1); + promptText.getTransforms().add(linesWrapper.promptTextScale); + linesWrapper.promptContainer.getChildren().add(promptText); + if (getSkinnable().isFocused() && ((IFXLabelFloatControl) getSkinnable()).isLabelFloat()) { + promptText.setTranslateY(-Math.floor(textPane.getHeight())); + linesWrapper.promptTextScale.setX(0.85); + linesWrapper.promptTextScale.setY(0.85); + } + + try { + Field field = ReflectionHelper.getField(TextFieldSkin.class, "promptNode"); + Object oldValue = field.get(this); + if (oldValue != null) { + textPane.getChildren().remove(oldValue); + } + field.set(this, promptText); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/MenuItem.java b/desktop/src/main/java/bisq/desktop/components/MenuItem.java new file mode 100644 index 0000000000..8c79a20410 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/MenuItem.java @@ -0,0 +1,154 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.View; + +import com.jfoenix.controls.JFXButton; + +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; + +import javafx.geometry.Pos; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class MenuItem extends JFXButton implements Toggle { + private final Navigation navigation; + private final ObjectProperty toggleGroupProperty = new SimpleObjectProperty<>(); + private final Class viewClass; + private final List> baseNavPath; + private final BooleanProperty selectedProperty = new SimpleBooleanProperty(); + private final ChangeListener listener; + + public MenuItem(Navigation navigation, + ToggleGroup toggleGroup, + String title, + Class viewClass, + List> baseNavPath) { + this.navigation = navigation; + this.viewClass = viewClass; + this.baseNavPath = baseNavPath; + + setLabelText(title); + setPrefHeight(40); + setPrefWidth(240); + setAlignment(Pos.CENTER_LEFT); + + toggleGroupProperty.set(toggleGroup); + toggleGroup.getToggles().add(this); + + setUserData(getUid()); + + listener = (observable, oldValue, newValue) -> { + Object userData = newValue.getUserData(); + String uid = getUid(); + if (newValue.isSelected() && userData != null && userData.equals(uid)) { + getStyleClass().add("action-button"); + } else { + getStyleClass().remove("action-button"); + } + }; + + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Toggle implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ToggleGroup getToggleGroup() { + return toggleGroupProperty.get(); + } + + @Override + public void setToggleGroup(ToggleGroup toggleGroup) { + toggleGroupProperty.set(toggleGroup); + } + + @Override + public ObjectProperty toggleGroupProperty() { + return toggleGroupProperty; + } + + @Override + public boolean isSelected() { + return selectedProperty.get(); + } + + @Override + public BooleanProperty selectedProperty() { + return selectedProperty; + } + + @Override + public void setSelected(boolean selected) { + selectedProperty.set(selected); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void activate() { + setOnAction((event) -> navigation.navigateTo(getNavPathClasses())); + toggleGroupProperty.get().selectedToggleProperty().addListener(listener); + } + + public void deactivate() { + setOnAction(null); + toggleGroupProperty.get().selectedToggleProperty().removeListener(listener); + } + + public void setLabelText(String value) { + setText(value.toUpperCase()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + @NotNull + private Class[] getNavPathClasses() { + List> list = new ArrayList<>(baseNavPath); + list.add(viewClass); + //noinspection unchecked + Class[] array = new Class[list.size()]; + list.toArray(array); + return array; + } + + private String getUid() { + return viewClass.getName(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/NewBadge.java b/desktop/src/main/java/bisq/desktop/components/NewBadge.java new file mode 100644 index 0000000000..80a1d48a0f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/NewBadge.java @@ -0,0 +1,34 @@ +package bisq.desktop.components; + +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import com.jfoenix.controls.JFXBadge; + +import javafx.scene.Node; + +import javafx.collections.MapChangeListener; + +public class NewBadge extends JFXBadge { + + private final String key; + + public NewBadge(Node control, String key, Preferences preferences) { + super(control); + + this.key = key; + + setText(Res.get("shared.new")); + getStyleClass().add("new"); + + setEnabled(!preferences.getDontShowAgainMap().containsKey(key)); + refreshBadge(); + + preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { + if (change.getKey().equals(key)) { + setEnabled(!change.wasAdded()); + refreshBadge(); + } + }); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/PasswordTextField.java b/desktop/src/main/java/bisq/desktop/components/PasswordTextField.java new file mode 100644 index 0000000000..7d99985fb7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PasswordTextField.java @@ -0,0 +1,35 @@ +/* + * 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 bisq.desktop.components; + +import com.jfoenix.controls.JFXPasswordField; + +import javafx.scene.control.Skin; + +public class PasswordTextField extends JFXPasswordField { + public PasswordTextField() { + super(); + setLabelFloat(true); + setMaxWidth(380); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXTextFieldSkinBisqStyle<>(this, 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java new file mode 100644 index 0000000000..e400b29692 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java @@ -0,0 +1,372 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.main.overlays.editor.PeerInfoWithTagEditor; +import bisq.desktop.util.DisplayUtils; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.trade.Trade; +import bisq.core.user.Preferences; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.util.Tuple5; + +import com.google.common.base.Charsets; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.Group; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; + +import javafx.geometry.Point2D; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.util.Date; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class PeerInfoIcon extends Group { + private final String tooltipText; + private final int numTrades; + private final AccountAgeWitnessService accountAgeWitnessService; + private final Map peerTagMap; + private final Label numTradesLabel; + private final Label tagLabel; + final Pane tagPane; + final Pane numTradesPane; + private final String fullAddress; + + public PeerInfoIcon(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + Offer offer, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + this(nodeAddress, + role, + numTrades, + privateNotificationManager, + offer, + null, + preferences, + accountAgeWitnessService, + useDevPrivilegeKeys); + + } + + public PeerInfoIcon(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + Trade trade, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + this(nodeAddress, + role, + numTrades, + privateNotificationManager, + trade.getOffer(), + trade, + preferences, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + private PeerInfoIcon(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + @Nullable Offer offer, + @Nullable Trade trade, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + this.numTrades = numTrades; + this.accountAgeWitnessService = accountAgeWitnessService; + + double scaleFactor = getScaleFactor(); + fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : ""; + + peerTagMap = preferences.getPeerTagMap(); + + boolean hasTraded = numTrades > 0; + Tuple5 peersAccount = getPeersAccountAge(trade, offer); + + Long accountAge = peersAccount.first; + Long signAge = peersAccount.second; + + if (offer == null) { + checkNotNull(trade, "Trade must not be null if offer is null."); + offer = trade.getOffer(); + } + + checkNotNull(offer, "Offer must not be null"); + + boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()); + + String accountAgeTooltip = isFiatCurrency ? + accountAge > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(accountAge)) : + Res.get("peerInfoIcon.tooltip.unknownAge") : + ""; + tooltipText = hasTraded ? + Res.get("peerInfoIcon.tooltip.trade.traded", role, fullAddress, numTrades, accountAgeTooltip) : + Res.get("peerInfoIcon.tooltip.trade.notTraded", role, fullAddress, accountAgeTooltip); + + // outer circle + Color ringColor; + if (isFiatCurrency) { + + switch (accountAgeWitnessService.getPeersAccountAgeCategory(hasChargebackRisk(trade, offer) ? signAge : accountAge)) { + case TWO_MONTHS_OR_MORE: + ringColor = Color.rgb(0, 225, 0); // > 2 months green + break; + case ONE_TO_TWO_MONTHS: + ringColor = Color.rgb(0, 139, 205); // 1-2 months blue + break; + case LESS_ONE_MONTH: + ringColor = Color.rgb(255, 140, 0); //< 1 month orange + break; + case UNVERIFIED: + default: + ringColor = Color.rgb(255, 0, 0); // not signed, red + break; + } + + + } else { + // for altcoins we always display green + ringColor = Color.rgb(0, 225, 0); + } + + double outerSize = 26 * scaleFactor; + Canvas outerBackground = new Canvas(outerSize, outerSize); + GraphicsContext outerBackgroundGc = outerBackground.getGraphicsContext2D(); + outerBackgroundGc.setFill(ringColor); + outerBackgroundGc.fillOval(0, 0, outerSize, outerSize); + outerBackground.setLayoutY(1 * scaleFactor); + + // inner circle + int maxIndices = 15; + int intValue = 0; + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + byte[] bytes = md.digest(fullAddress.getBytes(Charsets.UTF_8)); + intValue = Math.abs(((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) + | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF)); + + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + log.error(e.toString()); + } + + int index = (intValue % maxIndices) + 1; + double saturation = (intValue % 1000) / 1000d; + int red = (intValue >> 8) % 256; + int green = (intValue >> 16) % 256; + int blue = (intValue >> 24) % 256; + + Color innerColor = Color.rgb(red, green, blue); + innerColor = innerColor.deriveColor(1, saturation, 0.8, 1); // reduce saturation and brightness + + double innerSize = scaleFactor * 22; + Canvas innerBackground = new Canvas(innerSize, innerSize); + GraphicsContext innerBackgroundGc = innerBackground.getGraphicsContext2D(); + innerBackgroundGc.setFill(innerColor); + innerBackgroundGc.fillOval(0, 0, innerSize, innerSize); + innerBackground.setLayoutY(3 * scaleFactor); + innerBackground.setLayoutX(2 * scaleFactor); + + ImageView avatarImageView = new ImageView(); + avatarImageView.setId("avatar_" + index); + avatarImageView.setLayoutX(0); + avatarImageView.setLayoutY(1 * scaleFactor); + avatarImageView.setFitHeight(scaleFactor * 26); + avatarImageView.setFitWidth(scaleFactor * 26); + + numTradesPane = new Pane(); + numTradesPane.relocate(scaleFactor * 18, scaleFactor * 14); + numTradesPane.setMouseTransparent(true); + ImageView numTradesCircle = new ImageView(); + numTradesCircle.setId("image-green_circle"); + numTradesLabel = new AutoTooltipLabel(); + numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); + numTradesLabel.setId("ident-num-label"); + numTradesPane.getChildren().addAll(numTradesCircle, numTradesLabel); + + tagPane = new Pane(); + tagPane.relocate(Math.round(scaleFactor * 18), scaleFactor * -2); + tagPane.setMouseTransparent(true); + ImageView tagCircle = new ImageView(); + tagCircle.setId("image-blue_circle"); + tagLabel = new AutoTooltipLabel(); + tagLabel.relocate(Math.round(scaleFactor * 5), scaleFactor * 1); + tagLabel.setId("ident-num-label"); + tagPane.getChildren().addAll(tagCircle, tagLabel); + + updatePeerInfoIcon(); + + getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane); + + addMouseListener(numTrades, privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys, + isFiatCurrency, accountAge, signAge, peersAccount.third, peersAccount.fourth, peersAccount.fifth); + } + + /** + * @param trade Open trade for trading peer info to be shown + * @param offer Open offer for trading peer info to be shown + * @return account age, sign age, account info, sign info, sign state + */ + private Tuple5 getPeersAccountAge(@Nullable Trade trade, + @Nullable Offer offer) { + AccountAgeWitnessService.SignState signState; + long signAge = -1L; + long accountAge = -1L; + + if (trade != null) { + offer = trade.getOffer(); + if (offer == null) { + // unexpected + return new Tuple5<>(signAge, accountAge, Res.get("peerInfo.age.noRisk"), null, null); + } + signState = accountAgeWitnessService.getSignState(trade); + signAge = accountAgeWitnessService.getWitnessSignAge(trade, new Date()); + accountAge = accountAgeWitnessService.getAccountAge(trade); + } else { + checkNotNull(offer, "Offer must not be null if trade is null."); + signState = accountAgeWitnessService.getSignState(offer); + signAge = accountAgeWitnessService.getWitnessSignAge(offer, new Date()); + accountAge = accountAgeWitnessService.getAccountAge(offer); + } + + if (hasChargebackRisk(trade, offer)) { + String signAgeInfo = Res.get("peerInfo.age.chargeBackRisk"); + String accountSigningState = StringUtils.capitalize(signState.getDisplayString()); + if (signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) + signAgeInfo = null; + + return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), signAgeInfo, accountSigningState); + } + return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), null, null); + } + + private boolean hasChargebackRisk(@Nullable Trade trade, @Nullable Offer offer) { + Offer offerToCheck = trade != null ? trade.getOffer() : offer; + + return offerToCheck != null && + PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCurrencyCode()); + } + + protected void addMouseListener(int numTrades, + PrivateNotificationManager privateNotificationManager, + @Nullable Trade trade, + Offer offer, + Preferences preferences, + boolean useDevPrivilegeKeys, + boolean isFiatCurrency, + long peersAccountAge, + long peersSignAge, + String peersAccountAgeInfo, + String peersSignAgeInfo, + String accountSigningState) { + + final String accountAgeFormatted = isFiatCurrency ? + peersAccountAge > -1 ? + DisplayUtils.formatAccountAge(peersAccountAge) : + Res.get("peerInfo.unknownAge") : + null; + + final String signAgeFormatted = isFiatCurrency && peersSignAgeInfo != null ? + peersSignAge > -1 ? + DisplayUtils.formatAccountAge(peersSignAge) : + Res.get("peerInfo.unknownAge") : + null; + + setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys) + .fullAddress(fullAddress) + .numTrades(numTrades) + .accountAge(accountAgeFormatted) + .signAge(signAgeFormatted) + .accountAgeInfo(peersAccountAgeInfo) + .signAgeInfo(peersSignAgeInfo) + .accountSigningState(accountSigningState) + .position(localToScene(new Point2D(0, 0))) + .onSave(newTag -> { + preferences.setTagForPeer(fullAddress, newTag); + updatePeerInfoIcon(); + }) + .show()); + } + + protected double getScaleFactor() { + return 1; + } + + protected void updatePeerInfoIcon() { + String tag; + if (peerTagMap.containsKey(fullAddress)) { + tag = peerTagMap.get(fullAddress); + final String text = !tag.isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag) : tooltipText; + Tooltip.install(this, new Tooltip(text)); + } else { + tag = ""; + Tooltip.install(this, new Tooltip(tooltipText)); + } + + if (!tag.isEmpty()) + tagLabel.setText(tag.substring(0, 1)); + + if (numTrades > 0) { + numTradesLabel.setText(numTrades > 99 ? "*" : String.valueOf(numTrades)); + + double scaleFactor = getScaleFactor(); + if (numTrades > 9 && numTrades < 100) { + numTradesLabel.relocate(scaleFactor * 2, scaleFactor * 1); + } else { + numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); + } + } + + numTradesPane.setVisible(numTrades > 0); + + tagPane.setVisible(!tag.isEmpty()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java new file mode 100644 index 0000000000..cee0bec7fd --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java @@ -0,0 +1,56 @@ +package bisq.desktop.components; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.user.Preferences; + +import bisq.network.p2p.NodeAddress; + +import javax.annotation.Nullable; + +public class PeerInfoIconSmall extends PeerInfoIcon { + public PeerInfoIconSmall(NodeAddress nodeAddress, + String role, + Offer offer, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + // We don't want to show number of trades in that case as it would be unreadable. + // Also we don't need the privateNotificationManager as no interaction will take place with this icon. + super(nodeAddress, role, + 0, + null, + offer, + preferences, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected double getScaleFactor() { + return 0.6; + } + + @Override + protected void addMouseListener(int numTrades, + PrivateNotificationManager privateNotificationManager, + @Nullable Trade trade, + Offer offer, + Preferences preferences, + boolean useDevPrivilegeKeys, + boolean isFiatCurrency, + long peersAccountAge, + long peersSignAge, + String peersAccountAgeInfo, + String peersSignAgeInfo, + String accountSigningState) { + } + + @Override + protected void updatePeerInfoIcon() { + numTradesPane.setVisible(false); + tagPane.setVisible(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java b/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java new file mode 100644 index 0000000000..fce42467b5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; + +import bisq.common.UserThread; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class PopOverWrapper { + + private PopOver popover; + private Supplier popoverSupplier; + private boolean hidePopover; + private PopOverState state = PopOverState.HIDDEN; + + enum PopOverState { + HIDDEN, SHOWING, SHOWN, HIDING + } + + public void showPopOver(Supplier popoverSupplier) { + this.popoverSupplier = popoverSupplier; + hidePopover = false; + + if (state == PopOverState.HIDDEN) { + state = PopOverState.SHOWING; + popover = popoverSupplier.get(); + + UserThread.runAfter(() -> { + state = PopOverState.SHOWN; + if (hidePopover) { + // For some reason, this can result in a brief flicker when invoked + // from a 'runAfter' callback, rather than directly. So make the delay + // very short (25ms) so that we don't reach here often: + hidePopOver(); + } + }, 25, TimeUnit.MILLISECONDS); + } + } + + public void hidePopOver() { + hidePopover = true; + + if (state == PopOverState.SHOWN) { + state = PopOverState.HIDING; + popover.hide(); + + UserThread.runAfter(() -> { + state = PopOverState.HIDDEN; + if (!hidePopover) { + showPopOver(popoverSupplier); + } + }, 250, TimeUnit.MILLISECONDS); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java b/desktop/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java new file mode 100644 index 0000000000..6867df4b8a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java @@ -0,0 +1,173 @@ +/* + * 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 bisq.desktop.components; + +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.locale.Res; + +import com.jfoenix.controls.JFXProgressBar; + +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.geometry.Pos; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SeparatedPhaseBars extends VBox { + // Last day for creating github compensation request issue, as decided by general consensus + private static final double LAST_COMP_REQ_GH_ISSUE = (double) 18 / 25; + private double labelMinWidth = 150; + private double breakMinWidth = 20; + private int totalDuration; + private List items; + + public SeparatedPhaseBars(List items) { + this.items = items; + setSpacing(10); + + HBox titlesBars = new HBox(); + titlesBars.setSpacing(5); + getChildren().add(titlesBars); + + HBox progressBars = new HBox(); + progressBars.setSpacing(5); + getChildren().add(progressBars); + + items.forEach(item -> { + String text = item.phase.name().startsWith("BREAK") ? "" : Res.get("dao.phase.separatedPhaseBar." + item.phase); + Label titleLabel = new Label(text); + titleLabel.setEllipsisString(""); + titleLabel.setAlignment(Pos.CENTER); + item.setTitleLabel(titleLabel); + titlesBars.getChildren().addAll(titleLabel); + + JFXProgressBar progressBar = new JFXProgressBar(); + progressBar.setMinHeight(9); + progressBar.setMaxHeight(9); + progressBar.progressProperty().bind(item.progressProperty); + progressBar.setOpacity(item.isShowBlocks() ? 1 : 0.25); + if (item.phase.name().startsWith("PROPOSAL")) { + progressBar.setSecondaryProgress(LAST_COMP_REQ_GH_ISSUE); + } + progressBars.getChildren().add(progressBar); + item.setProgressBar(progressBar); + }); + + widthProperty().addListener((observable, oldValue, newValue) -> { + updateWidth((double) newValue); + }); + } + + public void updateWidth() { + updateWidth(getWidth()); + } + + private void updateWidth(double availableWidth) { + if (availableWidth > 0) { + totalDuration = items.stream().mapToInt(SeparatedPhaseBarsItem::getDuration).sum(); + if (totalDuration > 0) { + // We want to have a min. width for the breaks and for the phases which are important to the user but + // quite short (blind vote, vote reveal, result). If we display it correctly most of the space is + // consumed by the proposal phase. We apply a min and max width and adjust the available width so + // we have all phases displayed so that the text is fully readable. The proposal phase is shorter as + // it would be with correct display but we take that into account to have a better overall overview. + final double finalAvailableWidth = availableWidth; + AtomicReference adjustedAvailableWidth = new AtomicReference<>(availableWidth); + items.forEach(item -> { + double calculatedWidth = (double) item.duration / (double) totalDuration * finalAvailableWidth; + double minWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : labelMinWidth; + double maxWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : calculatedWidth; + if (calculatedWidth < minWidth) { + double missing = minWidth - calculatedWidth; + adjustedAvailableWidth.set(adjustedAvailableWidth.get() - missing); + } else if (calculatedWidth > maxWidth) { + double remaining = calculatedWidth - maxWidth; + adjustedAvailableWidth.set(adjustedAvailableWidth.get() + remaining); + } + }); + + items.forEach(item -> { + double calculatedWidth = (double) item.duration / (double) totalDuration * adjustedAvailableWidth.get(); + double minWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : labelMinWidth; + double maxWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : calculatedWidth; + double width = calculatedWidth; + if (calculatedWidth < minWidth) { + width = minWidth; + } else if (calculatedWidth > maxWidth) { + width = maxWidth; + } + item.getTitleLabel().setPrefWidth(width); + item.getProgressBar().setPrefWidth(width); + }); + } + } + } + + @Getter + public static class SeparatedPhaseBarsItem { + private final DaoPhase.Phase phase; + private final boolean showBlocks; + private final IntegerProperty startBlockProperty = new SimpleIntegerProperty(); + private final IntegerProperty lastBlockProperty = new SimpleIntegerProperty(); + private final DoubleProperty progressProperty = new SimpleDoubleProperty(); + private int duration; + @Setter + private javafx.scene.layout.VBox progressVBox; + @Setter + private Label titleLabel; + @Setter + private ProgressBar progressBar; + @Setter + private int indicatorBlock; + private ProgressBar indicatorBar; + + public SeparatedPhaseBarsItem(DaoPhase.Phase phase, boolean showBlocks) { + this.phase = phase; + this.showBlocks = showBlocks; + } + + public void setInActive() { + titleLabel.getStyleClass().add("separated-phase-bar-inactive"); + } + + public void setActive() { + titleLabel.getStyleClass().add("separated-phase-bar-active"); + } + + public void setPeriodRange(int firstBlock, int lastBlock, int duration) { + startBlockProperty.set(firstBlock); + lastBlockProperty.set(lastBlock); + this.duration = duration; + } + + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/TableGroupHeadline.java b/desktop/src/main/java/bisq/desktop/components/TableGroupHeadline.java new file mode 100644 index 0000000000..e9fc8bb6bc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TableGroupHeadline.java @@ -0,0 +1,89 @@ +/* + * 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 bisq.desktop.components; + +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; + +import javafx.geometry.Insets; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TableGroupHeadline extends Pane { + + private final Label label; + private final StringProperty text = new SimpleStringProperty(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TableGroupHeadline() { + this(""); + } + + public TableGroupHeadline(String title) { + text.set(title); + + GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); + GridPane.setColumnSpan(this, 2); + + Pane bg = new StackPane(); + bg.setId("table-group-headline"); + bg.prefWidthProperty().bind(widthProperty()); + bg.prefHeightProperty().bind(heightProperty()); + + label = new AutoTooltipLabel(); + label.textProperty().bind(text); + label.setLayoutX(8); + label.setPadding(new Insets(-8, 7, 0, 5)); + setActive(); + getChildren().addAll(bg, label); + } + + public void setInactive() { + setId("titled-group-bg"); + label.setId("titled-group-bg-label"); + } + + private void setActive() { + setId("titled-group-bg-active"); + label.setId("titled-group-bg-label-active"); + label.getStyleClass().add("highlight-static"); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public void setText(String text) { + this.text.set(text); + } + + +} diff --git a/desktop/src/main/java/bisq/desktop/components/TextFieldWithCopyIcon.java b/desktop/src/main/java/bisq/desktop/components/TextFieldWithCopyIcon.java new file mode 100644 index 0000000000..5d5323bf18 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TextFieldWithCopyIcon.java @@ -0,0 +1,123 @@ +/* + * 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 bisq.desktop.components; + +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class TextFieldWithCopyIcon extends AnchorPane { + + private final StringProperty text = new SimpleStringProperty(); + private final TextField textField; + private boolean copyWithoutCurrencyPostFix; + private boolean copyTextAfterDelimiter; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TextFieldWithCopyIcon() { + this(null); + } + + public TextFieldWithCopyIcon(String customStyleClass) { + Label copyIcon = new Label(); + copyIcon.setLayoutY(3); + copyIcon.getStyleClass().addAll("icon", "highlight"); + copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + copyIcon.setOnMouseClicked(e -> { + String text = getText(); + if (text != null && text.length() > 0) { + String copyText; + if (copyWithoutCurrencyPostFix) { + String[] strings = text.split(" "); + if (strings.length > 1) + copyText = strings[0]; // exclude the BTC postfix + else + copyText = text; + } else if (copyTextAfterDelimiter) { + String[] strings = text.split(" "); + if (strings.length > 1) + copyText = strings[2]; // exclude the part before / (slash included) + else + copyText = text; + } else { + copyText = text; + } + Utilities.copyToClipboard(copyText); + } + }); + textField = new JFXTextField(); + textField.setEditable(false); + if (customStyleClass != null) textField.getStyleClass().add(customStyleClass); + textField.textProperty().bindBidirectional(text); + AnchorPane.setRightAnchor(copyIcon, 5.0); + AnchorPane.setRightAnchor(textField, 30.0); + AnchorPane.setLeftAnchor(textField, 0.0); + textField.focusTraversableProperty().set(focusTraversableProperty().get()); + getChildren().addAll(textField, copyIcon); + } + + public void setPromptText(String value) { + textField.setPromptText(value); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter/Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public void setText(String text) { + this.text.set(text); + } + + public void setTooltip(Tooltip toolTip) { + textField.setTooltip(toolTip); + } + + public void setCopyWithoutCurrencyPostFix(boolean copyWithoutCurrencyPostFix) { + this.copyWithoutCurrencyPostFix = copyWithoutCurrencyPostFix; + } + + public void setCopyTextAfterDelimiter(boolean copyTextAfterDelimiter) { + this.copyTextAfterDelimiter = copyTextAfterDelimiter; + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java b/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java new file mode 100644 index 0000000000..97e237c831 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java @@ -0,0 +1,81 @@ +/* + * 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 bisq.desktop.components; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.text.TextAlignment; + +import javafx.geometry.Pos; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; + +public class TextFieldWithIcon extends AnchorPane { + public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); + @Getter + private final Label iconLabel; + @Getter + private final TextField textField; + private final Label dummyTextField; + + public TextFieldWithIcon() { + textField = new JFXTextField(); + textField.setEditable(false); + textField.setMouseTransparent(true); + textField.setFocusTraversable(false); + setLeftAnchor(textField, 0d); + setRightAnchor(textField, 0d); + + dummyTextField = new Label(); + dummyTextField.setWrapText(true); + dummyTextField.setAlignment(Pos.CENTER_LEFT); + dummyTextField.setTextAlignment(TextAlignment.LEFT); + dummyTextField.setMouseTransparent(true); + dummyTextField.setFocusTraversable(false); + setLeftAnchor(dummyTextField, 0d); + dummyTextField.setVisible(false); + + iconLabel = new Label(); + iconLabel.setLayoutX(0); + iconLabel.setLayoutY(3); + + dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> { + iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20); + }); + + getChildren().addAll(textField, dummyTextField, iconLabel); + } + + public void setIcon(AwesomeIcon iconLabel) { + AwesomeDude.setIcon(this.iconLabel, iconLabel); + } + + public void setText(String text) { + textField.setText(text); + dummyTextField.setText(text); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java b/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java new file mode 100644 index 0000000000..1b53e3ed41 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java @@ -0,0 +1,84 @@ +/* + * 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 bisq.desktop.components; + +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; + +import javafx.geometry.Insets; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class TitledGroupBg extends Pane { + + private final Label label; + private final StringProperty text = new SimpleStringProperty(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TitledGroupBg() { + GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); + GridPane.setColumnSpan(this, 2); + + label = new AutoTooltipLabel(); + label.textProperty().bind(text); + label.setLayoutX(4); + label.setLayoutY(-8); + label.setPadding(new Insets(0, 7, 0, 5)); + setActive(); + getChildren().add(label); + } + + public void setInactive() { + resetStyles(); + getStyleClass().add("titled-group-bg"); + label.getStyleClass().add("titled-group-bg-label"); + } + + private void resetStyles() { + getStyleClass().removeAll("titled-group-bg", "titled-group-bg-active"); + label.getStyleClass().removeAll("titled-group-bg-label", "titled-group-bg-label-active"); + } + + private void setActive() { + resetStyles(); + getStyleClass().add("titled-group-bg-active"); + label.getStyleClass().add("titled-group-bg-label-active"); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public void setText(String text) { + this.text.set(text); + } + + public Label getLabel() { + return label; + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/TooltipUtil.java b/desktop/src/main/java/bisq/desktop/components/TooltipUtil.java new file mode 100644 index 0000000000..421c45a791 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TooltipUtil.java @@ -0,0 +1,47 @@ +/* + * 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 bisq.desktop.components; + +import javafx.scene.control.Labeled; +import javafx.scene.control.SkinBase; +import javafx.scene.control.Tooltip; +import javafx.scene.text.Text; + +public class TooltipUtil { + + public static void showTooltipIfTruncated(SkinBase skinBase, Labeled labeled) { + for (Object node : skinBase.getChildren()) { + if (node instanceof Text) { + String displayedText = ((Text) node).getText(); + String untruncatedText = labeled.getText(); + if (displayedText.equals(untruncatedText)) { + if (labeled.getTooltip() != null) { + labeled.setTooltip(null); + } + } else if (untruncatedText != null && !untruncatedText.trim().isEmpty()) { + final Tooltip tooltip = new Tooltip(untruncatedText); + + // Force tooltip to use color, as it takes in some cases the color of the parent label + // and can't be overridden by class or id + tooltip.setStyle("-fx-text-fill: -bs-rd-tooltip-truncated;"); + labeled.setTooltip(tooltip); + } + } + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java b/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java new file mode 100644 index 0000000000..e658cf9ee6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java @@ -0,0 +1,80 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.indicator.TxConfidenceIndicator; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.listeners.TxConfidenceListener; +import bisq.core.btc.wallet.BsqWalletService; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import javafx.scene.control.Tooltip; + +import lombok.Data; + + +@Data +public class TxConfidenceListItem { + protected final BsqWalletService bsqWalletService; + protected final String txId; + protected int confirmations = 0; + protected TxConfidenceIndicator txConfidenceIndicator; + protected TxConfidenceListener txConfidenceListener; + + protected TxConfidenceListItem(Transaction transaction, + BsqWalletService bsqWalletService) { + this.bsqWalletService = bsqWalletService; + + txId = transaction.getTxId().toString(); + txConfidenceIndicator = new TxConfidenceIndicator(); + txConfidenceIndicator.setId("funds-confidence"); + Tooltip tooltip = new Tooltip(); + txConfidenceIndicator.setProgress(0); + txConfidenceIndicator.setPrefSize(24, 24); + txConfidenceIndicator.setTooltip(tooltip); + + txConfidenceListener = new TxConfidenceListener(txId) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + updateConfidence(confidence, tooltip); + } + }; + bsqWalletService.addTxConfidenceListener(txConfidenceListener); + updateConfidence(bsqWalletService.getConfidenceForTxId(txId), tooltip); + } + + protected TxConfidenceListItem() { + this.bsqWalletService = null; + this.txId = null; + } + + private void updateConfidence(TransactionConfidence confidence, Tooltip tooltip) { + if (confidence != null) { + GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator); + confirmations = confidence.getDepthInBlocks(); + } + } + + public void cleanup() { + bsqWalletService.removeTxConfidenceListener(txConfidenceListener); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java new file mode 100644 index 0000000000..84cea87e54 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java @@ -0,0 +1,196 @@ +/* + * 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 bisq.desktop.components; + +import bisq.desktop.components.indicator.TxConfidenceIndicator; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.listeners.TxConfidenceListener; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.Res; +import bisq.core.user.BlockChainExplorer; +import bisq.core.user.Preferences; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.TransactionConfidence; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +public class TxIdTextField extends AnchorPane { + private static Preferences preferences; + + public static void setPreferences(Preferences preferences) { + TxIdTextField.preferences = preferences; + } + + private static BtcWalletService walletService; + + public static void setWalletService(BtcWalletService walletService) { + TxIdTextField.walletService = walletService; + } + + @Getter + private final TextField textField; + private final Tooltip progressIndicatorTooltip; + private final TxConfidenceIndicator txConfidenceIndicator; + private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon; + private TxConfidenceListener txConfidenceListener; + @Setter + private boolean isBsq; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxIdTextField() { + txConfidenceIndicator = new TxConfidenceIndicator(); + txConfidenceIndicator.setFocusTraversable(false); + txConfidenceIndicator.setMaxSize(20, 20); + txConfidenceIndicator.setId("funds-confidence"); + txConfidenceIndicator.setLayoutY(1); + txConfidenceIndicator.setProgress(0); + txConfidenceIndicator.setVisible(false); + AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); + AnchorPane.setTopAnchor(txConfidenceIndicator, 3.0); + progressIndicatorTooltip = new Tooltip("-"); + txConfidenceIndicator.setTooltip(progressIndicatorTooltip); + + copyIcon = new Label(); + copyIcon.setLayoutY(3); + copyIcon.getStyleClass().addAll("icon", "highlight"); + copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + AnchorPane.setRightAnchor(copyIcon, 30.0); + + Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip")); + + blockExplorerIcon = new Label(); + blockExplorerIcon.getStyleClass().addAll("icon", "highlight"); + blockExplorerIcon.setTooltip(tooltip); + AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); + blockExplorerIcon.setMinWidth(20); + AnchorPane.setRightAnchor(blockExplorerIcon, 52.0); + AnchorPane.setTopAnchor(blockExplorerIcon, 4.0); + + missingTxWarningIcon = new Label(); + missingTxWarningIcon.getStyleClass().addAll("icon", "error-icon"); + AwesomeDude.setIcon(missingTxWarningIcon, AwesomeIcon.WARNING_SIGN); + missingTxWarningIcon.setTooltip(new Tooltip(Res.get("txIdTextField.missingTx.warning.tooltip"))); + missingTxWarningIcon.setMinWidth(20); + AnchorPane.setRightAnchor(missingTxWarningIcon, 52.0); + AnchorPane.setTopAnchor(missingTxWarningIcon, 4.0); + missingTxWarningIcon.setVisible(false); + missingTxWarningIcon.setManaged(false); + + textField = new JFXTextField(); + textField.setId("address-text-field"); + textField.setEditable(false); + textField.setTooltip(tooltip); + AnchorPane.setRightAnchor(textField, 80.0); + AnchorPane.setLeftAnchor(textField, 0.0); + textField.focusTraversableProperty().set(focusTraversableProperty().get()); + getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyIcon, txConfidenceIndicator); + } + + public void setup(@Nullable String txId) { + if (txConfidenceListener != null) + walletService.removeTxConfidenceListener(txConfidenceListener); + + if (txId == null) { + textField.setText(Res.get("shared.na")); + textField.setId("address-text-field-error"); + blockExplorerIcon.setVisible(false); + blockExplorerIcon.setManaged(false); + copyIcon.setVisible(false); + copyIcon.setManaged(false); + txConfidenceIndicator.setVisible(false); + missingTxWarningIcon.setVisible(true); + missingTxWarningIcon.setManaged(true); + return; + } + + txConfidenceListener = new TxConfidenceListener(txId) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + updateConfidence(confidence); + } + }; + walletService.addTxConfidenceListener(txConfidenceListener); + updateConfidence(walletService.getConfidenceForTxId(txId)); + + textField.setText(txId); + textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); + blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); + copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId)); + } + + public void cleanup() { + if (walletService != null && txConfidenceListener != null) + walletService.removeTxConfidenceListener(txConfidenceListener); + + textField.setOnMouseClicked(null); + blockExplorerIcon.setOnMouseClicked(null); + copyIcon.setOnMouseClicked(null); + textField.setText(""); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void openBlockExplorer(String txId) { + if (preferences != null) { + BlockChainExplorer blockChainExplorer = isBsq ? + preferences.getBsqBlockChainExplorer() : + preferences.getBlockChainExplorer(); + GUIUtil.openWebPage(blockChainExplorer.txUrl + txId, false); + } + } + + private void updateConfidence(TransactionConfidence confidence) { + GUIUtil.updateConfidence(confidence, progressIndicatorTooltip, txConfidenceIndicator); + if (confidence != null) { + if (txConfidenceIndicator.getProgress() != 0) { + txConfidenceIndicator.setVisible(true); + AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); + } + } else { + //TODO we should show some placeholder in case of a tx which we are not aware of but which can be + // confirmed already. This is for instance the case of the other peers trade fee tx, as it is not related + // to our wallet we don't have a confidence object but we should show that it is in an unknown state instead + // of not showing anything which causes confusion that the tx was not broadcasted. Best would be to request + // it from a block explorer service but that is a bit too heavy for that use case... + // Maybe a question mark with a tooltip explaining why we don't know about the confidence might be ok... + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java new file mode 100644 index 0000000000..610ed63b03 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java @@ -0,0 +1,95 @@ +/* + * 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 bisq.desktop.components.chart; + +import bisq.desktop.common.model.ActivatableDataModel; + +import java.time.Instant; +import java.time.temporal.TemporalAdjuster; + +import java.util.Map; +import java.util.function.BinaryOperator; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class ChartDataModel extends ActivatableDataModel { + protected final TemporalAdjusterModel temporalAdjusterModel = new TemporalAdjusterModel(); + protected Predicate dateFilter = e -> true; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChartDataModel() { + super(); + } + + @Override + public void activate() { + dateFilter = e -> true; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TemporalAdjusterModel delegates + /////////////////////////////////////////////////////////////////////////////////////////// + + void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster); + } + + TemporalAdjuster getTemporalAdjuster() { + return temporalAdjusterModel.getTemporalAdjuster(); + } + + public long toTimeInterval(Instant instant) { + return temporalAdjusterModel.toTimeInterval(instant); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Date filter predicate + /////////////////////////////////////////////////////////////////////////////////////////// + + public Predicate getDateFilter() { + return dateFilter; + } + + void setDateFilter(long from, long to) { + dateFilter = value -> value >= from && value <= to; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void invalidateCache(); + + protected Map getMergedMap(Map map1, + Map map2, + BinaryOperator mergeFunction) { + return Stream.concat(map1.entrySet().stream(), + map2.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + mergeFunction)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java new file mode 100644 index 0000000000..da278463d0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -0,0 +1,774 @@ +/* + * 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 bisq.desktop.components.chart; + +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; +import bisq.desktop.components.AutoTooltipToggleButton; + +import bisq.core.locale.Res; + +import bisq.common.UserThread; + +import javafx.stage.PopupWindow; +import javafx.stage.Stage; + +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Side; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import javafx.event.EventHandler; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import javafx.util.Duration; + +import java.time.temporal.TemporalAdjuster; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class ChartView> extends ActivatableViewAndModel { + private Pane center; + private SplitPane timelineNavigation; + protected NumberAxis xAxis, yAxis; + protected LineChart chart; + private HBox timelineLabels, legendBox2; + private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); + + protected final Set> activeSeries = new HashSet<>(); + protected final Map seriesIndexMap = new HashMap<>(); + protected final Map legendToggleBySeriesName = new HashMap<>(); + private final List dividerNodes = new ArrayList<>(); + private final List dividerNodesTooltips = new ArrayList<>(); + private ChangeListener widthListener; + private ChangeListener timeIntervalChangeListener; + private ListChangeListener nodeListChangeListener; + private int maxSeriesSize; + private boolean centerPanePressed; + private double x; + + @Setter + protected boolean isRadioButtonBehaviour; + @Setter + private int maxDataPointsForShowingSymbols = 100; + private ChangeListener yAxisWidthListener; + private EventHandler dividerMouseDraggedEventHandler; + private StringProperty fromProperty = new SimpleStringProperty(); + private StringProperty toProperty = new SimpleStringProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChartView(T model) { + super(model); + + root = new VBox(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + // We need to call prepareInitialize as we are not using FXMLLoader + prepareInitialize(); + + maxSeriesSize = 0; + centerPanePressed = false; + x = 0; + + // Series + createSeries(); + + // Time interval + HBox timeIntervalBox = getTimeIntervalBox(); + + // chart + xAxis = getXAxis(); + yAxis = getYAxis(); + chart = getChart(); + + // Timeline navigation + addTimelineNavigation(); + + // Legend + HBox legendBox1 = initLegendsAndGetLegendBox(getSeriesForLegend1()); + + Collection> seriesForLegend2 = getSeriesForLegend2(); + if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) { + legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2); + } + + // Set active series/legends + defineAndAddActiveSeries(); + + // Put all together + VBox timelineNavigationBox = new VBox(); + double paddingLeft = 15; + double paddingRight = 89; + // Y-axis width depends on data so we register a listener to get correct value + yAxisWidthListener = (observable, oldValue, newValue) -> { + double width = newValue.doubleValue(); + if (width > 0) { + double rightPadding = width + 14; + VBox.setMargin(timeIntervalBox, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineNavigation, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(10, rightPadding, 0, paddingLeft)); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft)); + } + + if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { + resetTimeNavigation(); + } + } + }; + + VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().add(legendBox2); + } + root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); + + // Listeners + widthListener = (observable, oldValue, newValue) -> { + timelineNavigation.setDividerPosition(0, model.getDividerPositions()[0]); + timelineNavigation.setDividerPosition(1, model.getDividerPositions()[1]); + }; + + timeIntervalChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + onTimeIntervalChanged(newValue); + } + }; + + nodeListChangeListener = c -> { + while (c.next()) { + if (c.wasAdded()) { + c.getAddedSubList().stream() + .filter(node -> node instanceof Text) + .forEach(node -> node.getStyleClass().add("axis-tick-mark-text-node")); + } + } + }; + } + + @Override + public void activate() { + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + UserThread.execute(this::applyTimeLineNavigationLabels); + UserThread.execute(this::onTimelineChanged); + + TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); + applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeIntervalToggleGroup::selectToggle); + + defineAndAddActiveSeries(); + applyData(); + initBoundsForTimelineNavigation(); + + updateChartAfterDataChange(); + + // Apply listeners and handlers + root.widthProperty().addListener(widthListener); + xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); + yAxis.widthProperty().addListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().addListener(timeIntervalChangeListener); + + timelineNavigation.setOnMousePressed(this::onMousePressedSplitPane); + timelineNavigation.setOnMouseDragged(this::onMouseDragged); + center.setOnMousePressed(this::onMousePressedCenter); + center.setOnMouseReleased(this::onMouseReleasedCenter); + + addLegendToggleActionHandlers(getSeriesForLegend1()); + addLegendToggleActionHandlers(getSeriesForLegend2()); + addActionHandlersToDividers(); + } + + @Override + public void deactivate() { + root.widthProperty().removeListener(widthListener); + xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); + yAxis.widthProperty().removeListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().removeListener(timeIntervalChangeListener); + + timelineNavigation.setOnMousePressed(null); + timelineNavigation.setOnMouseDragged(null); + center.setOnMousePressed(null); + center.setOnMouseReleased(null); + + removeLegendToggleActionHandlers(getSeriesForLegend1()); + removeLegendToggleActionHandlers(getSeriesForLegend2()); + removeActionHandlersToDividers(); + + // clear data, reset states. We keep timeInterval state though + activeSeries.clear(); + chart.getData().clear(); + legendToggleBySeriesName.values().forEach(e -> e.setSelected(false)); + dividerNodes.clear(); + dividerNodesTooltips.clear(); + model.invalidateCache(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TimeInterval/TemporalAdjuster + /////////////////////////////////////////////////////////////////////////////////////////// + + protected HBox getTimeIntervalBox() { + ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, + timeIntervalToggleGroup, "toggle-left"); + ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton week = getTimeIntervalToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, + timeIntervalToggleGroup, "toggle-center"); + HBox toggleBox = new HBox(); + toggleBox.setSpacing(0); + toggleBox.setAlignment(Pos.CENTER_LEFT); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + toggleBox.getChildren().addAll(spacer, year, month, week, day); + return toggleBox; + } + + private ToggleButton getTimeIntervalToggleButton(String label, + TemporalAdjusterModel.Interval interval, + ToggleGroup toggleGroup, + String style) { + ToggleButton toggleButton = new AutoTooltipToggleButton(label); + toggleButton.setUserData(interval); + toggleButton.setToggleGroup(toggleGroup); + toggleButton.setId(style); + return toggleButton; + } + + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + model.applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster) + .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) + .ifPresent(model::setDateFormatPattern); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + protected NumberAxis getXAxis() { + NumberAxis xAxis = new NumberAxis(); + xAxis.setForceZeroInRange(false); + xAxis.setAutoRanging(true); + xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter()); + return xAxis; + } + + protected NumberAxis getYAxis() { + NumberAxis yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(true); + yAxis.setSide(Side.RIGHT); + yAxis.setTickLabelFormatter(model.getYAxisStringConverter()); + return yAxis; + } + + // Add implementation if update of the y axis is required at series change + protected void onSetYAxisFormatter(XYChart.Series series) { + } + + protected LineChart getChart() { + LineChart chart = new LineChart<>(xAxis, yAxis); + chart.setAnimated(false); + chart.setLegendVisible(false); + chart.setMinHeight(200); + chart.setId("charts-dao"); + return chart; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + protected HBox initLegendsAndGetLegendBox(Collection> collection) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + collection.forEach(series -> { + AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); + toggle.setMinWidth(200); + toggle.setAlignment(Pos.TOP_LEFT); + String seriesId = getSeriesId(series); + legendToggleBySeriesName.put(seriesId, toggle); + toggle.setText(seriesId); + toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesId)); + toggle.setSelected(false); + hBox.getChildren().add(toggle); + }); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + hBox.getChildren().add(spacer); + return hBox; + } + + private void addLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(e -> onSelectLegendToggle(series))); + } + } + + private void removeLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(null)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addTimelineNavigation() { + Pane left = new Pane(); + center = new Pane(); + center.setId("chart-navigation-center-pane"); + Pane right = new Pane(); + timelineNavigation = new SplitPane(left, center, right); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + timelineNavigation.setMinHeight(25); + timelineLabels = new HBox(); + } + + // After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation. + protected void applyTimeLineNavigationLabels() { + timelineLabels.getChildren().clear(); + ObservableList> tickMarks = xAxis.getTickMarks(); + int size = tickMarks.size(); + for (int i = 0; i < size; i++) { + Axis.TickMark tickMark = tickMarks.get(i); + Number xValue = tickMark.getValue(); + String xValueString; + if (xAxis.getTickLabelFormatter() != null) { + xValueString = xAxis.getTickLabelFormatter().toString(xValue); + } else { + xValueString = String.valueOf(xValue); + } + Label label = new Label(xValueString); + label.setMinHeight(30); + label.setId("chart-navigation-label"); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + if (i < size - 1) { + timelineLabels.getChildren().addAll(label, spacer); + } else { + // After last label we don't add a spacer + timelineLabels.getChildren().add(label); + } + } + } + + private void onMousePressedSplitPane(MouseEvent e) { + x = e.getX(); + applyFromToDates(); + showDividerTooltips(); + } + + private void onMousePressedCenter(MouseEvent e) { + centerPanePressed = true; + applyFromToDates(); + showDividerTooltips(); + } + + private void onMouseReleasedCenter(MouseEvent e) { + centerPanePressed = false; + onTimelineChanged(); + hideDividerTooltips(); + } + + private void onMouseDragged(MouseEvent e) { + if (centerPanePressed) { + double newX = e.getX(); + double width = timelineNavigation.getWidth(); + double relativeDelta = (x - newX) / width; + double leftPos = timelineNavigation.getDividerPositions()[0] - relativeDelta; + double rightPos = timelineNavigation.getDividerPositions()[1] - relativeDelta; + + // Model might limit application of new values if we hit a boundary + model.onTimelineMouseDrag(leftPos, rightPos); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + x = newX; + + applyFromToDates(); + showDividerTooltips(); + } + } + + private void addActionHandlersToDividers() { + // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) + // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm + // and set action handler in activate. + timelineNavigation.requestLayout(); + timelineNavigation.applyCss(); + dividerMouseDraggedEventHandler = event -> { + applyFromToDates(); + showDividerTooltips(); + }; + + for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) { + dividerNodes.add(node); + node.setOnMouseReleased(e -> { + hideDividerTooltips(); + onTimelineChanged(); + }); + node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + + Tooltip tooltip = new Tooltip(""); + dividerNodesTooltips.add(tooltip); + tooltip.setShowDelay(Duration.millis(300)); + tooltip.setShowDuration(Duration.seconds(3)); + tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty); + Tooltip.install(node, tooltip); + } + } + + private void removeActionHandlersToDividers() { + dividerNodes.forEach(node -> { + node.setOnMouseReleased(null); + node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + }); + for (int i = 0; i < dividerNodesTooltips.size(); i++) { + Tooltip tooltip = dividerNodesTooltips.get(i); + tooltip.textProperty().unbind(); + Tooltip.uninstall(dividerNodes.get(i), tooltip); + } + } + + private void resetTimeNavigation() { + timelineNavigation.setDividerPositions(0d, 1d); + model.onTimelineNavigationChanged(0, 1); + } + + private void showDividerTooltips() { + showDividerTooltip(0); + showDividerTooltip(1); + } + + private void hideDividerTooltips() { + dividerNodesTooltips.forEach(PopupWindow::hide); + } + + private void showDividerTooltip(int index) { + Node divider = dividerNodes.get(index); + Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); + Tooltip tooltip = dividerNodesTooltips.get(index); + double xOffset = index == 0 ? -90 : 10; + Stage stage = (Stage) root.getScene().getWindow(); + tooltip.show(stage, stage.getX() + bounds.getMaxX() + xOffset, + stage.getY() + bounds.getMaxY() - 40); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void createSeries(); + + protected abstract Collection> getSeriesForLegend1(); + + // If a second legend is used this has to be overridden + protected Collection> getSeriesForLegend2() { + return null; + } + + protected abstract void defineAndAddActiveSeries(); + + protected void activateSeries(XYChart.Series series) { + if (activeSeries.contains(series)) { + return; + } + + chart.getData().add(series); + activeSeries.add(series); + legendToggleBySeriesName.get(getSeriesId(series)).setSelected(true); + updateChartAfterDataChange(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void applyData(); + + /** + * Implementations define which series will be used for setBoundsForTimelineNavigation + */ + protected abstract void initBoundsForTimelineNavigation(); + + /** + * @param data The series data which determines the min/max x values for the time line navigation. + * If not applicable initBoundsForTimelineNavigation requires custom implementation. + */ + protected void setBoundsForTimelineNavigation(ObservableList> data) { + model.initBounds(data); + xAxis.setLowerBound(model.getLowerBound().doubleValue()); + xAxis.setUpperBound(model.getUpperBound().doubleValue()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handlers triggering a data/chart update + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onTimeIntervalChanged(Toggle newValue) { + TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); + applyTemporalAdjuster(interval.getAdjuster()); + model.invalidateCache(); + applyData(); + updateChartAfterDataChange(); + } + + private void onTimelineChanged() { + updateTimeLinePositions(); + + model.invalidateCache(); + applyData(); + updateChartAfterDataChange(); + } + + private void updateTimeLinePositions() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.onTimelineNavigationChanged(leftPos, rightPos); + // We need to update as model might have adjusted the values + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); + } + + private void applyFromToDates() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.applyFromToDates(leftPos, rightPos); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); + } + + private void onSelectLegendToggle(XYChart.Series series) { + boolean isSelected = legendToggleBySeriesName.get(getSeriesId(series)).isSelected(); + // If we have set that flag we deselect all other toggles + if (isRadioButtonBehaviour) { + new ArrayList<>(chart.getData()).stream() // We need to copy to a new list to avoid ConcurrentModificationException + .filter(activeSeries::contains) + .forEach(seriesToRemove -> { + chart.getData().remove(seriesToRemove); + String seriesId = getSeriesId(seriesToRemove); + activeSeries.remove(seriesToRemove); + legendToggleBySeriesName.get(seriesId).setSelected(false); + }); + } + + if (isSelected) { + chart.getData().add(series); + activeSeries.add(series); + //model.invalidateCache(); + applyData(); + + if (isRadioButtonBehaviour) { + // We support different y-axis formats only if isRadioButtonBehaviour is set, otherwise we would get + // mixed data on y-axis + onSetYAxisFormatter(series); + } + } else if (!isRadioButtonBehaviour) { // if isRadioButtonBehaviour we have removed it already via the code above + chart.getData().remove(series); + activeSeries.remove(series); + + } + updateChartAfterDataChange(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart update after data change + /////////////////////////////////////////////////////////////////////////////////////////// + + // Update of the chart data can be triggered by: + // 1. activate() + // 2. TimeInterval toggle change + // 3. Timeline navigation change + // 4. Legend/series toggle change + + // Timeline navigation and legend/series toggles get reset at activate. + // Time interval toggle keeps its state at screen changes. + protected void updateChartAfterDataChange() { + // If a series got no data points after update we need to clear it from the chart + cleanupDanglingSeries(); + + // Hides symbols if too many data points are created + updateSymbolsVisibility(); + + // When series gets added/removed the JavaFx charts framework would try to apply styles by the index of + // addition, but we want to use a static color assignment which is synced with the legend color. + applySeriesStyles(); + + // Set tooltip on symbols + applyTooltip(); + } + + private void cleanupDanglingSeries() { + List> activeSeriesList = new ArrayList<>(activeSeries); + activeSeriesList.forEach(series -> { + ObservableList> seriesOnChart = chart.getData(); + if (series.getData().isEmpty()) { + seriesOnChart.remove(series); + } else if (!seriesOnChart.contains(series)) { + seriesOnChart.add(series); + } + }); + } + + private void updateSymbolsVisibility() { + maxDataPointsForShowingSymbols = 100; + long numDataPoints = chart.getData().stream() + .map(XYChart.Series::getData) + .mapToLong(List::size) + .max() + .orElse(0); + boolean prevValue = chart.getCreateSymbols(); + boolean newValue = numDataPoints < maxDataPointsForShowingSymbols; + if (prevValue != newValue) { + chart.setCreateSymbols(newValue); + } + } + + // The chart framework assigns the colored depending on the order it got added, but want to keep colors + // the same so they match with the legend toggle. + private void applySeriesStyles() { + for (int index = 0; index < chart.getData().size(); index++) { + XYChart.Series series = chart.getData().get(index); + int staticIndex = seriesIndexMap.get(getSeriesId(series)); + Set lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line"); + Stream symbols = series.getData().stream().map(XYChart.Data::getNode) + .flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream()); + Stream.concat(lines.stream(), symbols).forEach(node -> { + removeStyles(node); + node.getStyleClass().add("default-color" + staticIndex); + }); + } + } + + private void applyTooltip() { + chart.getData().forEach(series -> { + series.getData().forEach(data -> { + Node node = data.getNode(); + if (node == null) { + return; + } + String xValue = model.getTooltipDateConverter(data.getXValue()); + String yValue = model.getYAxisStringConverter().toString(data.getYValue()); + Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); + }); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void removeStyles(Node node) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + node.getStyleClass().remove("default-color" + i); + } + } + + private Set getNodesForStyle(Node node, String style) { + Set result = new HashSet<>(); + if (node != null) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + result.addAll(node.lookupAll(String.format(style, i))); + } + } + return result; + } + + private int getMaxSeriesSize() { + maxSeriesSize = Math.max(maxSeriesSize, chart.getData().size()); + return maxSeriesSize; + } + + private Optional findTimeIntervalToggleByTemporalAdjuster(TemporalAdjuster adjuster) { + return timeIntervalToggleGroup.getToggles().stream() + .filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) + .findAny(); + } + + // We use the name as id as there is no other suitable data inside series + protected String getSeriesId(XYChart.Series series) { + return series.getName(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java new file mode 100644 index 0000000000..76d7f39631 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -0,0 +1,255 @@ +/* + * 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 bisq.desktop.components.chart; + +import bisq.desktop.common.model.ActivatableWithDataModel; +import bisq.desktop.util.DisplayUtils; + +import bisq.common.util.Tuple2; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.time.temporal.TemporalAdjuster; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class ChartViewModel extends ActivatableWithDataModel { + private final static double LEFT_TIMELINE_SNAP_VALUE = 0.01; + private final static double RIGHT_TIMELINE_SNAP_VALUE = 0.99; + + @Getter + private final Double[] dividerPositions = new Double[]{0d, 1d}; + @Getter + protected Number lowerBound; + @Getter + protected Number upperBound; + @Getter + protected String dateFormatPatters = "dd MMM\nyyyy"; + @Getter + long fromDate; + @Getter + long toDate; + + public ChartViewModel(T dataModel) { + super(dataModel); + } + + @Override + public void activate() { + dividerPositions[0] = 0d; + dividerPositions[1] = 1d; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TimerInterval/TemporalAdjuster + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + dataModel.setTemporalAdjuster(temporalAdjuster); + } + + void setDateFormatPattern(TemporalAdjusterModel.Interval interval) { + switch (interval) { + case YEAR: + dateFormatPatters = "yyyy"; + break; + case MONTH: + dateFormatPatters = "MMM\nyyyy"; + break; + default: + dateFormatPatters = "MMM dd\nyyyy"; + break; + } + } + + protected TemporalAdjuster getTemporalAdjuster() { + return dataModel.getTemporalAdjuster(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TimelineNavigation + /////////////////////////////////////////////////////////////////////////////////////////// + + void onTimelineNavigationChanged(double leftPos, double rightPos) { + applyFromToDates(leftPos, rightPos); + // TODO find better solution + // The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of + // the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected + // A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would + // filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in + // the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and + // to date to one year ahead to be sure we include all. + + long from, to; + + // We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those + // are used by the view for tooltip over the time line navigation dividers + if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { + from = 0; + } else { + from = fromDate; + } + if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { + to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365); + } else { + to = toDate; + } + + dividerPositions[0] = leftPos; + dividerPositions[1] = rightPos; + dataModel.setDateFilter(from, to); + } + + void applyFromToDates(double leftPos, double rightPos) { + // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we + // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of + // drag operations. + if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { + leftPos = 0; + } + if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { + rightPos = 1; + } + + long lowerBoundAsLong = lowerBound.longValue(); + long totalRange = upperBound.longValue() - lowerBoundAsLong; + + fromDate = (long) (lowerBoundAsLong + totalRange * leftPos); + toDate = (long) (lowerBoundAsLong + totalRange * rightPos); + } + + void onTimelineMouseDrag(double leftPos, double rightPos) { + // Limit drag operation if we have hit a boundary + if (leftPos > LEFT_TIMELINE_SNAP_VALUE) { + dividerPositions[1] = rightPos; + } + if (rightPos < RIGHT_TIMELINE_SNAP_VALUE) { + dividerPositions[0] = leftPos; + } + } + + void initBounds(List> data1, + List> data2) { + Tuple2 xMinMaxTradeFee = getMinMax(data1); + Tuple2 xMinMaxCompensationRequest = getMinMax(data2); + + lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first); + upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); + } + + void initBounds(List> data) { + Tuple2 xMinMaxTradeFee = getMinMax(data); + lowerBound = xMinMaxTradeFee.first; + upperBound = xMinMaxTradeFee.second; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + StringConverter getTimeAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number epochSeconds) { + Date date = new Date(epochSeconds.longValue() * 1000); + return DisplayUtils.formatDateAxis(date, getDateFormatPatters()); + } + + @Override + public Number fromString(String string) { + return 0; + } + }; + } + + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return String.valueOf(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + String getTooltipDateConverter(Number date) { + return getTimeAxisStringConverter().toString(date).replace("\n", " "); + } + + protected String getTooltipValueConverter(Number value) { + return getYAxisStringConverter().toString(value); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void invalidateCache() { + dataModel.invalidateCache(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + protected List> toChartData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + protected List> toChartDoubleData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + protected List> toChartLongData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private Tuple2 getMinMax(List> chartData) { + long min = Long.MAX_VALUE, max = 0; + for (XYChart.Data data : chartData) { + min = Math.min(data.getXValue().longValue(), min); + max = Math.max(data.getXValue().longValue(), max); + } + return new Tuple2<>((double) min, (double) max); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java new file mode 100644 index 0000000000..b304cfc7e4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.desktop.components.chart; + +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TemporalAdjusterModel { + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + public enum Interval { + YEAR(TemporalAdjusters.firstDayOfYear()), + MONTH(TemporalAdjusters.firstDayOfMonth()), + WEEK(TemporalAdjusters.next(DayOfWeek.MONDAY)), + DAY(TemporalAdjusters.ofDateAdjuster(d -> d)); + + @Getter + private final TemporalAdjuster adjuster; + + Interval(TemporalAdjuster adjuster) { + this.adjuster = adjuster; + } + } + + protected TemporalAdjuster temporalAdjuster = Interval.DAY.getAdjuster(); + + public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + this.temporalAdjuster = temporalAdjuster; + } + + public TemporalAdjuster getTemporalAdjuster() { + return temporalAdjuster; + } + + public long toTimeInterval(Instant instant) { + return toTimeInterval(instant, temporalAdjuster); + } + + public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { + return instant + .atZone(ZONE_ID) + .toLocalDate() + .with(temporalAdjuster) + .atStartOfDay(ZONE_ID) + .toInstant() + .getEpochSecond(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/README.md b/desktop/src/main/java/bisq/desktop/components/controlsfx/README.md new file mode 100644 index 0000000000..900fa9477e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/README.md @@ -0,0 +1,13 @@ +This package is a very minimal subset of the external library `controlsfx`. + +Three files were embedded into the project to avoid having `controlsfx` as dependency. + +This is based on version `8.0.6_20` tagged in commit 6a52afec3ef16094cda281abc80b4daa3d3bf1fd: + +[https://github.com/controlsfx/controlsfx/commit/6a52afec3ef16094cda281abc80b4daa3d3bf1fd] + +These specific files got raw copied (with package name adjustment): + +[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/org/controlsfx/control/PopOver.java] +[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java] +[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/resources/org/controlsfx/control/popover.css] diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java new file mode 100644 index 0000000000..9593cbc430 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java @@ -0,0 +1,787 @@ +/** + * Copyright (c) 2013, ControlsFX + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of ControlsFX, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package bisq.desktop.components.controlsfx.control; + +import static java.util.Objects.requireNonNull; +import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; +import bisq.desktop.components.controlsfx.skin.PopOverSkin; +import javafx.animation.FadeTransition; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WeakChangeListener; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.PopupControl; +import javafx.scene.control.Skin; +import javafx.scene.input.MouseEvent; +import javafx.stage.Window; +import javafx.stage.WindowEvent; +import javafx.util.Duration; + +/** + * The PopOver control provides detailed information about an owning node in a + * popup window. The popup window has a very lightweight appearance (no default + * window decorations) and an arrow pointing at the owner. Due to the nature of + * popup windows the PopOver will move around with the parent window when the + * user drags it.
    + *

    + * The PopOver can be detached from the owning node by dragging it away from the + * owner. It stops displaying an arrow and starts displaying a title and a close + * icon.
    + *
    + *

    + * The following image shows a popover with an accordion content node. PopOver + * controls are automatically resizing themselves when the content node changes + * its size.
    + *
    + *

    + */ +public class PopOver extends PopupControl { + + private static final String DEFAULT_STYLE_CLASS = "popover"; //$NON-NLS-1$ + + private static final Duration DEFAULT_FADE_DURATION = Duration.seconds(.2); + + private double targetX; + + private double targetY; + + /** + * Creates a pop over with a label as the content node. + */ + public PopOver() { + super(); + + getStyleClass().add(DEFAULT_STYLE_CLASS); + + setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT); + setOnHiding(new EventHandler() { + @Override + public void handle(WindowEvent evt) { + setDetached(false); + } + }); + + /* + * Create some initial content. + */ + Label label = new Label(""); //$NON-NLS-1$ + label.setPrefSize(200, 200); + label.setPadding(new Insets(4)); + setContentNode(label); + + ChangeListener repositionListener = new ChangeListener() { + @Override + public void changed(ObservableValue value, + Object oldObject, Object newObject) { + if (isShowing() && !isDetached()) { + show(getOwnerNode(), targetX, targetY); + adjustWindowLocation(); + } + } + }; + + arrowSize.addListener(repositionListener); + cornerRadius.addListener(repositionListener); + arrowLocation.addListener(repositionListener); + arrowIndent.addListener(repositionListener); + } + + /** + * Creates a pop over with the given node as the content node. + * + * @param content The content shown by the pop over + */ + public PopOver(Node content) { + this(); + + setContentNode(content); + } + + @Override + protected Skin createDefaultSkin() { + return new PopOverSkin(this); + } + + // Content support. + + private final ObjectProperty contentNode = new SimpleObjectProperty( + this, "contentNode") { //$NON-NLS-1$ + @Override + public void setValue(Node node) { + if (node == null) { + throw new IllegalArgumentException( + "content node can not be null"); //$NON-NLS-1$ + } + }; + }; + + /** + * Returns the content shown by the pop over. + * + * @return the content node property + */ + public final ObjectProperty contentNodeProperty() { + return contentNode; + } + + /** + * Returns the value of the content property + * + * @return the content node + * + * @see #contentNodeProperty() + */ + public final Node getContentNode() { + return contentNodeProperty().get(); + } + + /** + * Sets the value of the content property. + * + * @param content + * the new content node value + * + * @see #contentNodeProperty() + */ + public final void setContentNode(Node content) { + contentNodeProperty().set(content); + } + + private InvalidationListener hideListener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + if (!isDetached()) { + hide(Duration.ZERO); + } + } + }; + + private WeakInvalidationListener weakHideListener = new WeakInvalidationListener( + hideListener); + + private ChangeListener xListener = new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldX, Number newX) { + setX(getX() + (newX.doubleValue() - oldX.doubleValue())); + } + }; + + private WeakChangeListener weakXListener = new WeakChangeListener<>( + xListener); + + private ChangeListener yListener = new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldY, Number newY) { + setY(getY() + (newY.doubleValue() - oldY.doubleValue())); + } + }; + + private WeakChangeListener weakYListener = new WeakChangeListener<>( + yListener); + + private Window ownerWindow; + + /** + * Shows the pop over in a position relative to the edges of the given owner + * node. The position is dependent on the arrow location. If the arrow is + * pointing to the right then the pop over will be placed to the left of the + * given owner. If the arrow points up then the pop over will be placed + * below the given owner node. The arrow will slightly overlap with the + * owner node. + * + * @param owner + * the owner of the pop over + */ + public final void show(Node owner) { + show(owner, 4); + } + + /** + * Shows the pop over in a position relative to the edges of the given owner + * node. The position is dependent on the arrow location. If the arrow is + * pointing to the right then the pop over will be placed to the left of the + * given owner. If the arrow points up then the pop over will be placed + * below the given owner node. + * + * @param owner + * the owner of the pop over + * @param offset + * if negative specifies the distance to the owner node or when + * positive specifies the number of pixels that the arrow will + * overlap with the owner node (positive values are recommended) + */ + public final void show(Node owner, double offset) { + requireNonNull(owner); + + Bounds bounds = owner.localToScreen(owner.getBoundsInLocal()); + + switch (getArrowLocation()) { + case BOTTOM_CENTER: + case BOTTOM_LEFT: + case BOTTOM_RIGHT: + show(owner, bounds.getMinX() + bounds.getWidth() / 2, + bounds.getMinY() + offset); + break; + case LEFT_BOTTOM: + case LEFT_CENTER: + case LEFT_TOP: + show(owner, bounds.getMaxX() - offset, + bounds.getMinY() + bounds.getHeight() / 2); + break; + case RIGHT_BOTTOM: + case RIGHT_CENTER: + case RIGHT_TOP: + show(owner, bounds.getMinX() + offset, + bounds.getMinY() + bounds.getHeight() / 2); + break; + case TOP_CENTER: + case TOP_LEFT: + case TOP_RIGHT: + show(owner, bounds.getMinX() + bounds.getWidth() / 2, + bounds.getMinY() + bounds.getHeight() - offset); + break; + default: + break; + } + } + + /** + * Makes the pop over visible at the give location and associates it with + * the given owner node. The x and y coordinate will be the target location + * of the arrow of the pop over and not the location of the window. + * + * @param owner + * the owning node + * @param x + * the x coordinate for the pop over arrow tip + * @param y + * the y coordinate for the pop over arrow tip + */ + @Override + public final void show(Node owner, double x, double y) { + show(owner, x, y, DEFAULT_FADE_DURATION); + } + + /** + * Makes the pop over visible at the give location and associates it with + * the given owner node. The x and y coordinate will be the target location + * of the arrow of the pop over and not the location of the window. + * + * @param owner + * the owning node + * @param x + * the x coordinate for the pop over arrow tip + * @param y + * the y coordinate for the pop over arrow tip + * @param fadeInDuration + * the time it takes for the pop over to be fully visible + */ + public final void show(Node owner, double x, double y, + Duration fadeInDuration) { + + /* + * Calling show() a second time without first closing the + * pop over causes it to be placed at the wrong location. + */ + if (ownerWindow != null && isShowing()) { + super.hide(); + } + + targetX = x; + targetY = y; + + if (owner == null) { + throw new IllegalArgumentException("owner can not be null"); //$NON-NLS-1$ + } + + if (fadeInDuration == null) { + fadeInDuration = DEFAULT_FADE_DURATION; + } + + /* + * This is all needed because children windows do not get their x and y + * coordinate updated when the owning window gets moved by the user. + */ + if (ownerWindow != null) { + ownerWindow.xProperty().removeListener(weakXListener); + ownerWindow.yProperty().removeListener(weakYListener); + ownerWindow.widthProperty().removeListener(weakHideListener); + ownerWindow.heightProperty().removeListener(weakHideListener); + } + + ownerWindow = owner.getScene().getWindow(); + ownerWindow.xProperty().addListener(weakXListener); + ownerWindow.yProperty().addListener(weakYListener); + ownerWindow.widthProperty().addListener(weakHideListener); + ownerWindow.heightProperty().addListener(weakHideListener); + + setOnShown(evt -> { + + /* + * The user clicked somewhere into the transparent background. If + * this is the case the hide the window (when attached). + */ + getScene().addEventHandler(MOUSE_CLICKED, + new EventHandler() { + public void handle(MouseEvent evt) { + if (evt.getTarget().equals(getScene().getRoot())) { + if (!isDetached()) { + hide(); + } + } + }; + }); + + /* + * Move the window so that the arrow will end up pointing at the + * target coordinates. + */ + adjustWindowLocation(); + }); + + super.show(owner, x, y); + + // Fade In + Node skinNode = getSkin().getNode(); + skinNode.setOpacity(0); + + FadeTransition fadeIn = new FadeTransition(fadeInDuration, skinNode); + fadeIn.setFromValue(0); + fadeIn.setToValue(1); + fadeIn.play(); + } + + /** + * Hides the pop over by quickly changing its opacity to 0. + * + * @see #hide(Duration) + */ + @Override + public final void hide() { + hide(DEFAULT_FADE_DURATION); + } + + /** + * Hides the pop over by quickly changing its opacity to 0. + * + * @param fadeOutDuration + * the duration of the fade transition that is being used to + * change the opacity of the pop over + * @since 1.0 + */ + public final void hide(Duration fadeOutDuration) { + if (fadeOutDuration == null) { + fadeOutDuration = DEFAULT_FADE_DURATION; + } + + if (isShowing()) { + // Fade Out + Node skinNode = getSkin().getNode(); + skinNode.setOpacity(0); + + FadeTransition fadeOut = new FadeTransition(fadeOutDuration, + skinNode); + fadeOut.setFromValue(1); + fadeOut.setToValue(0); + fadeOut.setOnFinished(evt -> super.hide()); + fadeOut.play(); + } + } + + private void adjustWindowLocation() { + Bounds bounds = PopOver.this.getSkin().getNode().getBoundsInParent(); + + switch (getArrowLocation()) { + case TOP_CENTER: + case TOP_LEFT: + case TOP_RIGHT: + setX(getX() + bounds.getMinX() - computeXOffset()); + setY(getY() + bounds.getMinY() + getArrowSize()); + break; + case LEFT_TOP: + case LEFT_CENTER: + case LEFT_BOTTOM: + setX(getX() + bounds.getMinX() + getArrowSize()); + setY(getY() + bounds.getMinY() - computeYOffset()); + break; + case BOTTOM_CENTER: + case BOTTOM_LEFT: + case BOTTOM_RIGHT: + setX(getX() + bounds.getMinX() - computeXOffset()); + setY(getY() - bounds.getMinY() - bounds.getMaxY() - 1); + break; + case RIGHT_TOP: + case RIGHT_BOTTOM: + case RIGHT_CENTER: + setX(getX() - bounds.getMinX() - bounds.getMaxX() - 1); + setY(getY() + bounds.getMinY() - computeYOffset()); + break; + } + } + + private double computeXOffset() { + switch (getArrowLocation()) { + case TOP_LEFT: + case BOTTOM_LEFT: + return getCornerRadius() + getArrowIndent() + getArrowSize(); + case TOP_CENTER: + case BOTTOM_CENTER: + return getContentNode().prefWidth(-1) / 2; + case TOP_RIGHT: + case BOTTOM_RIGHT: + return getContentNode().prefWidth(-1) - getArrowIndent() + - getCornerRadius() - getArrowSize(); + default: + return 0; + } + } + + private double computeYOffset() { + double prefContentHeight = getContentNode().prefHeight(-1); + + switch (getArrowLocation()) { + case LEFT_TOP: + case RIGHT_TOP: + return getCornerRadius() + getArrowIndent() + getArrowSize(); + case LEFT_CENTER: + case RIGHT_CENTER: + return Math.max(prefContentHeight, 2 * (getCornerRadius() + + getArrowIndent() + getArrowSize())) / 2; + case LEFT_BOTTOM: + case RIGHT_BOTTOM: + return Math.max(prefContentHeight - getCornerRadius() + - getArrowIndent() - getArrowSize(), getCornerRadius() + + getArrowIndent() + getArrowSize()); + default: + return 0; + } + } + + /** + * Detaches the pop over from the owning node. The pop over will no longer + * display an arrow pointing at the owner node. + */ + public final void detach() { + if (isDetachable()) { + setDetached(true); + } + } + + // detach support + + private final BooleanProperty detachable = new SimpleBooleanProperty(this, + "detachable", true); //$NON-NLS-1$ + + /** + * Determines if the pop over is detachable at all. + */ + public final BooleanProperty detachableProperty() { + return detachable; + } + + /** + * Sets the value of the detachable property. + * + * @param detachable + * if true then the user can detach / tear off the pop over + * + * @see #detachableProperty() + */ + public final void setDetachable(boolean detachable) { + detachableProperty().set(detachable); + } + + /** + * Returns the value of the detachable property. + * + * @return true if the user is allowed to detach / tear off the pop over + * + * @see #detachableProperty() + */ + public final boolean isDetachable() { + return detachableProperty().get(); + } + + private final BooleanProperty detached = new SimpleBooleanProperty(this, + "detached", false); //$NON-NLS-1$ + + /** + * Determines whether the pop over is detached from the owning node or not. + * A detached pop over no longer shows an arrow pointing at the owner and + * features its own title bar. + * + * @return the detached property + */ + public final BooleanProperty detachedProperty() { + return detached; + } + + /** + * Sets the value of the detached property. + * + * @param detached + * if true the pop over will change its appearance to "detached" + * mode + * + * @see #detachedProperty() + */ + public final void setDetached(boolean detached) { + detachedProperty().set(detached); + } + + /** + * Returns the value of the detached property. + * + * @return true if the pop over is currently detached. + * + * @see #detachedProperty() + */ + public final boolean isDetached() { + return detachedProperty().get(); + } + + // arrow size support + + // TODO: make styleable + + private final DoubleProperty arrowSize = new SimpleDoubleProperty(this, + "arrowSize", 12); //$NON-NLS-1$ + + /** + * Controls the size of the arrow. Default value is 12. + * + * @return the arrow size property + */ + public final DoubleProperty arrowSizeProperty() { + return arrowSize; + } + + /** + * Returns the value of the arrow size property. + * + * @return the arrow size property value + * + * @see #arrowSizeProperty() + */ + public final double getArrowSize() { + return arrowSizeProperty().get(); + } + + /** + * Sets the value of the arrow size property. + * + * @param size + * the new value of the arrow size property + * + * @see #arrowSizeProperty() + */ + public final void setArrowSize(double size) { + arrowSizeProperty().set(size); + } + + // arrow indent support + + // TODO: make styleable + + private final DoubleProperty arrowIndent = new SimpleDoubleProperty(this, + "arrowIndent", 12); //$NON-NLS-1$ + + /** + * Controls the distance between the arrow and the corners of the pop over. + * The default value is 12. + * + * @return the arrow indent property + */ + public final DoubleProperty arrowIndentProperty() { + return arrowIndent; + } + + /** + * Returns the value of the arrow indent property. + * + * @return the arrow indent value + * + * @see #arrowIndentProperty() + */ + public final double getArrowIndent() { + return arrowIndentProperty().get(); + } + + /** + * Sets the value of the arrow indent property. + * + * @param size + * the arrow indent value + * + * @see #arrowIndentProperty() + */ + public final void setArrowIndent(double size) { + arrowIndentProperty().set(size); + } + + // radius support + + // TODO: make styleable + + private final DoubleProperty cornerRadius = new SimpleDoubleProperty(this, + "cornerRadius", 6); //$NON-NLS-1$ + + /** + * Returns the corner radius property for the pop over. + * + * @return the corner radius property (default is 6) + */ + public final DoubleProperty cornerRadiusProperty() { + return cornerRadius; + } + + /** + * Returns the value of the corner radius property. + * + * @return the corner radius + * + * @see #cornerRadiusProperty() + */ + public final double getCornerRadius() { + return cornerRadiusProperty().get(); + } + + /** + * Sets the value of the corner radius property. + * + * @param radius + * the corner radius + * + * @see #cornerRadiusProperty() + */ + public final void setCornerRadius(double radius) { + cornerRadiusProperty().set(radius); + } + + // Detached stage title + + private final StringProperty detachedTitle = new SimpleStringProperty(this, + "detachedTitle", "Info"); //$NON-NLS-1$ //$NON-NLS-2$ + + /** + * Stores the title to display when the pop over becomes detached. + * + * @return the detached title property + */ + public final StringProperty detachedTitleProperty() { + return detachedTitle; + } + + /** + * Returns the value of the detached title property. + * + * @return the detached title + * + * @see #detachedTitleProperty() + */ + public final String getDetachedTitle() { + return detachedTitleProperty().get(); + } + + /** + * Sets the value of the detached title property. + * + * @param title + * the title to use when detached + * + * @see #detachedTitleProperty() + */ + public final void setDetachedTitle(String title) { + if (title == null) { + throw new IllegalArgumentException("title can not be null"); //$NON-NLS-1$ + } + + detachedTitleProperty().set(title); + } + + private final ObjectProperty arrowLocation = new SimpleObjectProperty( + this, "arrowLocation", ArrowLocation.LEFT_TOP); //$NON-NLS-1$ + + /** + * Stores the preferred arrow location. This might not be the actual + * location of the arrow if auto fix is enabled. + * + * @see #setAutoFix(boolean) + * + * @return the arrow location property + */ + public final ObjectProperty arrowLocationProperty() { + return arrowLocation; + } + + /** + * Sets the value of the arrow location property. + * + * @see #arrowLocationProperty() + * + * @param location + * the requested location + */ + public final void setArrowLocation(ArrowLocation location) { + arrowLocationProperty().set(location); + } + + /** + * Returns the value of the arrow location property. + * + * @see #arrowLocationProperty() + * + * @return the preferred arrow location + */ + public final ArrowLocation getArrowLocation() { + return arrowLocationProperty().get(); + } + + /** + * All possible arrow locations. + */ + public enum ArrowLocation { + LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css new file mode 100644 index 0000000000..ccc6d5e42f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css @@ -0,0 +1,36 @@ +.popover { + -fx-background-color: transparent; +} + +.popover > .border { + -fx-stroke: linear-gradient(to bottom, rgba(0,0,0, .3), rgba(0, 0, 0, .7)) ; + -fx-stroke-width: 0.5; + -fx-fill: rgba(255.0,255.0,255.0, .95); + -fx-effect: dropshadow(gaussian, rgba(0,0,0,.2), 10.0, 0.5, 2.0, 2.0); +} + +.popover > .content { +} + +.popover > .detached { +} + +.popover > .content > .title > .text { + -fx-padding: 6.0 6.0 0.0 6.0; + -fx-text-fill: rgba(120, 120, 120, .8); + -fx-font-weight: bold; +} + +.popover > .content > .title > .icon { + -fx-padding: 6.0 0.0 0.0 10.0; +} + +.popover > .content > .title > .icon > .graphics > .circle { + -fx-fill: gray ; + -fx-effect: innershadow(gaussian, rgba(0,0,0,.2), 3, 0.5, 1.0, 1.0); +} + +.popover > .content > .title > .icon > .graphics > .line { + -fx-stroke: white ; + -fx-stroke-width: 2; +} diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java new file mode 100644 index 0000000000..3ebdc843bf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java @@ -0,0 +1,693 @@ +/** + * Copyright (c) 2013 - 2015, ControlsFX + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of ControlsFX, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package bisq.desktop.components.controlsfx.skin; + +import static java.lang.Double.MAX_VALUE; +import static javafx.geometry.Pos.CENTER_LEFT; +import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; +import static bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation.*; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Skin; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Circle; +import javafx.scene.shape.HLineTo; +import javafx.scene.shape.Line; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.scene.shape.PathElement; +import javafx.scene.shape.QuadCurveTo; +import javafx.scene.shape.VLineTo; +import javafx.stage.Window; + +import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation; + +public class PopOverSkin implements Skin { + + private static final String DETACHED_STYLE_CLASS = "detached"; //$NON-NLS-1$ + + private double xOffset; + private double yOffset; + + private boolean tornOff; + + private Label title; + private Label closeIcon; + + private Path path; + private BorderPane content; + private StackPane titlePane; + private StackPane stackPane; + + private Point2D dragStartLocation; + + private PopOver popOver; + + public PopOverSkin(final PopOver popOver) { + + this.popOver = popOver; + + stackPane = new StackPane(); + stackPane.getStylesheets().add( + PopOver.class.getResource("popover.css").toExternalForm()); //$NON-NLS-1$ + stackPane.setPickOnBounds(false); + stackPane.getStyleClass().add("popover"); //$NON-NLS-1$ + + /* + * The min width and height equal 2 * corner radius + 2 * arrow indent + + * 2 * arrow size. + */ + stackPane.minWidthProperty().bind( + Bindings.add(Bindings.multiply(2, popOver.arrowSizeProperty()), + Bindings.add( + Bindings.multiply(2, + popOver.cornerRadiusProperty()), + Bindings.multiply(2, + popOver.arrowIndentProperty())))); + + stackPane.minHeightProperty().bind(stackPane.minWidthProperty()); + + title = new Label(); + title.textProperty().bind(popOver.detachedTitleProperty()); + title.setMaxSize(MAX_VALUE, MAX_VALUE); + title.setAlignment(Pos.CENTER); + title.getStyleClass().add("text"); //$NON-NLS-1$ + + closeIcon = new Label(); + closeIcon.setGraphic(createCloseIcon()); + closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE); + closeIcon.setContentDisplay(GRAPHIC_ONLY); + closeIcon.visibleProperty().bind(popOver.detachedProperty()); + closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$ + closeIcon.setAlignment(CENTER_LEFT); + closeIcon.getGraphic().setOnMouseClicked( + new EventHandler() { + @Override + public void handle(MouseEvent evt) { + popOver.hide(); + } + }); + + titlePane = new StackPane(); + titlePane.getChildren().add(title); + titlePane.getChildren().add(closeIcon); + titlePane.getStyleClass().add("title"); //$NON-NLS-1$ + + content = new BorderPane(); + content.setCenter(popOver.getContentNode()); + content.getStyleClass().add("content"); //$NON-NLS-1$ + + if (popOver.isDetached()) { + content.setTop(titlePane); + popOver.getStyleClass().add(DETACHED_STYLE_CLASS); + content.getStyleClass().add(DETACHED_STYLE_CLASS); + } + + InvalidationListener updatePathListener = new InvalidationListener() { + + @Override + public void invalidated(Observable observable) { + updatePath(); + } + }; + + getPopupWindow().xProperty().addListener(updatePathListener); + getPopupWindow().yProperty().addListener(updatePathListener); + + popOver.arrowLocationProperty().addListener(updatePathListener); + + popOver.contentNodeProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Node oldContent, Node newContent) { + content.setCenter(newContent); + } + }); + + popOver.detachedProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Boolean oldDetached, Boolean newDetached) { + + updatePath(); + + if (newDetached) { + popOver.getStyleClass().add(DETACHED_STYLE_CLASS); + content.getStyleClass().add(DETACHED_STYLE_CLASS); + content.setTop(titlePane); + } else { + popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); + content.getStyleClass().remove(DETACHED_STYLE_CLASS); + content.setTop(null); + } + } + }); + + path = new Path(); + path.getStyleClass().add("border"); //$NON-NLS-1$ + path.setManaged(false); + + createPathElements(); + updatePath(); + + final EventHandler mousePressedHandler = new EventHandler() { + public void handle(MouseEvent evt) { + if (popOver.isDetachable() || popOver.isDetached()) { + tornOff = false; + + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); + + dragStartLocation = new Point2D(xOffset, yOffset); + } + }; + }; + + final EventHandler mouseReleasedHandler = new EventHandler() { + public void handle(MouseEvent evt) { + if (tornOff && !getSkinnable().isDetached()) { + tornOff = false; + getSkinnable().detach(); + } + }; + }; + + final EventHandler mouseDragHandler = new EventHandler() { + + public void handle(MouseEvent evt) { + if (popOver.isDetachable() || popOver.isDetached()) { + double deltaX = evt.getScreenX() - xOffset; + double deltaY = evt.getScreenY() - yOffset; + + Window window = getSkinnable().getScene().getWindow(); + + window.setX(window.getX() + deltaX); + window.setY(window.getY() + deltaY); + + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); + + if (dragStartLocation.distance(xOffset, yOffset) > 20) { + tornOff = true; + updatePath(); + } else if (tornOff) { + tornOff = false; + updatePath(); + } + } + }; + }; + + stackPane.setOnMousePressed(mousePressedHandler); + stackPane.setOnMouseDragged(mouseDragHandler); + stackPane.setOnMouseReleased(mouseReleasedHandler); + + stackPane.getChildren().add(path); + stackPane.getChildren().add(content); + } + + @Override + public Node getNode() { + return stackPane; + } + + @Override + public PopOver getSkinnable() { + return popOver; + } + + @Override + public void dispose() { + } + + private Node createCloseIcon() { + Group group = new Group(); + group.getStyleClass().add("graphics"); //$NON-NLS-1$ + + Circle circle = new Circle(); + circle.getStyleClass().add("circle"); //$NON-NLS-1$ + circle.setRadius(6); + circle.setCenterX(6); + circle.setCenterY(6); + group.getChildren().add(circle); + + Line line1 = new Line(); + line1.getStyleClass().add("line"); //$NON-NLS-1$ + line1.setStartX(4); + line1.setStartY(4); + line1.setEndX(8); + line1.setEndY(8); + group.getChildren().add(line1); + + Line line2 = new Line(); + line2.getStyleClass().add("line"); //$NON-NLS-1$ + line2.setStartX(8); + line2.setStartY(4); + line2.setEndX(4); + line2.setEndY(8); + group.getChildren().add(line2); + + return group; + } + + private MoveTo moveTo; + + private QuadCurveTo topCurveTo, rightCurveTo, bottomCurveTo, leftCurveTo; + + private HLineTo lineBTop, lineETop, lineHTop, lineKTop; + private LineTo lineCTop, lineDTop, lineFTop, lineGTop, lineITop, lineJTop; + + private VLineTo lineBRight, lineERight, lineHRight, lineKRight; + private LineTo lineCRight, lineDRight, lineFRight, lineGRight, lineIRight, + lineJRight; + + private HLineTo lineBBottom, lineEBottom, lineHBottom, lineKBottom; + private LineTo lineCBottom, lineDBottom, lineFBottom, lineGBottom, + lineIBottom, lineJBottom; + + private VLineTo lineBLeft, lineELeft, lineHLeft, lineKLeft; + private LineTo lineCLeft, lineDLeft, lineFLeft, lineGLeft, lineILeft, + lineJLeft; + + private void createPathElements() { + DoubleProperty centerYProperty = new SimpleDoubleProperty(); + DoubleProperty centerXProperty = new SimpleDoubleProperty(); + + DoubleProperty leftEdgeProperty = new SimpleDoubleProperty(); + DoubleProperty leftEdgePlusRadiusProperty = new SimpleDoubleProperty(); + + DoubleProperty topEdgeProperty = new SimpleDoubleProperty(); + DoubleProperty topEdgePlusRadiusProperty = new SimpleDoubleProperty(); + + DoubleProperty rightEdgeProperty = new SimpleDoubleProperty(); + DoubleProperty rightEdgeMinusRadiusProperty = new SimpleDoubleProperty(); + + DoubleProperty bottomEdgeProperty = new SimpleDoubleProperty(); + DoubleProperty bottomEdgeMinusRadiusProperty = new SimpleDoubleProperty(); + + DoubleProperty cornerProperty = getSkinnable().cornerRadiusProperty(); + + DoubleProperty arrowSizeProperty = getSkinnable().arrowSizeProperty(); + DoubleProperty arrowIndentProperty = getSkinnable() + .arrowIndentProperty(); + + centerYProperty.bind(Bindings.divide(stackPane.heightProperty(), 2)); + centerXProperty.bind(Bindings.divide(stackPane.widthProperty(), 2)); + + leftEdgePlusRadiusProperty.bind(Bindings.add(leftEdgeProperty, + getSkinnable().cornerRadiusProperty())); + + topEdgePlusRadiusProperty.bind(Bindings.add(topEdgeProperty, + getSkinnable().cornerRadiusProperty())); + + rightEdgeProperty.bind(stackPane.widthProperty()); + rightEdgeMinusRadiusProperty.bind(Bindings.subtract(rightEdgeProperty, + getSkinnable().cornerRadiusProperty())); + + bottomEdgeProperty.bind(stackPane.heightProperty()); + bottomEdgeMinusRadiusProperty.bind(Bindings.subtract( + bottomEdgeProperty, getSkinnable().cornerRadiusProperty())); + + // INIT + moveTo = new MoveTo(); + moveTo.xProperty().bind(leftEdgePlusRadiusProperty); + moveTo.yProperty().bind(topEdgeProperty); + + // + // TOP EDGE + // + lineBTop = new HLineTo(); + lineBTop.xProperty().bind( + Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty)); + + lineCTop = new LineTo(); + lineCTop.xProperty().bind( + Bindings.add(lineBTop.xProperty(), arrowSizeProperty)); + lineCTop.yProperty().bind( + Bindings.subtract(topEdgeProperty, arrowSizeProperty)); + + lineDTop = new LineTo(); + lineDTop.xProperty().bind( + Bindings.add(lineCTop.xProperty(), arrowSizeProperty)); + lineDTop.yProperty().bind(topEdgeProperty); + + lineETop = new HLineTo(); + lineETop.xProperty().bind( + Bindings.subtract(centerXProperty, arrowSizeProperty)); + + lineFTop = new LineTo(); + lineFTop.xProperty().bind(centerXProperty); + lineFTop.yProperty().bind( + Bindings.subtract(topEdgeProperty, arrowSizeProperty)); + + lineGTop = new LineTo(); + lineGTop.xProperty().bind( + Bindings.add(centerXProperty, arrowSizeProperty)); + lineGTop.yProperty().bind(topEdgeProperty); + + lineHTop = new HLineTo(); + lineHTop.xProperty().bind( + Bindings.subtract(Bindings.subtract( + rightEdgeMinusRadiusProperty, arrowIndentProperty), + Bindings.multiply(arrowSizeProperty, 2))); + + lineITop = new LineTo(); + lineITop.xProperty().bind( + Bindings.subtract(Bindings.subtract( + rightEdgeMinusRadiusProperty, arrowIndentProperty), + arrowSizeProperty)); + lineITop.yProperty().bind( + Bindings.subtract(topEdgeProperty, arrowSizeProperty)); + + lineJTop = new LineTo(); + lineJTop.xProperty().bind( + Bindings.subtract(rightEdgeMinusRadiusProperty, + arrowIndentProperty)); + lineJTop.yProperty().bind(topEdgeProperty); + + lineKTop = new HLineTo(); + lineKTop.xProperty().bind(rightEdgeMinusRadiusProperty); + + // + // RIGHT EDGE + // + rightCurveTo = new QuadCurveTo(); + rightCurveTo.xProperty().bind(rightEdgeProperty); + rightCurveTo.yProperty().bind( + Bindings.add(topEdgeProperty, cornerProperty)); + rightCurveTo.controlXProperty().bind(rightEdgeProperty); + rightCurveTo.controlYProperty().bind(topEdgeProperty); + + lineBRight = new VLineTo(); + lineBRight.yProperty().bind( + Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty)); + + lineCRight = new LineTo(); + lineCRight.xProperty().bind( + Bindings.add(rightEdgeProperty, arrowSizeProperty)); + lineCRight.yProperty().bind( + Bindings.add(lineBRight.yProperty(), arrowSizeProperty)); + + lineDRight = new LineTo(); + lineDRight.xProperty().bind(rightEdgeProperty); + lineDRight.yProperty().bind( + Bindings.add(lineCRight.yProperty(), arrowSizeProperty)); + + lineERight = new VLineTo(); + lineERight.yProperty().bind( + Bindings.subtract(centerYProperty, arrowSizeProperty)); + + lineFRight = new LineTo(); + lineFRight.xProperty().bind( + Bindings.add(rightEdgeProperty, arrowSizeProperty)); + lineFRight.yProperty().bind(centerYProperty); + + lineGRight = new LineTo(); + lineGRight.xProperty().bind(rightEdgeProperty); + lineGRight.yProperty().bind( + Bindings.add(centerYProperty, arrowSizeProperty)); + + lineHRight = new VLineTo(); + lineHRight.yProperty().bind( + Bindings.subtract(Bindings.subtract( + bottomEdgeMinusRadiusProperty, arrowIndentProperty), + Bindings.multiply(arrowSizeProperty, 2))); + + lineIRight = new LineTo(); + lineIRight.xProperty().bind( + Bindings.add(rightEdgeProperty, arrowSizeProperty)); + lineIRight.yProperty().bind( + Bindings.subtract(Bindings.subtract( + bottomEdgeMinusRadiusProperty, arrowIndentProperty), + arrowSizeProperty)); + + lineJRight = new LineTo(); + lineJRight.xProperty().bind(rightEdgeProperty); + lineJRight.yProperty().bind( + Bindings.subtract(bottomEdgeMinusRadiusProperty, + arrowIndentProperty)); + + lineKRight = new VLineTo(); + lineKRight.yProperty().bind(bottomEdgeMinusRadiusProperty); + + // + // BOTTOM EDGE + // + + bottomCurveTo = new QuadCurveTo(); + bottomCurveTo.xProperty().bind(rightEdgeMinusRadiusProperty); + bottomCurveTo.yProperty().bind(bottomEdgeProperty); + bottomCurveTo.controlXProperty().bind(rightEdgeProperty); + bottomCurveTo.controlYProperty().bind(bottomEdgeProperty); + + lineBBottom = new HLineTo(); + lineBBottom.xProperty().bind( + Bindings.subtract(rightEdgeMinusRadiusProperty, + arrowIndentProperty)); + + lineCBottom = new LineTo(); + lineCBottom.xProperty().bind( + Bindings.subtract(lineBBottom.xProperty(), arrowSizeProperty)); + lineCBottom.yProperty().bind( + Bindings.add(bottomEdgeProperty, arrowSizeProperty)); + + lineDBottom = new LineTo(); + lineDBottom.xProperty().bind( + Bindings.subtract(lineCBottom.xProperty(), arrowSizeProperty)); + lineDBottom.yProperty().bind(bottomEdgeProperty); + + lineEBottom = new HLineTo(); + lineEBottom.xProperty().bind( + Bindings.add(centerXProperty, arrowSizeProperty)); + + lineFBottom = new LineTo(); + lineFBottom.xProperty().bind(centerXProperty); + lineFBottom.yProperty().bind( + Bindings.add(bottomEdgeProperty, arrowSizeProperty)); + + lineGBottom = new LineTo(); + lineGBottom.xProperty().bind( + Bindings.subtract(centerXProperty, arrowSizeProperty)); + lineGBottom.yProperty().bind(bottomEdgeProperty); + + lineHBottom = new HLineTo(); + lineHBottom.xProperty().bind( + Bindings.add(Bindings.add(leftEdgePlusRadiusProperty, + arrowIndentProperty), Bindings.multiply( + arrowSizeProperty, 2))); + + lineIBottom = new LineTo(); + lineIBottom.xProperty().bind( + Bindings.add(Bindings.add(leftEdgePlusRadiusProperty, + arrowIndentProperty), arrowSizeProperty)); + lineIBottom.yProperty().bind( + Bindings.add(bottomEdgeProperty, arrowSizeProperty)); + + lineJBottom = new LineTo(); + lineJBottom.xProperty().bind( + Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty)); + lineJBottom.yProperty().bind(bottomEdgeProperty); + + lineKBottom = new HLineTo(); + lineKBottom.xProperty().bind(leftEdgePlusRadiusProperty); + + // + // LEFT EDGE + // + leftCurveTo = new QuadCurveTo(); + leftCurveTo.xProperty().bind(leftEdgeProperty); + leftCurveTo.yProperty().bind( + Bindings.subtract(bottomEdgeProperty, cornerProperty)); + leftCurveTo.controlXProperty().bind(leftEdgeProperty); + leftCurveTo.controlYProperty().bind(bottomEdgeProperty); + + lineBLeft = new VLineTo(); + lineBLeft.yProperty().bind( + Bindings.subtract(bottomEdgeMinusRadiusProperty, + arrowIndentProperty)); + + lineCLeft = new LineTo(); + lineCLeft.xProperty().bind( + Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); + lineCLeft.yProperty().bind( + Bindings.subtract(lineBLeft.yProperty(), arrowSizeProperty)); + + lineDLeft = new LineTo(); + lineDLeft.xProperty().bind(leftEdgeProperty); + lineDLeft.yProperty().bind( + Bindings.subtract(lineCLeft.yProperty(), arrowSizeProperty)); + + lineELeft = new VLineTo(); + lineELeft.yProperty().bind( + Bindings.add(centerYProperty, arrowSizeProperty)); + + lineFLeft = new LineTo(); + lineFLeft.xProperty().bind( + Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); + lineFLeft.yProperty().bind(centerYProperty); + + lineGLeft = new LineTo(); + lineGLeft.xProperty().bind(leftEdgeProperty); + lineGLeft.yProperty().bind( + Bindings.subtract(centerYProperty, arrowSizeProperty)); + + lineHLeft = new VLineTo(); + lineHLeft.yProperty().bind( + Bindings.add(Bindings.add(topEdgePlusRadiusProperty, + arrowIndentProperty), Bindings.multiply( + arrowSizeProperty, 2))); + + lineILeft = new LineTo(); + lineILeft.xProperty().bind( + Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); + lineILeft.yProperty().bind( + Bindings.add(Bindings.add(topEdgePlusRadiusProperty, + arrowIndentProperty), arrowSizeProperty)); + + lineJLeft = new LineTo(); + lineJLeft.xProperty().bind(leftEdgeProperty); + lineJLeft.yProperty().bind( + Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty)); + + lineKLeft = new VLineTo(); + lineKLeft.yProperty().bind(topEdgePlusRadiusProperty); + + topCurveTo = new QuadCurveTo(); + topCurveTo.xProperty().bind(leftEdgePlusRadiusProperty); + topCurveTo.yProperty().bind(topEdgeProperty); + topCurveTo.controlXProperty().bind(leftEdgeProperty); + topCurveTo.controlYProperty().bind(topEdgeProperty); + } + + private Window getPopupWindow() { + return getSkinnable().getScene().getWindow(); + } + + private boolean showArrow(ArrowLocation location) { + ArrowLocation arrowLocation = getSkinnable().getArrowLocation(); + return location.equals(arrowLocation) && !getSkinnable().isDetached() + && !tornOff; + } + + private void updatePath() { + List elements = new ArrayList<>(); + elements.add(moveTo); + + if (showArrow(TOP_LEFT)) { + elements.add(lineBTop); + elements.add(lineCTop); + elements.add(lineDTop); + } + if (showArrow(TOP_CENTER)) { + elements.add(lineETop); + elements.add(lineFTop); + elements.add(lineGTop); + } + if (showArrow(TOP_RIGHT)) { + elements.add(lineHTop); + elements.add(lineITop); + elements.add(lineJTop); + } + elements.add(lineKTop); + elements.add(rightCurveTo); + + if (showArrow(RIGHT_TOP)) { + elements.add(lineBRight); + elements.add(lineCRight); + elements.add(lineDRight); + } + if (showArrow(RIGHT_CENTER)) { + elements.add(lineERight); + elements.add(lineFRight); + elements.add(lineGRight); + } + if (showArrow(RIGHT_BOTTOM)) { + elements.add(lineHRight); + elements.add(lineIRight); + elements.add(lineJRight); + } + elements.add(lineKRight); + elements.add(bottomCurveTo); + + if (showArrow(BOTTOM_RIGHT)) { + elements.add(lineBBottom); + elements.add(lineCBottom); + elements.add(lineDBottom); + } + if (showArrow(BOTTOM_CENTER)) { + elements.add(lineEBottom); + elements.add(lineFBottom); + elements.add(lineGBottom); + } + if (showArrow(BOTTOM_LEFT)) { + elements.add(lineHBottom); + elements.add(lineIBottom); + elements.add(lineJBottom); + } + elements.add(lineKBottom); + elements.add(leftCurveTo); + + if (showArrow(LEFT_BOTTOM)) { + elements.add(lineBLeft); + elements.add(lineCLeft); + elements.add(lineDLeft); + } + if (showArrow(LEFT_CENTER)) { + elements.add(lineELeft); + elements.add(lineFLeft); + elements.add(lineGLeft); + } + if (showArrow(LEFT_TOP)) { + elements.add(lineHLeft); + elements.add(lineILeft); + elements.add(lineJLeft); + } + elements.add(lineKLeft); + elements.add(topCurveTo); + + path.getElements().setAll(elements); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/indicator/TxConfidenceIndicator.java b/desktop/src/main/java/bisq/desktop/components/indicator/TxConfidenceIndicator.java new file mode 100644 index 0000000000..39484521a9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/indicator/TxConfidenceIndicator.java @@ -0,0 +1,275 @@ +/* + * 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 . + */ + +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package bisq.desktop.components.indicator; + +import bisq.desktop.components.indicator.skin.StaticProgressIndicatorSkin; + +import javafx.scene.control.Control; +import javafx.scene.control.Skin; + +import javafx.css.PseudoClass; +import javafx.css.StyleableProperty; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.DoublePropertyBase; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; + +// TODO Copied form OpenJFX, check license issues and way how we integrated it +// We changed behaviour which was not exposed via APIs + +/** + * A circular control which is used for indicating progress, either + * infinite (aka indeterminate) or finite. Often used with the Task API for + * representing progress of background Tasks. + *

    + * ProgressIndicator sets focusTraversable to false. + *

    + *

    + *

    + * This first example creates a ProgressIndicator with an indeterminate value : + *

    
    + * import javafx.scene.control.ProgressIndicator;
    + * ProgressIndicator p1 = new ProgressIndicator();
    + * 
    + *

    + *

    + * This next example creates a ProgressIndicator which is 25% complete : + *

    
    + * import javafx.scene.control.ProgressIndicator;
    + * ProgressIndicator p2 = new ProgressIndicator();
    + * p2.setProgress(0.25F);
    + * 
    + *

    + * Implementation of ProgressIndicator According to JavaFX UI Control API Specification + * + * @since JavaFX 2.0 + */ + +@SuppressWarnings({"SameParameterValue", "WeakerAccess"}) +public class TxConfidenceIndicator extends Control { + + /** + * Value for progress indicating that the progress is indeterminate. + * + * @see #setProgress + */ + public static final double INDETERMINATE_PROGRESS = -1; + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + /** + * Initialize the style class to 'progress-indicator'. + *

    + * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "progress-indicator"; + /** + * Pseudoclass indicating this is a determinate (i.e., progress can be + * determined) progress indicator. + */ + private static final PseudoClass PSEUDO_CLASS_DETERMINATE = PseudoClass.getPseudoClass("determinate"); + /*************************************************************************** + * * + * Properties * + * * + **************************************************************************/ + /** + * Pseudoclass indicating this is an indeterminate (i.e., progress cannot + * be determined) progress indicator. + */ + private static final PseudoClass PSEUDO_CLASS_INDETERMINATE = PseudoClass.getPseudoClass("indeterminate"); + /** + * A flag indicating whether it is possible to determine the progress + * of the ProgressIndicator. Typically indeterminate progress bars are + * rendered with some form of animation indicating potentially "infinite" + * progress. + */ + private ReadOnlyBooleanWrapper indeterminate; + /** + * The actual progress of the ProgressIndicator. A negative value for + * progress indicates that the progress is indeterminate. A positive value + * between 0 and 1 indicates the percentage of progress where 0 is 0% and 1 + * is 100%. Any value greater than 1 is interpreted as 100%. + */ + private DoubleProperty progress; + + /** + * Creates a new indeterminate ProgressIndicator. + */ + public TxConfidenceIndicator() { + this(INDETERMINATE_PROGRESS); + } + + /** + * Creates a new ProgressIndicator with the given progress value. + */ + @SuppressWarnings("unchecked") + public TxConfidenceIndicator(double progress) { + // focusTraversable is styleable through css. Calling setFocusTraversable + // makes it look to css like the user set the value and css will not + // override. Initializing focusTraversable by calling applyStyle with null + // StyleOrigin ensures that css will be able to override the value. + ((StyleableProperty) focusTraversableProperty()).applyStyle(null, Boolean.FALSE); + setProgress(progress); + getStyleClass().setAll(DEFAULT_STYLE_CLASS); + + // need to initialize pseudo-class state + final int c = Double.compare(INDETERMINATE_PROGRESS, progress); + pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, c == 0); + pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, c != 0); + } + + public final boolean isIndeterminate() { + return indeterminate == null || indeterminate.get(); + } + + private void setIndeterminate(boolean value) { + indeterminatePropertyImpl().set(value); + } + + public final ReadOnlyBooleanProperty indeterminateProperty() { + return indeterminatePropertyImpl().getReadOnlyProperty(); + } + + private ReadOnlyBooleanWrapper indeterminatePropertyImpl() { + if (indeterminate == null) { + indeterminate = new ReadOnlyBooleanWrapper(true) { + @Override + protected void invalidated() { + final boolean active = get(); + pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, active); + pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, !active); + } + + + @Override + public Object getBean() { + return TxConfidenceIndicator.this; + } + + + @Override + public String getName() { + return "indeterminate"; + } + }; + } + return indeterminate; + } + + /** + * ************************************************************************ + * * + * Methods * + * * + * ************************************************************************ + */ + + public final double getProgress() { + return progress == null ? INDETERMINATE_PROGRESS : progress.get(); + } + + /** + * ************************************************************************ + * * + * Stylesheet Handling * + * * + * ************************************************************************ + */ + + public final void setProgress(double value) { + progressProperty().set(value); + } + + public final DoubleProperty progressProperty() { + if (progress == null) { + progress = new DoublePropertyBase(-1.0) { + @Override + protected void invalidated() { + setIndeterminate(getProgress() < 0.0); + } + + + @Override + public Object getBean() { + return TxConfidenceIndicator.this; + } + + + @Override + public String getName() { + return "progress"; + } + }; + } + return progress; + } + + /** + * {@inheritDoc} + */ + + @Override + protected Skin createDefaultSkin() { + return new StaticProgressIndicatorSkin(this); + } + + /** + * Most Controls return true for focusTraversable, so Control overrides + * this method to return true, but ProgressIndicator returns false for + * focusTraversable's initial value; hence the override of the override. + * This method is called from CSS code to get the correct initial value. + */ + @Deprecated + @SuppressWarnings("deprecation") + protected /*do not make final*/ Boolean impl_cssGetFocusTraversableInitialValue() { + return Boolean.FALSE; + } + + +} diff --git a/desktop/src/main/java/bisq/desktop/components/indicator/skin/StaticProgressIndicatorSkin.java b/desktop/src/main/java/bisq/desktop/components/indicator/skin/StaticProgressIndicatorSkin.java new file mode 100644 index 0000000000..343bb0a567 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/indicator/skin/StaticProgressIndicatorSkin.java @@ -0,0 +1,767 @@ +/* + * 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 . + */ + +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package bisq.desktop.components.indicator.skin; + +import bisq.desktop.components.indicator.TxConfidenceIndicator; + +import javafx.scene.Node; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Arc; +import javafx.scene.shape.ArcType; +import javafx.scene.shape.Circle; +import javafx.scene.transform.Scale; + +import javafx.css.CssMetaData; +import javafx.css.StyleOrigin; +import javafx.css.Styleable; +import javafx.css.StyleableBooleanProperty; +import javafx.css.StyleableIntegerProperty; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.css.converter.BooleanConverter; +import javafx.css.converter.PaintConverter; +import javafx.css.converter.SizeConverter; + +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; + +import javafx.collections.ObservableList; +import javafx.collections.FXCollections; + +import java.util.ArrayList; +import java.util.List; + +// TODO Copied form OpenJFX, check license issues and way how we integrated it +// We changed behaviour which was not exposed via APIs + +public class StaticProgressIndicatorSkin extends SkinBase { + + /** + * ************************************************************************ + * * + * UI SubComponents * + * * + * ************************************************************************ + */ + + private IndeterminateSpinner spinner; + /** + * The number of segments in the spinner. + */ + + private final IntegerProperty indeterminateSegmentCount = new StyleableIntegerProperty(8) { + + @Override + protected void invalidated() { + if (spinner != null) { + spinner.rebuild(); + } + } + + + @Override + public Object getBean() { + return StaticProgressIndicatorSkin.this; + } + + + @Override + public String getName() { + return "indeterminateSegmentCount"; + } + + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.INDETERMINATE_SEGMENT_COUNT; + } + }; + /** + * True if the progress indicator should rotate as well as animate opacity. + */ + + private final BooleanProperty spinEnabled = new StyleableBooleanProperty(false) { + @Override + protected void invalidated() { + if (spinner != null) { + spinner.setSpinEnabled(get()); + } + } + + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.SPIN_ENABLED; + } + + + @Override + public Object getBean() { + return StaticProgressIndicatorSkin.this; + } + + + @Override + public String getName() { + return "spinEnabled"; + } + }; + + private DeterminateIndicator determinateIndicator; + /** + * The colour of the progress segment. + */ + + private final ObjectProperty progressColor = new StyleableObjectProperty<>(null) { + + @Override + public void set(Paint newProgressColor) { + final Paint color = (newProgressColor instanceof Color) ? newProgressColor : null; + super.set(color); + } + + @Override + protected void invalidated() { + if (spinner != null) { + spinner.setFillOverride(get()); + } + if (determinateIndicator != null) { + determinateIndicator.setFillOverride(get()); + } + } + + + @Override + public Object getBean() { + return StaticProgressIndicatorSkin.this; + } + + + @Override + public String getName() { + return "progressColorProperty"; + } + + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.PROGRESS_COLOR; + } + }; + private boolean timeLineNulled = false; + + /** + * ************************************************************************ + * * + * Constructors * + * * + * ************************************************************************ + */ + @SuppressWarnings("deprecation") + public StaticProgressIndicatorSkin(TxConfidenceIndicator control) { + super(control); + + InvalidationListener indeterminateListener = valueModel -> initialize(); + control.indeterminateProperty().addListener(indeterminateListener); + + InvalidationListener visibilityListener = valueModel -> { + if (getSkinnable().isIndeterminate() && timeLineNulled && spinner == null) { + timeLineNulled = false; + spinner = new IndeterminateSpinner( + getSkinnable(), StaticProgressIndicatorSkin.this, + spinEnabled.get(), progressColor.get()); + getChildren().add(spinner); + } + + if (spinner != null) { + if (getSkinnable().getScene() != null) { + getChildren().remove(spinner); + spinner = null; + timeLineNulled = true; + } + } + }; + control.visibleProperty().addListener(visibilityListener); + control.parentProperty().addListener(visibilityListener); + + InvalidationListener sceneListener = valueModel -> { + if (spinner != null) { + if (getSkinnable().getScene() == null) { + getChildren().remove(spinner); + spinner = null; + timeLineNulled = true; + } + } else { + if (getSkinnable().getScene() != null && getSkinnable().isIndeterminate()) { + timeLineNulled = false; + spinner = new IndeterminateSpinner( + getSkinnable(), StaticProgressIndicatorSkin.this, + spinEnabled.get(), progressColor.get()); + getChildren().add(spinner); + getSkinnable().requestLayout(); + } + } + }; + control.sceneProperty().addListener(sceneListener); + + initialize(); + getSkinnable().requestLayout(); + } + + /** + * @return The CssMetaData associated with this class, which may include the + * CssMetaData of its super classes. + */ + @SuppressWarnings("SameReturnValue") + public static ObservableList> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } + + @SuppressWarnings("deprecation") + private void initialize() { + TxConfidenceIndicator control = getSkinnable(); + boolean isIndeterminate = control.isIndeterminate(); + if (isIndeterminate) { + // clean up determinateIndicator + determinateIndicator = null; + // create spinner + spinner = new IndeterminateSpinner(control, this, spinEnabled.get(), progressColor.get()); + getChildren().clear(); + getChildren().add(spinner); + } else { + // clean up after spinner + if (spinner != null) { + spinner = null; + } + // create determinateIndicator + determinateIndicator = + new StaticProgressIndicatorSkin.DeterminateIndicator(control, this, progressColor.get()); + getChildren().clear(); + getChildren().add(determinateIndicator); + } + } + + @Override + public void dispose() { + super.dispose(); + if (spinner != null) { + spinner = null; + } + } + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + if (spinner != null && getSkinnable().isIndeterminate()) { + spinner.layoutChildren(); + spinner.resizeRelocate(0, 0, w, h); + } else if (determinateIndicator != null) { + determinateIndicator.layoutChildren(); + determinateIndicator.resizeRelocate(0, 0, w, h); + } + } + + public Paint getProgressColor() { + return progressColor.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public ObservableList> getCssMetaData() { + return getClassCssMetaData(); + } + + // *********** Stylesheet Handling ***************************************** + + /** + * ************************************************************************ + * * + * DeterminateIndicator * + * * + * ************************************************************************ + */ + + @SuppressWarnings({"SameReturnValue", "UnusedParameters"}) + static class DeterminateIndicator extends Region { + //private double textGap = 2.0F; + + + private final TxConfidenceIndicator control; + //private Text text; + + private final StackPane indicator; + + private final StackPane progress; + + private final StackPane tick; + + private final Arc arcShape; + + private final Circle indicatorCircle; + // only update progress text on whole percentages + private int intProgress; + // only update pie arc to nearest degree + private int degProgress; + + DeterminateIndicator(TxConfidenceIndicator control, StaticProgressIndicatorSkin s, + Paint fillOverride) { + this.control = control; + + getStyleClass().add("determinate-indicator"); + + intProgress = (int) Math.round(control.getProgress() * 100.0); + degProgress = (int) (360 * control.getProgress()); + + InvalidationListener progressListener = valueModel -> updateProgress(); + control.progressProperty().addListener(progressListener); + + getChildren().clear(); + + // The circular background for the progress pie piece + indicator = new StackPane(); + indicator.setScaleShape(false); + indicator.setCenterShape(false); + indicator.getStyleClass().setAll("indicator"); + indicatorCircle = new Circle(); + indicator.setShape(indicatorCircle); + + // The shape for our progress pie piece + arcShape = new Arc(); + arcShape.setType(ArcType.ROUND); + arcShape.setStartAngle(90.0F); + + // Our progress pie piece + progress = new StackPane(); + progress.getStyleClass().setAll("progress"); + progress.setScaleShape(false); + progress.setCenterShape(false); + progress.setShape(arcShape); + progress.getChildren().clear(); + setFillOverride(fillOverride); + + // The check mark that's drawn at 100% + tick = new StackPane(); + tick.getStyleClass().setAll("tick"); + + getChildren().setAll(indicator, progress, /*text,*/ tick); + updateProgress(); + } + + private void setFillOverride(Paint fillOverride) { + if (fillOverride instanceof Color) { + Color c = (Color) fillOverride; + progress.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," + + "" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," + + "" + c.getOpacity() + ");"); + } else { + progress.setStyle(null); + } + } + + //@Override + public boolean isAutomaticallyMirrored() { + // This is used instead of setting NodeOrientation, + // allowing the Text node to inherit the current + // orientation. + return false; + } + + private void updateProgress() { + intProgress = (int) Math.round(control.getProgress() * 100.0); + // text.setText((control.getProgress() >= 1) ? (DONE) : ("" + intProgress + "%")); + + degProgress = (int) (360 * control.getProgress()); + arcShape.setLength(-degProgress); + indicator.setOpacity(control.getProgress() == 0 ? 0 : 1); + requestLayout(); + } + + @Override + protected void layoutChildren() { + // Position and size the circular background + //double doneTextHeight = doneText.getLayoutBounds().getHeight(); + final Insets controlInsets = control.getInsets(); + final double left = snapSizeX(controlInsets.getLeft()); + final double right = snapSizeX(controlInsets.getRight()); + final double top = snapSizeY(controlInsets.getTop()); + final double bottom = snapSizeY(controlInsets.getBottom()); + + /* + ** use the min of width, or height, keep it a circle + */ + final double areaW = control.getWidth() - left - right; + final double areaH = control.getHeight() - top - bottom /*- textGap - doneTextHeight*/; + final double radiusW = areaW / 2; + final double radiusH = areaH / 2; + final double radius = Math.round(Math.min(radiusW, radiusH)); // use round instead of floor + final double centerX = snapPositionX(left + radiusW); + final double centerY = snapPositionY(top + radius); + + // find radius that fits inside radius - insetsPadding + final Insets indicatorInsets = indicator.getInsets(); + final double iLeft = snapSizeX(indicatorInsets.getLeft()); + final double iRight = snapSizeX(indicatorInsets.getRight()); + final double iTop = snapSizeY(indicatorInsets.getTop()); + final double iBottom = snapSizeY(indicatorInsets.getBottom()); + final double progressRadius = snapSizeX(Math.min(Math.min(radius - iLeft, radius - iRight), + Math.min(radius - iTop, radius - iBottom))); + + indicatorCircle.setRadius(radius); + indicator.setLayoutX(centerX); + indicator.setLayoutY(centerY); + + arcShape.setRadiusX(progressRadius); + arcShape.setRadiusY(progressRadius); + progress.setLayoutX(centerX); + progress.setLayoutY(centerY); + + // find radius that fits inside progressRadius - progressInsets + final Insets progressInsets = progress.getInsets(); + final double pLeft = snapSizeX(progressInsets.getLeft()); + final double pRight = snapSizeX(progressInsets.getRight()); + final double pTop = snapSizeY(progressInsets.getTop()); + final double pBottom = snapSizeY(progressInsets.getBottom()); + final double indicatorRadius = snapSizeX(Math.min(Math.min(progressRadius - pLeft, + progressRadius - pRight), Math.min(progressRadius - pTop, progressRadius - pBottom))); + + // find size of spare box that fits inside indicator radius + double squareBoxHalfWidth = Math.ceil(Math.sqrt((indicatorRadius * indicatorRadius) / 2)); + // double squareBoxHalfWidth2 = indicatorRadius * (Math.sqrt(2) / 2); + + tick.setLayoutX(centerX - squareBoxHalfWidth); + tick.setLayoutY(centerY - squareBoxHalfWidth); + tick.resize(squareBoxHalfWidth + squareBoxHalfWidth, squareBoxHalfWidth + squareBoxHalfWidth); + tick.setVisible(control.getProgress() >= 1); + } + + @Override + protected double computePrefWidth(double height) { + final Insets controlInsets = control.getInsets(); + final double left = snapSizeX(controlInsets.getLeft()); + final double right = snapSizeX(controlInsets.getRight()); + final Insets indicatorInsets = indicator.getInsets(); + final double iLeft = snapSizeX(indicatorInsets.getLeft()); + final double iRight = snapSizeX(indicatorInsets.getRight()); + final double iTop = snapSizeY(indicatorInsets.getTop()); + final double iBottom = snapSizeY(indicatorInsets.getBottom()); + final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); + final Insets progressInsets = progress.getInsets(); + final double pLeft = snapSizeX(progressInsets.getLeft()); + final double pRight = snapSizeX(progressInsets.getRight()); + final double pTop = snapSizeY(progressInsets.getTop()); + final double pBottom = snapSizeY(progressInsets.getBottom()); + final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); + final Insets tickInsets = tick.getInsets(); + final double tLeft = snapSizeX(tickInsets.getLeft()); + final double tRight = snapSizeX(tickInsets.getRight()); + final double indicatorWidth = indicatorMax + progressMax + tLeft + tRight + progressMax + indicatorMax; + return left + indicatorWidth + /*Math.max(indicatorWidth, doneText.getLayoutBounds().getWidth()) + */right; + } + + @Override + protected double computePrefHeight(double width) { + final Insets controlInsets = control.getInsets(); + final double top = snapSizeY(controlInsets.getTop()); + final double bottom = snapSizeY(controlInsets.getBottom()); + final Insets indicatorInsets = indicator.getInsets(); + final double iLeft = snapSizeX(indicatorInsets.getLeft()); + final double iRight = snapSizeX(indicatorInsets.getRight()); + final double iTop = snapSizeY(indicatorInsets.getTop()); + final double iBottom = snapSizeY(indicatorInsets.getBottom()); + final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); + final Insets progressInsets = progress.getInsets(); + final double pLeft = snapSizeX(progressInsets.getLeft()); + final double pRight = snapSizeX(progressInsets.getRight()); + final double pTop = snapSizeY(progressInsets.getTop()); + final double pBottom = snapSizeY(progressInsets.getBottom()); + final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); + final Insets tickInsets = tick.getInsets(); + final double tTop = snapSizeY(tickInsets.getTop()); + final double tBottom = snapSizeY(tickInsets.getBottom()); + final double indicatorHeight = indicatorMax + progressMax + tTop + tBottom + progressMax + indicatorMax; + return top + indicatorHeight /*+ textGap + doneText.getLayoutBounds().getHeight()*/ + bottom; + } + + @Override + protected double computeMaxWidth(double height) { + return computePrefWidth(height); + } + + @Override + protected double computeMaxHeight(double width) { + return computePrefHeight(width); + } + } + + /** + * ************************************************************************ + * * + * IndeterminateSpinner * + * * + * ************************************************************************ + */ + + @SuppressWarnings({"ConstantConditions", "MismatchedQueryAndUpdateOfCollection"}) + static class IndeterminateSpinner extends Region { + private final TxConfidenceIndicator control; + private final StaticProgressIndicatorSkin skin; + + private final IndicatorPaths pathsG; + + private final List opacities = new ArrayList<>(); + + private Paint fillOverride = null; + + IndeterminateSpinner(TxConfidenceIndicator control, StaticProgressIndicatorSkin s, + boolean spinEnabled, Paint fillOverride) { + this.control = control; + this.skin = s; + this.fillOverride = fillOverride; + + setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); + getStyleClass().setAll("spinner"); + + pathsG = new IndicatorPaths(this); + getChildren().add(pathsG); + + rebuild(); + } + + void setFillOverride(Paint fillOverride) { + this.fillOverride = fillOverride; + rebuild(); + } + + void setSpinEnabled(boolean spinEnabled) { + } + + @Override + protected void layoutChildren() { + Insets controlInsets = control.getInsets(); + final double w = control.getWidth() - controlInsets.getLeft() - controlInsets.getRight(); + final double h = control.getHeight() - controlInsets.getTop() - controlInsets.getBottom(); + final double prefW = pathsG.prefWidth(-1); + final double prefH = pathsG.prefHeight(-1); + double scaleX = w / prefW; + double scale = scaleX; + if ((scaleX * prefH) > h) { + scale = h / prefH; + } + double indicatorW = prefW * scale - 3; + double indicatorH = prefH * scale - 3; + pathsG.resizeRelocate((w - indicatorW) / 2, (h - indicatorH) / 2, indicatorW, indicatorH); + } + + private void rebuild() { + // update indeterminate indicator + final int segments = skin.indeterminateSegmentCount.get(); + opacities.clear(); + pathsG.getChildren().clear(); + final double step = 0.8 / (segments - 1); + for (int i = 0; i < segments; i++) { + Region region = new Region(); + region.setScaleShape(false); + region.setCenterShape(false); + region.getStyleClass().addAll("segment", "segment" + i); + if (fillOverride instanceof Color) { + Color c = (Color) fillOverride; + region.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," + + "" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," + + "" + c.getOpacity() + ");"); + } else { + region.setStyle(null); + } + double opacity = Math.min(1, i * step); + opacities.add(opacity); + region.setOpacity(opacity); + pathsG.getChildren().add(region); + } + } + + @SuppressWarnings("deprecation") + private class IndicatorPaths extends Pane { + final IndeterminateSpinner piSkin; + + IndicatorPaths(IndeterminateSpinner pi) { + super(); + piSkin = pi; + } + + @Override + protected double computePrefWidth(double height) { + double w = 0; + for (Node child : getChildren()) { + if (child instanceof Region) { + Region region = (Region) child; + if (region.getShape() != null) { + w = Math.max(w, region.getShape().getLayoutBounds().getMaxX()); + } else { + w = Math.max(w, region.prefWidth(height)); + } + } + } + return w; + } + + @Override + protected double computePrefHeight(double width) { + double h = 0; + for (Node child : getChildren()) { + if (child instanceof Region) { + Region region = (Region) child; + if (region.getShape() != null) { + h = Math.max(h, region.getShape().getLayoutBounds().getMaxY()); + } else { + h = Math.max(h, region.prefHeight(width)); + } + } + } + return h; + } + + @Override + protected void layoutChildren() { + // calculate scale + double scale = getWidth() / computePrefWidth(-1); + getChildren().stream().filter(child -> child instanceof Region).forEach(child -> { + Region region = (Region) child; + if (region.getShape() != null) { + region.resize(region.getShape().getLayoutBounds().getMaxX(), + region.getShape().getLayoutBounds().getMaxY()); + region.getTransforms().setAll(new Scale(scale, scale, 0, 0)); + } else { + region.autosize(); + } + }); + } + } + } + + /** + * Super-lazy instantiation pattern from Bill Pugh. + */ + @SuppressWarnings({"deprecation", "unchecked", "ConstantConditions"}) + private static class StyleableProperties { + static final ObservableList> STYLEABLES; + + private static final CssMetaData PROGRESS_COLOR = + new CssMetaData<>( + "-fx-progress-color", PaintConverter.getInstance(), null) { + + @Override + public boolean isSettable(TxConfidenceIndicator n) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); + return skin.progressColor == null || !skin.progressColor.isBound(); + } + + + @Override + public StyleableProperty getStyleableProperty(TxConfidenceIndicator n) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); + return (StyleableProperty) skin.progressColor; + } + }; + + + private static final CssMetaData INDETERMINATE_SEGMENT_COUNT = + new CssMetaData<>( + "-fx-indeterminate-segment-count", SizeConverter.getInstance(), 8) { + + @Override + public void set(TxConfidenceIndicator node, Number value, StyleOrigin origin) { + super.set(node, value.intValue(), origin); + } + + @Override + public boolean isSettable(TxConfidenceIndicator n) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); + return skin.indeterminateSegmentCount == null || !skin.indeterminateSegmentCount.isBound(); + } + + + @Override + public StyleableProperty getStyleableProperty(TxConfidenceIndicator n) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); + return (StyleableProperty) skin.indeterminateSegmentCount; + } + }; + + private static final CssMetaData SPIN_ENABLED = new + CssMetaData<>("-fx-spin-enabled", + BooleanConverter.getInstance(), + Boolean.FALSE) { + + @Override + public boolean isSettable(TxConfidenceIndicator node) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin(); + return skin.spinEnabled == null || !skin.spinEnabled.isBound(); + } + + @Override + public StyleableProperty getStyleableProperty(TxConfidenceIndicator node) { + final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin(); + return (StyleableProperty) skin.spinEnabled; + } + }; + + static { + final ObservableList> styleables = + FXCollections.observableArrayList(SkinBase.getClassCssMetaData()); + styleables.add(PROGRESS_COLOR); + styleables.add(INDETERMINATE_SEGMENT_COUNT); + styleables.add(SPIN_ENABLED); + STYLEABLES = FXCollections.unmodifiableObservableList(styleables); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java new file mode 100644 index 0000000000..f384b5a1c6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java @@ -0,0 +1,132 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AdvancedCashValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.payment.AdvancedCashAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AdvancedCashAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; + +@Deprecated +public class AdvancedCashForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(AdvancedCashForm.class); + + private final AdvancedCashAccount advancedCashAccount; + private final AdvancedCashValidator advancedCashValidator; + private InputTextField accountNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.wallet"), + ((AdvancedCashAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + public AdvancedCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AdvancedCashValidator advancedCashValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.advancedCashAccount = (AdvancedCashAccount) paymentAccount; + this.advancedCashValidator = advancedCashValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.wallet")); + accountNrInputTextField.setValidator(advancedCashValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + advancedCashAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0); + + FlowPane flowPane = labelFlowPaneTuple2.second; + + if (isEditable) + flowPane.setId("flow-pane-checkboxes-bg"); + else + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + + CurrencyUtil.getAllAdvancedCashCurrencies().stream().forEach(e -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, advancedCashAccount)); + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + String accountNr = accountNrInputTextField.getText(); + accountNr = StringUtils.abbreviate(accountNr, 9); + String method = Res.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(": ").concat(accountNr)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addCompactTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + advancedCashAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(advancedCashAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.wallet"), + advancedCashAccount.getAccountNr()); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && advancedCashValidator.validate(advancedCashAccount.getAccountNr()).isValid + && advancedCashAccount.getTradeCurrencies().size() > 0); + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AliPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AliPayForm.java new file mode 100644 index 0000000000..f29712a0f0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AliPayForm.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.util.validation.AliPayValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.AliPayAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AliPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class AliPayForm extends GeneralAccountNumberForm { + + private final AliPayAccount aliPayAccount; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((AliPayAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + public AliPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AliPayValidator aliPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.aliPayAccount = (AliPayAccount) paymentAccount; + } + + @Override + void setAccountNumber(String newValue) { + aliPayAccount.setAccountNr(newValue); + } + + @Override + String getAccountNr() { + return aliPayAccount.getAccountNr(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java new file mode 100644 index 0000000000..9694510fde --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java @@ -0,0 +1,185 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AmazonGiftCardAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.*; + +@Slf4j +public class AmazonGiftCardForm extends PaymentMethodForm { + private InputTextField accountNrInputTextField; + ComboBox countryCombo; + private final AmazonGiftCardAccount amazonGiftCardAccount; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + AmazonGiftCardAccountPayload amazonGiftCardAccountPayload = (AmazonGiftCardAccountPayload) paymentAccountPayload; + + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.amazon.site"), + countryToAmazonSite(amazonGiftCardAccountPayload.getCountryCode()), + Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile"), + amazonGiftCardAccountPayload.getEmailOrMobileNr()); + String countryText = CountryUtil.getNameAndCode(amazonGiftCardAccountPayload.getCountryCode()); + if (countryText.isEmpty()) { + countryText = Res.get("payment.ask"); + } + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, + Res.get("shared.country"), + countryText); + return gridRow; + } + + public AmazonGiftCardForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + + this.amazonGiftCardAccount = (AmazonGiftCardAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); + accountNrInputTextField.setValidator(inputValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + amazonGiftCardAccount.setEmailOrMobileNr(newValue); + updateFromInputs(); + }); + + countryCombo = addComboBox(gridPane, ++gridRow, Res.get("shared.country")); + countryCombo.setPromptText(Res.get("payment.select.country")); + countryCombo.setItems(FXCollections.observableArrayList(CountryUtil.getAllAmazonGiftCardCountries())); + TextField ccyField = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), "").second; + countryCombo.setConverter(new StringConverter<>() { + @Override + public String toString(Country country) { + return country.name + " (" + country.code + ")"; + } + @Override + public Country fromString(String s) { + return null; + } + }); + countryCombo.setOnAction(e -> { + Country countryCode = countryCombo.getValue(); + amazonGiftCardAccount.setCountry(countryCode); + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode.code); + paymentAccount.setSingleTradeCurrency(currency); + ccyField.setText(currency.getNameAndCode()); + updateFromInputs(); + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + addFormForAccountNumberDisplayAccount(paymentAccount.getAccountName(), paymentAccount.getPaymentMethod(), + amazonGiftCardAccount.getEmailOrMobileNr(), + paymentAccount.getSingleTradeCurrency()); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(amazonGiftCardAccount.getEmailOrMobileNr()).isValid + && paymentAccount.getTradeCurrencies().size() > 0); + } + + private void addFormForAccountNumberDisplayAccount(String accountName, + PaymentMethod paymentMethod, + String accountNr, + TradeCurrency singleTradeCurrency) { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), accountName, + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentMethod.getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.email.mobile"), accountNr).second; + field.setMouseTransparent(false); + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + amazonGiftCardAccount.getCountry() != null ? amazonGiftCardAccount.getCountry().name : ""); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + + addLimitations(true); + } + + private static String countryToAmazonSite(String countryCode) { + HashMap mapCountryToSite = new HashMap<>() {{ + put("AU", "https://www.amazon.au"); + put("CA", "https://www.amazon.ca"); + put("FR", "https://www.amazon.fr"); + put("DE", "https://www.amazon.de"); + put("IT", "https://www.amazon.it"); + put("NL", "https://www.amazon.nl"); + put("ES", "https://www.amazon.es"); + put("UK", "https://www.amazon.co.uk"); + put("IN", "https://www.amazon.in"); + put("JP", "https://www.amazon.co.jp"); + put("SA", "https://www.amazon.sa"); + put("SE", "https://www.amazon.se"); + put("SG", "https://www.amazon.sg"); + put("TR", "https://www.amazon.tr"); + put("US", "https://www.amazon.com"); + put("", Res.get("payment.ask")); + }}; + return mapCountryToSite.get(countryCode); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java new file mode 100644 index 0000000000..2933099ee1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java @@ -0,0 +1,243 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.AutocompleteComboBox; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.validation.AltCoinAddressValidator; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.UserThread; +import bisq.common.util.Tuple3; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.util.StringConverter; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addLabelCheckBox; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; + +public class AssetsForm extends PaymentMethodForm { + public static final String INSTANT_TRADE_NEWS = "instantTradeNews0.9.5"; + private final AssetAccount assetAccount; + private final AltCoinAddressValidator altCoinAddressValidator; + private final AssetService assetService; + private final FilterManager filterManager; + + private InputTextField addressInputTextField; + private CheckBox tradeInstantCheckBox; + private boolean tradeInstant; + + public static int addFormForBuyer(GridPane gridPane, + int gridRow, + PaymentAccountPayload paymentAccountPayload, + String labelTitle) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, labelTitle, + ((AssetsAccountPayload) paymentAccountPayload).getAddress()); + return gridRow; + } + + public AssetsForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + AltCoinAddressValidator altCoinAddressValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter, + AssetService assetService, + FilterManager filterManager) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.assetAccount = (AssetAccount) paymentAccount; + this.altCoinAddressValidator = altCoinAddressValidator; + this.assetService = assetService; + this.filterManager = filterManager; + + tradeInstant = paymentAccount instanceof InstantCryptoCurrencyAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + addTradeCurrencyComboBox(); + currencyComboBox.setPrefWidth(250); + + tradeInstantCheckBox = addLabelCheckBox(gridPane, ++gridRow, + Res.get("payment.altcoin.tradeInstantCheckbox"), 10); + tradeInstantCheckBox.setSelected(tradeInstant); + tradeInstantCheckBox.setOnAction(e -> { + tradeInstant = tradeInstantCheckBox.isSelected(); + if (tradeInstant) + new Popup().information(Res.get("payment.altcoin.tradeInstant.popup")).show(); + paymentLimitationsTextField.setText(getLimitationsText()); + }); + + gridPane.getChildren().remove(tradeInstantCheckBox); + tradeInstantCheckBox.setPadding(new Insets(0, 40, 0, 0)); + + gridPane.getChildren().add(tradeInstantCheckBox); + + addressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.altcoin.address")); + addressInputTextField.setValidator(altCoinAddressValidator); + + addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + if (newValue.startsWith("monero:")) { + UserThread.execute(() -> { + String addressWithoutPrefix = newValue.replace("monero:", ""); + addressInputTextField.setText(addressWithoutPrefix); + }); + return; + } + assetAccount.setAddress(newValue); + updateFromInputs(); + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + public PaymentAccount getPaymentAccount() { + if (tradeInstant) { + InstantCryptoCurrencyAccount instantCryptoCurrencyAccount = new InstantCryptoCurrencyAccount(); + instantCryptoCurrencyAccount.init(); + instantCryptoCurrencyAccount.setAccountName(paymentAccount.getAccountName()); + instantCryptoCurrencyAccount.setSaltAsHex(paymentAccount.getSaltAsHex()); + instantCryptoCurrencyAccount.setSalt(paymentAccount.getSalt()); + instantCryptoCurrencyAccount.setSingleTradeCurrency(paymentAccount.getSingleTradeCurrency()); + instantCryptoCurrencyAccount.setSelectedTradeCurrency(paymentAccount.getSelectedTradeCurrency()); + instantCryptoCurrencyAccount.setAddress(assetAccount.getAddress()); + return instantCryptoCurrencyAccount; + } else { + return paymentAccount; + } + } + + @Override + public void updateFromInputs() { + if (addressInputTextField != null && assetAccount.getSingleTradeCurrency() != null) + addressInputTextField.setPromptText(Res.get("payment.altcoin.address.dyn", + assetAccount.getSingleTradeCurrency().getName())); + super.updateFromInputs(); + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : ""; + if (currency != null) { + String address = addressInputTextField.getText(); + address = StringUtils.abbreviate(address, 9); + accountNameTextField.setText(currency.concat(": ").concat(address)); + } + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + assetAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(assetAccount.getPaymentMethod().getId())); + Tuple3 tuple2 = addCompactTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.altcoin.address"), assetAccount.getAddress()); + TextField field = tuple2.second; + field.setMouseTransparent(false); + final TradeCurrency singleTradeCurrency = assetAccount.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin"), + nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + TradeCurrency selectedTradeCurrency = assetAccount.getSelectedTradeCurrency(); + if (selectedTradeCurrency != null) { + altCoinAddressValidator.setCurrencyCode(selectedTradeCurrency.getCode()); + allInputsValid.set(isAccountNameValid() + && altCoinAddressValidator.validate(assetAccount.getAddress()).isValid + && assetAccount.getSingleTradeCurrency() != null); + } + } + + @Override + protected void addTradeCurrencyComboBox() { + currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + currencyComboBox.setPromptText(Res.get("payment.select.altcoin")); + currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.altcoin"), currencyComboBox)); + + currencyComboBox.getEditor().focusedProperty().addListener(observable -> + currencyComboBox.setPromptText("")); + + ((AutocompleteComboBox) currencyComboBox).setAutocompleteItems( + CurrencyUtil.getActiveSortedCryptoCurrencies(assetService, filterManager)); + currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); + + currencyComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; + } + + @Override + public TradeCurrency fromString(String s) { + return currencyComboBox.getItems().stream(). + filter(item -> item.getNameAndCode().equals(s)). + findAny().orElse(null); + } + }); + + ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> { + addressInputTextField.resetValidation(); + addressInputTextField.validate(); + paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + }); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java new file mode 100644 index 0000000000..a1d6337ea8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -0,0 +1,122 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AustraliaPayidValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class AustraliaPayidForm extends PaymentMethodForm { + private final AustraliaPayid australiaPayid; + private final AustraliaPayidValidator australiaPayidValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + ((AustraliaPayidPayload) paymentAccountPayload).getPayid()); + return gridRow; + } + + public AustraliaPayidForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + AustraliaPayidValidator australiaPayidValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.australiaPayid = (AustraliaPayid) paymentAccount; + this.australiaPayidValidator = australiaPayidValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + australiaPayid.setBankAccountName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid")); + mobileNrInputTextField.setValidator(australiaPayidValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + australiaPayid.setPayid(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + australiaPayid.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(australiaPayid.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + australiaPayid.getPayid()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + australiaPayid.getBankAccountName()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && australiaPayidValidator.validate(australiaPayid.getPayid()).isValid + && inputValidator.validate(australiaPayid.getBankAccountName()).isValid + && australiaPayid.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java new file mode 100644 index 0000000000..15508227d3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java @@ -0,0 +1,444 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple4; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import static bisq.desktop.util.FormBuilder.*; + +abstract class BankForm extends GeneralBankForm { + + static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + BankAccountPayload data = (BankAccountPayload) paymentAccountPayload; + String countryCode = ((BankAccountPayload) paymentAccountPayload).getCountryCode(); + + int colIndex = 0; + + if (data.getHolderTaxId() != null) { + final String title = Res.get("payment.account.owner") + " / " + BankUtil.getHolderIdLabelShort(countryCode); + final String value = data.getHolderName() + " / " + data.getHolderTaxId(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value); + } else { + final String title = Res.get("payment.account.owner"); + final String value = data.getHolderName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value); + } + + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.bank.country"), + CountryUtil.getNameAndCode(countryCode)); + + // We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally + int nrRows = 0; + if (BankUtil.isBankNameRequired(countryCode)) + nrRows++; + if (BankUtil.isBankIdRequired(countryCode)) + nrRows++; + if (BankUtil.isBranchIdRequired(countryCode)) + nrRows++; + if (BankUtil.isAccountNrRequired(countryCode)) + nrRows++; + if (BankUtil.isAccountTypeRequired(countryCode)) + nrRows++; + if (BankUtil.isNationalAccountIdRequired(countryCode)) + nrRows++; + + String bankNameLabel = BankUtil.getBankNameLabel(countryCode); + String bankIdLabel = BankUtil.getBankIdLabel(countryCode); + String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); + String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode); + String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); + String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode); + + + accountNrAccountTypeCombined = false; + nationalAccountIdAccountNrCombined = false; + bankNameBankIdCombined = false; + bankIdBranchIdCombined = false; + bankNameBranchIdCombined = false; + branchIdAccountNrCombined = false; + + prepareFormLayoutFlags(countryCode, nrRows); + + if (bankNameBankIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankNameLabel + " / " + + bankIdLabel + ":", + data.getBankName() + " / " + data.getBankId(), true); + } + if (bankNameBranchIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankNameLabel + " / " + + branchIdLabel + ":", + data.getBankName() + " / " + data.getBranchId(), true); + } + + if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName()); + + if (!bankNameBankIdCombined && !bankNameBranchIdCombined && + !branchIdAccountNrCombined && bankIdBranchIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankIdLabel + " / " + + branchIdLabel + ":", + data.getBankId() + " / " + data.getBranchId()); + } + + if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId()); + + if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + branchIdLabel + " / " + + accountNrLabel + ":", + data.getBranchId() + " / " + data.getAccountNr()); + } + + if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined && + BankUtil.isBranchIdRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId()); + + if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + accountNrLabel + " / " + accountTypeLabel, + data.getAccountNr() + " / " + data.getAccountType()); + } + + if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined && + BankUtil.isAccountNrRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr()); + + if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType()); + + if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + nationalAccountIdLabel + " / " + + accountNrLabel, data.getNationalAccountId() + + " / " + data.getAccountNr()); + + return gridRow; + } + + private final BankAccountPayload bankAccountPayload; + private InputTextField holderNameInputTextField; + private ComboBox accountTypeComboBox; + private Country selectedCountry; + + BankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.bankAccountPayload = (BankAccountPayload) paymentAccount.paymentAccountPayload; + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + String countryCode = bankAccountPayload.getCountryCode(); + + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addAcceptedBanksForDisplayAccount(); + addHolderNameAndIdForDisplayAccount(); + + if (BankUtil.isBankNameRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), + bankAccountPayload.getBankName()).second.setMouseTransparent(false); + + if (BankUtil.isBankIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode), + bankAccountPayload.getBankId()).second.setMouseTransparent(false); + + if (BankUtil.isBranchIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode), + bankAccountPayload.getBranchId()).second.setMouseTransparent(false); + + if (BankUtil.isNationalAccountIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode), + bankAccountPayload.getNationalAccountId()).second.setMouseTransparent(false); + + if (BankUtil.isAccountNrRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode), + bankAccountPayload.getAccountNr()).second.setMouseTransparent(false); + + if (BankUtil.isAccountTypeRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode), + bankAccountPayload.getAccountType()).second.setMouseTransparent(false); + + addLimitations(true); + } + + @Override + public void addFormForAddAccount() { + accountNrInputTextFieldEdited = false; + gridRowFrom = gridRow + 1; + + Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); + currencyComboBox = tuple.first; + gridRow = tuple.second; + + addAcceptedBanksForAddAccount(); + + addHolderNameAndId(); + + nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel("")); + + nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setNationalAccountId(newValue); + updateFromInputs(); + + }); + + bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); + + bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setBankName(newValue.trim()); + updateFromInputs(); + + }); + + bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel("")); + bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setBankId(newValue.trim()); + updateFromInputs(); + + }); + + branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel("")); + branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setBranchId(newValue.trim()); + updateFromInputs(); + + }); + + accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel("")); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setAccountNr(newValue.trim()); + updateFromInputs(); + + }); + + accountTypeComboBox = addComboBox(gridPane, ++gridRow, ""); + accountTypeComboBox.setPromptText(Res.get("payment.select.account")); + accountTypeComboBox.setOnAction(e -> { + if (BankUtil.isAccountTypeRequired(bankAccountPayload.getCountryCode())) { + bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + } + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + updateFromInputs(); + } + + private void onCountrySelected(Country country) { + selectedCountry = country; + if (country != null) { + getCountryBasedPaymentAccount().setCountry(country); + String countryCode = country.code; + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); + paymentAccount.setSingleTradeCurrency(currency); + currencyComboBox.setDisable(false); + currencyComboBox.getSelectionModel().select(currency); + + bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode)); + branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode)); + nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode)); + accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode)); + accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode)); + + bankNameInputTextField.setText(""); + bankIdInputTextField.setText(""); + branchIdInputTextField.setText(""); + nationalAccountIdInputTextField.setText(""); + accountNrInputTextField.setText(""); + accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) accountNrInputTextFieldEdited = true; + }); + accountTypeComboBox.getSelectionModel().clearSelection(); + accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode))); + + validateInput(countryCode); + + holderNameInputTextField.resetValidation(); + holderNameInputTextField.validate(); + bankNameInputTextField.resetValidation(); + bankNameInputTextField.validate(); + bankIdInputTextField.resetValidation(); + bankIdInputTextField.validate(); + branchIdInputTextField.resetValidation(); + branchIdInputTextField.validate(); + accountNrInputTextField.resetValidation(); + accountNrInputTextField.validate(); + nationalAccountIdInputTextField.resetValidation(); + nationalAccountIdInputTextField.validate(); + + boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode); + if (requiresHolderId) { + holderNameInputTextField.minWidthProperty().unbind(); + holderNameInputTextField.setMinWidth(250); + } else { + holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); + } + + updateHolderIDInput(countryCode, requiresHolderId); + + boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode); + nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired); + nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired); + + boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode); + bankNameInputTextField.setVisible(bankNameRequired); + bankNameInputTextField.setManaged(bankNameRequired); + + boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode); + bankIdInputTextField.setVisible(bankIdRequired); + bankIdInputTextField.setManaged(bankIdRequired); + + boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode); + branchIdInputTextField.setVisible(branchIdRequired); + branchIdInputTextField.setManaged(branchIdRequired); + + boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode); + accountNrInputTextField.setVisible(accountNrRequired); + accountNrInputTextField.setManaged(accountNrRequired); + + boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode); + accountTypeComboBox.setVisible(accountTypeRequired); + accountTypeComboBox.setManaged(accountTypeRequired); + + updateFromInputs(); + + onCountryChanged(); + } + } + + private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { + FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); + applyTradeCurrency(tradeCurrency, defaultCurrency); + } + + private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { + return (CountryBasedPaymentAccount) this.paymentAccount; + } + + protected void onCountryChanged() { + } + + private void addHolderNameAndId() { + Tuple2 tuple = addInputTextFieldInputTextField(gridPane, + ++gridRow, Res.get("payment.account.owner"), BankUtil.getHolderIdLabel("")); + holderNameInputTextField = tuple.first; + holderNameInputTextField.setMinWidth(250); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setHolderName(newValue.trim()); + updateFromInputs(); + }); + holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); + holderNameInputTextField.setValidator(inputValidator); + + useHolderID = true; + + holderIdInputTextField = tuple.second; + holderIdInputTextField.setVisible(false); + holderIdInputTextField.setManaged(false); + holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setHolderTaxId(newValue); + updateFromInputs(); + }); + } + + @Override + protected void autoFillNameTextField() { + autoFillAccountTextFields(bankAccountPayload); + } + + @Override + public void updateAllInputsValid() { + boolean result = isAccountNameValid() + && paymentAccount.getSingleTradeCurrency() != null + && getCountryBasedPaymentAccount().getCountry() != null + && holderNameInputTextField.getValidator().validate(bankAccountPayload.getHolderName()).isValid; + + String countryCode = bankAccountPayload.getCountryCode(); + result = getValidationResult(result, countryCode, + bankAccountPayload.getBankName(), + bankAccountPayload.getBankId(), + bankAccountPayload.getBranchId(), + bankAccountPayload.getAccountNr(), + bankAccountPayload.getAccountType(), + bankAccountPayload.getHolderTaxId(), + bankAccountPayload.getNationalAccountId()); + allInputsValid.set(result); + } + + private void addHolderNameAndIdForDisplayAccount() { + String countryCode = bankAccountPayload.getCountryCode(); + if (BankUtil.isHolderIdRequired(countryCode)) { + Tuple4 tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(countryCode)); + TextField holderNameTextField = tuple.second; + holderNameTextField.setText(bankAccountPayload.getHolderName()); + holderNameTextField.setMinWidth(250); + tuple.fourth.setText(bankAccountPayload.getHolderTaxId()); + } else { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), bankAccountPayload.getHolderName()); + } + } + + protected void addAcceptedBanksForAddAccount() { + } + + public void addAcceptedBanksForDisplayAccount() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java new file mode 100644 index 0000000000..37a8be1edc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java @@ -0,0 +1,150 @@ +/* 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.CashByMailAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.CashByMailAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import com.jfoenix.controls.JFXTextArea; + +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import static bisq.desktop.util.FormBuilder.*; + +public class CashByMailForm extends PaymentMethodForm { + private final CashByMailAccount cashByMailAccount; + private TextArea postalAddressTextArea; + private InputTextField contactField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + CashByMailAccountPayload cbm = (CashByMailAccountPayload) paymentAccountPayload; + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, + Res.get("payment.account.owner"), + cbm.getHolderName(), + Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + + TextArea textAddress = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; + textAddress.setMinHeight(70); + textAddress.setEditable(false); + textAddress.setText(cbm.getPostalAddress()); + + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + textExtraInfo.setMinHeight(70); + textExtraInfo.setEditable(false); + textExtraInfo.setText(cbm.getExtraInfo()); + return gridRow; + } + + public CashByMailForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.cashByMailAccount = (CashByMailAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedFiatCurrencies())); + + contactField = addInputTextField(gridPane, ++gridRow, + Res.get("payment.cashByMail.contact")); + contactField.setPromptText(Res.get("payment.cashByMail.contact.prompt")); + contactField.setValidator(inputValidator); + contactField.textProperty().addListener((ov, oldValue, newValue) -> { + cashByMailAccount.setContact(newValue); + updateFromInputs(); + }); + + postalAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.postal.address"), "").second; + postalAddressTextArea.setMinHeight(70); + postalAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + cashByMailAccount.setPostalAddress(newValue); + updateFromInputs(); + }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.cashByMail.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + cashByMailAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(contactField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + cashByMailAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(cashByMailAccount.getPaymentMethod().getId())); + + TradeCurrency tradeCurrency = paymentAccount.getSingleTradeCurrency(); + String nameAndCode = tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact"), + cashByMailAccount.getContact()); + TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; + textArea.setText(cashByMailAccount.getPostalAddress()); + textArea.setMinHeight(70); + textArea.setEditable(false); + + TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textAreaExtra.setText(cashByMailAccount.getExtraInfo()); + textAreaExtra.setMinHeight(70); + textAreaExtra.setEditable(false); + + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && !postalAddressTextArea.getText().isEmpty() + && inputValidator.validate(cashByMailAccount.getContact()).isValid + && paymentAccount.getSingleTradeCurrency() != null); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java new file mode 100644 index 0000000000..c48f20d77a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java @@ -0,0 +1,487 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.CashDepositAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple4; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import static bisq.desktop.util.FormBuilder.*; + +public class CashDepositForm extends GeneralBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + CashDepositAccountPayload data = (CashDepositAccountPayload) paymentAccountPayload; + String countryCode = data.getCountryCode(); + String requirements = data.getRequirements(); + boolean showRequirements = requirements != null && !requirements.isEmpty(); + + int colIndex = 0; + + if (data.getHolderTaxId() != null) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + Res.get("payment.account.name.emailAndHolderId", BankUtil.getHolderIdLabel(countryCode)), + data.getHolderName() + " / " + data.getHolderEmail() + " / " + data.getHolderTaxId()); + else + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + Res.get("payment.account.name.email"), + data.getHolderName() + " / " + data.getHolderEmail()); + + if (!showRequirements) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.getWithCol("payment.bank.country"), + CountryUtil.getNameAndCode(countryCode)); + else + requirements += "\n" + Res.get("payment.bank.country") + " " + CountryUtil.getNameAndCode(countryCode); + + // We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally + int nrRows = 0; + if (BankUtil.isBankNameRequired(countryCode)) + nrRows++; + if (BankUtil.isBankIdRequired(countryCode)) + nrRows++; + if (BankUtil.isBranchIdRequired(countryCode)) + nrRows++; + if (BankUtil.isAccountNrRequired(countryCode)) + nrRows++; + if (BankUtil.isAccountTypeRequired(countryCode)) + nrRows++; + if (BankUtil.isNationalAccountIdRequired(countryCode)) + nrRows++; + + String bankNameLabel = BankUtil.getBankNameLabel(countryCode); + String bankIdLabel = BankUtil.getBankIdLabel(countryCode); + String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); + String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode); + String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); + String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode); + + accountNrAccountTypeCombined = false; + nationalAccountIdAccountNrCombined = false; + bankNameBankIdCombined = false; + bankIdBranchIdCombined = false; + bankNameBranchIdCombined = false; + branchIdAccountNrCombined = false; + + prepareFormLayoutFlags(countryCode, nrRows); + + if (bankNameBankIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankNameLabel + " / " + + bankIdLabel, + data.getBankName() + " / " + data.getBankId()); + } + if (bankNameBranchIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankNameLabel + " / " + + branchIdLabel, + data.getBankName() + " / " + data.getBranchId()); + } + + if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName()); + + if (!bankNameBankIdCombined && !bankNameBranchIdCombined && !branchIdAccountNrCombined && bankIdBranchIdCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankIdLabel + " / " + + branchIdLabel, + data.getBankId() + " / " + data.getBranchId()); + } + + if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId()); + + if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + branchIdLabel + " / " + + accountNrLabel, + data.getBranchId() + " / " + data.getAccountNr()); + } + + if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined && + BankUtil.isBranchIdRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId()); + + if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + accountNrLabel + " / " + accountTypeLabel, + data.getAccountNr() + " / " + data.getAccountType()); + } + + if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined && BankUtil.isAccountNrRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr()); + + if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode)) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType()); + + if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + nationalAccountIdLabel + " / " + + accountNrLabel, data.getNationalAccountId() + + " / " + data.getAccountNr()); + + if (showRequirements) { + TextArea textArea = addCompactTopLabelTextArea(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + Res.get("payment.extras"), "").second; + textArea.setMinHeight(45); + textArea.setMaxHeight(45); + textArea.setEditable(false); + textArea.setId("text-area-disabled"); + textArea.setText(requirements); + } + + return gridRow; + } + + private final CashDepositAccountPayload cashDepositAccountPayload; + private InputTextField holderNameInputTextField, emailInputTextField; + private ComboBox accountTypeComboBox; + + private final EmailValidator emailValidator; + private Country selectedCountry; + + public CashDepositForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.cashDepositAccountPayload = (CashDepositAccountPayload) paymentAccount.paymentAccountPayload; + + emailValidator = new EmailValidator(); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + String countryCode = cashDepositAccountPayload.getCountryCode(); + + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addHolderNameAndIdForDisplayAccount(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + cashDepositAccountPayload.getHolderEmail()); + + if (BankUtil.isBankNameRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), + cashDepositAccountPayload.getBankName()).second.setMouseTransparent(false); + + if (BankUtil.isBankIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode), + cashDepositAccountPayload.getBankId()).second.setMouseTransparent(false); + + if (BankUtil.isBranchIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode), + cashDepositAccountPayload.getBranchId()).second.setMouseTransparent(false); + + if (BankUtil.isNationalAccountIdRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode), + cashDepositAccountPayload.getNationalAccountId()).second.setMouseTransparent(false); + + if (BankUtil.isAccountNrRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode), + cashDepositAccountPayload.getAccountNr()).second.setMouseTransparent(false); + + if (BankUtil.isAccountTypeRequired(countryCode)) + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode), + cashDepositAccountPayload.getAccountType()).second.setMouseTransparent(false); + + String requirements = cashDepositAccountPayload.getRequirements(); + boolean showRequirements = requirements != null && !requirements.isEmpty(); + if (showRequirements) { + TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.extras"), "").second; + textArea.setMinHeight(30); + textArea.setMaxHeight(30); + textArea.setEditable(false); + textArea.setId("text-area-disabled"); + textArea.setText(requirements); + } + + addLimitations(true); + } + + @Override + public void addFormForAddAccount() { + accountNrInputTextFieldEdited = false; + gridRowFrom = gridRow + 1; + + Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); + currencyComboBox = tuple.first; + gridRow = tuple.second; + + addHolderNameAndId(); + + nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel("")); + + nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setNationalAccountId(newValue); + updateFromInputs(); + + }); + + bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); + + bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setBankName(newValue); + updateFromInputs(); + + }); + + bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel("")); + bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setBankId(newValue); + updateFromInputs(); + + }); + + branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel("")); + branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setBranchId(newValue); + updateFromInputs(); + + }); + + accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel("")); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setAccountNr(newValue); + updateFromInputs(); + + }); + + accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account")); + accountTypeComboBox.setOnAction(e -> { + if (BankUtil.isAccountTypeRequired(cashDepositAccountPayload.getCountryCode())) { + cashDepositAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + } + }); + + TextArea requirementsTextArea = addTextArea(gridPane, ++gridRow, Res.get("payment.extras")); + requirementsTextArea.setMinHeight(30); + requirementsTextArea.setMaxHeight(90); + requirementsTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setRequirements(newValue); + updateFromInputs(); + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + updateFromInputs(); + } + + private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { + FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); + applyTradeCurrency(tradeCurrency, defaultCurrency); + } + + + private void onCountrySelected(Country country) { + selectedCountry = country; + if (selectedCountry != null) { + getCountryBasedPaymentAccount().setCountry(selectedCountry); + String countryCode = selectedCountry.code; + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); + paymentAccount.setSingleTradeCurrency(currency); + currencyComboBox.setDisable(false); + currencyComboBox.getSelectionModel().select(currency); + + bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode)); + branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode)); + nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode)); + accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode)); + accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode)); + + bankNameInputTextField.setText(""); + bankIdInputTextField.setText(""); + branchIdInputTextField.setText(""); + nationalAccountIdInputTextField.setText(""); + accountNrInputTextField.setText(""); + accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) accountNrInputTextFieldEdited = true; + }); + accountTypeComboBox.getSelectionModel().clearSelection(); + accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode))); + + validateInput(countryCode); + + holderNameInputTextField.resetValidation(); + emailInputTextField.resetValidation(); + bankNameInputTextField.resetValidation(); + bankIdInputTextField.resetValidation(); + branchIdInputTextField.resetValidation(); + accountNrInputTextField.resetValidation(); + nationalAccountIdInputTextField.resetValidation(); + + holderNameInputTextField.validate(); + emailInputTextField.validate(); + bankNameInputTextField.validate(); + bankIdInputTextField.validate(); + branchIdInputTextField.validate(); + accountNrInputTextField.validate(); + nationalAccountIdInputTextField.validate(); + + boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode); + if (requiresHolderId) { + holderNameInputTextField.minWidthProperty().unbind(); + holderNameInputTextField.setMinWidth(300); + } else { + holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); + } + + updateHolderIDInput(countryCode, requiresHolderId); + + boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode); + nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired); + nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired); + + boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode); + bankNameInputTextField.setVisible(bankNameRequired); + bankNameInputTextField.setManaged(bankNameRequired); + + boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode); + bankIdInputTextField.setVisible(bankIdRequired); + bankIdInputTextField.setManaged(bankIdRequired); + + boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode); + branchIdInputTextField.setVisible(branchIdRequired); + branchIdInputTextField.setManaged(branchIdRequired); + + boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode); + accountNrInputTextField.setVisible(accountNrRequired); + accountNrInputTextField.setManaged(accountNrRequired); + + boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode); + accountTypeComboBox.setVisible(accountTypeRequired); + accountTypeComboBox.setManaged(accountTypeRequired); + + updateFromInputs(); + + onCountryChanged(); + } + } + + private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { + return (CountryBasedPaymentAccount) this.paymentAccount; + } + + private void onCountryChanged() { + } + + private void addHolderNameAndId() { + Tuple2 tuple = addInputTextFieldInputTextField(gridPane, + ++gridRow, Res.get("payment.account.owner"), BankUtil.getHolderIdLabel("")); + holderNameInputTextField = tuple.first; + holderNameInputTextField.setMinWidth(300); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setHolderName(newValue); + updateFromInputs(); + }); + holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); + holderNameInputTextField.setValidator(inputValidator); + + emailInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setHolderEmail(newValue); + updateFromInputs(); + }); + emailInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); + emailInputTextField.setValidator(emailValidator); + + useHolderID = true; + + holderIdInputTextField = tuple.second; + holderIdInputTextField.setMinWidth(250); + holderIdInputTextField.setVisible(false); + holderIdInputTextField.setManaged(false); + holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + cashDepositAccountPayload.setHolderTaxId(newValue); + updateFromInputs(); + }); + } + + @Override + protected void autoFillNameTextField() { + autoFillAccountTextFields(cashDepositAccountPayload); + } + + @Override + public void updateAllInputsValid() { + boolean result = isAccountNameValid() + && paymentAccount.getSingleTradeCurrency() != null + && getCountryBasedPaymentAccount().getCountry() != null + && holderNameInputTextField.getValidator().validate(cashDepositAccountPayload.getHolderName()).isValid + && emailInputTextField.getValidator().validate(cashDepositAccountPayload.getHolderEmail()).isValid; + + String countryCode = cashDepositAccountPayload.getCountryCode(); + result = getValidationResult(result, countryCode, + cashDepositAccountPayload.getBankName(), + cashDepositAccountPayload.getBankId(), + cashDepositAccountPayload.getBranchId(), + cashDepositAccountPayload.getAccountNr(), + cashDepositAccountPayload.getAccountType(), + cashDepositAccountPayload.getHolderTaxId(), + cashDepositAccountPayload.getNationalAccountId()); + allInputsValid.set(result); + } + + private void addHolderNameAndIdForDisplayAccount() { + String countryCode = cashDepositAccountPayload.getCountryCode(); + if (BankUtil.isHolderIdRequired(countryCode)) { + Tuple4 tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(countryCode)); + TextField holderNameTextField = tuple.second; + holderNameTextField.setText(cashDepositAccountPayload.getHolderName()); + holderNameTextField.setMinWidth(300); + tuple.fourth.setText(cashDepositAccountPayload.getHolderTaxId()); + } else { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + cashDepositAccountPayload.getHolderName()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java new file mode 100644 index 0000000000..67e7337ccc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.ChaseQuickPayValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.ChaseQuickPayAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.ChaseQuickPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class ChaseQuickPayForm extends PaymentMethodForm { + + private final ChaseQuickPayAccount chaseQuickPayAccount; + private final ChaseQuickPayValidator chaseQuickPayValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((ChaseQuickPayAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + ((ChaseQuickPayAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public ChaseQuickPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, ChaseQuickPayValidator chaseQuickPayValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.chaseQuickPayAccount = (ChaseQuickPayAccount) paymentAccount; + this.chaseQuickPayValidator = chaseQuickPayValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + chaseQuickPayAccount.setHolderName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + mobileNrInputTextField.setValidator(chaseQuickPayValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + chaseQuickPayAccount.setEmail(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + chaseQuickPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(chaseQuickPayAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + chaseQuickPayAccount.getHolderName()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + chaseQuickPayAccount.getEmail()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && chaseQuickPayValidator.validate(chaseQuickPayAccount.getEmail()).isValid + && inputValidator.validate(chaseQuickPayAccount.getHolderName()).isValid + && chaseQuickPayAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java new file mode 100644 index 0000000000..72ecbd4911 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java @@ -0,0 +1,119 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.ClearXchangeValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.ClearXchangeAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class ClearXchangeForm extends PaymentMethodForm { + private final ClearXchangeAccount clearXchangeAccount; + private final ClearXchangeValidator clearXchangeValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((ClearXchangeAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email.mobile"), + ((ClearXchangeAccountPayload) paymentAccountPayload).getEmailOrMobileNr()); + return gridRow; + } + + public ClearXchangeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, ClearXchangeValidator clearXchangeValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.clearXchangeAccount = (ClearXchangeAccount) paymentAccount; + this.clearXchangeValidator = clearXchangeValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + clearXchangeAccount.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.email.mobile")); + mobileNrInputTextField.setValidator(clearXchangeValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + clearXchangeAccount.setEmailOrMobileNr(newValue.trim()); + updateFromInputs(); + }); + final TradeCurrency singleTradeCurrency = clearXchangeAccount.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + clearXchangeAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(clearXchangeAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + clearXchangeAccount.getHolderName()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"), + clearXchangeAccount.getEmailOrMobileNr()).second; + field.setMouseTransparent(false); + final TradeCurrency singleTradeCurrency = clearXchangeAccount.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && clearXchangeValidator.validate(clearXchangeAccount.getEmailOrMobileNr()).isValid + && inputValidator.validate(clearXchangeAccount.getHolderName()).isValid + && clearXchangeAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java new file mode 100644 index 0000000000..cc338ad673 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java @@ -0,0 +1,185 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.F2FValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.F2FAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import com.jfoenix.controls.JFXTextArea; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.*; + +public class F2FForm extends PaymentMethodForm { + private final F2FAccount f2fAccount; + private final F2FValidator f2fValidator; + private InputTextField cityInputTextField; + private Country selectedCountry; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload, Offer offer, double top) { + 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()); + 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()); + return gridRow; + } + + public F2FForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, F2FValidator f2fValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + + this.f2fAccount = (F2FAccount) paymentAccount; + this.f2fValidator = f2fValidator; + } + + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); + currencyComboBox = tuple.first; + gridRow = tuple.second; + + InputTextField contactInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.f2f.contact")); + contactInputTextField.setPromptText(Res.get("payment.f2f.contact.prompt")); + contactInputTextField.setValidator(f2fValidator); + contactInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + f2fAccount.setContact(newValue); + updateFromInputs(); + }); + + cityInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.f2f.city")); + cityInputTextField.setPromptText(Res.get("payment.f2f.city.prompt")); + cityInputTextField.setValidator(f2fValidator); + cityInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + f2fAccount.setCity(newValue); + updateFromInputs(); + }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + //extraTextArea.setValidator(f2fValidator); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + f2fAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void onCountrySelected(Country country) { + selectedCountry = country; + if (selectedCountry != null) { + getCountryBasedPaymentAccount().setCountry(selectedCountry); + String countryCode = selectedCountry.code; + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); + paymentAccount.setSingleTradeCurrency(currency); + currencyComboBox.setDisable(false); + currencyComboBox.getSelectionModel().select(currency); + + updateFromInputs(); + } + } + + private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { + FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); + applyTradeCurrency(tradeCurrency, defaultCurrency); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(cityInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact", f2fAccount.getContact()), + f2fAccount.getContact()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.city", f2fAccount.getCity()), + f2fAccount.getCity()); + TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textArea.setText(f2fAccount.getExtraInfo()); + textArea.setMinHeight(70); + textArea.setEditable(false); + + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && f2fValidator.validate(f2fAccount.getContact()).isValid + && f2fValidator.validate(f2fAccount.getCity()).isValid + && f2fAccount.getTradeCurrencies().size() > 0); + } + + private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { + return (CountryBasedPaymentAccount) this.paymentAccount; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java new file mode 100644 index 0000000000..e431b1ce7e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java @@ -0,0 +1,144 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AccountNrValidator; +import bisq.desktop.util.validation.BranchIdValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.FasterPaymentsAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.FasterPaymentsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class FasterPaymentsForm extends PaymentMethodForm { + private static final String UK_SORT_CODE = "UK sort code"; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + if (!((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName().isEmpty()) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName()); + } + // do not translate as it is used in English only + addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE, + ((FasterPaymentsAccountPayload) paymentAccountPayload).getSortCode()); + addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.accountNr"), + ((FasterPaymentsAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + + private final FasterPaymentsAccount fasterPaymentsAccount; + private InputTextField holderNameInputTextField; + private InputTextField accountNrInputTextField; + private InputTextField sortCodeInputTextField; + + public FasterPaymentsForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.fasterPaymentsAccount = (FasterPaymentsAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + fasterPaymentsAccount.setHolderName(newValue); + updateFromInputs(); + }); + + // do not translate as it is used in English only + sortCodeInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, UK_SORT_CODE); + sortCodeInputTextField.setValidator(inputValidator); + sortCodeInputTextField.setValidator(new BranchIdValidator("GB")); + sortCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + fasterPaymentsAccount.setSortCode(newValue); + updateFromInputs(); + }); + + accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr")); + accountNrInputTextField.setValidator(new AccountNrValidator("GB")); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + fasterPaymentsAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + fasterPaymentsAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(fasterPaymentsAccount.getPaymentMethod().getId())); + if (!fasterPaymentsAccount.getHolderName().isEmpty()) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + fasterPaymentsAccount.getHolderName()); + } + // do not translate as it is used in English only + addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE, fasterPaymentsAccount.getSortCode()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accountNr"), + fasterPaymentsAccount.getAccountNr()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && holderNameInputTextField.getValidator().validate(fasterPaymentsAccount.getHolderName()).isValid + && sortCodeInputTextField.getValidator().validate(fasterPaymentsAccount.getSortCode()).isValid + && accountNrInputTextField.getValidator().validate(fasterPaymentsAccount.getAccountNr()).isValid + && fasterPaymentsAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java new file mode 100644 index 0000000000..8d620a0996 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java @@ -0,0 +1,88 @@ +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public abstract class GeneralAccountNumberForm extends PaymentMethodForm { + + private InputTextField accountNrInputTextField; + + GeneralAccountNumberForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.no")); + accountNrInputTextField.setValidator(inputValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + setAccountNumber(newValue); + updateFromInputs(); + }); + + addTradeCurrency(); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + public void addTradeCurrency() { + final TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + addFormForAccountNumberDisplayAccount(paymentAccount.getAccountName(), paymentAccount.getPaymentMethod(), getAccountNr(), + paymentAccount.getSingleTradeCurrency()); + } + + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(getAccountNr()).isValid + && paymentAccount.getTradeCurrencies().size() > 0); + } + + abstract void setAccountNumber(String newValue); + + abstract String getAccountNr(); + + private void addFormForAccountNumberDisplayAccount(String accountName, PaymentMethod paymentMethod, String accountNr, + TradeCurrency singleTradeCurrency) { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), accountName, Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentMethod.getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.no"), accountNr).second; + field.setMouseTransparent(false); + + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + + addLimitations(true); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java new file mode 100644 index 0000000000..2889abc130 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java @@ -0,0 +1,209 @@ +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.validation.AccountNrValidator; +import bisq.desktop.util.validation.BankIdValidator; +import bisq.desktop.util.validation.BranchIdValidator; +import bisq.desktop.util.validation.NationalAccountIdValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.CountryBasedPaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.layout.GridPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class GeneralBankForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(GeneralBankForm.class); + + static boolean accountNrAccountTypeCombined = false; + static boolean nationalAccountIdAccountNrCombined = false; + static boolean bankNameBankIdCombined = false; + static boolean bankIdBranchIdCombined = false; + static boolean bankNameBranchIdCombined = false; + static boolean branchIdAccountNrCombined = false; + + boolean validatorsApplied; + boolean useHolderID; + InputTextField bankNameInputTextField, bankIdInputTextField, branchIdInputTextField, accountNrInputTextField, + holderIdInputTextField, nationalAccountIdInputTextField; + + boolean accountNrInputTextFieldEdited; + + public GeneralBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } + + static int getIndexOfColumn(int colIndex) { + return colIndex % 2; + } + + static void prepareFormLayoutFlags(String countryCode, int currentNumberOfRows) { + int nrRows = currentNumberOfRows; + + if (nrRows > 2) { + // Try combine AccountNr + AccountType + accountNrAccountTypeCombined = BankUtil.isAccountNrRequired(countryCode) && BankUtil.isAccountTypeRequired(countryCode); + if (accountNrAccountTypeCombined) + nrRows--; + + if (nrRows > 2) { + + nationalAccountIdAccountNrCombined = BankUtil.isAccountNrRequired(countryCode) && + BankUtil.isNationalAccountIdRequired(countryCode); + + if (nationalAccountIdAccountNrCombined) + nrRows--; + + if (nrRows > 2) { + // Next we try BankName + BankId + bankNameBankIdCombined = BankUtil.isBankNameRequired(countryCode) && BankUtil.isBankIdRequired(countryCode); + if (bankNameBankIdCombined) + nrRows--; + + if (nrRows > 2) { + // Next we try BankId + BranchId + bankIdBranchIdCombined = !bankNameBankIdCombined && BankUtil.isBankIdRequired(countryCode) && + BankUtil.isBranchIdRequired(countryCode); + if (bankIdBranchIdCombined) + nrRows--; + + if (nrRows > 2) { + // Next we try BankId + BranchId + bankNameBranchIdCombined = !bankNameBankIdCombined && !bankIdBranchIdCombined && + BankUtil.isBankNameRequired(countryCode) && BankUtil.isBranchIdRequired(countryCode); + if (bankNameBranchIdCombined) + nrRows--; + + if (nrRows > 2) { + branchIdAccountNrCombined = !bankNameBranchIdCombined && !bankIdBranchIdCombined && + !accountNrAccountTypeCombined && + BankUtil.isBranchIdRequired(countryCode) && BankUtil.isAccountNrRequired(countryCode); + if (branchIdAccountNrCombined) + nrRows--; + + if (nrRows > 2) + log.warn("We still have too many rows...."); + } + } + } + } + } + } + } + + void validateInput(String countryCode) { + if (BankUtil.useValidation(countryCode)) { + validatorsApplied = true; + if (useHolderID) + holderIdInputTextField.setValidator(inputValidator); + bankNameInputTextField.setValidator(inputValidator); + bankIdInputTextField.setValidator(new BankIdValidator(countryCode)); + branchIdInputTextField.setValidator(new BranchIdValidator(countryCode)); + accountNrInputTextField.setValidator(new AccountNrValidator(countryCode)); + nationalAccountIdInputTextField.setValidator(new NationalAccountIdValidator(countryCode)); + } else { + validatorsApplied = false; + if (useHolderID) + holderIdInputTextField.setValidator(null); + bankNameInputTextField.setValidator(null); + bankIdInputTextField.setValidator(null); + branchIdInputTextField.setValidator(null); + accountNrInputTextField.setValidator(inputValidator); + nationalAccountIdInputTextField.setValidator(null); + } + } + + void updateHolderIDInput(String countryCode, boolean requiresHolderId) { + if (useHolderID) { + if (!requiresHolderId) + holderIdInputTextField.setText(""); + + holderIdInputTextField.resetValidation(); + holderIdInputTextField.setVisible(requiresHolderId); + holderIdInputTextField.setManaged(requiresHolderId); + + holderIdInputTextField.setPromptText(BankUtil.getHolderIdLabel(countryCode)); + } + } + + void autoFillAccountTextFields(CountryBasedPaymentAccountPayload paymentAccountPayload) { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + String bankId = null; + String countryCode = paymentAccountPayload.getCountryCode(); + if (countryCode == null) + countryCode = ""; + if (BankUtil.isBankIdRequired(countryCode)) { + bankId = bankIdInputTextField.getText().trim(); + if (bankId.length() > 9) + bankId = StringUtils.abbreviate(bankId, 9); + } else if (BankUtil.isBranchIdRequired(countryCode)) { + bankId = branchIdInputTextField.getText().trim(); + if (bankId.length() > 9) + bankId = StringUtils.abbreviate(bankId, 9); + } else if (BankUtil.isBankNameRequired(countryCode)) { + bankId = bankNameInputTextField.getText().trim(); + if (bankId.length() > 9) + bankId = StringUtils.abbreviate(bankId, 9); + } + + String accountNr = accountNrInputTextField.getText().trim(); + if (accountNr.length() > 9) + accountNr = StringUtils.abbreviate(accountNr, 9); + + String method = Res.get(paymentAccount.getPaymentMethod().getId()); + if (bankId != null && !bankId.isEmpty()) + accountNameTextField.setText(method.concat(": ").concat(bankId).concat(", ").concat(accountNr)); + else + accountNameTextField.setText(method.concat(": ").concat(accountNr)); + + if (BankUtil.isNationalAccountIdRequired(countryCode)) { + String nationalAccountId = nationalAccountIdInputTextField.getText(); + + if (countryCode.equals("AR") && nationalAccountId.length() == 22 && !accountNrInputTextFieldEdited) { + branchIdInputTextField.setText(nationalAccountId.substring(3, 7)); + accountNrInputTextField.setText(nationalAccountId.substring(8, 21)); + } + } + } + } + + boolean getValidationResult(boolean result, String countryCode, String bankName, String bankId, + String branchId, String accountNr, String accountType, String holderTaxId, + String nationalAccountId) { + if (validatorsApplied && BankUtil.useValidation(countryCode)) { + if (BankUtil.isBankNameRequired(countryCode)) + result = result && bankNameInputTextField.getValidator().validate(bankName).isValid; + + if (BankUtil.isBankIdRequired(countryCode)) + result = result && bankIdInputTextField.getValidator().validate(bankId).isValid; + + if (BankUtil.isBranchIdRequired(countryCode)) + result = result && branchIdInputTextField.getValidator().validate(branchId).isValid; + + if (BankUtil.isAccountNrRequired(countryCode)) + result = result && accountNrInputTextField.getValidator().validate(accountNr).isValid; + + if (BankUtil.isAccountTypeRequired(countryCode)) + result = result && accountType != null; + + if (useHolderID && BankUtil.isHolderIdRequired(countryCode)) + result = result && holderIdInputTextField.getValidator().validate(holderTaxId).isValid; + + if (BankUtil.isNationalAccountIdRequired(countryCode)) + result = result && nationalAccountIdInputTextField.getValidator().validate(nationalAccountId).isValid; + } else { // only account number not empty validation + result = result && accountNrInputTextField.getValidator().validate(accountNr).isValid; + } + + return result; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java new file mode 100644 index 0000000000..5435e9e4c9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java @@ -0,0 +1,131 @@ +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import org.apache.commons.lang3.StringUtils; + +import com.jfoenix.controls.JFXComboBox; +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import javafx.util.StringConverter; + +import java.util.ArrayList; +import java.util.List; + +import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; + +public abstract class GeneralSepaForm extends PaymentMethodForm { + + static final String BIC = "BIC"; + static final String IBAN = "IBAN"; + + final List euroCountryCheckBoxes = new ArrayList<>(); + final List nonEuroCountryCheckBoxes = new ArrayList<>(); + private TextField currencyTextField; + InputTextField ibanInputTextField; + + private FiatCurrency euroCurrency = CurrencyUtil.getFiatCurrency("EUR").get(); + + GeneralSepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + paymentAccount.setSingleTradeCurrency(euroCurrency); + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + TradeCurrency singleTradeCurrency = this.paymentAccount.getSingleTradeCurrency(); + String currency = singleTradeCurrency != null ? singleTradeCurrency.getCode() : null; + if (currency != null) { + String iban = ibanInputTextField.getText(); + if (iban.length() > 9) + iban = StringUtils.abbreviate(iban, 9); + String method = Res.get(paymentAccount.getPaymentMethod().getId()); + CountryBasedPaymentAccount countryBasedPaymentAccount = (CountryBasedPaymentAccount) this.paymentAccount; + String country = countryBasedPaymentAccount.getCountry() != null ? + countryBasedPaymentAccount.getCountry().code : null; + if (country != null) + accountNameTextField.setText(method.concat(" (").concat(currency).concat("/").concat(country) + .concat("): ").concat(iban)); + } + } + } + + void setCountryComboBoxAction(ComboBox countryComboBox, CountryBasedPaymentAccount paymentAccount) { + countryComboBox.setOnAction(e -> { + Country selectedItem = countryComboBox.getSelectionModel().getSelectedItem(); + paymentAccount.setCountry(selectedItem); + + updateCountriesSelection(euroCountryCheckBoxes); + updateCountriesSelection(nonEuroCountryCheckBoxes); + updateFromInputs(); + }); + } + + void addCountriesGrid(String title, List checkBoxList, + List dataProvider) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, title, 0).second; + + flowPane.setId("flow-pane-checkboxes-bg"); + + dataProvider.forEach(country -> + fillUpFlowPaneWithCountries(checkBoxList, flowPane, country)); + updateCountriesSelection(checkBoxList); + } + + ComboBox addCountrySelection() { + HBox hBox = new HBox(); + + hBox.setSpacing(10); + ComboBox countryComboBox = new JFXComboBox<>(); + currencyTextField = new JFXTextField(""); + currencyTextField.setEditable(false); + currencyTextField.setMouseTransparent(true); + currencyTextField.setFocusTraversable(false); + currencyTextField.setMinWidth(300); + + currencyTextField.setVisible(true); + currencyTextField.setManaged(true); + currencyTextField.setText(Res.get("payment.currencyWithSymbol", euroCurrency.getNameAndCode())); + + hBox.getChildren().addAll(countryComboBox, currencyTextField); + + addTopLabelWithVBox(gridPane, ++gridRow, Res.get("payment.bank.country"), hBox, 0); + + countryComboBox.setPromptText(Res.get("payment.select.bank.country")); + countryComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Country country) { + return country.name + " (" + country.code + ")"; + } + + @Override + public Country fromString(String s) { + return null; + } + }); + return countryComboBox; + } + + abstract void updateCountriesSelection(List checkBoxList); + +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java new file mode 100644 index 0000000000..f0aa34ce70 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java @@ -0,0 +1,106 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.HalCashValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.HalCashAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class HalCashForm extends PaymentMethodForm { + private final HalCashAccount halCashAccount; + private final HalCashValidator halCashValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), + ((HalCashAccountPayload) paymentAccountPayload).getMobileNr()); + return gridRow; + } + + public HalCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, HalCashValidator halCashValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.halCashAccount = (HalCashAccount) paymentAccount; + this.halCashValidator = halCashValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(halCashValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + halCashAccount.setMobileNr(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = halCashAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + halCashAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(halCashAccount.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), + halCashAccount.getMobileNr()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = halCashAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && halCashValidator.validate(halCashAccount.getMobileNr()).isValid + && halCashAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java new file mode 100644 index 0000000000..5a8fc6cea9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java @@ -0,0 +1,142 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.InteracETransferValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.InteracETransferAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.InteracETransferAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class InteracETransferForm extends PaymentMethodForm { + + private final InteracETransferAccount interacETransferAccount; + private final InteracETransferValidator interacETransferValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.emailOrMobile"), + ((InteracETransferAccountPayload) paymentAccountPayload).getEmail()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), + ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion()); + addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.answer"), + ((InteracETransferAccountPayload) paymentAccountPayload).getAnswer()); + return gridRow; + } + + public InteracETransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InteracETransferValidator interacETransferValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.interacETransferAccount = (InteracETransferAccount) paymentAccount; + this.interacETransferValidator = interacETransferValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + interacETransferAccount.setHolderName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.emailOrMobile")); + mobileNrInputTextField.setValidator(interacETransferValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + interacETransferAccount.setEmail(newValue); + updateFromInputs(); + }); + + InputTextField questionInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.secret")); + questionInputTextField.setValidator(interacETransferValidator.questionValidator); + questionInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + interacETransferAccount.setQuestion(newValue); + updateFromInputs(); + }); + + InputTextField answerInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.answer")); + answerInputTextField.setValidator(interacETransferValidator.answerValidator); + answerInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + interacETransferAccount.setAnswer(newValue); + updateFromInputs(); + }); + TradeCurrency singleTradeCurrency = interacETransferAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + interacETransferAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(interacETransferAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + interacETransferAccount.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + interacETransferAccount.getEmail()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), + interacETransferAccount.getQuestion()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.answer"), + interacETransferAccount.getAnswer()).second.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = interacETransferAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && interacETransferValidator.validate(interacETransferAccount.getEmail()).isValid + && inputValidator.validate(interacETransferAccount.getHolderName()).isValid + && interacETransferValidator.questionValidator.validate(interacETransferAccount.getQuestion()).isValid + && interacETransferValidator.answerValidator.validate(interacETransferAccount.getAnswer()).isValid + && interacETransferAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java new file mode 100644 index 0000000000..22eac439a5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java @@ -0,0 +1,379 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.AutocompleteComboBox; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.paymentmethods.data.JapanBankData; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.JapanBankAccountNameValidator; +import bisq.desktop.util.validation.JapanBankAccountNumberValidator; +import bisq.desktop.util.validation.JapanBankBranchCodeValidator; +import bisq.desktop.util.validation.JapanBankBranchNameValidator; +import bisq.desktop.util.validation.JapanBankTransferValidator; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.JapanBankAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.JapanBankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.RegexValidator; + +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; +import bisq.common.util.Tuple4; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.GridPane; + +import javafx.util.StringConverter; + +import static bisq.desktop.util.FormBuilder.*; +import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; + +public class JapanBankTransferForm extends PaymentMethodForm +{ + private final JapanBankAccount japanBankAccount; + protected ComboBox bankComboBox, bankAccountTypeComboBox; + private InputTextField bankAccountNumberInputTextField; + + private JapanBankTransferValidator japanBankTransferValidator; + private JapanBankBranchNameValidator japanBankBranchNameValidator; + private JapanBankBranchCodeValidator japanBankBranchCodeValidator; + private JapanBankAccountNameValidator japanBankAccountNameValidator; + private JapanBankAccountNumberValidator japanBankAccountNumberValidator; + + private LengthValidator lengthValidator; + private RegexValidator regexValidator; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, // {{{ + PaymentAccountPayload paymentAccountPayload) + { + JapanBankAccountPayload japanBankAccount = ((JapanBankAccountPayload) paymentAccountPayload); + + String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.japan.bank"), bankText); + + String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.branch"), branchText); + + String accountText = japanBankAccount.getBankAccountType() + " " + japanBankAccount.getBankAccountNumber(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.japan.account"), accountText); + + String accountNameText = japanBankAccount.getBankAccountName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.recipient"), accountNameText); + + return gridRow; + } // }}} + + public JapanBankTransferForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + JapanBankTransferValidator japanBankTransferValidator, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) + { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.japanBankAccount = (JapanBankAccount) paymentAccount; + + this.japanBankTransferValidator = japanBankTransferValidator; + this.japanBankBranchCodeValidator = new JapanBankBranchCodeValidator(); + this.japanBankAccountNumberValidator = new JapanBankAccountNumberValidator(); + + this.lengthValidator = new LengthValidator(); + this.regexValidator = new RegexValidator(); + this.japanBankBranchNameValidator = new JapanBankBranchNameValidator(lengthValidator, regexValidator); + this.japanBankAccountNameValidator = new JapanBankAccountNameValidator(lengthValidator, regexValidator); + } + + @Override + public void addFormForDisplayAccount() // {{{ + { + gridRowFrom = gridRow; + + addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.name"), + japanBankAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(japanBankAccount.getPaymentMethod().getId())); + + addBankDisplay(); + addBankBranchDisplay(); + addBankAccountDisplay(); + addBankAccountTypeDisplay(); + + addLimitations(true); + } // }}} + private void addBankDisplay() // {{{ + { + String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); + TextField bankTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("bank"), bankText).second; + bankTextField.setEditable(false); + } // }}} + private void addBankBranchDisplay() // {{{ + { + String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName(); + TextField branchTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("branch"), branchText).second; + branchTextField.setEditable(false); + } // }}} + private void addBankAccountDisplay() // {{{ + { + String accountText = japanBankAccount.getBankAccountNumber() + " " + japanBankAccount.getBankAccountName(); + TextField accountTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account"), accountText).second; + accountTextField.setEditable(false); + } // }}} + private void addBankAccountTypeDisplay() // {{{ + { + TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency(); + String currency = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + String accountTypeText = currency + " " + japanBankAccount.getBankAccountType(); + TextField accountTypeTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account.type"), accountTypeText).second; + accountTypeTextField.setEditable(false); + } // }}} + + @Override + public void addFormForAddAccount() // {{{ + { + gridRowFrom = gridRow; + + addBankInput(); + addBankBranchInput(); + addBankAccountInput(); + addBankAccountTypeInput(); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } // }}} + private void addBankInput() // {{{ + { + gridRow++; + + Tuple4> tuple4 = addTopLabelTextFieldAutocompleteComboBox(gridPane, gridRow, JapanBankData.getString("bank.code"), JapanBankData.getString("bank.name"), 10); + + // Bank Code (readonly) + TextField bankCodeField = tuple4.second; + bankCodeField.setPrefWidth(200); + bankCodeField.setMaxWidth(200); + bankCodeField.setEditable(false); + + // Bank Selector + bankComboBox = tuple4.fourth; + bankComboBox.setPromptText(JapanBankData.getString("bank.select")); + bankComboBox.setButtonCell(getComboBoxButtonCell(JapanBankData.getString("bank.name"), bankComboBox)); + bankComboBox.getEditor().focusedProperty().addListener(observable -> { + bankComboBox.setPromptText(""); + }); + bankComboBox.setConverter(new StringConverter() { + @Override + public String toString(String bank) { + return bank != null ? bank : ""; + } + public String fromString(String s) { + return s != null ? s : ""; + } + }); + ((AutocompleteComboBox) bankComboBox).setAutocompleteItems(JapanBankData.prettyPrintBankList()); + + bankComboBox.setPrefWidth(430); + bankComboBox.setVisibleRowCount(430); + + ((AutocompleteComboBox) bankComboBox).setOnChangeConfirmed(e -> { + // get selected value + String bank = bankComboBox.getSelectionModel().getSelectedItem(); + + // parse first 4 characters as bank code + String bankCode = StringUtils.substring(bank, 0, 4); + if (bankCode != null) + { + // set bank code field to this value + bankCodeField.setText(bankCode); + // save to payload + japanBankAccount.setBankCode(bankCode); + + // parse remainder as bank name + String bankNameFull = StringUtils.substringAfter(bank, JapanBankData.SPACE); + if (bankNameFull != null) + { + // parse beginning as Japanese bank name + String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE); + if (bankNameJa != null) + { + // set bank name field to this value + bankComboBox.getEditor().setText(bankNameJa); + // save to payload + japanBankAccount.setBankName(bankNameJa); + } + } + } + + + updateFromInputs(); + }); + } // }}} + private void addBankBranchInput() // {{{ + { + gridRow++; + Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("branch.code"), JapanBankData.getString("branch.name")); + + // branch code + InputTextField bankBranchCodeInputTextField = tuple2.first; + bankBranchCodeInputTextField.setValidator(japanBankBranchCodeValidator); + bankBranchCodeInputTextField.setPrefWidth(200); + bankBranchCodeInputTextField.setMaxWidth(200); + bankBranchCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + japanBankAccount.setBankBranchCode(newValue); + updateFromInputs(); + }); + + // branch name + InputTextField bankBranchNameInputTextField = tuple2.second; + bankBranchNameInputTextField.setValidator(japanBankBranchNameValidator); + bankBranchNameInputTextField.setPrefWidth(430); + bankBranchNameInputTextField.setMaxWidth(430); + bankBranchNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + japanBankAccount.setBankBranchName(newValue); + updateFromInputs(); + }); + } // }}} + private void addBankAccountInput() // {{{ + { + gridRow++; + Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("account.number"), JapanBankData.getString("account.name")); + + // account number + bankAccountNumberInputTextField = tuple2.first; + bankAccountNumberInputTextField.setValidator(japanBankAccountNumberValidator); + bankAccountNumberInputTextField.setPrefWidth(200); + bankAccountNumberInputTextField.setMaxWidth(200); + bankAccountNumberInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + japanBankAccount.setBankAccountNumber(newValue); + updateFromInputs(); + }); + + // account name + InputTextField bankAccountNameInputTextField = tuple2.second; + bankAccountNameInputTextField.setValidator(japanBankAccountNameValidator); + bankAccountNameInputTextField.setPrefWidth(430); + bankAccountNameInputTextField.setMaxWidth(430); + bankAccountNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + japanBankAccount.setBankAccountName(newValue); + updateFromInputs(); + }); + } // }}} + private void addBankAccountTypeInput() // {{{ + { + // account currency + gridRow++; + + TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, gridRow, Res.get("shared.currency"), nameAndCode, 20); + + // account type + gridRow++; + + ToggleGroup toggleGroup = new ToggleGroup(); + Tuple3 tuple3 = + addTopLabelRadioButtonRadioButton( + gridPane, gridRow, toggleGroup, + JapanBankData.getString("account.type.select"), + JapanBankData.getString("account.type.futsu"), + JapanBankData.getString("account.type.touza"), + 0 + ); + + toggleGroup.getToggles().get(0).setSelected(true); + japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); + + RadioButton futsu = tuple3.second; + RadioButton touza = tuple3.third; + + toggleGroup.selectedToggleProperty().addListener + ( + (ov, oldValue, newValue) -> + { + if (futsu.isSelected()) + japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); + if (touza.isSelected()) + japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja")); + } + ); + } // }}} + + @Override + public void updateFromInputs() // {{{ + { + System.out.println("JapanBankTransferForm: updateFromInputs()"); + System.out.println("bankName: "+japanBankAccount.getBankName()); + System.out.println("bankCode: "+japanBankAccount.getBankCode()); + System.out.println("bankBranchName: "+japanBankAccount.getBankBranchName()); + System.out.println("bankBranchCode: "+japanBankAccount.getBankBranchCode()); + System.out.println("bankAccountType: "+japanBankAccount.getBankAccountType()); + System.out.println("bankAccountName: "+japanBankAccount.getBankAccountName()); + System.out.println("bankAccountNumber: "+japanBankAccount.getBankAccountNumber()); + super.updateFromInputs(); + } // }}} + + @Override + protected void autoFillNameTextField() // {{{ + { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) + { + accountNameTextField.setText( + Res.get(paymentAccount.getPaymentMethod().getId()) + .concat(": ") + .concat(japanBankAccount.getBankName()) + .concat(" ") + .concat(japanBankAccount.getBankBranchName()) + .concat(" ") + .concat(japanBankAccount.getBankAccountNumber()) + .concat(" ") + .concat(japanBankAccount.getBankAccountName()) + ); + } + } // }}} + + @Override + public void updateAllInputsValid() // {{{ + { + boolean result = + ( + isAccountNameValid() && + inputValidator.validate(japanBankAccount.getBankCode()).isValid && + inputValidator.validate(japanBankAccount.getBankName()).isValid && + japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid && + japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid && + japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid && + japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid && + inputValidator.validate(japanBankAccount.getBankAccountType()).isValid + ); + allInputsValid.set(result); + } // }}} +} + +// vim:ts=4:sw=4:expandtab:foldmethod=marker:nowrap: diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java new file mode 100644 index 0000000000..6f20897395 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java @@ -0,0 +1,100 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.MoneyBeamValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.MoneyBeamAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.MoneyBeamAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class MoneyBeamForm extends PaymentMethodForm { + private final MoneyBeamAccount account; + private final MoneyBeamValidator validator; + private InputTextField accountIdInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), ((MoneyBeamAccountPayload) paymentAccountPayload).getAccountId()); + return gridRow; + } + + public MoneyBeamForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, MoneyBeamValidator moneyBeamValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (MoneyBeamAccount) paymentAccount; + this.validator = moneyBeamValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId")); + accountIdInputTextField.setValidator(validator); + accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setAccountId(newValue.trim()); + updateFromInputs(); + }); + + final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountIdInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), account.getAccountId()).second; + field.setMouseTransparent(false); + final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && validator.validate(account.getAccountId()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java new file mode 100644 index 0000000000..799a79a35f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java @@ -0,0 +1,191 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.MoneyGramAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.*; + +@Slf4j +public class MoneyGramForm extends PaymentMethodForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + final MoneyGramAccountPayload payload = (MoneyGramAccountPayload) paymentAccountPayload; + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.fullName"), + payload.getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email"), + payload.getEmail()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, + Res.get("payment.bank.country"), + CountryUtil.getNameAndCode(((MoneyGramAccountPayload) paymentAccountPayload).getCountryCode())); + if (BankUtil.isStateRequired(payload.getCountryCode())) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, + Res.get("payment.account.state"), + payload.getState()); + + return gridRow; + } + + private final MoneyGramAccountPayload moneyGramAccountPayload; + private InputTextField holderNameInputTextField; + private InputTextField stateInputTextField; + private final EmailValidator emailValidator; + + public MoneyGramForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.moneyGramAccountPayload = (MoneyGramAccountPayload) paymentAccount.paymentAccountPayload; + + emailValidator = new EmailValidator(); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + final Country country = getMoneyGramPaymentAccount().getCountry(); + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), country != null ? country.name : ""); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.fullName"), + moneyGramAccountPayload.getHolderName()); + if (BankUtil.isStateRequired(moneyGramAccountPayload.getCountryCode())) + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.state"), + moneyGramAccountPayload.getState()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + moneyGramAccountPayload.getEmail()); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + gridRow = GUIUtil.addRegionCountry(gridPane, gridRow, this::onCountrySelected); + + holderNameInputTextField = addInputTextField(gridPane, + ++gridRow, Res.get("payment.account.fullName")); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + moneyGramAccountPayload.setHolderName(newValue); + updateFromInputs(); + }); + holderNameInputTextField.setValidator(inputValidator); + + stateInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.state")); + stateInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + moneyGramAccountPayload.setState(newValue); + updateFromInputs(); + + }); + applyIsStateRequired(); + + InputTextField emailInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + moneyGramAccountPayload.setEmail(newValue); + updateFromInputs(); + }); + emailInputTextField.setValidator(emailValidator); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + updateFromInputs(); + } + + private void onCountrySelected(Country country) { + if (country != null) { + getMoneyGramPaymentAccount().setCountry(country); + updateFromInputs(); + applyIsStateRequired(); + stateInputTextField.setText(""); + } + } + + private void addCurrenciesGrid(boolean isEditable) { + final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), + Layout.FLOATING_LABEL_DISTANCE * 3, Layout.FLOATING_LABEL_DISTANCE * 3); + + FlowPane flowPane = labelFlowPaneTuple2.second; + + if (isEditable) + flowPane.setId("flow-pane-checkboxes-bg"); + else + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + + CurrencyUtil.getAllMoneyGramCurrencies().forEach(e -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, paymentAccount)); + } + + private void applyIsStateRequired() { + final boolean stateRequired = BankUtil.isStateRequired(moneyGramAccountPayload.getCountryCode()); + stateInputTextField.setManaged(stateRequired); + stateInputTextField.setVisible(stateRequired); + } + + private MoneyGramAccount getMoneyGramPaymentAccount() { + return (MoneyGramAccount) this.paymentAccount; + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + accountNameTextField.setText(Res.get(paymentAccount.getPaymentMethod().getId()) + .concat(": ") + .concat(StringUtils.abbreviate(holderNameInputTextField.getText(), 9))); + } + } + + @Override + public void updateAllInputsValid() { + boolean result = isAccountNameValid() + && getMoneyGramPaymentAccount().getCountry() != null + && inputValidator.validate(moneyGramAccountPayload.getHolderName()).isValid + && emailValidator.validate(moneyGramAccountPayload.getEmail()).isValid + && paymentAccount.getTradeCurrencies().size() > 0; + allInputsValid.set(result); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/NationalBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NationalBankForm.java new file mode 100644 index 0000000000..a6a37069c6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NationalBankForm.java @@ -0,0 +1,37 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class NationalBankForm extends BankForm { + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public NationalBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java new file mode 100644 index 0000000000..3aa4ba9363 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java @@ -0,0 +1,375 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.AutoTooltipCheckBox; +import bisq.desktop.components.InfoTextField; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import org.apache.commons.lang3.StringUtils; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.*; + +@Slf4j +public abstract class PaymentMethodForm { + protected final PaymentAccount paymentAccount; + private final AccountAgeWitnessService accountAgeWitnessService; + protected final InputValidator inputValidator; + protected final GridPane gridPane; + protected int gridRow; + private final CoinFormatter formatter; + protected final BooleanProperty allInputsValid = new SimpleBooleanProperty(); + + protected int gridRowFrom; + InputTextField accountNameTextField; + protected TextField paymentLimitationsTextField; + ToggleButton useCustomAccountNameToggleButton; + protected ComboBox currencyComboBox; + + public PaymentMethodForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + this.paymentAccount = paymentAccount; + this.accountAgeWitnessService = accountAgeWitnessService; + this.inputValidator = inputValidator; + this.gridPane = gridPane; + this.gridRow = gridRow; + this.formatter = formatter; + } + + protected void addTradeCurrencyComboBox() { + currencyComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, Res.get("shared.currency")); + currencyComboBox.setPromptText(Res.get("list.currency.select")); + currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getMainFiatCurrencies())); + currencyComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency.getNameAndCode(); + } + + @Override + public TradeCurrency fromString(String s) { + return null; + } + }); + currencyComboBox.setOnAction(e -> { + paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + }); + } + + protected void addAccountNameTextFieldWithAutoFillToggleButton() { + Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(gridPane, ++gridRow, + Res.get("payment.account.name"), Res.get("payment.useCustomAccountName")); + accountNameTextField = tuple.second; + accountNameTextField.setPrefWidth(300); + accountNameTextField.setEditable(false); + accountNameTextField.setValidator(inputValidator); + accountNameTextField.setFocusTraversable(false); + accountNameTextField.textProperty().addListener((ov, oldValue, newValue) -> { + paymentAccount.setAccountName(newValue); + updateAllInputsValid(); + }); + useCustomAccountNameToggleButton = tuple.third; + useCustomAccountNameToggleButton.setSelected(false); + useCustomAccountNameToggleButton.setOnAction(e -> { + boolean selected = useCustomAccountNameToggleButton.isSelected(); + accountNameTextField.setEditable(selected); + accountNameTextField.setFocusTraversable(selected); + autoFillNameTextField(); + }); + } + + public static InfoTextField addOpenTradeDuration(GridPane gridPane, + int gridRow, + Offer offer) { + long hours = offer.getMaxTradePeriod() / 3600_000; + final Tuple3 labelInfoTextFieldVBoxTuple3 = + addTopLabelInfoTextField(gridPane, gridRow, Res.get("payment.maxPeriod"), + getTimeText(hours), -Layout.FLOATING_LABEL_DISTANCE); + return labelInfoTextFieldVBoxTuple3.second; + } + + private static String getTimeText(long hours) { + String time = hours + " " + Res.get("time.hours"); + if (hours == 1) + time = Res.get("time.1hour"); + else if (hours == 24) + time = Res.get("time.1day"); + else if (hours > 24) + time = hours / 24 + " " + Res.get("time.days"); + + return time; + } + + protected String getLimitationsText() { + final PaymentAccount paymentAccount = getPaymentAccount(); + long hours = paymentAccount.getMaxTradePeriod() / 3600_000; + final TradeCurrency tradeCurrency; + if (paymentAccount.getSingleTradeCurrency() != null) + tradeCurrency = paymentAccount.getSingleTradeCurrency(); + else if (paymentAccount.getSelectedTradeCurrency() != null) + tradeCurrency = paymentAccount.getSelectedTradeCurrency(); + else if (!paymentAccount.getTradeCurrencies().isEmpty() && paymentAccount.getTradeCurrencies().get(0) != null) + tradeCurrency = paymentAccount.getTradeCurrencies().get(0); + else + tradeCurrency = paymentAccount instanceof AssetAccount ? + CurrencyUtil.getAllSortedCryptoCurrencies().iterator().next() : + CurrencyUtil.getDefaultTradeCurrency(); + final boolean isAddAccountScreen = paymentAccount.getAccountName() == null; + final long accountAge = !isAddAccountScreen ? accountAgeWitnessService.getMyAccountAge(paymentAccount.getPaymentAccountPayload()) : 0L; + + final String limitationsText = paymentAccount instanceof AssetAccount ? + Res.get("payment.maxPeriodAndLimitCrypto", + getTimeText(hours), + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY)))) + : + Res.get("payment.maxPeriodAndLimit", + getTimeText(hours), + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY))), + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.SELL))), + DisplayUtils.formatAccountAge(accountAge)); + return limitationsText; + } + + protected void addLimitations(boolean isDisplayForm) { + final boolean isAddAccountScreen = paymentAccount.getAccountName() == null; + + if (isDisplayForm) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), getLimitationsText()); + + String accountSigningStateText; + MaterialDesignIcon icon; + + boolean needsSigning = PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), + paymentAccount.getTradeCurrencies()); + + if (needsSigning) { + + AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness( + paymentAccount.paymentAccountPayload); + AccountAgeWitnessService.SignState signState = + accountAgeWitnessService.getSignState(myWitness); + + accountSigningStateText = StringUtils.capitalize(signState.getDisplayString()); + + long daysSinceSigning = TimeUnit.MILLISECONDS.toDays( + accountAgeWitnessService.getWitnessSignAge(myWitness, new Date())); + String timeSinceSigning = Res.get("offerbook.timeSinceSigning.daysSinceSigning.long", + Res.get("offerbook.timeSinceSigning.daysSinceSigning", + daysSinceSigning)); + + if (!signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { + accountSigningStateText += " / " + timeSinceSigning; + } + + icon = GUIUtil.getIconForSignState(signState); + + InfoTextField accountSigningField = addCompactTopLabelInfoTextField(gridPane, ++gridRow, Res.get("shared.accountSigningState"), + accountSigningStateText).second; + //TODO: add additional information regarding account signing + accountSigningField.setContent(icon, accountSigningStateText, "", 0.4); + } + + } else { + paymentLimitationsTextField = addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), getLimitationsText()).second; + } + + if (!(paymentAccount instanceof AssetAccount)) { + if (isAddAccountScreen) { + InputTextField inputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.salt"), 0); + inputTextField.setText(Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); + inputTextField.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.isEmpty()) { + try { + // test if input is hex + Utilities.decodeFromHex(newValue); + + paymentAccount.setSaltAsHex(newValue); + } catch (Throwable t) { + new Popup().warning(Res.get("payment.error.noHexSalt")).show(); + inputTextField.setText(Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); + log.warn(t.toString()); + } + } + }); + } else { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.salt", + Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())), + Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); + } + } + } + + void applyTradeCurrency(TradeCurrency tradeCurrency, FiatCurrency defaultCurrency) { + if (!defaultCurrency.equals(tradeCurrency)) { + new Popup().warning(Res.get("payment.foreign.currency")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + paymentAccount.setSingleTradeCurrency(tradeCurrency); + autoFillNameTextField(); + }) + .closeButtonText(Res.get("payment.restore.default")) + .onClose(() -> currencyComboBox.getSelectionModel().select(defaultCurrency)) + .show(); + } else { + paymentAccount.setSingleTradeCurrency(tradeCurrency); + autoFillNameTextField(); + } + } + + void setAccountNameWithString(String name) { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + name = name.trim(); + name = StringUtils.abbreviate(name, 9); + String method = Res.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(": ").concat(name)); + } + } + + void fillUpFlowPaneWithCurrencies(boolean isEditable, FlowPane flowPane, + TradeCurrency e, PaymentAccount paymentAccount) { + CheckBox checkBox = new AutoTooltipCheckBox(e.getCode()); + checkBox.setMouseTransparent(!isEditable); + checkBox.setSelected(paymentAccount.getTradeCurrencies().contains(e)); + checkBox.setMinWidth(60); + checkBox.setMaxWidth(checkBox.getMinWidth()); + checkBox.setTooltip(new Tooltip(e.getName())); + checkBox.setOnAction(event -> { + if (checkBox.isSelected()) + paymentAccount.addCurrency(e); + else + paymentAccount.removeCurrency(e); + + updateAllInputsValid(); + }); + flowPane.getChildren().add(checkBox); + } + + void fillUpFlowPaneWithCountries(List checkBoxList, FlowPane flowPane, Country country) { + final String countryCode = country.code; + CheckBox checkBox = new AutoTooltipCheckBox(countryCode); + checkBox.setUserData(countryCode); + checkBoxList.add(checkBox); + checkBox.setMouseTransparent(false); + checkBox.setMinWidth(45); + checkBox.setMaxWidth(45); + checkBox.setTooltip(new Tooltip(country.name)); + checkBox.setOnAction(event -> { + if (checkBox.isSelected()) { + addAcceptedCountry(countryCode); + } else { + removeAcceptedCountry(countryCode); + } + + updateAllInputsValid(); + }); + flowPane.getChildren().add(checkBox); + } + + protected abstract void autoFillNameTextField(); + + public abstract void addFormForAddAccount(); + + public abstract void addFormForDisplayAccount(); + + protected abstract void updateAllInputsValid(); + + public void updateFromInputs() { + autoFillNameTextField(); + updateAllInputsValid(); + } + + public boolean isAccountNameValid() { + return inputValidator.validate(paymentAccount.getAccountName()).isValid; + } + + public int getGridRow() { + return gridRow; + } + + public int getRowSpan() { + return gridRow - gridRowFrom + 2; + } + + public PaymentAccount getPaymentAccount() { + return paymentAccount; + } + + public BooleanProperty allInputsValidProperty() { + return allInputsValid; + } + + void removeAcceptedCountry(String countryCode) { + } + + void addAcceptedCountry(String countryCode) { + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PerfectMoneyForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PerfectMoneyForm.java new file mode 100644 index 0000000000..b91e036d79 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PerfectMoneyForm.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.util.validation.PerfectMoneyValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PerfectMoneyAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PerfectMoneyAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class PerfectMoneyForm extends GeneralAccountNumberForm { + + private final PerfectMoneyAccount perfectMoneyAccount; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((PerfectMoneyAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + public PerfectMoneyForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PerfectMoneyValidator perfectMoneyValidator, InputValidator inputValidator, GridPane gridPane, int + gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.perfectMoneyAccount = (PerfectMoneyAccount) paymentAccount; + } + + @Override + public void addTradeCurrency() { + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(new FiatCurrency("USD"), new FiatCurrency("EUR"))); + } + + @Override + void setAccountNumber(String newValue) { + perfectMoneyAccount.setAccountNr(newValue); + } + + @Override + String getAccountNr() { + return perfectMoneyAccount.getAccountNr(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java new file mode 100644 index 0000000000..7265ff3a30 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java @@ -0,0 +1,113 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.PopmoneyValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PopmoneyAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PopmoneyAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class PopmoneyForm extends PaymentMethodForm { + private final PopmoneyAccount account; + private final PopmoneyValidator validator; + private InputTextField accountIdInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((PopmoneyAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.popmoney.accountId"), ((PopmoneyAccountPayload) paymentAccountPayload).getAccountId()); + return gridRow; + } + + public PopmoneyForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PopmoneyValidator popmoneyValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PopmoneyAccount) paymentAccount; + this.validator = popmoneyValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId")); + accountIdInputTextField.setValidator(validator); + accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setAccountId(newValue.trim()); + updateFromInputs(); + }); + + final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountIdInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + account.getHolderName()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId"), account.getAccountId()).second; + field.setMouseTransparent(false); + final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getHolderName()).isValid + && validator.validate(account.getAccountId()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java new file mode 100644 index 0000000000..3f5e02a37c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java @@ -0,0 +1,106 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.PromptPayValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PromptPayAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PromptPayAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class PromptPayForm extends PaymentMethodForm { + private final PromptPayAccount promptPayAccount; + private final PromptPayValidator promptPayValidator; + private InputTextField promptPayIdInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId"), + ((PromptPayAccountPayload) paymentAccountPayload).getPromptPayId()); + return gridRow; + } + + public PromptPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PromptPayValidator promptPayValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.promptPayAccount = (PromptPayAccount) paymentAccount; + this.promptPayValidator = promptPayValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + promptPayIdInputTextField = addInputTextField(gridPane, ++gridRow, + Res.get("payment.promptPay.promptPayId")); + promptPayIdInputTextField.setValidator(promptPayValidator); + promptPayIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + promptPayAccount.setPromptPayId(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = promptPayAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(promptPayIdInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + promptPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(promptPayAccount.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId"), + promptPayAccount.getPromptPayId()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = promptPayAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && promptPayValidator.validate(promptPayAccount.getPromptPayId()).isValid + && promptPayAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java new file mode 100644 index 0000000000..8ed82f821e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -0,0 +1,132 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.RevolutValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.RevolutAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@Slf4j +public class RevolutForm extends PaymentMethodForm { + private final RevolutAccount account; + private RevolutValidator validator; + private InputTextField userNameInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + Tuple2 tuple = ((RevolutAccountPayload) paymentAccountPayload).getRecipientsAccountData(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, tuple.first, tuple.second); + return gridRow; + } + + public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (RevolutAccount) paymentAccount; + this.validator = revolutValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + userNameInputTextField.setValidator(validator); + userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setUserName(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrencies"), Layout.FLOATING_LABEL_DISTANCE * 3, + Layout.FLOATING_LABEL_DISTANCE * 3).second; + + if (isEditable) + flowPane.setId("flow-pane-checkboxes-bg"); + else + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + + CurrencyUtil.getAllRevolutCurrencies().forEach(e -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(userNameInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + + String userName = account.getUserName(); + TextField userNameTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; + userNameTf.setMouseTransparent(false); + + if (account.hasOldAccountId()) { + String accountId = account.getAccountId(); + TextField accountIdTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.phoneNr"), accountId).second; + accountIdTf.setMouseTransparent(false); + } + + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && validator.validate(account.getUserName()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SameBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SameBankForm.java new file mode 100644 index 0000000000..17ae884a29 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SameBankForm.java @@ -0,0 +1,38 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class SameBankForm extends BankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public SameBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java new file mode 100644 index 0000000000..b4e74e47bf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java @@ -0,0 +1,217 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.IBANValidator; +import bisq.desktop.util.normalization.IBANNormalizer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SepaAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import java.util.List; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class SepaForm extends GeneralSepaForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + SepaAccountPayload sepaAccountPayload = (SepaAccountPayload) paymentAccountPayload; + + final String title = Res.get("payment.account.owner"); + final String value = sepaAccountPayload.getHolderName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value); + + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, + Res.get("payment.bank.country"), + CountryUtil.getNameAndCode(sepaAccountPayload.getCountryCode())); + // IBAN, BIC will not be translated + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaAccountPayload.getIban()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaAccountPayload.getBic()); + return gridRow; + } + + private final SepaAccount sepaAccount; + private final IBANValidator ibanValidator; + private final BICValidator bicValidator; + + public SepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, IBANValidator ibanValidator, + BICValidator bicValidator, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.sepaAccount = (SepaAccount) paymentAccount; + this.ibanValidator = ibanValidator; + this.bicValidator = bicValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setHolderName(newValue); + updateFromInputs(); + }); + + ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); + ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); + ibanInputTextField.setValidator(ibanValidator); + ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setIban(newValue); + updateFromInputs(); + + }); + InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); + bicInputTextField.setValidator(bicValidator); + bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setBic(newValue); + updateFromInputs(); + + }); + + ComboBox countryComboBox = addCountrySelection(); + + setCountryComboBoxAction(countryComboBox, sepaAccount); + + addEuroCountriesGrid(); + addNonEuroCountriesGrid(); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllSepaCountries())); + Country country = CountryUtil.getDefaultCountry(); + if (CountryUtil.getAllSepaCountries().contains(country)) { + countryComboBox.getSelectionModel().select(country); + sepaAccount.setCountry(country); + } + + updateFromInputs(); + } + + + private void addEuroCountriesGrid() { + addCountriesGrid(Res.get("payment.accept.euro"), euroCountryCheckBoxes, + CountryUtil.getAllSepaEuroCountries()); + } + + private void addNonEuroCountriesGrid() { + addCountriesGrid(Res.get("payment.accept.nonEuro"), nonEuroCountryCheckBoxes, + CountryUtil.getAllSepaNonEuroCountries()); + } + + @Override + void updateCountriesSelection(List checkBoxList) { + checkBoxList.forEach(checkBox -> { + String countryCode = (String) checkBox.getUserData(); + TradeCurrency selectedCurrency = sepaAccount.getSelectedTradeCurrency(); + if (selectedCurrency == null) { + Country country = CountryUtil.getDefaultCountry(); + if (CountryUtil.getAllSepaCountries().contains(country)) + selectedCurrency = CurrencyUtil.getCurrencyByCountryCode(country.code); + } + + boolean selected; + if (selectedCurrency != null) { + selected = true; + sepaAccount.addAcceptedCountry(countryCode); + } else { + selected = sepaAccount.getAcceptedCountryCodes().contains(countryCode); + } + checkBox.setSelected(selected); + }); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && bicValidator.validate(sepaAccount.getBic()).isValid + && ibanValidator.validate(sepaAccount.getIban()).isValid + && inputValidator.validate(sepaAccount.getHolderName()).isValid + && sepaAccount.getAcceptedCountryCodes().size() > 0 + && sepaAccount.getSingleTradeCurrency() != null + && sepaAccount.getCountry() != null); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), sepaAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(sepaAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), sepaAccount.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, IBAN, sepaAccount.getIban()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, BIC, sepaAccount.getBic()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.country"), + sepaAccount.getCountry() != null ? sepaAccount.getCountry().name : ""); + TradeCurrency singleTradeCurrency = sepaAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + String countries; + Tooltip tooltip = null; + if (CountryUtil.containsAllSepaEuroCountries(sepaAccount.getAcceptedCountryCodes())) { + countries = Res.get("shared.allEuroCountries"); + } else { + countries = CountryUtil.getCodesString(sepaAccount.getAcceptedCountryCodes()); + tooltip = new Tooltip(CountryUtil.getNamesByCodesString(sepaAccount.getAcceptedCountryCodes())); + } + TextField acceptedCountries = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.countries"), countries).second; + if (tooltip != null) { + acceptedCountries.setMouseTransparent(false); + acceptedCountries.setTooltip(tooltip); + } + addLimitations(true); + } + + @Override + void removeAcceptedCountry(String countryCode) { + sepaAccount.removeAcceptedCountry(countryCode); + } + + @Override + void addAcceptedCountry(String countryCode) { + sepaAccount.addAcceptedCountry(countryCode); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java new file mode 100644 index 0000000000..36de590229 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java @@ -0,0 +1,216 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.IBANValidator; +import bisq.desktop.util.normalization.IBANNormalizer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SepaInstantAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SepaInstantAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import java.util.List; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class SepaInstantForm extends GeneralSepaForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + SepaInstantAccountPayload sepaInstantAccountPayload = (SepaInstantAccountPayload) paymentAccountPayload; + + final String title = Res.get("payment.account.owner"); + final String value = sepaInstantAccountPayload.getHolderName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value); + + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, + Res.get("payment.bank.country"), + CountryUtil.getNameAndCode(sepaInstantAccountPayload.getCountryCode())); + // IBAN, BIC will not be translated + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaInstantAccountPayload.getIban()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaInstantAccountPayload.getBic()); + return gridRow; + } + + private final SepaInstantAccount sepaInstantAccount; + private final IBANValidator ibanValidator; + private final BICValidator bicValidator; + + public SepaInstantForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, IBANValidator ibanValidator, + BICValidator bicValidator, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.sepaInstantAccount = (SepaInstantAccount) paymentAccount; + this.ibanValidator = ibanValidator; + this.bicValidator = bicValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaInstantAccount.setHolderName(newValue); + updateFromInputs(); + }); + + ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); + ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); + ibanInputTextField.setValidator(ibanValidator); + ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaInstantAccount.setIban(newValue); + updateFromInputs(); + + }); + InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); + bicInputTextField.setValidator(bicValidator); + bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaInstantAccount.setBic(newValue); + updateFromInputs(); + + }); + + ComboBox countryComboBox = addCountrySelection(); + + setCountryComboBoxAction(countryComboBox, sepaInstantAccount); + + addEuroCountriesGrid(); + addNonEuroCountriesGrid(); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllSepaInstantCountries())); + Country country = CountryUtil.getDefaultCountry(); + if (CountryUtil.getAllSepaInstantCountries().contains(country)) { + countryComboBox.getSelectionModel().select(country); + sepaInstantAccount.setCountry(country); + } + + updateFromInputs(); + } + + private void addEuroCountriesGrid() { + addCountriesGrid(Res.get("payment.accept.euro"), euroCountryCheckBoxes, + CountryUtil.getAllSepaInstantEuroCountries()); + } + + private void addNonEuroCountriesGrid() { + addCountriesGrid(Res.get("payment.accept.nonEuro"), nonEuroCountryCheckBoxes, + CountryUtil.getAllSepaInstantNonEuroCountries()); + } + + @Override + void updateCountriesSelection(List checkBoxList) { + checkBoxList.forEach(checkBox -> { + String countryCode = (String) checkBox.getUserData(); + TradeCurrency selectedCurrency = sepaInstantAccount.getSelectedTradeCurrency(); + if (selectedCurrency == null) { + Country country = CountryUtil.getDefaultCountry(); + if (CountryUtil.getAllSepaInstantCountries().contains(country)) + selectedCurrency = CurrencyUtil.getCurrencyByCountryCode(country.code); + } + + boolean selected; + if (selectedCurrency != null) { + selected = true; + sepaInstantAccount.addAcceptedCountry(countryCode); + } else { + selected = sepaInstantAccount.getAcceptedCountryCodes().contains(countryCode); + } + checkBox.setSelected(selected); + }); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && bicValidator.validate(sepaInstantAccount.getBic()).isValid + && ibanValidator.validate(sepaInstantAccount.getIban()).isValid + && inputValidator.validate(sepaInstantAccount.getHolderName()).isValid + && sepaInstantAccount.getAcceptedCountryCodes().size() > 0 + && sepaInstantAccount.getSingleTradeCurrency() != null + && sepaInstantAccount.getCountry() != null); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), sepaInstantAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(sepaInstantAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), sepaInstantAccount.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, IBAN, sepaInstantAccount.getIban()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, BIC, sepaInstantAccount.getBic()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.country"), + sepaInstantAccount.getCountry() != null ? sepaInstantAccount.getCountry().name : ""); + TradeCurrency singleTradeCurrency = sepaInstantAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + String countries; + Tooltip tooltip = null; + if (CountryUtil.containsAllSepaInstantEuroCountries(sepaInstantAccount.getAcceptedCountryCodes())) { + countries = Res.get("shared.allEuroCountries"); + } else { + countries = CountryUtil.getCodesString(sepaInstantAccount.getAcceptedCountryCodes()); + tooltip = new Tooltip(CountryUtil.getNamesByCodesString(sepaInstantAccount.getAcceptedCountryCodes())); + } + TextField acceptedCountries = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.countries"), countries).second; + if (tooltip != null) { + acceptedCountries.setMouseTransparent(false); + acceptedCountries.setTooltip(tooltip); + } + addLimitations(true); + } + + @Override + void removeAcceptedCountry(String countryCode) { + sepaInstantAccount.removeAcceptedCountry(countryCode); + } + + @Override + void addAcceptedCountry(String countryCode) { + sepaInstantAccount.addAcceptedCountry(countryCode); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SpecificBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SpecificBankForm.java new file mode 100644 index 0000000000..614053ebe0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SpecificBankForm.java @@ -0,0 +1,117 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SpecificBanksAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple3; + +import com.google.common.base.Joiner; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.beans.binding.Bindings; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelInputTextFieldButton; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldButton; + +public class SpecificBankForm extends BankForm { + private final SpecificBanksAccountPayload specificBanksAccountPayload; + private TextField acceptedBanksTextField; + private Tooltip acceptedBanksTooltip; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public SpecificBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.specificBanksAccountPayload = (SpecificBanksAccountPayload) paymentAccount.paymentAccountPayload; + } + + @Override + protected void addAcceptedBanksForAddAccount() { + Tuple3 addBankTuple = addTopLabelInputTextFieldButton(gridPane, ++gridRow, + Res.get("payment.nameOfAcceptedBank"), Res.get("payment.addAcceptedBank")); + InputTextField addBankInputTextField = addBankTuple.second; + Button addButton = addBankTuple.third; + addButton.setMinWidth(200); + addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> addBankInputTextField.getText().isEmpty(), + addBankInputTextField.textProperty())); + + Tuple3 acceptedBanksTuple = addTopLabelTextFieldButton(gridPane, ++gridRow, + Res.get("payment.accepted.banks"), Res.get("payment.clearAcceptedBanks")); + acceptedBanksTextField = acceptedBanksTuple.second; + acceptedBanksTextField.setMouseTransparent(false); + acceptedBanksTooltip = new Tooltip(); + acceptedBanksTextField.setTooltip(acceptedBanksTooltip); + Button clearButton = acceptedBanksTuple.third; + clearButton.setMinWidth(200); + clearButton.setDefaultButton(false); + clearButton.disableProperty().bind(Bindings.createBooleanBinding(() -> acceptedBanksTextField.getText().isEmpty(), acceptedBanksTextField.textProperty())); + addButton.setOnAction(e -> { + specificBanksAccountPayload.addAcceptedBank(addBankInputTextField.getText()); + addBankInputTextField.setText(""); + String value = Joiner.on(", ").join(specificBanksAccountPayload.getAcceptedBanks()); + acceptedBanksTextField.setText(value); + acceptedBanksTooltip.setText(value); + updateAllInputsValid(); + }); + + clearButton.setOnAction(e -> resetAcceptedBanks()); + } + + private void resetAcceptedBanks() { + specificBanksAccountPayload.clearAcceptedBanks(); + acceptedBanksTextField.setText(""); + acceptedBanksTooltip.setText(""); + updateAllInputsValid(); + } + + @Override + protected void onCountryChanged() { + resetAcceptedBanks(); + } + + @Override + public void addAcceptedBanksForDisplayAccount() { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.banks"), + Joiner.on(", ").join(specificBanksAccountPayload.getAcceptedBanks())).second.setMouseTransparent(false); + } + + @Override + public void updateAllInputsValid() { + super.updateAllInputsValid(); + allInputsValid.set(allInputsValid.get() && inputValidator.validate(acceptedBanksTextField.getText()).isValid); + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java new file mode 100644 index 0000000000..cb138c84fc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java @@ -0,0 +1,131 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.SwishValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SwishAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SwishAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@Slf4j +public class SwishForm extends PaymentMethodForm { + private final SwishAccount swishAccount; + private final SwishValidator swishValidator; + private InputTextField mobileNrInputTextField; + + public SwishForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + SwishValidator swishValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.swishAccount = (SwishAccount) paymentAccount; + this.swishValidator = swishValidator; + } + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((SwishAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), + ((SwishAccountPayload) paymentAccountPayload).getMobileNr()); + return gridRow; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + swishAccount.setHolderName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(swishValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + swishAccount.setMobileNr(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = swishAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + swishAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(swishAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + swishAccount.getHolderName()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), + swishAccount.getMobileNr()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = swishAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + if (swishValidator.validate(swishAccount.getMobileNr()).isValid) { + swishAccount.setMobileNr(swishValidator.getNormalizedPhoneNumber()); + } + allInputsValid.set(isAccountNameValid() + && swishValidator.validate(swishAccount.getMobileNr()).isValid + && inputValidator.validate(swishAccount.getHolderName()).isValid + && swishAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java new file mode 100644 index 0000000000..a9212cd224 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.TransferwiseValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.TransferwiseAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.TransferwiseAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class TransferwiseForm extends PaymentMethodForm { + private final TransferwiseAccount account; + private TransferwiseValidator validator; + private InputTextField emailInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((TransferwiseAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public TransferwiseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + TransferwiseValidator validator, InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (TransferwiseAccount) paymentAccount; + this.validator = validator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + 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"); + } + + CurrencyUtil.getAllTransferwiseCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(emailInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + 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() + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java new file mode 100644 index 0000000000..dc51aaca3c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java @@ -0,0 +1,127 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.USPostalMoneyOrderValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.*; + +public class USPostalMoneyOrderForm extends PaymentMethodForm { + private final USPostalMoneyOrderAccount usPostalMoneyOrderAccount; + private final USPostalMoneyOrderValidator usPostalMoneyOrderValidator; + private TextArea postalAddressTextArea; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getHolderName()); + TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; + textArea.setMinHeight(70); + textArea.setEditable(false); + textArea.setId("text-area-disabled"); + textArea.setText(((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getPostalAddress()); + return gridRow; + } + + public USPostalMoneyOrderForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, USPostalMoneyOrderValidator usPostalMoneyOrderValidator, + InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.usPostalMoneyOrderAccount = (USPostalMoneyOrderAccount) paymentAccount; + this.usPostalMoneyOrderValidator = usPostalMoneyOrderValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + usPostalMoneyOrderAccount.setHolderName(newValue); + updateFromInputs(); + }); + + postalAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.postal.address"), "").second; + postalAddressTextArea.setMinHeight(70); + //postalAddressTextArea.setValidator(usPostalMoneyOrderValidator); + postalAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + usPostalMoneyOrderAccount.setPostalAddress(newValue); + updateFromInputs(); + }); + + + TradeCurrency singleTradeCurrency = usPostalMoneyOrderAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(postalAddressTextArea.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + usPostalMoneyOrderAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(usPostalMoneyOrderAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + usPostalMoneyOrderAccount.getHolderName()); + TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; + textArea.setText(usPostalMoneyOrderAccount.getPostalAddress()); + textArea.setMinHeight(70); + textArea.setEditable(false); + TradeCurrency singleTradeCurrency = usPostalMoneyOrderAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && usPostalMoneyOrderValidator.validate(usPostalMoneyOrderAccount.getPostalAddress()).isValid + && !postalAddressTextArea.getText().isEmpty() + && inputValidator.validate(usPostalMoneyOrderAccount.getHolderName()).isValid + && usPostalMoneyOrderAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java new file mode 100644 index 0000000000..e3c4f03248 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.UpholdValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.UpholdAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.UpholdAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class UpholdForm extends PaymentMethodForm { + private final UpholdAccount upholdAccount; + private UpholdValidator upholdValidator; + private InputTextField accountIdInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), + ((UpholdAccountPayload) paymentAccountPayload).getAccountId()); + return gridRow; + } + + public UpholdForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + UpholdValidator upholdValidator, InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.upholdAccount = (UpholdAccount) paymentAccount; + this.upholdValidator = upholdValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId")); + accountIdInputTextField.setValidator(upholdValidator); + accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + upholdAccount.setAccountId(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrencies"), 0).second; + + if (isEditable) + flowPane.setId("flow-pane-checkboxes-bg"); + else + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + + CurrencyUtil.getAllUpholdCurrencies().forEach(e -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, upholdAccount)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(accountIdInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + upholdAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(upholdAccount.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), + upholdAccount.getAccountId()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && upholdValidator.validate(upholdAccount.getAccountId()).isValid + && upholdAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/WeChatPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WeChatPayForm.java new file mode 100644 index 0000000000..8086e42f9a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WeChatPayForm.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.util.validation.WeChatPayValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.WeChatPayAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.WeChatPayAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class WeChatPayForm extends GeneralAccountNumberForm { + + private final WeChatPayAccount weChatPayAccount; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((WeChatPayAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + public WeChatPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, WeChatPayValidator weChatPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.weChatPayAccount = (WeChatPayAccount) paymentAccount; + } + + @Override + void setAccountNumber(String newValue) { + weChatPayAccount.setAccountNr(newValue); + } + + @Override + String getAccountNr() { + return weChatPayAccount.getAccountNr(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java new file mode 100644 index 0000000000..b00ebdbe36 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java @@ -0,0 +1,205 @@ +/* + * 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 bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.WesternUnionAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@Slf4j +public class WesternUnionForm extends PaymentMethodForm { + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + final WesternUnionAccountPayload payload = (WesternUnionAccountPayload) paymentAccountPayload; + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.fullName"), + payload.getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email"), + payload.getEmail()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.city"), + payload.getCity()); + if (BankUtil.isStateRequired(payload.getCountryCode())) + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.state"), + payload.getState()); + + return gridRow; + } + + private final WesternUnionAccountPayload westernUnionAccountPayload; + private InputTextField holderNameInputTextField; + private InputTextField cityInputTextField; + private InputTextField stateInputTextField; + private final EmailValidator emailValidator; + private Country selectedCountry; + + public WesternUnionForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.westernUnionAccountPayload = (WesternUnionAccountPayload) paymentAccount.paymentAccountPayload; + + emailValidator = new EmailValidator(); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), + nameAndCode); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.fullName"), + westernUnionAccountPayload.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.city"), + westernUnionAccountPayload.getCity()).second.setMouseTransparent(false); + if (BankUtil.isStateRequired(westernUnionAccountPayload.getCountryCode())) + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.state"), + westernUnionAccountPayload.getState()).second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + westernUnionAccountPayload.getEmail()); + addLimitations(true); + } + + private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { + FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); + applyTradeCurrency(tradeCurrency, defaultCurrency); + } + + private void onCountrySelected(Country country) { + selectedCountry = country; + if (country != null) { + getCountryBasedPaymentAccount().setCountry(country); + String countryCode = country.code; + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); + paymentAccount.setSingleTradeCurrency(currency); + currencyComboBox.setDisable(false); + currencyComboBox.getSelectionModel().select(currency); + updateFromInputs(); + applyIsStateRequired(); + cityInputTextField.setText(""); + stateInputTextField.setText(""); + } + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); + currencyComboBox = tuple.first; + gridRow = tuple.second; + + holderNameInputTextField = FormBuilder.addInputTextField(gridPane, + ++gridRow, Res.get("payment.account.fullName")); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + westernUnionAccountPayload.setHolderName(newValue); + updateFromInputs(); + }); + holderNameInputTextField.setValidator(inputValidator); + + cityInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.city")); + cityInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + westernUnionAccountPayload.setCity(newValue); + updateFromInputs(); + + }); + + stateInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.state")); + stateInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + westernUnionAccountPayload.setState(newValue); + updateFromInputs(); + + }); + applyIsStateRequired(); + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + westernUnionAccountPayload.setEmail(newValue); + updateFromInputs(); + }); + emailInputTextField.setValidator(emailValidator); + + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + + updateFromInputs(); + } + + private void applyIsStateRequired() { + final boolean stateRequired = BankUtil.isStateRequired(westernUnionAccountPayload.getCountryCode()); + stateInputTextField.setManaged(stateRequired); + stateInputTextField.setVisible(stateRequired); + } + + private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { + return (CountryBasedPaymentAccount) this.paymentAccount; + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + accountNameTextField.setText(Res.get(paymentAccount.getPaymentMethod().getId()) + .concat(": ") + .concat(StringUtils.abbreviate(holderNameInputTextField.getText(), 9))); + } + } + + @Override + public void updateAllInputsValid() { + boolean result = isAccountNameValid() + && paymentAccount.getSingleTradeCurrency() != null + && getCountryBasedPaymentAccount().getCountry() != null + && inputValidator.validate(westernUnionAccountPayload.getHolderName()).isValid + && inputValidator.validate(westernUnionAccountPayload.getCity()).isValid + && emailValidator.validate(westernUnionAccountPayload.getEmail()).isValid; + allInputsValid.set(result); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java new file mode 100644 index 0000000000..749912a7a5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java @@ -0,0 +1,885 @@ +/* + * 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 bisq.desktop.components.paymentmethods.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import com.google.common.collect.ImmutableMap; + +import bisq.core.locale.Country; +import bisq.desktop.util.GUIUtil; + +/* + Japan's National Banking Association assigns 4 digit codes to all + Financial Institutions, so we use that as the primary "Bank ID", + add the English names for the top ~30 major international banks, + and remove local farmers agricultural cooperative associations + to keep the list to a reasonable size. Please update annually. + + Source: Zengin Net list of Financial Institutions + Last Updated: July 16, 2019 + URL: https://www.zengin-net.jp/company/member/ + PDF: https://www.zengin-net.jp/company/pdf/member1.pdf + PDF: https://www.zengin-net.jp/company/pdf/member2.pdf + + Source: Bank of Japan list of Financial Institutions + Last Updated: July 16, 2019 + URL: https://www5.boj.or.jp/bojnet/codenew/mokujinew.htm + File: code1_20190716.xlsx + Excel sheet: 金融機関等コード一覧 +*/ + +public class JapanBankData +{ + /* + Returns the main list of ~500 banks in Japan with bank codes, + but since 90%+ of people will be using one of ~30 major banks, + we hard-code those at the top for easier pull-down selection, + and add their English names in parenthesis for foreigners. + */ + public static List prettyPrintBankList() // {{{ + { + List prettyList = new ArrayList(); + + // add mega banks at the top + for (Map.Entry bank: megaBanksEnglish.entrySet()) + { + String bankId = bank.getKey(); + String bankNameEn = bank.getValue(); + String bankNameJa = majorBanksJapanese.get(bankId); + if (bankNameJa == null) bankNameJa = minorBanksJapanese.get(bankId); + prettyList.add(prettyPrintMajorBank(bankId, bankNameJa, bankNameEn)); + } + + // append the major banks next + for (Map.Entry bank : majorBanksJapanese.entrySet()) + { + String bankId = bank.getKey(); + String bankNameJa = bank.getValue(); + // avoid duplicates + if (megaBanksEnglish.get(bankId) != null) continue; + prettyList.add(prettyPrintBank(bankId, bankNameJa)); + } + + // append the minor local banks last + for (Map.Entry bank : minorBanksJapanese.entrySet()) + { + String bankId = bank.getKey(); + String bankNameJa = bank.getValue(); + prettyList.add(prettyPrintBank(bankId, bankNameJa)); + } + + return prettyList; + } // }}} + + // Pretty print major banks like this: (0001) みずほ (Mizuho Bank) + private static String prettyPrintMajorBank(String bankId, String bankNameJa, String bankNameEn) // {{{ + { + return ID_OPEN + bankId + ID_CLOSE + SPACE + + JA_OPEN + bankNameJa + JA_CLOSE + SPACE + + EN_OPEN + bankNameEn + EN_CLOSE; + } // }}} + // Pretty print other banks like this: (9524) みずほ証券 + private static String prettyPrintBank(String bankId, String bankName) // {{{ + { + return ID_OPEN + bankId + ID_CLOSE + SPACE + + JA_OPEN + bankName + JA_CLOSE; + } // }}} + + // top 30 mega banks with english + private static final Map megaBanksEnglish = ImmutableMap. builder() + // {{{ japan post office + .put("9900", "Japan Post Bank Yucho") + // }}} + // {{{ japan mega-banks + .put("0001", "Mizuho Bank") + .put("0005", "Mitsubishi UFJ Bank (MUFG)") + .put("0009", "Sumitomo Mitsui Banking Corporation (SMBC)") + .put("0010", "Resona Bank") + // }}} + // {{{ major online banks + .put("0033", "Japan Net Bank") + .put("0034", "Seven Bank (7-11)") + .put("0035", "Sony Bank") + .put("0036", "Rakuten Bank") + .put("0038", "SBI Sumishin Net Bank") + .put("0039", "Jibun Bank") + .put("0040", "Aeon Bank") + .put("0042", "Lawson Bank") + // }}} + // {{{ major trust banks, etc. + .put("0150", "Suruga Bank") + .put("0288", "Mitsubishi UFJ Trust Bank") + .put("0289", "Mizuho Trust Bank") + .put("0294", "Sumitomo Trust Bank") + .put("0300", "SMBC Trust Bank (PRESTIA)") + .put("0304", "Nomura Trust Bank") + .put("0307", "Orix Trust Bank") + .put("0310", "GMO Aozora Net Bank") + .put("0321", "Japan Securities Trust Bank") + .put("0397", "Shinsei Bank") + .put("0398", "Aozora Bank") + .put("0402", "JP Morgan Chase Bank") + .put("0442", "BNY Mellon") + .put("0458", "DBS Bank") + .put("0472", "SBJ Shinhan Bank Japan") + // }}} + .build(); + + // major ~200 banks + private static final Map majorBanksJapanese = ImmutableMap. builder() + // {{{ ゆうちょ銀行 (9900) + .put("9900", "ゆうちょ銀行") + // }}} + // {{{ 都市銀行 (0001 ~ 0029) + .put("0001", "みずほ銀行") + .put("0005", "三菱UFJ銀行") + .put("0009", "三井住友銀行") + .put("0010", "りそな銀行") + .put("0017", "埼玉りそな銀行") + // }}} + // {{{ ネット専業銀行等 (0030 ~ 0049) + .put("0033", "ジャパンネット銀行") + .put("0034", "セブン銀行") + .put("0035", "ソニー銀行") + .put("0036", "楽天銀行") + .put("0038", "住信SBIネット銀行") + .put("0039", "じぶん銀行") + .put("0040", "イオン銀行") + .put("0041", "大和ネクスト銀行") + .put("0042", "ローソン銀行") + // }}} + // {{{ 協会 (0050 ~ 0099) + .put("0051", "全銀協") + .put("0052", "横浜銀行協会") + .put("0053", "釧路銀行協会") + .put("0054", "札幌銀行協会") + .put("0056", "函館銀行協会") + .put("0057", "青森銀行協会") + .put("0058", "秋田銀行協会") + .put("0059", "宮城銀行協会") + .put("0060", "福島銀行協会") + .put("0061", "群馬銀行協会") + .put("0062", "新潟銀行協会") + .put("0063", "石川銀行協会") + .put("0064", "山梨銀行協会") + .put("0065", "長野銀行協会") + .put("0066", "静岡銀行協会") + .put("0067", "名古屋銀行協会") + .put("0068", "京都銀行協会") + .put("0069", "大阪銀行協会") + .put("0070", "神戸銀行協会") + .put("0071", "岡山銀行協会") + .put("0072", "広島銀行協会") + .put("0073", "島根銀行協会") + .put("0074", "山口銀行協会") + .put("0075", "香川銀行協会") + .put("0076", "愛媛銀行協会") + .put("0077", "高知銀行協会") + .put("0078", "北九州銀行協会") + .put("0079", "福岡銀行協会") + .put("0080", "大分銀行協会") + .put("0081", "長崎銀行協会") + .put("0082", "熊本銀行協会") + .put("0083", "鹿児島銀行協会") + .put("0084", "沖縄銀行協会") + .put("0090", "全銀ネット") + .put("0095", "CLSBANK") + // }}} + // {{{ 地方銀行 (0116 ~ 0190) + .put("0116", "北海道銀行") + .put("0117", "青森銀行") + .put("0118", "みちのく銀行") + .put("0119", "秋田銀行") + .put("0120", "北都銀行") + .put("0121", "荘内銀行") + .put("0122", "山形銀行") + .put("0123", "岩手銀行") + .put("0124", "東北銀行") + .put("0125", "七十七銀行") + .put("0126", "東邦銀行") + .put("0128", "群馬銀行") + .put("0129", "足利銀行") + .put("0130", "常陽銀行") + .put("0131", "筑波銀行") + .put("0133", "武蔵野銀行") + .put("0134", "千葉銀行") + .put("0135", "千葉興業銀行") + .put("0137", "きらぼし銀行") + .put("0138", "横浜銀行") + .put("0140", "第四銀行") + .put("0141", "北越銀行") + .put("0142", "山梨中央銀行") + .put("0143", "八十二銀行") + .put("0144", "北陸銀行") + .put("0145", "富山銀行") + .put("0146", "北國銀行") + .put("0147", "福井銀行") + .put("0149", "静岡銀行") + .put("0150", "スルガ銀行") + .put("0151", "清水銀行") + .put("0152", "大垣共立銀行") + .put("0153", "十六銀行") + .put("0154", "三重銀行") + .put("0155", "百五銀行") + .put("0157", "滋賀銀行") + .put("0158", "京都銀行") + .put("0159", "関西みらい銀行") + .put("0161", "池田泉州銀行") + .put("0162", "南都銀行") + .put("0163", "紀陽銀行") + .put("0164", "但馬銀行") + .put("0166", "鳥取銀行") + .put("0167", "山陰合同銀行") + .put("0168", "中国銀行") + .put("0169", "広島銀行") + .put("0170", "山口銀行") + .put("0172", "阿波銀行") + .put("0173", "百十四銀行") + .put("0174", "伊予銀行") + .put("0175", "四国銀行") + .put("0177", "福岡銀行") + .put("0178", "筑邦銀行") + .put("0179", "佐賀銀行") + .put("0180", "十八銀行") + .put("0181", "親和銀行") + .put("0182", "肥後銀行") + .put("0183", "大分銀行") + .put("0184", "宮崎銀行") + .put("0185", "鹿児島銀行") + .put("0187", "琉球銀行") + .put("0188", "沖縄銀行") + .put("0190", "西日本シティ銀行") + .put("0191", "北九州銀行") + // }}} + // {{{ 信託銀行 (0288 ~ 0326) + .put("0288", "三菱UFJ信託銀行") + .put("0289", "みずほ信託銀行") + .put("0294", "三井住友信託銀行") + .put("0295", "BNYM信託") + .put("0297", "日本マスタートラスト信託銀行") + .put("0299", "ステート信託") + .put("0300", "SMBC信託銀行 プレスティア") + .put("0304", "野村信託銀行") + .put("0307", "オリックス銀行") + .put("0310", "GMOあおぞらネット銀行") + .put("0311", "農中信託") + .put("0320", "新生信託") + .put("0321", "日証金信託") + .put("0324", "日本トラスティサービス信託銀行") + .put("0325", "資産管理サービス信託銀行") + // }}} + // {{{ 旧長期信用銀行 (0397 ~ 0398) + .put("0397", "新生銀行") + .put("0398", "あおぞら銀行") + // }}} + // {{{ foreign banks (0400 ~ 0497) + .put("0401", "シティバンク、エヌ・エイ 銀行") + .put("0402", "JPモルガン・チェース銀行") + .put("0403", "アメリカ銀行") + .put("0411", "香港上海銀行") + .put("0413", "スタンチヤート") + .put("0414", "バークレイズ") + .put("0421", "アグリコル") + .put("0423", "ハナ") + .put("0424", "印度") + .put("0425", "兆豐國際商銀") + .put("0426", "バンコツク") + .put("0429", "バンクネガラ") + .put("0430", "ドイツ銀行") + .put("0432", "ブラジル") + .put("0438", "ユーオバシーズ") + .put("0439", "ユービーエス") + .put("0442", "BNYメロン") + .put("0443", "ビー・エヌ・ピー・パリバ銀行") + .put("0444", "チヤイニーズ") + .put("0445", "ソシエテ") + .put("0456", "ユバフ") + .put("0458", "DBS") + .put("0459", "パキスタン") + .put("0460", "クレデイスイス") + .put("0461", "コメルツ銀行") + .put("0463", "ウニクレデイト") + .put("0468", "インドステイト") + .put("0471", "カナダロイヤル") + .put("0472", "SBJ銀行") + .put("0477", "ウリイ") + .put("0482", "アイエヌジー") + .put("0484", "ナツトオース") + .put("0485", "アンズバンク") + .put("0487", "コモンウエルス") + .put("0489", "バンクチヤイナ") + .put("0495", "ステストリート") + .put("0498", "中小企業") + // }}} + // {{{ 第二地方銀行 (0501 ~ 0597) + .put("0501", "北洋銀行") + .put("0508", "きらやか銀行") + .put("0509", "北日本銀行") + .put("0512", "仙台銀行") + .put("0513", "福島銀行") + .put("0514", "大東銀行") + .put("0516", "東和銀行") + .put("0517", "栃木銀行") + .put("0522", "京葉銀行") + .put("0525", "東日本銀行") + .put("0526", "東京スター銀行") + .put("0530", "神奈川銀行") + .put("0532", "大光銀行") + .put("0533", "長野銀行") + .put("0534", "富山第一銀行") + .put("0537", "福邦銀行") + .put("0538", "静岡中央銀行") + .put("0542", "愛知銀行") + .put("0543", "名古屋銀行") + .put("0544", "中京銀行") + .put("0546", "第三銀行") + .put("0555", "大正銀行") + .put("0562", "みなと銀行") + .put("0565", "島根銀行") + .put("0566", "トマト銀行") + .put("0569", "もみじ銀行") + .put("0570", "西京銀行") + .put("0572", "徳島銀行") + .put("0573", "香川銀行") + .put("0576", "愛媛銀行") + .put("0578", "高知銀行") + .put("0582", "福岡中央銀行") + .put("0583", "佐賀共栄銀行") + .put("0585", "長崎銀行") + .put("0587", "熊本銀行") + .put("0590", "豊和銀行") + .put("0591", "宮崎太陽銀行") + .put("0594", "南日本銀行") + .put("0596", "沖縄海邦銀行") + // }}} + // {{{ more foreign banks (0600 ~ 0999) + .put("0603", "韓国産業") + .put("0607", "彰化商業") + .put("0608", "ウエルズフアゴ") + .put("0611", "第一商業") + .put("0612", "台湾") + .put("0615", "交通") + .put("0616", "メトロポリタン") + .put("0617", "フイリピン") + .put("0619", "中国工商") + .put("0621", "中國信託商業") + .put("0623", "インテーザ") + .put("0624", "國民") + .put("0625", "中国建設") + .put("0626", "イタウウニ") + .put("0627", "BBVA") + .put("0630", "中国農業") + .put("0631", "台新") + .put("0632", "玉山") + .put("0633", "台湾企銀") + .put("0808", "ドイツ証券") + .put("0813", "ソシエテ証券") + .put("0821", "ビーピー証券") + .put("0822", "バークレイ証券") + .put("0831", "アグリコル証券") + .put("0832", "ジエイピー証券") + .put("0842", "ゴルドマン証券") + .put("0845", "ナツトウエ証券") + .put("0900", "日本相互証券") + .put("0905", "東京金融取引所") + .put("0909", "日本クリア機構") + .put("0910", "ほふりクリア") + .put("0964", "しんきん証券") + .put("0966", "HSBC証券") + .put("0968", "セント東短証券") + .put("0971", "UBS証券") + .put("0972", "メリル日本証券") + // }}} + .build(); + + // minor ~280 lesser known banks + private static final Map minorBanksJapanese = ImmutableMap. builder() + // {{{ 信用金庫 (1001 ~ 1996) + .put("1000", "信金中央金庫") + .put("1001", "北海道信金") + .put("1003", "室蘭信金") + .put("1004", "空知信金") + .put("1006", "苫小牧信金") + .put("1008", "北門信金") + .put("1009", "伊達信金") + .put("1010", "北空知信金") + .put("1011", "日高信金") + .put("1013", "渡島信金") + .put("1014", "道南うみ街信金") + .put("1020", "旭川信金") + .put("1021", "稚内信金") + .put("1022", "留萌信金") + .put("1024", "北星信金") + .put("1026", "帯広信金") + .put("1027", "釧路信金") + .put("1028", "大地みらい信金") + .put("1030", "北見信金") + .put("1031", "網走信金") + .put("1033", "遠軽信金") + .put("1104", "東奥信金") + .put("1105", "青い森信金") + .put("1120", "秋田信金") + .put("1123", "羽後信金") + .put("1140", "山形信金") + .put("1141", "米沢信金") + .put("1142", "鶴岡信金") + .put("1143", "新庄信金") + .put("1150", "盛岡信金") + .put("1152", "宮古信金") + .put("1153", "一関信金") + .put("1154", "北上信金") + .put("1155", "花巻信金") + .put("1156", "水沢信金") + .put("1170", "杜の都信金") + .put("1171", "宮城第一信金") + .put("1172", "石巻信金") + .put("1174", "仙南信金") + .put("1181", "会津信金") + .put("1182", "郡山信金") + .put("1184", "白河信金") + .put("1185", "須賀川信金") + .put("1186", "ひまわり信金") + .put("1188", "あぶくま信金") + .put("1189", "二本松信金") + .put("1190", "福島信金") + .put("1203", "高崎信金") + .put("1204", "桐生信金") + .put("1206", "アイオー信金") + .put("1208", "利根郡信金") + .put("1209", "館林信金") + .put("1210", "北群馬信金") + .put("1211", "しののめ信金") + .put("1221", "足利小山信金") + .put("1222", "栃木信金") + .put("1223", "鹿沼相互信金") + .put("1224", "佐野信金") + .put("1225", "大田原信金") + .put("1227", "烏山信金") + .put("1240", "水戸信金") + .put("1242", "結城信金") + .put("1250", "埼玉県信金") + .put("1251", "川口信金") + .put("1252", "青木信金") + .put("1253", "飯能信金") + .put("1260", "千葉信金") + .put("1261", "銚子信金") + .put("1262", "東京ベイ信金") + .put("1264", "館山信金") + .put("1267", "佐原信金") + .put("1280", "横浜信金") + .put("1281", "かながわ信金") + .put("1282", "湘南信金") + .put("1283", "川崎信金") + .put("1286", "平塚信金") + .put("1288", "さがみ信金") + .put("1289", "中栄信金") + .put("1290", "中南信金") + .put("1303", "朝日信金") + .put("1305", "興産信金") + .put("1310", "さわやか信金") + .put("1311", "東京シテイ信金") + .put("1319", "芝信金") + .put("1320", "東京東信金") + .put("1321", "東栄信金") + .put("1323", "亀有信金") + .put("1326", "小松川信金") + .put("1327", "足立成和信金") + .put("1333", "東京三協信金") + .put("1336", "西京信金") + .put("1341", "西武信金") + .put("1344", "城南信金") + .put("1345", "東京)昭和信金") + .put("1346", "目黒信金") + .put("1348", "世田谷信金") + .put("1349", "東京信金") + .put("1351", "城北信金") + .put("1352", "滝野川信金") + .put("1356", "巣鴨信金") + .put("1358", "青梅信金") + .put("1360", "多摩信金") + .put("1370", "新潟信金") + .put("1371", "長岡信金") + .put("1373", "三条信金") + .put("1374", "新発田信金") + .put("1375", "柏崎信金") + .put("1376", "上越信金") + .put("1377", "新井信金") + .put("1379", "村上信金") + .put("1380", "加茂信金") + .put("1385", "甲府信金") + .put("1386", "山梨信金") + .put("1390", "長野信金") + .put("1391", "松本信金") + .put("1392", "上田信金") + .put("1393", "諏訪信金") + .put("1394", "飯田信金") + .put("1396", "アルプス信金") + .put("1401", "富山信金") + .put("1402", "高岡信金") + .put("1405", "にいかわ信金") + .put("1406", "氷見伏木信金") + .put("1412", "砺波信金") + .put("1413", "石動信金") + .put("1440", "金沢信金") + .put("1442", "のと共栄信金") + .put("1444", "北陸信金") + .put("1445", "鶴来信金") + .put("1448", "興能信金") + .put("1470", "福井信金") + .put("1471", "敦賀信金") + .put("1473", "小浜信金") + .put("1475", "越前信金") + .put("1501", "しず焼津信金") + .put("1502", "静清信金") + .put("1503", "浜松磐田信金") + .put("1505", "沼津信金") + .put("1506", "三島信金") + .put("1507", "富士宮信金") + .put("1513", "島田掛川信金") + .put("1515", "静岡)富士信金") + .put("1517", "遠州信金") + .put("1530", "岐阜信金") + .put("1531", "大垣西濃信金") + .put("1532", "高山信金") + .put("1533", "東濃信金") + .put("1534", "関信金") + .put("1538", "八幡信金") + .put("1550", "愛知信金") + .put("1551", "豊橋信金") + .put("1552", "岡崎信金") + .put("1553", "いちい信金") + .put("1554", "瀬戸信金") + .put("1555", "半田信金") + .put("1556", "知多信金") + .put("1557", "豊川信金") + .put("1559", "豊田信金") + .put("1560", "碧海信金") + .put("1561", "西尾信金") + .put("1562", "蒲郡信金") + .put("1563", "尾西信金") + .put("1565", "中日信金") + .put("1566", "東春信金") + .put("1580", "津信金") + .put("1581", "北伊勢上野信金") + .put("1583", "桑名三重信金") + .put("1585", "紀北信金") + .put("1602", "滋賀中央信金") + .put("1603", "長浜信金") + .put("1604", "湖東信金") + .put("1610", "京都信金") + .put("1611", "京都中央信金") + .put("1620", "京都北都信金") + .put("1630", "大阪信金") + .put("1633", "大阪厚生信金") + .put("1635", "大阪シテイ信金") + .put("1636", "大阪商工信金") + .put("1643", "永和信金") + .put("1645", "北おおさか信金") + .put("1656", "枚方信金") + .put("1666", "奈良信金") + .put("1667", "大和信金") + .put("1668", "奈良中央信金") + .put("1671", "新宮信金") + .put("1674", "きのくに信金") + .put("1680", "神戸信金") + .put("1685", "姫路信金") + .put("1686", "播州信金") + .put("1687", "兵庫信金") + .put("1688", "尼崎信金") + .put("1689", "日新信金") + .put("1691", "淡路信金") + .put("1692", "但馬信金") + .put("1694", "西兵庫信金") + .put("1695", "中兵庫信金") + .put("1696", "但陽信金") + .put("1701", "鳥取信金") + .put("1702", "米子信金") + .put("1703", "倉吉信金") + .put("1710", "しまね信金") + .put("1711", "日本海信金") + .put("1712", "島根中央信金") + .put("1732", "おかやま信金") + .put("1734", "水島信金") + .put("1735", "津山信金") + .put("1738", "玉島信金") + .put("1740", "備北信金") + .put("1741", "吉備信金") + .put("1742", "日生信金") + .put("1743", "備前信金") + .put("1750", "広島信金") + .put("1752", "呉信金") + .put("1756", "しまなみ信金") + .put("1758", "広島みどり信金") + .put("1780", "萩山口信金") + .put("1781", "西中国信金") + .put("1789", "東山口信金") + .put("1801", "徳島信金") + .put("1803", "阿南信金") + .put("1830", "高松信金") + .put("1833", "観音寺信金") + .put("1860", "愛媛信金") + .put("1862", "宇和島信金") + .put("1864", "東予信金") + .put("1866", "川之江信金") + .put("1880", "幡多信金") + .put("1881", "高知信金") + .put("1901", "福岡信金") + .put("1903", "福岡ひびき信金") + .put("1908", "大牟田柳川信金") + .put("1909", "筑後信金") + .put("1910", "飯塚信金") + .put("1917", "大川信金") + .put("1920", "遠賀信金") + .put("1930", "唐津信金") + .put("1931", "佐賀信金") + .put("1933", "九州ひぜん信金") + .put("1942", "たちばな信金") + .put("1951", "熊本信金") + .put("1952", "熊本第一信金") + .put("1954", "熊本中央信金") + .put("1960", "大分信金") + .put("1962", "大分みらい信金") + .put("1980", "宮崎都城信金") + .put("1985", "高鍋信金") + .put("1990", "鹿児島信金") + .put("1991", "鹿児島相互信金") + .put("1993", "奄美大島信金") + .put("1996", "コザ信金") + // }}} + // {{{ 信用組合 (2011 ~ 2895) + .put("2004", "商工組合中央金庫") + .put("2010", "全国信用協同組合連合会") + .put("2213", "整理回収機構") + // }}} + // {{{ 労働金庫 (2951 ~ 2997) + .put("2950", "労働金庫連合会") + // }}} + // {{{ 農林中央金庫 (3000) + .put("3000", "農林中央金庫") + // }}} + // {{{ 信用農業協同組合連合会 (3001 ~ 3046) + .put("3001", "北海道信用農業協同組合連合会") + .put("3003", "岩手県信用農業協同組合連合会") + .put("3008", "茨城県信用農業協同組合連合会") + .put("3011", "埼玉県信用農業協同組合連合会") + .put("3013", "東京都信用農業協同組合連合会") + .put("3014", "神奈川県信用農業協同組合連合会") + .put("3015", "山梨県信用農業協同組合連合会") + .put("3016", "長野県信用農業協同組合連合会") + .put("3017", "新潟県信用農業協同組合連合会") + .put("3019", "石川県信用農業協同組合連合会") + .put("3020", "岐阜県信用農業協同組合連合会") + .put("3021", "静岡県信用農業協同組合連合会") + .put("3022", "愛知県信用農業協同組合連合会") + .put("3023", "三重県信用農業協同組合連合会") + .put("3024", "福井県信用農業協同組合連合会") + .put("3025", "滋賀県信用農業協同組合連合会") + .put("3026", "京都府信用農業協同組合連合会") + .put("3027", "大阪府信用農業協同組合連合会") + .put("3028", "兵庫県信用農業協同組合連合会") + .put("3030", "和歌山県信用農業協同組合連合会") + .put("3031", "鳥取県信用農業協同組合連合会") + .put("3034", "広島県信用農業協同組合連合会") + .put("3035", "山口県信用農業協同組合連合会") + .put("3036", "徳島県信用農業協同組合連合会") + .put("3037", "香川県信用農業協同組合連合会") + .put("3038", "愛媛県信用農業協同組合連合会") + .put("3039", "高知県信用農業協同組合連合会") + .put("3040", "福岡県信用農業協同組合連合会") + .put("3041", "佐賀県信用農業協同組合連合会") + .put("3044", "大分県信用農業協同組合連合会") + .put("3045", "宮崎県信用農業協同組合連合会") + .put("3046", "鹿児島県信用農業協同組合連合会") + // }}} + // {{{ "JA Bank" agricultural cooperative associations (3056 ~ 9375) + // REMOVED: the farmers should use a real bank if they want to sell bitcoin + // }}} + // {{{ 信用漁業協同組合連合会 (9450 ~ 9496) + .put("9450", "北海道信用漁業協同組合連合会") + .put("9451", "青森県信用漁業協同組合連合会") + .put("9452", "岩手県信用漁業協同組合連合会") + .put("9453", "宮城県漁業協同組合") + .put("9456", "福島県信用漁業協同組合連合会") + .put("9457", "茨城県信用漁業協同組合連合会") + .put("9461", "千葉県信用漁業協同組合連合会") + .put("9462", "東京都信用漁業協同組合連合会") + .put("9466", "新潟県信用漁業協同組合連合会") + .put("9467", "富山県信用漁業協同組合連合会") + .put("9468", "石川県信用漁業協同組合連合会") + .put("9470", "静岡県信用漁業協同組合連合会") + .put("9471", "愛知県信用漁業協同組合連合会") + .put("9472", "三重県信用漁業協同組合連合会") + .put("9473", "福井県信用漁業協同組合連合会") + .put("9475", "京都府信用漁業協同組合連合会") + .put("9477", "なぎさ信用漁業協同組合連合会") + .put("9480", "鳥取県信用漁業協同組合連合会") + .put("9481", "JFしまね漁業協同組合") + .put("9483", "広島県信用漁業協同組合連合会") + .put("9484", "山口県漁業協同組合") + .put("9485", "徳島県信用漁業協同組合連合会") + .put("9486", "香川県信用漁業協同組合連合会") + .put("9487", "愛媛県信用漁業協同組合連合会") + .put("9488", "高知県信用漁業協同組合連合会") + .put("9489", "福岡県信用漁業協同組合連合会") + .put("9490", "佐賀県信用漁業協同組合連合会") + .put("9491", "長崎県信用漁業協同組合連合会") + .put("9493", "大分県漁業協同組合") + .put("9494", "宮崎県信用漁業協同組合連合会") + .put("9495", "鹿児島県信用漁業協同組合連合会") + .put("9496", "沖縄県信用漁業協同組合連合会") + // }}} + // {{{ securities firms + .put("9500", "東京短資") + .put("9501", "セントラル短資") + .put("9507", "上田八木短資") + .put("9510", "日本証券金融") + .put("9520", "野村証券") + .put("9521", "日興証券") + .put("9523", "大和証券") + .put("9524", "みずほ証券") + .put("9528", "岡三証券") + .put("9530", "岩井コスモ証券") + .put("9532", "三菱UFJ証券") + .put("9534", "丸三証券") + .put("9535", "東洋証券") + .put("9537", "水戸証券") + .put("9539", "東海東京証券") + .put("9542", "むさし証券") + .put("9545", "いちよし証券") + .put("9573", "極東証券") + .put("9574", "立花証券") + .put("9579", "光世証券") + .put("9584", "ちばぎん証券") + .put("9589", "シテイ証券") + .put("9594", "CS証券") + .put("9595", "スタンレー証券") + .put("9930", "日本政策投資") + .put("9932", "政策金融公庫") + .put("9933", "国際協力") + .put("9945", "預金保険機構") + // }}} + .build(); + + private final static String ID_OPEN = ""; + private final static String ID_CLOSE = ""; + private final static String JA_OPEN = ""; + private final static String JA_CLOSE = ""; + private final static String EN_OPEN = ""; + private final static String EN_CLOSE = ""; + public final static String SPACE = " "; + + // don't localize these strings into all languages, + // all we want is either Japanese or English here. + public static final String getString(String id) + { + boolean ja = GUIUtil.getUserLanguage().equals("ja"); + + switch (id) + { + case "bank": + if (ja) return "銀行名 ・金融機関名"; + return "Bank or Financial Institution"; + case "bank.select": + if (ja) return "金融機関 ・銀行検索 (名称入力検索)"; + return "Search for Bank or Financial Institution"; + case "bank.code": + if (ja) return "銀行コード"; + return "Zengin Bank Code"; + case "bank.name": + if (ja) return "金融機関名 ・銀行名"; + return "Financial Institution / Bank Name"; + + case "branch": + if (ja) return "支店名"; + return "Bank Branch"; + case "branch.code": + if (ja) return "支店コード"; + return "Zengin Branch Code"; + case "branch.code.validation.error": + if (ja) return "入力は3桁の支店コードでなければなりません"; + return "Input must be a 3 digit branch code"; + case "branch.name": + if (ja) return "支店名"; + return "Bank Branch Name"; + + case "account": + if (ja) return "銀行口座"; + return "Bank Account"; + + case "account.type": + if (ja) return "口座科目"; + return "Bank Account Type"; + case "account.type.select": + if (ja) return "口座科目"; + return "Select Account Type"; + // displayed while creating account + case "account.type.futsu": + if (ja) return "普通"; + return "FUTSUU (ordinary) account"; + case "account.type.touza": + if (ja) return "当座"; + return "TOUZA (checking) account"; + case "account.type.chochiku": + if (ja) return "貯金"; + return "CHOCHIKU (special) account"; + // used when saving account info + case "account.type.futsu.ja": + return "普通"; + case "account.type.touza.ja": + return "当座"; + case "account.type.chochiku.ja": + return "貯金"; + + case "account.number": + if (ja) return "口座番号"; + return "Bank Account Number"; + case "account.number.validation.error": + if (ja) return "入力は4〜8桁の口座番号でなければなりません"; + return "Input must be 4 ~ 8 digit account number"; + case "account.name": + if (ja) return "口座名義"; + return "Bank Account Name"; + + // for japanese-only input fields + case "japanese.validation.error": + if (ja) return "入力は漢字、ひらがな、またはカタカナでなければなりません"; + return "Input must be Kanji, Hiragana, or Katakana"; + case "japanese.validation.regex": + // epic regex to only match Japanese input + return "[" + // match any of these characters: + // "A-z" + // full-width alphabet + // "0-9" + // full-width numerals + "一-龯" + // all Japanese kanji (0x4e00 ~ 0x9fcf) + "ぁ-ゔ" + // full-width hiragana (0x3041 ~ 0x3094) + "ァ-・" + // full-width katakana (0x30a1 ~ 0x30fb) + "ぁ-ゞ" + // half-width hiragana + "ァ-ン゙゚" + // half-width katakana + "ヽヾ゛゜ー" + // 0x309e, 0x309b, 0x309c, 0x30fc + " " + // full-width space + " " + // half-width space + "]+"; // for any length + } + + return "null"; + } +} + +// vim:ts=4:sw=4:expandtab:foldmethod=marker:nowrap: diff --git a/desktop/src/main/java/bisq/desktop/images.css b/desktop/src/main/java/bisq/desktop/images.css new file mode 100644 index 0000000000..f943661fed --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/images.css @@ -0,0 +1,288 @@ +/* splash screen */ +/*noinspection CssUnknownTarget*/ +#image-splash-logo { + -fx-image: url("../../images/logo_splash.png"); +} + +/* shared*/ +#image-info { + -fx-image: url("../../images/info.png"); +} + +#light_close { + -fx-image: url("../../images/light_close.png"); +} + +#image-alert-round { + -fx-image: url("../../images/alert_round.png"); +} + +#image-green_circle { + -fx-image: url("../../images/green_circle.png"); +} + +#image-blue_circle { + -fx-image: url("../../images/blue_circle.png"); +} + +#image-remove { + -fx-image: url("../../images/remove.png"); +} + +#image-buy-white { + -fx-image: url("../../images/buy_white.png"); +} + +#image-buy-green { + -fx-image: url("../../images/buy_green.png"); +} + +#image-sell-white { + -fx-image: url("../../images/sell_white.png"); +} + +#image-sell-red { + -fx-image: url("../../images/sell_red.png"); +} + +#image-expand { + -fx-image: url("../../images/expand.png"); +} + +#image-collapse { + -fx-image: url("../../images/collapse.png"); +} + +/* navigation buttons */ +#image-nav-market { + -fx-image: url("../../images/nav/market.png"); +} + +#image-nav-market-active { + -fx-image: url("../../images/nav/market_active.png"); +} + +#image-nav-buyoffer { + -fx-image: url("../../images/nav/buyoffer.png"); +} + +#image-nav-buyoffer-active { + -fx-image: url("../../images/nav/buyoffer_active.png"); +} + +#image-nav-selloffer { + -fx-image: url("../../images/nav/selloffer.png"); +} + +#image-nav-selloffer-active { + -fx-image: url("../../images/nav/selloffer_active.png"); +} + +#image-nav-portfolio { + -fx-image: url("../../images/nav/portfolio.png"); +} + +#image-nav-portfolio-active { + -fx-image: url("../../images/nav/portfolio_active.png"); +} + +#image-nav-funds { + -fx-image: url("../../images/nav/funds.png"); +} + +#image-nav-funds-active { + -fx-image: url("../../images/nav/funds_active.png"); +} + +#image-nav-disputes { + -fx-image: url("../../images/nav/disputes.png"); +} + +#image-nav-disputes-active { + -fx-image: url("../../images/nav/disputes_active.png"); +} + +#image-nav-settings { + -fx-image: url("../../images/nav/settings.png"); +} + +#image-nav-settings-active { + -fx-image: url("../../images/nav/settings_active.png"); +} + +#image-nav-account { + -fx-image: url("../../images/nav/account.png"); +} + +#image-nav-account-active { + -fx-image: url("../../images/nav/account_active.png"); +} + +#image-nav-dao { + -fx-image: url("../../images/nav/dao.png"); +} + +#image-nav-dao-active { + -fx-image: url("../../images/nav/dao_active.png"); +} + +/* account*/ +#image-tick { + -fx-image: url("../../images/tick.png"); +} + +#image-arrow-blue { + -fx-image: url("../../images/arrow_blue.png"); +} + +#image-arrow-grey { + -fx-image: url("../../images/arrow_grey.png"); +} + +/* connection state*/ +#image-connection-tor { + -fx-image: url("../../images/connection/tor.png"); +} + +#image-connection-direct { + -fx-image: url("../../images/connection/direct.png"); +} + +#image-connection-nat { + -fx-image: url("../../images/connection/nat.png"); +} + +#image-connection-relay { + -fx-image: url("../../images/connection/relay.png"); +} + +#image-connection-synced { + -fx-image: url("../../images/connection/synced.png"); +} + +/* software update*/ + +#image-update-in-progress { + -fx-image: url("../../images/update/update_in_progress.png"); +} + +#image-update-up-to-date { + -fx-image: url("../../images/update/update_up_to_date.png"); +} + +#image-update-available { + -fx-image: url("../../images/update/update_available.png"); +} + +#image-update-failed { + -fx-image: url("../../images/update/update_failed.png"); +} + +/* offer state */ +#image-offer_state_unknown { + -fx-image: url("../../images/offer/offer_state_unknown.png"); +} + +#image-offer_state_available { + -fx-image: url("../../images/offer/offer_state_available.png"); +} + +#image-offer_state_not_available { + -fx-image: url("../../images/offer/offer_state_not_available.png"); +} + +#image-attachment { + -fx-image: url("../../images/attachment.png"); +} + +#bubble_arrow_grey_left { + -fx-image: url("../../images/bubble_arrow_grey_left.png"); +} + +#bubble_arrow_blue_left { + -fx-image: url("../../images/bubble_arrow_blue_left.png"); +} + +#bubble_arrow_grey_right { + -fx-image: url("../../images/bubble_arrow_grey_right.png"); +} + +#bubble_arrow_blue_right { + -fx-image: url("../../images/bubble_arrow_blue_right.png"); +} + +#link { + -fx-image: url("../../images/link.png"); +} + +#invert { + -fx-image: url("../../images/invert.png"); +} + +#avatar_1 { + -fx-image: url("../../images/avatars/avatar_1.png"); +} + +#avatar_2 { + -fx-image: url("../../images/avatars/avatar_2.png"); +} + +#avatar_3 { + -fx-image: url("../../images/avatars/avatar_3.png"); +} + +#avatar_4 { + -fx-image: url("../../images/avatars/avatar_4.png"); +} + +#avatar_5 { + -fx-image: url("../../images/avatars/avatar_5.png"); +} + +#avatar_6 { + -fx-image: url("../../images/avatars/avatar_6.png"); +} + +#avatar_7 { + -fx-image: url("../../images/avatars/avatar_7.png"); +} + +#avatar_8 { + -fx-image: url("../../images/avatars/avatar_8.png"); +} + +#avatar_9 { + -fx-image: url("../../images/avatars/avatar_9.png"); +} + +#avatar_10 { + -fx-image: url("../../images/avatars/avatar_10.png"); +} + +#avatar_11 { + -fx-image: url("../../images/avatars/avatar_11.png"); +} + +#avatar_12 { + -fx-image: url("../../images/avatars/avatar_12.png"); +} + +#avatar_13 { + -fx-image: url("../../images/avatars/avatar_13.png"); +} + +#avatar_14 { + -fx-image: url("../../images/avatars/avatar_14.png"); +} + +#avatar_15 { + -fx-image: url("../../images/avatars/avatar_15.png"); +} + +#image-account-signing-screenshot { + -fx-image: url("../../images/account_signing_screenshot.png"); +} + +#image-new-trade-protocol-screenshot { + -fx-image: url("../../images/new_trade_protocol_screenshot.png"); +} diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.fxml b/desktop/src/main/java/bisq/desktop/main/MainView.fxml new file mode 100644 index 0000000000..b4b62f89fc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/MainView.fxml @@ -0,0 +1,22 @@ + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java new file mode 100644 index 0000000000..854e8af974 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -0,0 +1,824 @@ +/* + * 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 bisq.desktop.main; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.InitializableView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipToggleButton; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.main.account.AccountView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.funds.FundsView; +import bisq.desktop.main.market.MarketView; +import bisq.desktop.main.market.offerbook.OfferBookChartView; +import bisq.desktop.main.offer.BuyOfferView; +import bisq.desktop.main.offer.SellOfferView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.portfolio.PortfolioView; +import bisq.desktop.main.settings.SettingsView; +import bisq.desktop.main.shared.PriceFeedComboBoxItem; +import bisq.desktop.main.support.SupportView; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.Transitions; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.LanguageUtil; +import bisq.core.locale.Res; +import bisq.core.provider.price.MarketPrice; + +import bisq.common.BisqException; +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javax.inject.Inject; + +import com.jfoenix.controls.JFXBadge; +import com.jfoenix.controls.JFXComboBox; +import com.jfoenix.controls.JFXProgressBar; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Separator; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; + +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; + +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.util.Date; +import java.util.Locale; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static javafx.scene.layout.AnchorPane.setBottomAnchor; +import static javafx.scene.layout.AnchorPane.setLeftAnchor; +import static javafx.scene.layout.AnchorPane.setRightAnchor; +import static javafx.scene.layout.AnchorPane.setTopAnchor; + +@FxmlView +@Slf4j +public class MainView extends InitializableView + implements DaoStateMonitoringService.Listener { + // If after 30 sec we have not got connected we show "open network settings" button + private final static int SHOW_TOR_SETTINGS_DELAY_SEC = 90; + @Setter + private Runnable onApplicationStartedHandler; + + public static StackPane getRootContainer() { + return MainView.rootContainer; + } + + public static void blurLight() { + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 5); + } + + public static void blurUltraLight() { + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 2); + } + + public static void darken() { + transitions.darken(MainView.rootContainer, Transitions.DEFAULT_DURATION, false); + } + + public static void removeEffect() { + transitions.removeEffect(MainView.rootContainer); + } + + private static Transitions transitions; + private static StackPane rootContainer; + + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private final ToggleGroup navButtons = new ToggleGroup(); + private ChangeListener walletServiceErrorMsgListener; + private ChangeListener btcSyncIconIdListener; + private ChangeListener splashP2PNetworkErrorMsgListener; + private ChangeListener splashP2PNetworkIconIdListener; + private ChangeListener splashP2PNetworkVisibleListener; + private BusyAnimation splashP2PNetworkBusyAnimation; + private Label splashP2PNetworkLabel; + private ProgressBar btcSyncIndicator, p2pNetworkProgressBar; + private Label btcSplashInfo; + private Popup p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup; + private final DaoStateMonitoringService daoStateMonitoringService; + + @Inject + public MainView(MainViewModel model, + CachingViewLoader viewLoader, + Navigation navigation, + Transitions transitions, + DaoStateMonitoringService daoStateMonitoringService) { + super(model); + this.viewLoader = viewLoader; + this.navigation = navigation; + MainView.transitions = transitions; + this.daoStateMonitoringService = daoStateMonitoringService; + } + + @Override + protected void initialize() { + MainView.rootContainer = root; + if (LanguageUtil.isDefaultLanguageRTL()) + MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); + + ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market").toUpperCase()); + ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyBtc").toUpperCase()); + ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellBtc").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 daoButton = new NavButton(DaoView.class, Res.get("mainView.menu.dao")); + + JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); + JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); + JFXBadge settingsButtonWithBadge = new JFXBadge(settingsButton); + settingsButtonWithBadge.getStyleClass().add("new"); + + Locale locale = GlobalSettings.getLocale(); + DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); + currencyFormat.setMinimumFractionDigits(2); + currencyFormat.setMaximumFractionDigits(2); + + root.sceneProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + newValue.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT1, keyEvent)) { + marketButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT2, keyEvent)) { + buyButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT3, keyEvent)) { + sellButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT4, keyEvent)) { + portfolioButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT5, keyEvent)) { + 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.DIGIT9, keyEvent)) { + if (daoButton.isVisible()) + daoButton.fire(); + } + }); + } + }); + + + Tuple2, VBox> marketPriceBox = getMarketPriceBox(); + ComboBox priceComboBox = marketPriceBox.first; + + priceComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> + model.setPriceFeedComboBoxItem(newValue)); + ChangeListener selectedPriceFeedItemListener = (observable, oldValue, newValue) -> { + if (newValue != null) + priceComboBox.getSelectionModel().select(newValue); + + }; + model.getSelectedPriceFeedComboBoxItemProperty().addListener(selectedPriceFeedItemListener); + priceComboBox.setItems(model.getPriceFeedComboBoxItems()); + + Tuple2 availableBalanceBox = getBalanceBox(Res.get("mainView.balance.available")); + availableBalanceBox.first.textProperty().bind(model.getAvailableBalance()); + availableBalanceBox.first.setPrefWidth(100); + availableBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { + { + bind(model.getAvailableBalance()); + bind(model.getMarketPrice()); + } + + @Override + protected Tooltip computeValue() { + String tooltipText = Res.get("mainView.balance.available"); + try { + String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); + double availableBalance = Double.parseDouble( + model.getAvailableBalance().getValue().replace("BTC", "")); + double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); + tooltipText += "\n" + currencyFormat.format(availableBalance * marketPrice) + + " " + preferredTradeCurrency; + } catch (NullPointerException | NumberFormatException e) { + // Either the balance or market price is not available yet + } + return new Tooltip(tooltipText); + } + }); + + Tuple2 reservedBalanceBox = getBalanceBox(Res.get("mainView.balance.reserved.short")); + reservedBalanceBox.first.textProperty().bind(model.getReservedBalance()); + reservedBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { + { + bind(model.getReservedBalance()); + bind(model.getMarketPrice()); + } + + @Override + protected Tooltip computeValue() { + String tooltipText = Res.get("mainView.balance.reserved"); + try { + String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); + double reservedBalance = Double.parseDouble( + model.getReservedBalance().getValue().replace("BTC", "")); + double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); + tooltipText += "\n" + currencyFormat.format(reservedBalance * marketPrice) + + " " + preferredTradeCurrency; + } catch (NullPointerException | NumberFormatException e) { + // Either the balance or market price is not available yet + } + return new Tooltip(tooltipText); + } + }); + + Tuple2 lockedBalanceBox = getBalanceBox(Res.get("mainView.balance.locked.short")); + lockedBalanceBox.first.textProperty().bind(model.getLockedBalance()); + lockedBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { + { + bind(model.getLockedBalance()); + bind(model.getMarketPrice()); + } + + @Override + protected Tooltip computeValue() { + String tooltipText = Res.get("mainView.balance.locked"); + try { + String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); + double lockedBalance = Double.parseDouble( + model.getLockedBalance().getValue().replace("BTC", "")); + double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); + tooltipText += "\n" + currencyFormat.format(lockedBalance * marketPrice) + + " " + preferredTradeCurrency; + } catch (NullPointerException | NumberFormatException e) { + // Either the balance or market price is not available yet + } + return new Tooltip(tooltipText); + } + }); + + HBox primaryNav = new HBox(marketButton, getNavigationSeparator(), buyButton, getNavigationSeparator(), + sellButton, getNavigationSeparator(), portfolioButtonWithBadge, getNavigationSeparator(), fundsButton); + + primaryNav.setAlignment(Pos.CENTER_LEFT); + primaryNav.getStyleClass().add("nav-primary"); + HBox.setHgrow(primaryNav, Priority.SOMETIMES); + + HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButtonWithBadge, + getNavigationSpacer(), accountButton, getNavigationSpacer(), daoButton); + secondaryNav.getStyleClass().add("nav-secondary"); + HBox.setHgrow(secondaryNav, Priority.SOMETIMES); + + secondaryNav.setAlignment(Pos.CENTER); + + HBox priceAndBalance = new HBox(marketPriceBox.second, getNavigationSeparator(), availableBalanceBox.second, + getNavigationSeparator(), reservedBalanceBox.second, getNavigationSeparator(), lockedBalanceBox.second); + priceAndBalance.setMaxHeight(41); + + priceAndBalance.setAlignment(Pos.CENTER); + priceAndBalance.setSpacing(9); + priceAndBalance.getStyleClass().add("nav-price-balance"); + + HBox navPane = new HBox(primaryNav, secondaryNav, getNavigationSpacer(), + priceAndBalance) {{ + setLeftAnchor(this, 0d); + setRightAnchor(this, 0d); + setTopAnchor(this, 0d); + setPadding(new Insets(0, 0, 0, 0)); + getStyleClass().add("top-navigation"); + }}; + navPane.setAlignment(Pos.CENTER); + + AnchorPane contentContainer = new AnchorPane() {{ + getStyleClass().add("content-pane"); + setLeftAnchor(this, 0d); + setRightAnchor(this, 0d); + setTopAnchor(this, 57d); + setBottomAnchor(this, 0d); + }}; + + AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer) {{ + setId("application-container"); + }}; + + BorderPane baseApplicationContainer = new BorderPane(applicationContainer) {{ + setId("base-content-container"); + }}; + baseApplicationContainer.setBottom(createFooter()); + + setupBadge(portfolioButtonWithBadge, model.getNumPendingTrades(), model.getShowPendingTradesNotification()); + setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification()); + setupBadge(settingsButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowSettingsUpdatesNotification()); + + navigation.addListener((viewPath, data) -> { + if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) + return; + + Class viewClass = viewPath.tip(); + View view = viewLoader.load(viewClass); + contentContainer.getChildren().setAll(view.getRoot()); + + try { + navButtons.getToggles().stream() + .filter(toggle -> toggle instanceof NavButton) + .filter(button -> viewClass == ((NavButton) button).viewClass) + .findFirst() + .orElseThrow(() -> new BisqException("No button matching %s found", viewClass)) + .setSelected(true); + } catch (BisqException e) { + navigation.navigateTo(MainView.class, MarketView.class, OfferBookChartView.class); + } + }); + + VBox splashScreen = createSplashScreen(); + + root.getChildren().addAll(baseApplicationContainer, splashScreen); + + model.getShowAppScreen().addListener((ov, oldValue, newValue) -> { + if (newValue) { + + navigation.navigateToPreviousVisitedView(); + + transitions.fadeOutAndRemove(splashScreen, 1500, actionEvent -> disposeSplashScreen()); + } + }); + + daoStateMonitoringService.addListener(this); + + // Delay a bit to give time for rendering the splash screen + UserThread.execute(() -> onApplicationStartedHandler.run()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateMonitoringService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onChangeAfterBatchProcessing() { + } + + @Override + public void onCheckpointFail() { + new Popup().attention(Res.get("dao.monitor.daoState.checkpoint.popup")) + .useShutDownButton() + .show(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + /////////////////////////////////////////////////////////////////////////////////////////// + + @NotNull + private Separator getNavigationSeparator() { + final Separator separator = new Separator(Orientation.VERTICAL); + HBox.setHgrow(separator, Priority.ALWAYS); + separator.setMaxHeight(22); + separator.setMaxWidth(Double.MAX_VALUE); + return separator; + } + + @NotNull + private Region getNavigationSpacer() { + final Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + return spacer; + } + + private Tuple2 getBalanceBox(String text) { + Label balanceDisplay = new Label(); + balanceDisplay.getStyleClass().add("nav-balance-display"); + + Label label = new Label(text); + label.getStyleClass().add("nav-balance-label"); + label.maxWidthProperty().bind(balanceDisplay.widthProperty()); + label.setPadding(new Insets(0, 0, 0, 0)); + VBox vBox = new VBox(); + vBox.setAlignment(Pos.CENTER_LEFT); + vBox.getChildren().addAll(balanceDisplay, label); + return new Tuple2<>(balanceDisplay, vBox); + } + + private ListCell getPriceFeedComboBoxListCell() { + return new ListCell<>() { + @Override + protected void updateItem(PriceFeedComboBoxItem item, boolean empty) { + super.updateItem(item, empty); + + if (!empty && item != null) { + textProperty().bind(item.displayStringProperty); + } else { + textProperty().unbind(); + } + } + }; + } + + private Tuple2, VBox> getMarketPriceBox() { + + VBox marketPriceBox = new VBox(); + marketPriceBox.setAlignment(Pos.CENTER_LEFT); + + ComboBox priceComboBox = new JFXComboBox<>(); + priceComboBox.setVisibleRowCount(12); + priceComboBox.setFocusTraversable(false); + priceComboBox.setId("price-feed-combo"); + priceComboBox.setPadding(new Insets(0, -4, -4, 0)); + priceComboBox.setCellFactory(p -> getPriceFeedComboBoxListCell()); + ListCell buttonCell = getPriceFeedComboBoxListCell(); + buttonCell.setId("price-feed-combo"); + priceComboBox.setButtonCell(buttonCell); + + Label marketPriceLabel = new Label(); + + updateMarketPriceLabel(marketPriceLabel); + + marketPriceLabel.getStyleClass().add("nav-balance-label"); + marketPriceLabel.setPadding(new Insets(-2, 0, 4, 9)); + + marketPriceBox.getChildren().addAll(priceComboBox, marketPriceLabel); + + model.getMarketPriceUpdated().addListener((observable, oldValue, newValue) -> + updateMarketPriceLabel(marketPriceLabel)); + + return new Tuple2<>(priceComboBox, marketPriceBox); + } + + @NotNull + private String getPriceProvider() { + return model.getIsFiatCurrencyPriceFeedSelected().get() ? "BitcoinAverage" : "Poloniex"; + } + + private void updateMarketPriceLabel(Label label) { + if (model.getIsPriceAvailable().get()) { + if (model.getIsExternallyProvidedPrice().get()) { + label.setText(Res.get("mainView.marketPriceWithProvider.label", "Bisq Price Index")); + label.setTooltip(new Tooltip(getPriceProviderTooltipString())); + } else { + label.setText(Res.get("mainView.marketPrice.bisqInternalPrice")); + final Tooltip tooltip = new Tooltip(Res.get("mainView.marketPrice.tooltip.bisqInternalPrice")); + label.setTooltip(tooltip); + } + } else { + label.setText(""); + label.setTooltip(null); + } + } + + @NotNull + private String getPriceProviderTooltipString() { + + String selectedCurrencyCode = model.getPriceFeedService().getCurrencyCode(); + MarketPrice selectedMarketPrice = model.getPriceFeedService().getMarketPrice(selectedCurrencyCode); + + return Res.get("mainView.marketPrice.tooltip", + "Bisq Price Index for " + selectedCurrencyCode, + "", + selectedMarketPrice != null ? DisplayUtils.formatTime(new Date(selectedMarketPrice.getTimestampSec())) : Res.get("shared.na"), + model.getPriceFeedService().getProviderNodeAddress()); + } + + private VBox createSplashScreen() { + VBox vBox = new VBox(); + vBox.setAlignment(Pos.CENTER); + vBox.setSpacing(10); + vBox.setId("splash"); + + ImageView logo = new ImageView(); + logo.setId("image-splash-logo"); + + + // createBitcoinInfoBox + btcSplashInfo = new AutoTooltipLabel(); + btcSplashInfo.textProperty().bind(model.getBtcInfo()); + walletServiceErrorMsgListener = (ov, oldValue, newValue) -> { + btcSplashInfo.setId("splash-error-state-msg"); + btcSplashInfo.getStyleClass().add("error-text"); + }; + model.getWalletServiceErrorMsg().addListener(walletServiceErrorMsgListener); + + btcSyncIndicator = new JFXProgressBar(); + btcSyncIndicator.setPrefWidth(305); + btcSyncIndicator.progressProperty().bind(model.getCombinedSyncProgress()); + + ImageView btcSyncIcon = new ImageView(); + btcSyncIcon.setVisible(false); + btcSyncIcon.setManaged(false); + + btcSyncIconIdListener = (ov, oldValue, newValue) -> { + btcSyncIcon.setId(newValue); + btcSyncIcon.setVisible(true); + btcSyncIcon.setManaged(true); + + btcSyncIndicator.setVisible(false); + btcSyncIndicator.setManaged(false); + }; + model.getBtcSplashSyncIconId().addListener(btcSyncIconIdListener); + + + HBox blockchainSyncBox = new HBox(); + blockchainSyncBox.setSpacing(10); + blockchainSyncBox.setAlignment(Pos.CENTER); + blockchainSyncBox.setPadding(new Insets(40, 0, 0, 0)); + blockchainSyncBox.setPrefHeight(50); + blockchainSyncBox.getChildren().addAll(btcSplashInfo, btcSyncIcon); + + + // create P2PNetworkBox + splashP2PNetworkLabel = new AutoTooltipLabel(); + splashP2PNetworkLabel.setWrapText(true); + splashP2PNetworkLabel.setMaxWidth(500); + splashP2PNetworkLabel.setTextAlignment(TextAlignment.CENTER); + splashP2PNetworkLabel.getStyleClass().add("sub-info"); + splashP2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); + + Button showTorNetworkSettingsButton = new AutoTooltipButton(Res.get("settings.net.openTorSettingsButton")); + showTorNetworkSettingsButton.setVisible(false); + showTorNetworkSettingsButton.setManaged(false); + showTorNetworkSettingsButton.setOnAction(e -> model.getTorNetworkSettingsWindow().show()); + + splashP2PNetworkBusyAnimation = new BusyAnimation(false); + + splashP2PNetworkErrorMsgListener = (ov, oldValue, newValue) -> { + if (newValue != null) { + splashP2PNetworkLabel.setId("splash-error-state-msg"); + splashP2PNetworkLabel.getStyleClass().remove("sub-info"); + splashP2PNetworkLabel.getStyleClass().add("error-text"); + splashP2PNetworkBusyAnimation.setDisable(true); + splashP2PNetworkBusyAnimation.stop(); + showTorNetworkSettingsButton.setVisible(true); + showTorNetworkSettingsButton.setManaged(true); + if (model.getUseTorForBTC().get()) { + // If using tor for BTC, hide the BTC status since tor is not working + btcSyncIndicator.setVisible(false); + btcSplashInfo.setVisible(false); + } + } else if (model.getSplashP2PNetworkAnimationVisible().get()) { + splashP2PNetworkBusyAnimation.setDisable(false); + splashP2PNetworkBusyAnimation.play(); + } + }; + model.getP2pNetworkWarnMsg().addListener(splashP2PNetworkErrorMsgListener); + + ImageView splashP2PNetworkIcon = new ImageView(); + splashP2PNetworkIcon.setId("image-connection-tor"); + splashP2PNetworkIcon.setVisible(false); + splashP2PNetworkIcon.setManaged(false); + HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0)); + + Timer showTorNetworkSettingsTimer = UserThread.runAfter(() -> { + showTorNetworkSettingsButton.setVisible(true); + showTorNetworkSettingsButton.setManaged(true); + }, SHOW_TOR_SETTINGS_DELAY_SEC); + + splashP2PNetworkIconIdListener = (ov, oldValue, newValue) -> { + splashP2PNetworkIcon.setId(newValue); + splashP2PNetworkIcon.setVisible(true); + splashP2PNetworkIcon.setManaged(true); + + // if we can connect in 10 sec. we know that tor is working + showTorNetworkSettingsTimer.stop(); + }; + model.getP2PNetworkIconId().addListener(splashP2PNetworkIconIdListener); + + splashP2PNetworkVisibleListener = (ov, oldValue, newValue) -> { + splashP2PNetworkBusyAnimation.setDisable(!newValue); + if (newValue) splashP2PNetworkBusyAnimation.play(); + }; + + model.getSplashP2PNetworkAnimationVisible().addListener(splashP2PNetworkVisibleListener); + + HBox splashP2PNetworkBox = new HBox(); + splashP2PNetworkBox.setSpacing(10); + splashP2PNetworkBox.setAlignment(Pos.CENTER); + splashP2PNetworkBox.setPrefHeight(40); + splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton); + + vBox.getChildren().addAll(logo, blockchainSyncBox, btcSyncIndicator, splashP2PNetworkBox); + return vBox; + } + + private void disposeSplashScreen() { + model.getWalletServiceErrorMsg().removeListener(walletServiceErrorMsgListener); + model.getBtcSplashSyncIconId().removeListener(btcSyncIconIdListener); + + model.getP2pNetworkWarnMsg().removeListener(splashP2PNetworkErrorMsgListener); + model.getP2PNetworkIconId().removeListener(splashP2PNetworkIconIdListener); + model.getSplashP2PNetworkAnimationVisible().removeListener(splashP2PNetworkVisibleListener); + + btcSplashInfo.textProperty().unbind(); + btcSyncIndicator.progressProperty().unbind(); + + splashP2PNetworkLabel.textProperty().unbind(); + + model.onSplashScreenRemoved(); + } + + + private AnchorPane createFooter() { + // line + Separator separator = new Separator(); + separator.setId("footer-pane-line"); + separator.setPrefHeight(1); + setLeftAnchor(separator, 0d); + setRightAnchor(separator, 0d); + setTopAnchor(separator, 0d); + + // BTC + Label btcInfoLabel = new AutoTooltipLabel(); + btcInfoLabel.setId("footer-pane"); + btcInfoLabel.textProperty().bind(model.getBtcInfo()); + setLeftAnchor(btcInfoLabel, 10d); + setBottomAnchor(btcInfoLabel, 7d); + + // temporarily disabled due to high CPU usage (per issue #4649) + //ProgressBar blockchainSyncIndicator = new JFXProgressBar(-1); + //blockchainSyncIndicator.setPrefWidth(80); + //blockchainSyncIndicator.setMaxHeight(10); + //blockchainSyncIndicator.progressProperty().bind(model.getCombinedSyncProgress()); + + model.getWalletServiceErrorMsg().addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + btcInfoLabel.setId("splash-error-state-msg"); + btcInfoLabel.getStyleClass().add("error-text"); + if (btcNetworkWarnMsgPopup == null) { + btcNetworkWarnMsgPopup = new Popup().warning(newValue); + btcNetworkWarnMsgPopup.show(); + } + } else { + btcInfoLabel.setId("footer-pane"); + if (btcNetworkWarnMsgPopup != null) + btcNetworkWarnMsgPopup.hide(); + } + }); + + // temporarily disabled due to high CPU usage (per issue #4649) + //model.getCombinedSyncProgress().addListener((ov, oldValue, newValue) -> { + // if ((double) newValue >= 1) { + // blockchainSyncIndicator.setVisible(false); + // blockchainSyncIndicator.setManaged(false); + // } + //}); + + // version + Label versionLabel = new AutoTooltipLabel(); + versionLabel.setId("footer-pane"); + versionLabel.setTextAlignment(TextAlignment.CENTER); + versionLabel.setAlignment(Pos.BASELINE_CENTER); + versionLabel.textProperty().bind(model.getCombinedFooterInfo()); + root.widthProperty().addListener((ov, oldValue, newValue) -> + versionLabel.setLayoutX(((double) newValue - versionLabel.getWidth()) / 2)); + model.getNewVersionAvailableProperty().addListener((observable, oldValue, newValue) -> { + versionLabel.getStyleClass().removeAll("version-new", "version"); + if (newValue) { + versionLabel.getStyleClass().add("version-new"); + versionLabel.setOnMouseClicked(e -> model.onOpenDownloadWindow()); + } else { + versionLabel.getStyleClass().add("version"); + versionLabel.setOnMouseClicked(null); + } + }); + HBox versionBox = new HBox(); + versionBox.setSpacing(10); + versionBox.setAlignment(Pos.CENTER); + versionBox.setAlignment(Pos.BASELINE_CENTER); + versionBox.getChildren().addAll(versionLabel); //blockchainSyncIndicator removed per issue #4649 + setLeftAnchor(versionBox, 10d); + setRightAnchor(versionBox, 10d); + setBottomAnchor(versionBox, 7d); + + // P2P Network + Label p2PNetworkLabel = new AutoTooltipLabel(); + p2PNetworkLabel.setId("footer-pane"); + p2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); + + ImageView p2PNetworkIcon = new ImageView(); + setRightAnchor(p2PNetworkIcon, 10d); + setBottomAnchor(p2PNetworkIcon, 5d); + p2PNetworkIcon.setOpacity(0.4); + p2PNetworkIcon.idProperty().bind(model.getP2PNetworkIconId()); + p2PNetworkLabel.idProperty().bind(model.getP2pNetworkLabelId()); + model.getP2pNetworkWarnMsg().addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + p2PNetworkWarnMsgPopup = new Popup().warning(newValue); + p2PNetworkWarnMsgPopup.show(); + } else if (p2PNetworkWarnMsgPopup != null) { + p2PNetworkWarnMsgPopup.hide(); + } + }); + + model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> { + p2PNetworkIcon.setOpacity(1); + p2pNetworkProgressBar.setProgress(0); + }); + + p2pNetworkProgressBar = new JFXProgressBar(-1); + p2pNetworkProgressBar.setMaxHeight(2); + p2pNetworkProgressBar.prefWidthProperty().bind(p2PNetworkLabel.widthProperty()); + + VBox vBox = new VBox(); + vBox.setAlignment(Pos.CENTER_RIGHT); + vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); + setRightAnchor(vBox, 33d); + setBottomAnchor(vBox, 5d); + + return new AnchorPane(separator, btcInfoLabel, versionBox, vBox, p2PNetworkIcon) {{ + setId("footer-pane"); + setMinHeight(30); + setMaxHeight(30); + }}; + } + + private void setupBadge(JFXBadge buttonWithBadge, StringProperty badgeNumber, BooleanProperty badgeEnabled) { + buttonWithBadge.textProperty().bind(badgeNumber); + buttonWithBadge.setEnabled(badgeEnabled.get()); + badgeEnabled.addListener((observable, oldValue, newValue) -> { + buttonWithBadge.setEnabled(newValue); + buttonWithBadge.refreshBadge(); + }); + + buttonWithBadge.setPosition(Pos.TOP_RIGHT); + buttonWithBadge.setMinHeight(34); + buttonWithBadge.setMaxHeight(34); + } + + private class NavButton extends AutoTooltipToggleButton { + + private final Class viewClass; + + NavButton(Class viewClass, String title) { + super(title); + + this.viewClass = viewClass; + + this.setToggleGroup(navButtons); + this.getStyleClass().add("nav-button"); + // Japanese fonts are dense, increase top nav button text size + if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { + this.getStyleClass().add("nav-button-japanese"); + } + + this.selectedProperty().addListener((ov, oldValue, newValue) -> this.setMouseTransparent(newValue)); + + this.setOnAction(e -> navigation.navigateTo(MainView.class, viewClass)); + } + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java new file mode 100644 index 0000000000..d9e14837de --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -0,0 +1,807 @@ +/* + * 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 bisq.desktop.main; + +import bisq.desktop.app.BisqApp; +import bisq.desktop.common.model.ViewModel; +import bisq.desktop.components.TxIdTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.overlays.notifications.NotificationCenter; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow; +import bisq.desktop.main.overlays.windows.TacWindow; +import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; +import bisq.desktop.main.overlays.windows.UpdateAmazonGiftCardAccountWindow; +import bisq.desktop.main.overlays.windows.UpdateRevolutAccountWindow; +import bisq.desktop.main.overlays.windows.WalletPasswordWindow; +import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; +import bisq.desktop.main.presentation.AccountPresentation; +import bisq.desktop.main.presentation.DaoPresentation; +import bisq.desktop.main.presentation.MarketPricePresentation; +import bisq.desktop.main.presentation.SettingsPresentation; +import bisq.desktop.main.shared.PriceFeedComboBoxItem; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.BisqSetup; +import bisq.core.btc.nodes.LocalBitcoinNode; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.AliPayAccount; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.presentation.BalancePresentation; +import bisq.core.presentation.SupportTicketsPresentation; +import bisq.core.presentation.TradePresentation; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.TradeManager; +import bisq.core.user.DontShowAgainLookup; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.util.Tuple2; + +import com.google.inject.Inject; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { + private final BisqSetup bisqSetup; + private final WalletsSetup walletsSetup; + private final User user; + private final BalancePresentation balancePresentation; + private final TradePresentation tradePresentation; + private final SupportTicketsPresentation supportTicketsPresentation; + private final MarketPricePresentation marketPricePresentation; + private final DaoPresentation daoPresentation; + private final AccountPresentation accountPresentation; + private final SettingsPresentation settingsPresentation; + private final P2PService p2PService; + private final TradeManager tradeManager; + private final OpenOfferManager openOfferManager; + @Getter + private final Preferences preferences; + private final PrivateNotificationManager privateNotificationManager; + private final WalletPasswordWindow walletPasswordWindow; + private final NotificationCenter notificationCenter; + private final TacWindow tacWindow; + @Getter + private final PriceFeedService priceFeedService; + private final Config config; + private final LocalBitcoinNode localBitcoinNode; + private final AccountAgeWitnessService accountAgeWitnessService; + @Getter + private final TorNetworkSettingsWindow torNetworkSettingsWindow; + private final CorruptedStorageFileHandler corruptedStorageFileHandler; + + @Getter + private final BooleanProperty showAppScreen = new SimpleBooleanProperty(); + private final DoubleProperty combinedSyncProgress = new SimpleDoubleProperty(-1); + private final BooleanProperty isSplashScreenRemoved = new SimpleBooleanProperty(); + private final StringProperty footerVersionInfo = new SimpleStringProperty(); + private Timer checkNumberOfBtcPeersTimer; + private Timer checkNumberOfP2pNetworkPeersTimer; + @SuppressWarnings("FieldCanBeLocal") + private MonadicBinding tradesAndUIReady; + private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MainViewModel(BisqSetup bisqSetup, + WalletsSetup walletsSetup, + BtcWalletService btcWalletService, + User user, + BalancePresentation balancePresentation, + TradePresentation tradePresentation, + SupportTicketsPresentation supportTicketsPresentation, + MarketPricePresentation marketPricePresentation, + DaoPresentation daoPresentation, + AccountPresentation accountPresentation, + SettingsPresentation settingsPresentation, + P2PService p2PService, + TradeManager tradeManager, + OpenOfferManager openOfferManager, + Preferences preferences, + PrivateNotificationManager privateNotificationManager, + WalletPasswordWindow walletPasswordWindow, + NotificationCenter notificationCenter, + TacWindow tacWindow, + FeeService feeService, + PriceFeedService priceFeedService, + Config config, + LocalBitcoinNode localBitcoinNode, + AccountAgeWitnessService accountAgeWitnessService, + TorNetworkSettingsWindow torNetworkSettingsWindow, + CorruptedStorageFileHandler corruptedStorageFileHandler) { + this.bisqSetup = bisqSetup; + this.walletsSetup = walletsSetup; + this.user = user; + this.balancePresentation = balancePresentation; + this.tradePresentation = tradePresentation; + this.supportTicketsPresentation = supportTicketsPresentation; + this.marketPricePresentation = marketPricePresentation; + this.daoPresentation = daoPresentation; + this.accountPresentation = accountPresentation; + this.settingsPresentation = settingsPresentation; + this.p2PService = p2PService; + this.tradeManager = tradeManager; + this.openOfferManager = openOfferManager; + this.preferences = preferences; + this.privateNotificationManager = privateNotificationManager; + this.walletPasswordWindow = walletPasswordWindow; + this.notificationCenter = notificationCenter; + this.tacWindow = tacWindow; + this.priceFeedService = priceFeedService; + this.config = config; + this.localBitcoinNode = localBitcoinNode; + this.accountAgeWitnessService = accountAgeWitnessService; + this.torNetworkSettingsWindow = torNetworkSettingsWindow; + this.corruptedStorageFileHandler = corruptedStorageFileHandler; + + TxIdTextField.setPreferences(preferences); + + TxIdTextField.setWalletService(btcWalletService); + + GUIUtil.setFeeService(feeService); + GUIUtil.setPreferences(preferences); + + setupHandlers(); + bisqSetup.addBisqSetupListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BisqSetupListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onSetupComplete() { + // We handle the trade period here as we display a global popup if we reached dispute time + tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.persistedTradesInitializedProperty(), + (a, b) -> a && b); + tradesAndUIReady.subscribe((observable, oldValue, newValue) -> { + if (newValue) { + tradeManager.applyTradePeriodState(); + + tradeManager.getObservableList().forEach(trade -> { + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + String key; + switch (trade.getTradePeriodState()) { + case FIRST_HALF: + break; + case SECOND_HALF: + key = "displayHalfTradePeriodOver" + trade.getId(); + if (DontShowAgainLookup.showAgain(key)) { + DontShowAgainLookup.dontShowAgain(key, true); + new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached", + trade.getShortId(), + DisplayUtils.formatDateTime(maxTradePeriodDate))) + .show(); + } + break; + case TRADE_PERIOD_OVER: + key = "displayTradePeriodOver" + trade.getId(); + if (DontShowAgainLookup.showAgain(key)) { + DontShowAgainLookup.dontShowAgain(key, true); + new Popup().warning(Res.get("popup.warning.tradePeriod.ended", + trade.getShortId(), + DisplayUtils.formatDateTime(maxTradePeriodDate))) + .show(); + } + break; + } + }); + } + }); + + setupP2PNumPeersWatcher(); + setupBtcNumPeersWatcher(); + + marketPricePresentation.setup(); + daoPresentation.setup(); + accountPresentation.setup(); + settingsPresentation.setup(); + + if (DevEnv.isDevMode()) { + preferences.setShowOwnOffersInOfferBook(true); + setupDevDummyPaymentAccounts(); + } + + getShowAppScreen().set(true); + + // We only show the popup if the user has already set up any fiat account. For new users it is not a rule + // change and for altcoins its not relevant. + String key = "newFeatureDuplicateOffer"; + if (DontShowAgainLookup.showAgain(key)) { + UserThread.runAfter(() -> { + new Popup().attention(Res.get("popup.attention.newFeatureDuplicateOffer")). + dontShowAgainId(key) + .closeButtonText(Res.get("shared.iUnderstand")) + .show(); + }, 1); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI handlers + /////////////////////////////////////////////////////////////////////////////////////////// + + // After showAppScreen is set and splash screen is faded out + void onSplashScreenRemoved() { + isSplashScreenRemoved.set(true); + + // Delay that as we want to know what is the current path of the navigation which is set + // in MainView showAppScreen handler + notificationCenter.onAllServicesAndViewsInitialized(); + + maybeShowPopupsFromQueue(); + } + + void onOpenDownloadWindow() { + bisqSetup.displayAlertIfPresent(user.getDisplayedAlert(), true); + } + + void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { + marketPricePresentation.setPriceFeedComboBoxItem(item); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void setupHandlers() { + bisqSetup.setDisplayTacHandler(acceptedHandler -> UserThread.runAfter(() -> { + //noinspection FunctionalExpressionCanBeFolded + tacWindow.onAction(acceptedHandler::run).show(); + }, 1)); + + bisqSetup.setDisplayTorNetworkSettingsHandler(show -> { + if (show) { + torNetworkSettingsWindow.show(); + } else if (torNetworkSettingsWindow.isDisplayed()) { + torNetworkSettingsWindow.hide(); + } + }); + bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) + .actionButtonText(Res.get("settings.net.reSyncSPVChainButton")) + .onAction(() -> GUIUtil.reSyncSPVChain(preferences)) + .show()); + bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn(voteResultException.toString())); + + bisqSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) + .useShutDownButton() + .show()); + bisqSetup.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); + bisqSetup.setShowFirstPopupIfResyncSPVRequestedHandler(this::showFirstPopupIfResyncSPVRequested); + bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> walletPasswordWindow + .onAesKey(aesKeyHandler::accept) + .onClose(() -> BisqApp.getShutDownHandler().run()) + .show()); + + bisqSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config) + .actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater")) + .onAction(() -> { + preferences.dontShowAgain(key, false); // update later + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(() -> { + preferences.dontShowAgain(key, true); // ignore update + }) + .show()); + bisqSetup.setDisplayAlertHandler(alert -> new DisplayAlertMessageWindow() + .alertMessage(alert) + .closeButtonText(Res.get("shared.close")) + .onClose(() -> user.setDisplayedAlert(alert)) + .show()); + bisqSetup.setDisplayPrivateNotificationHandler(privateNotification -> + new Popup().headLine(Res.get("popup.privateNotification.headline")) + .attention(privateNotification.getMessage()) + .onClose(privateNotificationManager::removePrivateNotification) + .useIUnderstandButton() + .show()); + bisqSetup.setDaoErrorMessageHandler(errorMessage -> new Popup().error(errorMessage).show()); + bisqSetup.setDaoWarnMessageHandler(warnMessage -> new Popup().warning(warnMessage).show()); + bisqSetup.setDisplaySecurityRecommendationHandler(key -> + new Popup().headLine(Res.get("popup.securityRecommendation.headline")) + .information(Res.get("popup.securityRecommendation.msg")) + .dontShowAgainId(key) + .show()); + bisqSetup.setDisplayLocalhostHandler(key -> { + if (!DevEnv.isDevMode()) { + Popup popup = new Popup().backgroundInfo(Res.get("popup.bitcoinLocalhostNode.msg")) + .dontShowAgainId(key); + popup.setDisplayOrderPriority(5); + popupQueue.add(popup); + } + }); + bisqSetup.setDisplaySignedByArbitratorHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + key, "popup.accountSigning.signedByArbitrator")); + bisqSetup.setDisplaySignedByPeerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + key, "popup.accountSigning.signedByPeer", String.valueOf(SignedWitnessService.SIGNER_AGE_DAYS))); + bisqSetup.setDisplayPeerLimitLiftedHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + key, "popup.accountSigning.peerLimitLifted")); + bisqSetup.setDisplayPeerSignerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + key, "popup.accountSigning.peerSigner")); + + bisqSetup.setWrongOSArchitectureHandler(msg -> new Popup().warning(msg).show()); + + bisqSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); + + bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { + // We copy the array as we will mutate it later + showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); + }); + bisqSetup.setAmazonGiftCardAccountsUpdateHandler(amazonGiftCardAccountList -> { + // We copy the array as we will mutate it later + showAmazonGiftCardAccountUpdateWindow(new ArrayList<>(amazonGiftCardAccountList)); + }); + bisqSetup.setOsxKeyLoggerWarningHandler(() -> { + String key = "osxKeyLoggerWarning"; + if (preferences.showAgain(key)) { + new Popup().warning(Res.get("popup.warning.osxKeyLoggerWarning")) + .closeButtonText(Res.get("shared.iUnderstand")) + .dontShowAgainId(key) + .show(); + } + }); + bisqSetup.setQubesOSInfoHandler(() -> { + String key = "qubesOSSetupInfo"; + if (preferences.showAgain(key)) { + new Popup().information(Res.get("popup.info.qubesOSSetupInfo")) + .closeButtonText(Res.get("shared.iUnderstand")) + .dontShowAgainId(key) + .show(); + } + }); + + bisqSetup.setDownGradePreventionHandler(lastVersion -> { + new Popup().warning(Res.get("popup.warn.downGradePrevention", lastVersion, Version.VERSION)) + .useShutDownButton() + .hideCloseButton() + .show(); + }); + + bisqSetup.setDaoRequiresRestartHandler(() -> new Popup().warning("popup.warn.daoRequiresRestart") + .useShutDownButton() + .hideCloseButton() + .show()); + + corruptedStorageFileHandler.getFiles().ifPresent(files -> new Popup() + .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) + .useShutDownButton() + .show()); + + tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> new Popup() + .warning(Res.get("popup.error.takeOfferRequestFailed", errorMessage)) + .show()); + + bisqSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); + daoPresentation.getBsqSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); + + bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); + + this.footerVersionInfo.setValue("v" + Version.VERSION); + this.getNewVersionAvailableProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + this.footerVersionInfo.setValue("v" + Version.VERSION + " " + Res.get("mainView.version.update")); + } else { + this.footerVersionInfo.setValue("v" + Version.VERSION); + } + }); + + if (p2PService.isBootstrapped()) { + setupInvalidOpenOffersHandler(); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + setupInvalidOpenOffersHandler(); + } + }); + } + } + + private void showRevolutAccountUpdateWindow(List revolutAccountList) { + if (!revolutAccountList.isEmpty()) { + RevolutAccount revolutAccount = revolutAccountList.get(0); + revolutAccountList.remove(0); + new UpdateRevolutAccountWindow(revolutAccount, user).onClose(() -> { + // We delay a bit in case we have multiple account for better UX + UserThread.runAfter(() -> showRevolutAccountUpdateWindow(revolutAccountList), 300, TimeUnit.MILLISECONDS); + }).show(); + } + } + + private void showAmazonGiftCardAccountUpdateWindow(List amazonGiftCardAccountList) { + if (!amazonGiftCardAccountList.isEmpty()) { + AmazonGiftCardAccount amazonGiftCardAccount = amazonGiftCardAccountList.get(0); + amazonGiftCardAccountList.remove(0); + new UpdateAmazonGiftCardAccountWindow(amazonGiftCardAccount, user).onClose(() -> { + // We delay a bit in case we have multiple account for better UX + UserThread.runAfter(() -> showAmazonGiftCardAccountUpdateWindow(amazonGiftCardAccountList), 300, TimeUnit.MILLISECONDS); + }).show(); + } + } + + private void setupP2PNumPeersWatcher() { + p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { + int numPeers = (int) newValue; + if ((int) oldValue > 0 && numPeers == 0) { + // give a bit of tolerance + if (checkNumberOfP2pNetworkPeersTimer != null) + checkNumberOfP2pNetworkPeersTimer.stop(); + + checkNumberOfP2pNetworkPeersTimer = UserThread.runAfter(() -> { + // check again numPeers + if (p2PService.getNumConnectedPeers().get() == 0) { + getP2pNetworkWarnMsg().set(Res.get("mainView.networkWarning.allConnectionsLost", Res.get("shared.P2P"))); + getP2pNetworkLabelId().set("splash-error-state-msg"); + } else { + getP2pNetworkWarnMsg().set(null); + getP2pNetworkLabelId().set("footer-pane"); + } + }, 5); + } else if ((int) oldValue == 0 && numPeers > 0) { + if (checkNumberOfP2pNetworkPeersTimer != null) + checkNumberOfP2pNetworkPeersTimer.stop(); + + getP2pNetworkWarnMsg().set(null); + getP2pNetworkLabelId().set("footer-pane"); + } + }); + } + + private void setupBtcNumPeersWatcher() { + walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { + int numPeers = (int) newValue; + if ((int) oldValue > 0 && numPeers == 0) { + if (checkNumberOfBtcPeersTimer != null) + checkNumberOfBtcPeersTimer.stop(); + + checkNumberOfBtcPeersTimer = UserThread.runAfter(() -> { + // check again numPeers + if (walletsSetup.numPeersProperty().get() == 0) { + if (localBitcoinNode.shouldBeUsed()) + getWalletServiceErrorMsg().set( + Res.get("mainView.networkWarning.localhostBitcoinLost", + Res.getBaseCurrencyName().toLowerCase())); + else + getWalletServiceErrorMsg().set( + Res.get("mainView.networkWarning.allConnectionsLost", + Res.getBaseCurrencyName().toLowerCase())); + } else { + getWalletServiceErrorMsg().set(null); + } + }, 5); + } else if ((int) oldValue == 0 && numPeers > 0) { + if (checkNumberOfBtcPeersTimer != null) + checkNumberOfBtcPeersTimer.stop(); + getWalletServiceErrorMsg().set(null); + } + }); + } + + private void showFirstPopupIfResyncSPVRequested() { + Popup firstPopup = new Popup(); + firstPopup.information(Res.get("settings.net.reSyncSPVAfterRestart")).show(); + if (bisqSetup.getBtcSyncProgress().get() == 1) { + showSecondPopupIfResyncSPVRequested(firstPopup); + } else { + bisqSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> { + if ((double) newValue == 1) + showSecondPopupIfResyncSPVRequested(firstPopup); + }); + } + } + + private void showSecondPopupIfResyncSPVRequested(Popup firstPopup) { + firstPopup.hide(); + BisqSetup.setResyncSpvSemaphore(false); + new Popup().information(Res.get("settings.net.reSyncSPVAfterRestartCompleted")) + .hideCloseButton() + .useShutDownButton() + .show(); + } + + private void showPopupIfInvalidBtcConfig() { + preferences.setBitcoinNodesOptionOrdinal(0); + new Popup().warning(Res.get("settings.net.warn.invalidBtcConfig")) + .hideCloseButton() + .useShutDownButton() + .show(); + } + + private void setupDevDummyPaymentAccounts() { + if (user.getPaymentAccounts() != null && user.getPaymentAccounts().isEmpty()) { + AliPayAccount aliPayAccount = new AliPayAccount(); + aliPayAccount.init(); + aliPayAccount.setAccountNr("dummy_" + new Random().nextInt(100)); + aliPayAccount.setAccountName("AliPayAccount dummy");// Don't translate only for dev + user.addPaymentAccount(aliPayAccount); + + if (p2PService.isBootstrapped()) { + accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); + } + }); + } + + CryptoCurrencyAccount cryptoCurrencyAccount = new CryptoCurrencyAccount(); + cryptoCurrencyAccount.init(); + cryptoCurrencyAccount.setAccountName("ETH dummy");// Don't translate only for dev + cryptoCurrencyAccount.setAddress("0x" + new Random().nextInt(1000000)); + Optional eth = CurrencyUtil.getCryptoCurrency("ETH"); + eth.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); + + user.addPaymentAccount(cryptoCurrencyAccount); + } + } + + private void updateBtcSyncProgress() { + final DoubleProperty btcSyncProgress = bisqSetup.getBtcSyncProgress(); + + if (btcSyncProgress.doubleValue() < 1) { + combinedSyncProgress.set(btcSyncProgress.doubleValue()); + } else { + combinedSyncProgress.set(daoPresentation.getBsqSyncProgress().doubleValue()); + } + } + + private void setupInvalidOpenOffersHandler() { + openOfferManager.getInvalidOffers().addListener((ListChangeListener>) c -> { + c.next(); + if (c.wasAdded()) { + handleInvalidOpenOffers(c.getAddedSubList()); + } + }); + handleInvalidOpenOffers(openOfferManager.getInvalidOffers()); + } + + private void handleInvalidOpenOffers(List> list) { + list.forEach(tuple2 -> { + String errorMsg = tuple2.second; + OpenOffer openOffer = tuple2.first; + new Popup().warning(errorMsg) + .width(1000) + .actionButtonText(Res.get("shared.removeOffer")) + .onAction(() -> { + openOfferManager.removeOpenOffer(openOffer, () -> { + log.info("Invalid open offer with ID {} was successfully removed.", openOffer.getId()); + }, log::error); + + }) + .hideCloseButton() + .show(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MainView delegate getters + /////////////////////////////////////////////////////////////////////////////////////////// + + BooleanProperty getNewVersionAvailableProperty() { + return bisqSetup.getNewVersionAvailableProperty(); + } + + StringProperty getNumOpenSupportTickets() { + return supportTicketsPresentation.getNumOpenSupportTickets(); + } + + BooleanProperty getShowOpenSupportTicketsNotification() { + return supportTicketsPresentation.getShowOpenSupportTicketsNotification(); + } + + BooleanProperty getShowPendingTradesNotification() { + return tradePresentation.getShowPendingTradesNotification(); + } + + StringProperty getNumPendingTrades() { + return tradePresentation.getNumPendingTrades(); + } + + StringProperty getAvailableBalance() { + return balancePresentation.getAvailableBalance(); + } + + StringProperty getReservedBalance() { + return balancePresentation.getReservedBalance(); + } + + StringProperty getLockedBalance() { + return balancePresentation.getLockedBalance(); + } + + + // Wallet + StringProperty getBtcInfo() { + final StringProperty combinedInfo = new SimpleStringProperty(); + combinedInfo.bind(bisqSetup.getBtcInfo()); + return combinedInfo; + } + + StringProperty getCombinedFooterInfo() { + final StringProperty combinedInfo = new SimpleStringProperty(); + combinedInfo.bind(Bindings.concat(this.footerVersionInfo, " ", daoPresentation.getBsqInfo())); + return combinedInfo; + } + + DoubleProperty getCombinedSyncProgress() { + return combinedSyncProgress; + } + + StringProperty getWalletServiceErrorMsg() { + return bisqSetup.getWalletServiceErrorMsg(); + } + + StringProperty getBtcSplashSyncIconId() { + return bisqSetup.getBtcSplashSyncIconId(); + } + + BooleanProperty getUseTorForBTC() { + return bisqSetup.getUseTorForBTC(); + } + + // P2P + StringProperty getP2PNetworkInfo() { + return bisqSetup.getP2PNetworkInfo(); + } + + BooleanProperty getSplashP2PNetworkAnimationVisible() { + return bisqSetup.getSplashP2PNetworkAnimationVisible(); + } + + StringProperty getP2pNetworkWarnMsg() { + return bisqSetup.getP2pNetworkWarnMsg(); + } + + StringProperty getP2PNetworkIconId() { + return bisqSetup.getP2PNetworkIconId(); + } + + BooleanProperty getUpdatedDataReceived() { + return bisqSetup.getUpdatedDataReceived(); + } + + StringProperty getP2pNetworkLabelId() { + return bisqSetup.getP2pNetworkLabelId(); + } + + // marketPricePresentation + ObjectProperty getSelectedPriceFeedComboBoxItemProperty() { + return marketPricePresentation.getSelectedPriceFeedComboBoxItemProperty(); + } + + BooleanProperty getIsFiatCurrencyPriceFeedSelected() { + return marketPricePresentation.getIsFiatCurrencyPriceFeedSelected(); + } + + BooleanProperty getIsExternallyProvidedPrice() { + return marketPricePresentation.getIsExternallyProvidedPrice(); + } + + BooleanProperty getIsPriceAvailable() { + return marketPricePresentation.getIsPriceAvailable(); + } + + IntegerProperty getMarketPriceUpdated() { + return marketPricePresentation.getMarketPriceUpdated(); + } + + StringProperty getMarketPrice() { + return marketPricePresentation.getMarketPrice(); + } + + StringProperty getMarketPrice(String currencyCode) { + return marketPricePresentation.getMarketPrice(currencyCode); + } + + public ObservableList getPriceFeedComboBoxItems() { + return marketPricePresentation.getPriceFeedComboBoxItems(); + } + + // We keep daoPresentation and accountPresentation support even it is not used atm. But if we add a new feature and + // add a badge again it will be needed. + @SuppressWarnings({"unused"}) + public BooleanProperty getShowDaoUpdatesNotification() { + return daoPresentation.getShowDaoUpdatesNotification(); + } + + // We keep daoPresentation and accountPresentation support even it is not used atm. But if we add a new feature and + // add a badge again it will be needed. + @SuppressWarnings("unused") + public BooleanProperty getShowAccountUpdatesNotification() { + return accountPresentation.getShowAccountUpdatesNotification(); + } + + public BooleanProperty getShowSettingsUpdatesNotification() { + return settingsPresentation.getShowSettingsUpdatesNotification(); + } + + private void maybeShowPopupsFromQueue() { + if (!popupQueue.isEmpty()) { + Overlay overlay = popupQueue.poll(); + overlay.getIsHiddenProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + UserThread.runAfter(this::maybeShowPopupsFromQueue, 2); + } + }); + overlay.show(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java new file mode 100644 index 0000000000..7901c4689c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java @@ -0,0 +1,244 @@ +/* + * 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 bisq.desktop.main; + +import bisq.desktop.util.validation.AltcoinValidator; +import bisq.desktop.util.validation.FiatPriceValidator; +import bisq.desktop.util.validation.MonetaryValidator; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Optional; + +import javax.annotation.Nullable; + +import static bisq.desktop.main.shared.ChatView.log; +import static com.google.common.base.Preconditions.checkNotNull; + +@Singleton +public class PriceUtil { + private final PriceFeedService priceFeedService; + private final TradeStatisticsManager tradeStatisticsManager; + private final Preferences preferences; + @Nullable + private Price bsq30DayAveragePrice; + + @Inject + public PriceUtil(PriceFeedService priceFeedService, + TradeStatisticsManager tradeStatisticsManager, + Preferences preferences) { + this.priceFeedService = priceFeedService; + this.tradeStatisticsManager = tradeStatisticsManager; + this.preferences = preferences; + } + + public static MonetaryValidator getPriceValidator(boolean isFiatCurrency) { + return isFiatCurrency ? + new FiatPriceValidator() : + new AltcoinValidator(); + } + + public static InputValidator.ValidationResult isTriggerPriceValid(String triggerPriceAsString, + Price price, + boolean isSellOffer, + boolean isFiatCurrency) { + if (triggerPriceAsString == null || triggerPriceAsString.isEmpty()) { + return new InputValidator.ValidationResult(true); + } + + InputValidator.ValidationResult result = getPriceValidator(isFiatCurrency).validate(triggerPriceAsString); + if (!result.isValid) { + return result; + } + + long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, price.getCurrencyCode()); + long priceAsLong = price.getValue(); + String priceAsString = FormattingUtils.formatPrice(price); + if ((isSellOffer && isFiatCurrency) || (!isSellOffer && !isFiatCurrency)) { + if (triggerPriceAsLong >= priceAsLong) { + return new InputValidator.ValidationResult(false, + Res.get("createOffer.triggerPrice.invalid.tooHigh", priceAsString)); + } else { + return new InputValidator.ValidationResult(true); + } + } else { + if (triggerPriceAsLong <= priceAsLong) { + return new InputValidator.ValidationResult(false, + Res.get("createOffer.triggerPrice.invalid.tooLow", priceAsString)); + } else { + return new InputValidator.ValidationResult(true); + } + } + } + + public static Price marketPriceToPrice(MarketPrice marketPrice) { + String currencyCode = marketPrice.getCurrencyCode(); + double priceAsDouble = marketPrice.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(priceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } + + public void recalculateBsq30DayAveragePrice() { + bsq30DayAveragePrice = null; + bsq30DayAveragePrice = getBsq30DayAveragePrice(); + } + + public Price getBsq30DayAveragePrice() { + if (bsq30DayAveragePrice == null) { + bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences, + tradeStatisticsManager, 30).second; + } + return bsq30DayAveragePrice; + } + + public boolean hasMarketPrice(Offer offer) { + String currencyCode = offer.getCurrencyCode(); + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + Price price = offer.getPrice(); + return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable(); + } + + public Optional getMarketBasedPrice(Offer offer, + OfferPayload.Direction direction) { + if (offer.isUseMarketBasedPrice()) { + return Optional.of(offer.getMarketPriceMargin()); + } + + if (!hasMarketPrice(offer)) { + if (offer.getCurrencyCode().equals("BSQ")) { + Price bsq30DayAveragePrice = getBsq30DayAveragePrice(); + if (bsq30DayAveragePrice.isPositive()) { + double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8); + return calculatePercentage(offer, scaled, direction); + } else { + return Optional.empty(); + } + } else { + log.trace("We don't have a market price. " + + "That case could only happen if you don't have a price feed."); + return Optional.empty(); + } + } + + String currencyCode = offer.getCurrencyCode(); + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + double marketPriceAsDouble = checkNotNull(marketPrice).getPrice(); + return calculatePercentage(offer, marketPriceAsDouble, direction); + } + + public Optional calculatePercentage(Offer offer, + double marketPrice, + OfferPayload.Direction direction) { + // If the offer did not use % price we calculate % from current market price + String currencyCode = offer.getCurrencyCode(); + Price price = offer.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long priceAsLong = checkNotNull(price).getValue(); + double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); + double value; + if (direction == OfferPayload.Direction.SELL) { + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + if (marketPrice == 0) { + return Optional.empty(); + } + value = 1 - scaled / marketPrice; + } else { + if (marketPrice == 1) { + return Optional.empty(); + } + value = scaled / marketPrice - 1; + } + } else { + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + if (marketPrice == 1) { + return Optional.empty(); + } + value = scaled / marketPrice - 1; + } else { + if (marketPrice == 0) { + return Optional.empty(); + } + value = 1 - scaled / marketPrice; + } + } + return Optional.of(value); + } + + public static long getMarketPriceAsLong(String inputValue, String currencyCode) { + if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { + return 0; + } + + try { + int precision = getMarketPricePrecision(currencyCode); + String stringValue = reformatMarketPrice(inputValue, currencyCode); + return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision); + } catch (Throwable t) { + return 0; + } + } + + public static String reformatMarketPrice(String inputValue, String currencyCode) { + if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { + return ""; + } + + double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue); + int precision = getMarketPricePrecision(currencyCode); + return FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision); + } + + public static String formatMarketPrice(long price, String currencyCode) { + int marketPricePrecision = getMarketPricePrecision(currencyCode); + double scaled = MathUtils.scaleDownByPowerOf10(price, marketPricePrecision); + return FormattingUtils.formatMarketPrice(scaled, marketPricePrecision); + } + + public static int getMarketPricePrecision(String currencyCode) { + return CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java b/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java new file mode 100644 index 0000000000..5b7e6e5b80 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java @@ -0,0 +1,87 @@ +/* + * 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 bisq.desktop.main; + +import bisq.desktop.app.BisqApp; +import bisq.desktop.main.overlays.popups.Popup; + +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; + +import bisq.common.UserThread; +import bisq.common.file.FileUtil; + +import org.bitcoinj.wallet.DeterministicSeed; + +import java.io.File; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +/** + * This serves as shared space for static methods used from different views where no common parent view would fit as + * owner of that code. We keep it strictly static. It should replace GUIUtil for those methods which are not utility + * methods. + */ +@Slf4j +public class SharedPresentation { + public static void restoreSeedWords(WalletsManager walletsManager, + OpenOfferManager openOfferManager, + DeterministicSeed seed, + File storageDir) { + if (!openOfferManager.getObservableList().isEmpty()) { + UserThread.runAfter(() -> + new Popup().warning(Res.get("seed.restore.openOffers.warn")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + openOfferManager.removeAllOpenOffers(() -> { + doRestoreSeedWords(walletsManager, seed, storageDir); + }); + }) + .show(), 100, TimeUnit.MILLISECONDS); + } else { + doRestoreSeedWords(walletsManager, seed, storageDir); + } + } + + private static void doRestoreSeedWords(WalletsManager walletsManager, + DeterministicSeed seed, + File storageDir) { + try { + File backup = new File(storageDir, "AddressEntryList_backup_pre_wallet_restore_" + System.currentTimeMillis()); + FileUtil.copyFile(new File(storageDir, "AddressEntryList"), backup); + } catch (Throwable t) { + new Popup().error(Res.get("error.deleteAddressEntryListFailed", t)).show(); + } + + walletsManager.restoreSeedWords( + seed, + () -> UserThread.execute(() -> { + log.info("Wallets restored with seed words"); + new Popup().feedback(Res.get("seed.restore.success")).hideCloseButton().show(); + BisqApp.getShutDownHandler().run(); + }), + throwable -> UserThread.execute(() -> { + log.error(throwable.toString()); + new Popup().error(Res.get("seed.restore.error", Res.get("shared.errorMessageInline", throwable))) + .show(); + })); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/AccountView.fxml b/desktop/src/main/java/bisq/desktop/main/account/AccountView.fxml new file mode 100644 index 0000000000..cf17546702 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/AccountView.fxml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java new file mode 100644 index 0000000000..40993a913e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java @@ -0,0 +1,349 @@ +/* + * 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 bisq.desktop.main.account; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.main.MainView; +import bisq.desktop.main.account.content.altcoinaccounts.AltCoinAccountsView; +import bisq.desktop.main.account.content.backup.BackupView; +import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; +import bisq.desktop.main.account.content.notifications.MobileNotificationsView; +import bisq.desktop.main.account.content.password.PasswordView; +import bisq.desktop.main.account.content.seedwords.SeedWordsView; +import bisq.desktop.main.account.content.walletinfo.WalletInfoView; +import bisq.desktop.main.account.register.arbitrator.ArbitratorRegistrationView; +import bisq.desktop.main.account.register.mediator.MediatorRegistrationView; +import bisq.desktop.main.account.register.refundagent.RefundAgentRegistrationView; +import bisq.desktop.main.account.register.signing.SigningView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.presentation.AccountPresentation; + +import bisq.core.locale.Res; +import bisq.core.user.DontShowAgainLookup; + +import bisq.common.app.DevEnv; +import bisq.common.util.Utilities; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.Scene; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; + +import javafx.beans.value.ChangeListener; + +import javafx.event.EventHandler; + +import javafx.collections.ListChangeListener; + +import java.util.List; + +@FxmlView +public class AccountView extends ActivatableView { + + @FXML + Tab fiatAccountsTab, altcoinAccountsTab, notificationTab, + passwordTab, seedWordsTab, walletInfoTab, backupTab; + + private Navigation.Listener navigationListener; + private ChangeListener tabChangeListener; + + private final ViewLoader viewLoader; + private final Navigation navigation; + private Tab selectedTab; + private Tab arbitratorRegistrationTab; + private Tab mediatorRegistrationTab; + private Tab refundAgentRegistrationTab; + private Tab signingTab; + private ArbitratorRegistrationView arbitratorRegistrationView; + private MediatorRegistrationView mediatorRegistrationView; + private RefundAgentRegistrationView refundAgentRegistrationView; + private Scene scene; + private EventHandler keyEventEventHandler; + private ListChangeListener tabListChangeListener; + + @Inject + private AccountView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + + root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); + + fiatAccountsTab.setText(Res.get("account.menu.paymentAccount").toUpperCase()); + altcoinAccountsTab.setText(Res.get("account.menu.altCoinsAccountView").toUpperCase()); + notificationTab.setText(Res.get("account.menu.notifications").toUpperCase()); + passwordTab.setText(Res.get("account.menu.password").toUpperCase()); + seedWordsTab.setText(Res.get("account.menu.seedWords").toUpperCase()); + walletInfoTab.setText(Res.get("account.menu.walletInfo").toUpperCase()); + backupTab.setText(Res.get("account.menu.backup").toUpperCase()); + + navigationListener = (viewPath, data) -> { + if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) { + if (arbitratorRegistrationTab == null && viewPath.get(2).equals(ArbitratorRegistrationView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else if (mediatorRegistrationTab == null && viewPath.get(2).equals(MediatorRegistrationView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else if (refundAgentRegistrationTab == null && viewPath.get(2).equals(RefundAgentRegistrationView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else if (signingTab == null && viewPath.get(2).equals(SigningView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else { + loadView(viewPath.tip()); + } + } else { + resetSelectedTab(); + } + }; + + keyEventEventHandler = event -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.D, event) && mediatorRegistrationTab == null) { + closeOtherExtraTabs(mediatorRegistrationTab); + mediatorRegistrationTab = new Tab(Res.get("account.tab.mediatorRegistration").toUpperCase()); + mediatorRegistrationTab.setClosable(true); + root.getTabs().add(mediatorRegistrationTab); + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.N, event) && refundAgentRegistrationTab == null) { + closeOtherExtraTabs(refundAgentRegistrationTab); + refundAgentRegistrationTab = new Tab(Res.get("account.tab.refundAgentRegistration").toUpperCase()); + refundAgentRegistrationTab.setClosable(true); + root.getTabs().add(refundAgentRegistrationTab); + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.I, event) && signingTab == null) { + closeOtherExtraTabs(signingTab); + signingTab = new Tab(Res.get("account.tab.signing").toUpperCase()); + signingTab.setClosable(true); + root.getTabs().add(signingTab); + navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); + } + }; + + tabChangeListener = (ov, oldValue, newValue) -> { + if (arbitratorRegistrationTab != null && selectedTab != arbitratorRegistrationTab) { + navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); + } else if (mediatorRegistrationTab != null && selectedTab != mediatorRegistrationTab) { + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + } else if (refundAgentRegistrationTab != null && selectedTab != refundAgentRegistrationTab) { + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); + } else if (signingTab != null && !selectedTab.equals(signingTab)) { + navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); + } else if (newValue == fiatAccountsTab && selectedTab != fiatAccountsTab) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else if (newValue == altcoinAccountsTab && selectedTab != altcoinAccountsTab) { + navigation.navigateTo(MainView.class, AccountView.class, AltCoinAccountsView.class); + } else if (newValue == notificationTab && selectedTab != notificationTab) { + navigation.navigateTo(MainView.class, AccountView.class, MobileNotificationsView.class); + } else if (newValue == passwordTab && selectedTab != passwordTab) { + navigation.navigateTo(MainView.class, AccountView.class, PasswordView.class); + } else if (newValue == seedWordsTab && selectedTab != seedWordsTab) { + navigation.navigateTo(MainView.class, AccountView.class, SeedWordsView.class); + } else if (newValue == walletInfoTab && selectedTab != walletInfoTab) { + navigation.navigateTo(MainView.class, AccountView.class, WalletInfoView.class); + } else if (newValue == backupTab && selectedTab != backupTab) { + navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); + } + }; + + tabListChangeListener = change -> { + change.next(); + List removedTabs = change.getRemoved(); + if (removedTabs.size() == 1 && removedTabs.get(0).equals(arbitratorRegistrationTab)) + onArbitratorRegistrationTabRemoved(); + + if (removedTabs.size() == 1 && removedTabs.get(0).equals(mediatorRegistrationTab)) + onMediatorRegistrationTabRemoved(); + + if (removedTabs.size() == 1 && removedTabs.get(0).equals(refundAgentRegistrationTab)) + onRefundAgentRegistrationTabRemoved(); + + if (removedTabs.size() == 1 && removedTabs.get(0).equals(signingTab)) + onSigningTabRemoved(); + }; + } + + private void closeOtherExtraTabs(Tab newTab) { + if (arbitratorRegistrationTab != null && !arbitratorRegistrationTab.equals(newTab)) { + root.getTabs().remove(arbitratorRegistrationTab); + } + if (mediatorRegistrationTab != null && !mediatorRegistrationTab.equals(newTab)) { + root.getTabs().remove(mediatorRegistrationTab); + } + if (refundAgentRegistrationTab != null && !refundAgentRegistrationTab.equals(newTab)) { + root.getTabs().remove(refundAgentRegistrationTab); + } + if (signingTab != null && !signingTab.equals(newTab)) { + root.getTabs().remove(signingTab); + } + } + + private void onArbitratorRegistrationTabRemoved() { + arbitratorRegistrationTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + + private void onMediatorRegistrationTabRemoved() { + mediatorRegistrationTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + + private void onRefundAgentRegistrationTabRemoved() { + refundAgentRegistrationTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + + private void onSigningTabRemoved() { + signingTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + + @Override + protected void activate() { + // Hide account new badge if user saw this section + DontShowAgainLookup.dontShowAgain(AccountPresentation.ACCOUNT_NEWS, true); + + navigation.addListener(navigationListener); + + root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + root.getTabs().addListener(tabListChangeListener); + + scene = root.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + + if (navigation.getCurrentPath().size() == 2 && navigation.getCurrentPath().get(1) == AccountView.class) { + if (arbitratorRegistrationTab != null) + navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); + else if (mediatorRegistrationTab != null) + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + else if (refundAgentRegistrationTab != null) + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); + else if (signingTab != null) + navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); + else if (root.getSelectionModel().getSelectedItem() == fiatAccountsTab) + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + else if (root.getSelectionModel().getSelectedItem() == altcoinAccountsTab) + navigation.navigateTo(MainView.class, AccountView.class, AltCoinAccountsView.class); + else if (root.getSelectionModel().getSelectedItem() == notificationTab) + navigation.navigateTo(MainView.class, AccountView.class, MobileNotificationsView.class); + else if (root.getSelectionModel().getSelectedItem() == passwordTab) + navigation.navigateTo(MainView.class, AccountView.class, PasswordView.class); + else if (root.getSelectionModel().getSelectedItem() == seedWordsTab) + navigation.navigateTo(MainView.class, AccountView.class, SeedWordsView.class); + else if (root.getSelectionModel().getSelectedItem() == walletInfoTab) + navigation.navigateTo(MainView.class, AccountView.class, WalletInfoView.class); + else if (root.getSelectionModel().getSelectedItem() == backupTab) + navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); + else + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + + String key = "accountPrivacyInfo"; + if (!DevEnv.isDevMode()) + new Popup() + .headLine(Res.get("account.info.headline")) + .backgroundInfo(Res.get("account.info.msg")) + .dontShowAgainId(key) + .show(); + } + + @Override + protected void deactivate() { + navigation.removeListener(navigationListener); + root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); + root.getTabs().removeListener(tabListChangeListener); + + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + + resetSelectedTab(); + + if (view instanceof ArbitratorRegistrationView) { + if (arbitratorRegistrationTab != null) { + selectedTab = arbitratorRegistrationTab; + arbitratorRegistrationView = (ArbitratorRegistrationView) view; + arbitratorRegistrationView.onTabSelection(true); + } + } else if (view instanceof MediatorRegistrationView) { + if (mediatorRegistrationTab != null) { + selectedTab = mediatorRegistrationTab; + mediatorRegistrationView = (MediatorRegistrationView) view; + mediatorRegistrationView.onTabSelection(true); + } + } else if (view instanceof RefundAgentRegistrationView) { + if (refundAgentRegistrationTab != null) { + selectedTab = refundAgentRegistrationTab; + refundAgentRegistrationView = (RefundAgentRegistrationView) view; + refundAgentRegistrationView.onTabSelection(true); + } + } else if (view instanceof SigningView) { + if (signingTab != null) { + selectedTab = signingTab; + } + } else if (view instanceof FiatAccountsView) { + selectedTab = fiatAccountsTab; + } else if (view instanceof AltCoinAccountsView) { + selectedTab = altcoinAccountsTab; + } else if (view instanceof MobileNotificationsView) { + selectedTab = notificationTab; + } else if (view instanceof PasswordView) { + selectedTab = passwordTab; + } else if (view instanceof SeedWordsView) { + selectedTab = seedWordsTab; + } else if (view instanceof WalletInfoView) { + selectedTab = walletInfoTab; + } else if (view instanceof BackupView) { + selectedTab = backupTab; + } else { + throw new IllegalArgumentException("View not supported: " + view); + } + + if (selectedTab.getContent() != null && selectedTab.getContent() instanceof ScrollPane) { + ((ScrollPane) selectedTab.getContent()).setContent(view.getRoot()); + } else { + selectedTab.setContent(view.getRoot()); + } + root.getSelectionModel().select(selectedTab); + } + + private void resetSelectedTab() { + if (selectedTab != null && selectedTab.getContent() != null) { + if (selectedTab.getContent() instanceof ScrollPane) { + ((ScrollPane) selectedTab.getContent()).setContent(null); + } else { + selectedTab.setContent(null); + } + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java new file mode 100644 index 0000000000..34184860d3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java @@ -0,0 +1,182 @@ +package bisq.desktop.main.account.content; + +import bisq.desktop.common.model.ActivatableWithDataModel; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.InfoAutoTooltipLabel; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.ImageUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; + +import bisq.common.UserThread; +import bisq.common.util.Utilities; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; + +import javafx.beans.value.ChangeListener; + +import javafx.event.EventHandler; + +import javafx.collections.ObservableList; + +import javafx.util.Callback; + +import java.util.concurrent.TimeUnit; + +public abstract class PaymentAccountsView extends ActivatableViewAndModel { + + protected ListView paymentAccountsListView; + private ChangeListener paymentAccountChangeListener; + protected Button addAccountButton, exportButton, importButton; + protected AccountAgeWitnessService accountAgeWitnessService; + private EventHandler keyEventEventHandler; + + public PaymentAccountsView(M model, AccountAgeWitnessService accountAgeWitnessService) { + super(model); + this.accountAgeWitnessService = accountAgeWitnessService; + } + + @Override + public void initialize() { + keyEventEventHandler = event -> { + if (Utilities.isCtrlShiftPressed(KeyCode.L, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logSignedWitnesses(); + } else if (Utilities.isCtrlShiftPressed(KeyCode.S, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logSigners(); + } else if (Utilities.isCtrlShiftPressed(KeyCode.U, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logUnsignedSignerPubKeys(); + } else if (Utilities.isCtrlShiftPressed(KeyCode.C, event)) { + copyAccount(); + } + }; + + buildForm(); + paymentAccountChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) + onSelectAccount(newValue); + }; + Label placeholder = new AutoTooltipLabel(Res.get("shared.noAccountsSetupYet")); + placeholder.setWrapText(true); + paymentAccountsListView.setPlaceholder(placeholder); + } + + @Override + protected void activate() { + paymentAccountsListView.setItems(getPaymentAccounts()); + paymentAccountsListView.getSelectionModel().selectedItemProperty().addListener(paymentAccountChangeListener); + addAccountButton.setOnAction(event -> addNewAccount()); + exportButton.setOnAction(event -> exportAccounts()); + importButton.setOnAction(event -> importAccounts()); + if (root.getScene() != null) + root.getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + @Override + protected void deactivate() { + paymentAccountsListView.getSelectionModel().selectedItemProperty().removeListener(paymentAccountChangeListener); + addAccountButton.setOnAction(null); + exportButton.setOnAction(null); + importButton.setOnAction(null); + if (root.getScene() != null) + root.getScene().removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + protected void onDeleteAccount(PaymentAccount paymentAccount) { + new Popup().warning(Res.get("shared.askConfirmDeleteAccount")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + boolean isPaymentAccountUsed = deleteAccountFromModel(paymentAccount); + if (!isPaymentAccountUsed) + removeSelectAccountForm(); + else + UserThread.runAfter(() -> new Popup().warning( + Res.get("shared.cannotDeleteAccount")) + .show(), 100, TimeUnit.MILLISECONDS); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } + + protected void setPaymentAccountsCellFactory() { + paymentAccountsListView.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView list) { + return new ListCell<>() { + final InfoAutoTooltipLabel label = new InfoAutoTooltipLabel("", ContentDisplay.RIGHT); + final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); + final Button removeButton = new AutoTooltipButton("", icon); + final AnchorPane pane = new AnchorPane(label, removeButton); + + { + label.setLayoutY(5); + removeButton.setId("icon-button"); + AnchorPane.setRightAnchor(removeButton, 0d); + } + + @Override + public void updateItem(final PaymentAccount item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(item.getAccountName()); + + boolean needsSigning = PaymentMethod.hasChargebackRisk(item.getPaymentMethod(), + item.getTradeCurrencies()); + + if (needsSigning) { + AccountAgeWitnessService.SignState signState = + accountAgeWitnessService.getSignState(accountAgeWitnessService.getMyWitness( + item.paymentAccountPayload)); + + String info = StringUtils.capitalize(signState.getDisplayString()); + label.setIcon(GUIUtil.getIconForSignState(signState), info); + } else { + label.hideIcon(); + } + + removeButton.setOnAction(e -> onDeleteAccount(item)); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + } + }); + } + + protected abstract void removeSelectAccountForm(); + + protected abstract boolean deleteAccountFromModel(PaymentAccount paymentAccount); + + protected abstract void importAccounts(); + + protected abstract void exportAccounts(); + + protected abstract void addNewAccount(); + + protected abstract ObservableList getPaymentAccounts(); + + protected abstract void buildForm(); + + protected abstract void onSelectAccount(PaymentAccount paymentAccount); + + protected void copyAccount() { + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java new file mode 100644 index 0000000000..04883b53ac --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java @@ -0,0 +1,164 @@ +/* + * 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 bisq.desktop.main.account.content.altcoinaccounts; + +import bisq.desktop.common.model.ActivatableDataModel; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.proto.persistable.PersistenceProtoResolver; + +import com.google.inject.Inject; + +import javafx.stage.Stage; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +class AltCoinAccountsDataModel extends ActivatableDataModel { + + private final User user; + private final Preferences preferences; + private final OpenOfferManager openOfferManager; + private final TradeManager tradeManager; + private final AccountAgeWitnessService accountAgeWitnessService; + final ObservableList paymentAccounts = FXCollections.observableArrayList(); + private final SetChangeListener setChangeListener; + private final String accountsFileName = "AltcoinPaymentAccounts"; + private final PersistenceProtoResolver persistenceProtoResolver; + private final CorruptedStorageFileHandler corruptedStorageFileHandler; + + @Inject + public AltCoinAccountsDataModel(User user, + Preferences preferences, + OpenOfferManager openOfferManager, + TradeManager tradeManager, + AccountAgeWitnessService accountAgeWitnessService, + PersistenceProtoResolver persistenceProtoResolver, + CorruptedStorageFileHandler corruptedStorageFileHandler) { + this.user = user; + this.preferences = preferences; + this.openOfferManager = openOfferManager; + this.tradeManager = tradeManager; + this.accountAgeWitnessService = accountAgeWitnessService; + this.persistenceProtoResolver = persistenceProtoResolver; + this.corruptedStorageFileHandler = corruptedStorageFileHandler; + setChangeListener = change -> fillAndSortPaymentAccounts(); + } + + @Override + protected void activate() { + user.getPaymentAccountsAsObservable().addListener(setChangeListener); + fillAndSortPaymentAccounts(); + } + + private void fillAndSortPaymentAccounts() { + if (user.getPaymentAccounts() != null) { + paymentAccounts.setAll(user.getPaymentAccounts().stream() + .filter(paymentAccount -> paymentAccount.getPaymentMethod().isAsset()) + .collect(Collectors.toList())); + paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); + } + } + + @Override + protected void deactivate() { + user.getPaymentAccountsAsObservable().removeListener(setChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + List tradeCurrencies = paymentAccount.getTradeCurrencies(); + if (singleTradeCurrency != null) { + if (singleTradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); + } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { + tradeCurrencies.forEach(tradeCurrency -> { + if (tradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) tradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); + }); + } + + user.addPaymentAccount(paymentAccount); + + if (!(paymentAccount instanceof AssetAccount)) + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + } + + public boolean onDeleteAccount(PaymentAccount paymentAccount) { + boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() + .filter(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())) + .findAny() + .isPresent(); + isPaymentAccountUsed = isPaymentAccountUsed || tradeManager.getObservableList().stream() + .filter(t -> t.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId()) || + paymentAccount.getId().equals(t.getTakerPaymentAccountId())) + .findAny() + .isPresent(); + if (!isPaymentAccountUsed) + user.removePaymentAccount(paymentAccount); + return isPaymentAccountUsed; + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + user.setCurrentPaymentAccount(paymentAccount); + } + + public void exportAccounts(Stage stage) { + if (user.getPaymentAccounts() != null) { + ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() + .filter(paymentAccount -> paymentAccount instanceof AssetAccount) + .collect(Collectors.toList())); + GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); + } + } + + public void importAccounts(Stage stage) { + GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); + } + + public int getNumPaymentAccounts() { + return user.getPaymentAccounts() != null ? user.getPaymentAccounts().size() : 0; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.fxml new file mode 100644 index 0000000000..0d2efd9cad --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java new file mode 100644 index 0000000000..258dfb9dfa --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java @@ -0,0 +1,271 @@ +/* + * 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 bisq.desktop.main.account.content.altcoinaccounts; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.paymentmethods.AssetsForm; +import bisq.desktop.components.paymentmethods.PaymentMethodForm; +import bisq.desktop.main.account.content.PaymentAccountsView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.validation.AltCoinAddressValidator; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.asset.AltCoinAccountDisclaimer; +import bisq.asset.Asset; +import bisq.asset.coins.Monero; + +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.stage.Stage; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.collections.ObservableList; + +import java.util.Optional; + +import static bisq.desktop.components.paymentmethods.AssetsForm.INSTANT_TRADE_NEWS; +import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.add3ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelListView; + +@FxmlView +public class AltCoinAccountsView extends PaymentAccountsView { + + private final InputValidator inputValidator; + private final AltCoinAddressValidator altCoinAddressValidator; + private final AssetService assetService; + private final FilterManager filterManager; + private final CoinFormatter formatter; + private final Preferences preferences; + + private PaymentMethodForm paymentMethodForm; + private TitledGroupBg accountTitledGroupBg; + private Button saveNewAccountButton; + private int gridRow = 0; + + @Inject + public AltCoinAccountsView(AltCoinAccountsViewModel model, + InputValidator inputValidator, + AltCoinAddressValidator altCoinAddressValidator, + AccountAgeWitnessService accountAgeWitnessService, + AssetService assetService, + FilterManager filterManager, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences) { + super(model, accountAgeWitnessService); + + this.inputValidator = inputValidator; + this.altCoinAddressValidator = altCoinAddressValidator; + this.assetService = assetService; + this.filterManager = filterManager; + this.formatter = formatter; + this.preferences = preferences; + } + + @Override + protected ObservableList getPaymentAccounts() { + return model.getPaymentAccounts(); + } + + @Override + protected void importAccounts() { + model.dataModel.importAccounts((Stage) root.getScene().getWindow()); + } + + @Override + protected void exportAccounts() { + model.dataModel.exportAccounts((Stage) root.getScene().getWindow()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onSaveNewAccount(PaymentAccount paymentAccount) { + TradeCurrency selectedTradeCurrency = paymentAccount.getSelectedTradeCurrency(); + if (selectedTradeCurrency != null) { + if (selectedTradeCurrency instanceof CryptoCurrency && ((CryptoCurrency) selectedTradeCurrency).isAsset()) { + String name = selectedTradeCurrency.getName(); + new Popup().information(Res.get("account.altcoin.popup.wallet.msg", + selectedTradeCurrency.getCodeAndName(), + name, + name)) + .closeButtonText(Res.get("account.altcoin.popup.wallet.confirm")) + .show(); + } + + final Optional asset = CurrencyUtil.findAsset(selectedTradeCurrency.getCode()); + if (asset.isPresent()) { + final AltCoinAccountDisclaimer disclaimerAnnotation = asset.get().getClass().getAnnotation(AltCoinAccountDisclaimer.class); + if (disclaimerAnnotation != null) { + new Popup() + .width(asset.get() instanceof Monero ? 1000 : 669) + .maxMessageLength(2500) + .information(Res.get(disclaimerAnnotation.value())) + .useIUnderstandButton() + .show(); + } + } + + if (model.getPaymentAccounts().stream().noneMatch(e -> e.getAccountName() != null && + e.getAccountName().equals(paymentAccount.getAccountName()))) { + model.onSaveNewAccount(paymentAccount); + removeNewAccountForm(); + } else { + new Popup().warning(Res.get("shared.accountNameAlreadyUsed")).show(); + } + + preferences.dontShowAgain(INSTANT_TRADE_NEWS, true); + } + } + + private void onCancelNewAccount() { + removeNewAccountForm(); + + preferences.dontShowAgain(INSTANT_TRADE_NEWS, true); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Base form + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void buildForm() { + addTitledGroupBg(root, gridRow, 2, Res.get("shared.manageAccounts")); + + Tuple3, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.altcoin.yourAltcoinAccounts"), Layout.FIRST_ROW_DISTANCE); + paymentAccountsListView = tuple.second; + int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); + paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + setPaymentAccountsCellFactory(); + + Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), + Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); + addAccountButton = tuple3.first; + exportButton = tuple3.second; + importButton = tuple3.third; + } + + // Add new account form + protected void addNewAccount() { + paymentAccountsListView.getSelectionModel().clearSelection(); + removeAccountRows(); + addAccountButton.setDisable(true); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE); + + if (paymentMethodForm != null) { + FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + gridRow = 2; + paymentMethodForm = getPaymentMethodForm(PaymentMethod.BLOCK_CHAINS); + paymentMethodForm.addFormForAddAccount(); + gridRow = paymentMethodForm.getGridRow(); + Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); + saveNewAccountButton = tuple2.first; + saveNewAccountButton.setOnAction(event -> onSaveNewAccount(paymentMethodForm.getPaymentAccount())); + saveNewAccountButton.disableProperty().bind(paymentMethodForm.allInputsValidProperty().not()); + Button cancelButton = tuple2.second; + cancelButton.setOnAction(event -> onCancelNewAccount()); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + + // Select account form + protected void onSelectAccount(PaymentAccount paymentAccount) { + removeAccountRows(); + addAccountButton.setDisable(false); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.selectedAccount"), Layout.GROUP_DISTANCE); + paymentMethodForm = getPaymentMethodForm(paymentAccount); + paymentMethodForm.addFormForDisplayAccount(); + gridRow = paymentMethodForm.getGridRow(); + Tuple2 tuple = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.deleteAccount"), Res.get("shared.cancel")); + Button deleteAccountButton = tuple.first; + deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentAccount)); + Button cancelButton = tuple.second; + cancelButton.setOnAction(event -> removeSelectAccountForm()); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); + model.onSelectAccount(paymentAccount); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { + PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); + paymentAccount.init(); + return getPaymentMethodForm(paymentAccount); + } + + private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) { + return new AssetsForm(paymentAccount, accountAgeWitnessService, altCoinAddressValidator, + inputValidator, root, gridRow, formatter, assetService, filterManager); + } + + private void removeNewAccountForm() { + saveNewAccountButton.disableProperty().unbind(); + removeAccountRows(); + addAccountButton.setDisable(false); + } + + @Override + protected void removeSelectAccountForm() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + addAccountButton.setDisable(false); + paymentAccountsListView.getSelectionModel().clearSelection(); + } + + @Override + protected boolean deleteAccountFromModel(PaymentAccount paymentAccount) { + return model.onDeleteAccount(paymentAccount); + } + + private void removeAccountRows() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java new file mode 100644 index 0000000000..dbcea1df01 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.desktop.main.account.content.altcoinaccounts; + +import bisq.desktop.common.model.ActivatableWithDataModel; +import bisq.desktop.common.model.ViewModel; + +import bisq.core.payment.PaymentAccount; + +import com.google.inject.Inject; + +import javafx.collections.ObservableList; + +class AltCoinAccountsViewModel extends ActivatableWithDataModel implements ViewModel { + + @Inject + public AltCoinAccountsViewModel(AltCoinAccountsDataModel dataModel) { + super(dataModel); + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + dataModel.onSaveNewAccount(paymentAccount); + } + + public boolean onDeleteAccount(PaymentAccount paymentAccount) { + return dataModel.onDeleteAccount(paymentAccount); + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + dataModel.onSelectAccount(paymentAccount); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + ObservableList getPaymentAccounts() { + return dataModel.paymentAccounts; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.fxml new file mode 100644 index 0000000000..2d8a950b86 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.java b/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.java new file mode 100644 index 0000000000..14a30c820c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/backup/BackupView.java @@ -0,0 +1,205 @@ +/* + * 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 bisq.desktop.main.account.content.backup; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.Layout; + +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import bisq.common.config.Config; +import bisq.common.file.FileUtil; +import bisq.common.persistence.PersistenceManager; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javax.inject.Inject; + +import javafx.stage.DirectoryChooser; + +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.beans.value.ChangeListener; + +import java.text.SimpleDateFormat; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.Date; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.add2Buttons; +import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class BackupView extends ActivatableView { + private final File dataDir, logFile; + private int gridRow = 0; + private final Preferences preferences; + private Button selectBackupDir, backupNow; + private TextField backUpLocationTextField; + private Button openDataDirButton, openLogsButton; + private ChangeListener backUpLocationTextFieldFocusListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BackupView(Preferences preferences, Config config) { + super(); + this.preferences = preferences; + dataDir = new File(config.appDataDir.getPath()); + logFile = new File(Paths.get(dataDir.getPath(), "bisq.log").toString()); + } + + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 2, Res.get("account.backup.title")); + backUpLocationTextField = addInputTextField(root, gridRow, Res.get("account.backup.location"), Layout.FIRST_ROW_DISTANCE); + String backupDirectory = preferences.getBackupDirectory(); + if (backupDirectory != null) + backUpLocationTextField.setText(backupDirectory); + + backUpLocationTextFieldFocusListener = (observable, oldValue, newValue) -> { + if (oldValue && !newValue) + applyBackupDirectory(backUpLocationTextField.getText()); + }; + + Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, + Res.get("account.backup.selectLocation"), Res.get("account.backup.backupNow")); + selectBackupDir = tuple2.first; + backupNow = tuple2.second; + updateButtons(); + + addTitledGroupBg(root, ++gridRow, 2, Res.get("account.backup.appDir"), Layout.GROUP_DISTANCE); + + final Tuple2 applicationDataDirTuple2 = add2Buttons(root, gridRow, Res.get("account.backup.openDirectory"), + Res.get("account.backup.openLogFile"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, false); + + openDataDirButton = applicationDataDirTuple2.first; + openLogsButton = applicationDataDirTuple2.second; + } + + @Override + protected void activate() { + backUpLocationTextField.focusedProperty().addListener(backUpLocationTextFieldFocusListener); + selectBackupDir.setOnAction(e -> { + String path = preferences.getDirectoryChooserPath(); + if (!Utilities.isDirectory(path)) { + path = Utilities.getSystemHomeDirectory(); + backUpLocationTextField.setText(path); + } + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setInitialDirectory(new File(path)); + directoryChooser.setTitle(Res.get("account.backup.selectLocation")); + try { + File dir = directoryChooser.showDialog(root.getScene().getWindow()); + if (dir != null) { + applyBackupDirectory(dir.getAbsolutePath()); + } + } catch (Throwable t) { + showWrongPathWarningAndReset(t); + } + + }); + openFileOrShowWarning(openDataDirButton, dataDir); + openFileOrShowWarning(openLogsButton, logFile); + + backupNow.setOnAction(event -> { + String backupDirectory = preferences.getBackupDirectory(); + if (backupDirectory != null && backupDirectory.length() > 0) { // We need to flush data to disk + PersistenceManager.flushAllDataToDiskAtBackup(() -> { + try { + String dateString = new SimpleDateFormat("yyyy-MM-dd-HHmmss").format(new Date()); + String destination = Paths.get(backupDirectory, "bisq_backup_" + dateString).toString(); + FileUtil.copyDirectory(dataDir, new File(destination)); + new Popup().feedback(Res.get("account.backup.success", destination)).show(); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.getMessage()); + showWrongPathWarningAndReset(e); + } + }); + } + }); + } + + private void openFileOrShowWarning(Button button, File dataDir) { + button.setOnAction(event -> { + try { + Utilities.openFile(dataDir); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.getMessage()); + showWrongPathWarningAndReset(e); + } + }); + } + + @Override + protected void deactivate() { + backUpLocationTextField.focusedProperty().removeListener(backUpLocationTextFieldFocusListener); + selectBackupDir.setOnAction(null); + openDataDirButton.setOnAction(null); + openLogsButton.setOnAction(null); + backupNow.setOnAction(null); + } + + private void updateButtons() { + boolean noBackupSet = backUpLocationTextField.getText() == null || backUpLocationTextField.getText().length() == 0; + selectBackupDir.setDefaultButton(noBackupSet); + backupNow.setDefaultButton(!noBackupSet); + backupNow.setDisable(noBackupSet); + } + + private void showWrongPathWarningAndReset(@Nullable Throwable t) { + String error = t != null ? Res.get("shared.errorMessageInline", t.getMessage()) : ""; + new Popup().warning(Res.get("account.backup.directoryNotAccessible", error)).show(); + applyBackupDirectory(Utilities.getSystemHomeDirectory()); + } + + private void applyBackupDirectory(String path) { + if (isPathValid(path)) { + preferences.setDirectoryChooserPath(path); + backUpLocationTextField.setText(path); + preferences.setBackupDirectory(path); + updateButtons(); + } else { + showWrongPathWarningAndReset(null); + } + } + + private boolean isPathValid(String path) { + return path == null || path.isEmpty() || Utilities.isDirectory(path); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java new file mode 100644 index 0000000000..0a45a88384 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java @@ -0,0 +1,167 @@ +/* + * 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 bisq.desktop.main.account.content.fiataccounts; + +import bisq.desktop.common.model.ActivatableDataModel; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.file.CorruptedStorageFileHandler; +import bisq.common.proto.persistable.PersistenceProtoResolver; + +import com.google.inject.Inject; + +import javafx.stage.Stage; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +class FiatAccountsDataModel extends ActivatableDataModel { + + private final User user; + private final Preferences preferences; + private final OpenOfferManager openOfferManager; + private final TradeManager tradeManager; + private final AccountAgeWitnessService accountAgeWitnessService; + final ObservableList paymentAccounts = FXCollections.observableArrayList(); + private final SetChangeListener setChangeListener; + private final String accountsFileName = "FiatPaymentAccounts"; + private final PersistenceProtoResolver persistenceProtoResolver; + private final CorruptedStorageFileHandler corruptedStorageFileHandler; + + @Inject + public FiatAccountsDataModel(User user, + Preferences preferences, + OpenOfferManager openOfferManager, + TradeManager tradeManager, + AccountAgeWitnessService accountAgeWitnessService, + PersistenceProtoResolver persistenceProtoResolver, + CorruptedStorageFileHandler corruptedStorageFileHandler) { + this.user = user; + this.preferences = preferences; + this.openOfferManager = openOfferManager; + this.tradeManager = tradeManager; + this.accountAgeWitnessService = accountAgeWitnessService; + this.persistenceProtoResolver = persistenceProtoResolver; + this.corruptedStorageFileHandler = corruptedStorageFileHandler; + setChangeListener = change -> fillAndSortPaymentAccounts(); + } + + @Override + protected void activate() { + user.getPaymentAccountsAsObservable().addListener(setChangeListener); + fillAndSortPaymentAccounts(); + } + + private void fillAndSortPaymentAccounts() { + if (user.getPaymentAccounts() != null) { + List list = user.getPaymentAccounts().stream() + .filter(paymentAccount -> !paymentAccount.getPaymentMethod().isAsset()) + .collect(Collectors.toList()); + paymentAccounts.setAll(list); + paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); + } + } + + @Override + protected void deactivate() { + user.getPaymentAccountsAsObservable().removeListener(setChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + List tradeCurrencies = paymentAccount.getTradeCurrencies(); + if (singleTradeCurrency != null) { + if (singleTradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); + } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { + if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency())) + paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); + else + paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0)); + + tradeCurrencies.forEach(tradeCurrency -> { + if (tradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) tradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); + }); + } + + user.addPaymentAccount(paymentAccount); + + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + accountAgeWitnessService.signAndPublishSameNameAccounts(); + } + + public boolean onDeleteAccount(PaymentAccount paymentAccount) { + boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() + .anyMatch(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())); + isPaymentAccountUsed = isPaymentAccountUsed || tradeManager.getObservableList().stream() + .anyMatch(t -> t.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId()) || + paymentAccount.getId().equals(t.getTakerPaymentAccountId())); + if (!isPaymentAccountUsed) + user.removePaymentAccount(paymentAccount); + return isPaymentAccountUsed; + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + user.setCurrentPaymentAccount(paymentAccount); + } + + public void exportAccounts(Stage stage) { + if (user.getPaymentAccounts() != null) { + ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() + .filter(paymentAccount -> !(paymentAccount instanceof AssetAccount)) + .collect(Collectors.toList())); + GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); + } + } + + public void importAccounts(Stage stage) { + GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); + } + + public int getNumPaymentAccounts() { + return user.getPaymentAccounts() != null ? user.getPaymentAccounts().size() : 0; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.fxml new file mode 100644 index 0000000000..e701e4533b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java new file mode 100644 index 0000000000..7330a398de --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java @@ -0,0 +1,569 @@ +/* + * 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 bisq.desktop.main.account.content.fiataccounts; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.paymentmethods.AdvancedCashForm; +import bisq.desktop.components.paymentmethods.AliPayForm; +import bisq.desktop.components.paymentmethods.AmazonGiftCardForm; +import bisq.desktop.components.paymentmethods.AustraliaPayidForm; +import bisq.desktop.components.paymentmethods.CashByMailForm; +import bisq.desktop.components.paymentmethods.CashDepositForm; +import bisq.desktop.components.paymentmethods.ChaseQuickPayForm; +import bisq.desktop.components.paymentmethods.ClearXchangeForm; +import bisq.desktop.components.paymentmethods.F2FForm; +import bisq.desktop.components.paymentmethods.FasterPaymentsForm; +import bisq.desktop.components.paymentmethods.HalCashForm; +import bisq.desktop.components.paymentmethods.InteracETransferForm; +import bisq.desktop.components.paymentmethods.JapanBankTransferForm; +import bisq.desktop.components.paymentmethods.MoneyBeamForm; +import bisq.desktop.components.paymentmethods.MoneyGramForm; +import bisq.desktop.components.paymentmethods.NationalBankForm; +import bisq.desktop.components.paymentmethods.PaymentMethodForm; +import bisq.desktop.components.paymentmethods.PerfectMoneyForm; +import bisq.desktop.components.paymentmethods.PopmoneyForm; +import bisq.desktop.components.paymentmethods.PromptPayForm; +import bisq.desktop.components.paymentmethods.RevolutForm; +import bisq.desktop.components.paymentmethods.SameBankForm; +import bisq.desktop.components.paymentmethods.SepaForm; +import bisq.desktop.components.paymentmethods.SepaInstantForm; +import bisq.desktop.components.paymentmethods.SpecificBankForm; +import bisq.desktop.components.paymentmethods.SwishForm; +import bisq.desktop.components.paymentmethods.TransferwiseForm; +import bisq.desktop.components.paymentmethods.USPostalMoneyOrderForm; +import bisq.desktop.components.paymentmethods.UpholdForm; +import bisq.desktop.components.paymentmethods.WeChatPayForm; +import bisq.desktop.components.paymentmethods.WesternUnionForm; +import bisq.desktop.main.account.content.PaymentAccountsView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AdvancedCashValidator; +import bisq.desktop.util.validation.AliPayValidator; +import bisq.desktop.util.validation.AustraliaPayidValidator; +import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.ChaseQuickPayValidator; +import bisq.desktop.util.validation.ClearXchangeValidator; +import bisq.desktop.util.validation.F2FValidator; +import bisq.desktop.util.validation.HalCashValidator; +import bisq.desktop.util.validation.IBANValidator; +import bisq.desktop.util.validation.InteracETransferValidator; +import bisq.desktop.util.validation.JapanBankTransferValidator; +import bisq.desktop.util.validation.MoneyBeamValidator; +import bisq.desktop.util.validation.PerfectMoneyValidator; +import bisq.desktop.util.validation.PopmoneyValidator; +import bisq.desktop.util.validation.PromptPayValidator; +import bisq.desktop.util.validation.RevolutValidator; +import bisq.desktop.util.validation.SwishValidator; +import bisq.desktop.util.validation.TransferwiseValidator; +import bisq.desktop.util.validation.USPostalMoneyOrderValidator; +import bisq.desktop.util.validation.UpholdValidator; +import bisq.desktop.util.validation.WeChatPayValidator; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.offer.OfferRestrictions; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.CashByMailAccount; +import bisq.core.payment.CashDepositAccount; +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.WesternUnionAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.config.Config; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.stage.Stage; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import javafx.util.StringConverter; + +import java.util.List; +import java.util.stream.Collectors; + +import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.add3ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelListView; + +@FxmlView +public class FiatAccountsView extends PaymentAccountsView { + + private final IBANValidator ibanValidator; + private final BICValidator bicValidator; + private final LengthValidator inputValidator; + private final UpholdValidator upholdValidator; + private final MoneyBeamValidator moneyBeamValidator; + private final PopmoneyValidator popmoneyValidator; + private final RevolutValidator revolutValidator; + private final AliPayValidator aliPayValidator; + private final PerfectMoneyValidator perfectMoneyValidator; + private final SwishValidator swishValidator; + private final ClearXchangeValidator clearXchangeValidator; + private final ChaseQuickPayValidator chaseQuickPayValidator; + private final InteracETransferValidator interacETransferValidator; + private final JapanBankTransferValidator japanBankTransferValidator; + private final AustraliaPayidValidator australiapayidValidator; + private final USPostalMoneyOrderValidator usPostalMoneyOrderValidator; + private final WeChatPayValidator weChatPayValidator; + private final HalCashValidator halCashValidator; + private final F2FValidator f2FValidator; + private final PromptPayValidator promptPayValidator; + private final AdvancedCashValidator advancedCashValidator; + private final TransferwiseValidator transferwiseValidator; + private final CoinFormatter formatter; + private ComboBox paymentMethodComboBox; + private PaymentMethodForm paymentMethodForm; + private TitledGroupBg accountTitledGroupBg; + private Button saveNewAccountButton; + private int gridRow = 0; + + @Inject + public FiatAccountsView(FiatAccountsViewModel model, + IBANValidator ibanValidator, + BICValidator bicValidator, + LengthValidator inputValidator, + UpholdValidator upholdValidator, + MoneyBeamValidator moneyBeamValidator, + PopmoneyValidator popmoneyValidator, + RevolutValidator revolutValidator, + AliPayValidator aliPayValidator, + PerfectMoneyValidator perfectMoneyValidator, + SwishValidator swishValidator, + ClearXchangeValidator clearXchangeValidator, + ChaseQuickPayValidator chaseQuickPayValidator, + InteracETransferValidator interacETransferValidator, + JapanBankTransferValidator japanBankTransferValidator, + AustraliaPayidValidator australiaPayIDValidator, + USPostalMoneyOrderValidator usPostalMoneyOrderValidator, + WeChatPayValidator weChatPayValidator, + HalCashValidator halCashValidator, + F2FValidator f2FValidator, + PromptPayValidator promptPayValidator, + AdvancedCashValidator advancedCashValidator, + TransferwiseValidator transferwiseValidator, + AccountAgeWitnessService accountAgeWitnessService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { + super(model, accountAgeWitnessService); + + this.ibanValidator = ibanValidator; + this.bicValidator = bicValidator; + this.inputValidator = inputValidator; + this.inputValidator.setMaxLength(100); // restrict general field entry length + this.inputValidator.setMinLength(2); + this.upholdValidator = upholdValidator; + this.moneyBeamValidator = moneyBeamValidator; + this.popmoneyValidator = popmoneyValidator; + this.revolutValidator = revolutValidator; + this.aliPayValidator = aliPayValidator; + this.perfectMoneyValidator = perfectMoneyValidator; + this.swishValidator = swishValidator; + this.clearXchangeValidator = clearXchangeValidator; + this.chaseQuickPayValidator = chaseQuickPayValidator; + this.interacETransferValidator = interacETransferValidator; + this.japanBankTransferValidator = japanBankTransferValidator; + this.australiapayidValidator = australiaPayIDValidator; + this.usPostalMoneyOrderValidator = usPostalMoneyOrderValidator; + this.weChatPayValidator = weChatPayValidator; + this.halCashValidator = halCashValidator; + this.f2FValidator = f2FValidator; + this.promptPayValidator = promptPayValidator; + this.advancedCashValidator = advancedCashValidator; + this.transferwiseValidator = transferwiseValidator; + this.formatter = formatter; + } + + @Override + protected ObservableList getPaymentAccounts() { + return model.getPaymentAccounts(); + } + + @Override + protected void importAccounts() { + model.dataModel.importAccounts((Stage) root.getScene().getWindow()); + } + + @Override + protected void exportAccounts() { + model.dataModel.exportAccounts((Stage) root.getScene().getWindow()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onSaveNewAccount(PaymentAccount paymentAccount) { + Coin maxTradeLimitAsCoin = paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin("USD"); + Coin maxTradeLimitSecondMonth = maxTradeLimitAsCoin.divide(2L); + Coin maxTradeLimitFirstMonth = maxTradeLimitAsCoin.divide(4L); + if (paymentAccount instanceof F2FAccount) { + new Popup().information(Res.get("payment.f2f.info")) + .width(700) + .closeButtonText(Res.get("payment.f2f.info.openURL")) + .onClose(() -> GUIUtil.openWebPage("https://docs.bisq.network/trading-rules.html#f2f-trading")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof CashByMailAccount) { + // CashByMail has no chargeback risk so we don't show the text from payment.limits.info. + new Popup().information(Res.get("payment.cashByMail.info")) + .width(850) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof HalCashAccount) { + // HalCash has no chargeback risk so we don't show the text from payment.limits.info. + new Popup().information(Res.get("payment.halCash.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else { + + String limitsInfoKey = "payment.limits.info"; + String initialLimit = formatter.formatCoinWithCode(maxTradeLimitFirstMonth); + + if (PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), paymentAccount.getTradeCurrencies())) { + limitsInfoKey = "payment.limits.info.withSigning"; + initialLimit = formatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT); + } + + new Popup().information(Res.get(limitsInfoKey, + initialLimit, + formatter.formatCoinWithCode(maxTradeLimitSecondMonth), + formatter.formatCoinWithCode(maxTradeLimitAsCoin))) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> { + final String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); + if (paymentAccount instanceof ClearXchangeAccount) { + new Popup().information(Res.get("payment.clearXchange.info", currencyName, currencyName)) + .width(900) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iConfirm")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof WesternUnionAccount) { + new Popup().information(Res.get("payment.westernUnion.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof MoneyGramAccount) { + new Popup().information(Res.get("payment.moneyGram.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof CashDepositAccount) { + new Popup().information(Res.get("payment.cashDeposit.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iConfirm")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof RevolutAccount) { + new Popup().information(Res.get("payment.revolut.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iConfirm")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof USPostalMoneyOrderAccount) { + new Popup().information(Res.get("payment.usPostalMoneyOrder.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof AustraliaPayid) { + new Popup().information(Res.get("payment.payid.info", currencyName, currencyName)) + .width(900) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iConfirm")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else if (paymentAccount instanceof AmazonGiftCardAccount) { + new Popup().information(Res.get("payment.amazonGiftCard.info", currencyName, currencyName)) + .width(900) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); + } else { + doSaveNewAccount(paymentAccount); + } + }) + .show(); + } + } + + private void doSaveNewAccount(PaymentAccount paymentAccount) { + if (getPaymentAccounts().stream().noneMatch(e -> e.getAccountName() != null && + e.getAccountName().equals(paymentAccount.getAccountName()))) { + model.onSaveNewAccount(paymentAccount); + removeNewAccountForm(); + } else { + new Popup().warning(Res.get("shared.accountNameAlreadyUsed")).show(); + } + } + + private void onCancelNewAccount() { + removeNewAccountForm(); + } + + protected boolean deleteAccountFromModel(PaymentAccount paymentAccount) { + return model.onDeleteAccount(paymentAccount); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Base form + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void buildForm() { + addTitledGroupBg(root, gridRow, 2, Res.get("shared.manageAccounts")); + + Tuple3, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.fiat.yourFiatAccounts"), Layout.FIRST_ROW_DISTANCE); + paymentAccountsListView = tuple.second; + int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); + paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + setPaymentAccountsCellFactory(); + + Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), + Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); + addAccountButton = tuple3.first; + exportButton = tuple3.second; + importButton = tuple3.third; + } + + // Add new account form + @Override + protected void addNewAccount() { + paymentAccountsListView.getSelectionModel().clearSelection(); + removeAccountRows(); + addAccountButton.setDisable(true); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE); + paymentMethodComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + paymentMethodComboBox.setVisibleRowCount(11); + paymentMethodComboBox.setPrefWidth(250); + List list = PaymentMethod.getPaymentMethods().stream() + .filter(paymentMethod -> !paymentMethod.isAsset()) + .sorted() + .collect(Collectors.toList()); + paymentMethodComboBox.setItems(FXCollections.observableArrayList(list)); + paymentMethodComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(PaymentMethod paymentMethod) { + return paymentMethod != null ? Res.get(paymentMethod.getId()) : ""; + } + + @Override + public PaymentMethod fromString(String s) { + return null; + } + }); + paymentMethodComboBox.setOnAction(e -> { + if (paymentMethodForm != null) { + FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + gridRow = 2; + paymentMethodForm = getPaymentMethodForm(paymentMethodComboBox.getSelectionModel().getSelectedItem()); + if (paymentMethodForm != null) { + paymentMethodForm.addFormForAddAccount(); + gridRow = paymentMethodForm.getGridRow(); + Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); + saveNewAccountButton = tuple2.first; + saveNewAccountButton.setOnAction(event -> onSaveNewAccount(paymentMethodForm.getPaymentAccount())); + saveNewAccountButton.disableProperty().bind(paymentMethodForm.allInputsValidProperty().not()); + Button cancelButton = tuple2.second; + cancelButton.setOnAction(event -> onCancelNewAccount()); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + }); + } + + // Select account form + @Override + protected void onSelectAccount(PaymentAccount paymentAccount) { + removeAccountRows(); + addAccountButton.setDisable(false); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.selectedAccount"), Layout.GROUP_DISTANCE); + paymentMethodForm = getPaymentMethodForm(paymentAccount); + if (paymentMethodForm != null) { + paymentMethodForm.addFormForDisplayAccount(); + gridRow = paymentMethodForm.getGridRow(); + Tuple2 tuple = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.deleteAccount"), Res.get("shared.cancel")); + Button deleteAccountButton = tuple.first; + deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentMethodForm.getPaymentAccount())); + Button cancelButton = tuple.second; + cancelButton.setOnAction(event -> removeSelectAccountForm()); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); + model.onSelectAccount(paymentAccount); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) { + return getPaymentMethodForm(paymentAccount.getPaymentMethod(), paymentAccount); + } + + private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { + final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); + paymentAccount.init(); + return getPaymentMethodForm(paymentMethod, paymentAccount); + } + + private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod, PaymentAccount paymentAccount) { + switch (paymentMethod.getId()) { + case PaymentMethod.UPHOLD_ID: + return new UpholdForm(paymentAccount, accountAgeWitnessService, upholdValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.MONEY_BEAM_ID: + return new MoneyBeamForm(paymentAccount, accountAgeWitnessService, moneyBeamValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.POPMONEY_ID: + return new PopmoneyForm(paymentAccount, accountAgeWitnessService, popmoneyValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.REVOLUT_ID: + return new RevolutForm(paymentAccount, accountAgeWitnessService, revolutValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.PERFECT_MONEY_ID: + return new PerfectMoneyForm(paymentAccount, accountAgeWitnessService, perfectMoneyValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.SEPA_ID: + return new SepaForm(paymentAccount, accountAgeWitnessService, ibanValidator, bicValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.SEPA_INSTANT_ID: + return new SepaInstantForm(paymentAccount, accountAgeWitnessService, ibanValidator, bicValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.FASTER_PAYMENTS_ID: + return new FasterPaymentsForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.NATIONAL_BANK_ID: + return new NationalBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.SAME_BANK_ID: + return new SameBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.SPECIFIC_BANKS_ID: + return new SpecificBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.JAPAN_BANK_ID: + return new JapanBankTransferForm(paymentAccount, accountAgeWitnessService, japanBankTransferValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.AUSTRALIA_PAYID_ID: + return new AustraliaPayidForm(paymentAccount, accountAgeWitnessService, australiapayidValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.ALI_PAY_ID: + return new AliPayForm(paymentAccount, accountAgeWitnessService, aliPayValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.WECHAT_PAY_ID: + return new WeChatPayForm(paymentAccount, accountAgeWitnessService, weChatPayValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.SWISH_ID: + return new SwishForm(paymentAccount, accountAgeWitnessService, swishValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.CLEAR_X_CHANGE_ID: + return new ClearXchangeForm(paymentAccount, accountAgeWitnessService, clearXchangeValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.CHASE_QUICK_PAY_ID: + return new ChaseQuickPayForm(paymentAccount, accountAgeWitnessService, chaseQuickPayValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.INTERAC_E_TRANSFER_ID: + return new InteracETransferForm(paymentAccount, accountAgeWitnessService, interacETransferValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: + return new USPostalMoneyOrderForm(paymentAccount, accountAgeWitnessService, usPostalMoneyOrderValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.MONEY_GRAM_ID: + return new MoneyGramForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.WESTERN_UNION_ID: + return new WesternUnionForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.CASH_DEPOSIT_ID: + return new CashDepositForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.CASH_BY_MAIL_ID: + return new CashByMailForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + case PaymentMethod.HAL_CASH_ID: + return new HalCashForm(paymentAccount, accountAgeWitnessService, halCashValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.F2F_ID: + return new F2FForm(paymentAccount, accountAgeWitnessService, f2FValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.PROMPT_PAY_ID: + return new PromptPayForm(paymentAccount, accountAgeWitnessService, promptPayValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.ADVANCED_CASH_ID: + return new AdvancedCashForm(paymentAccount, accountAgeWitnessService, advancedCashValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.TRANSFERWISE_ID: + return new TransferwiseForm(paymentAccount, accountAgeWitnessService, transferwiseValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.AMAZON_GIFT_CARD_ID: + return new AmazonGiftCardForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); + default: + log.error("Not supported PaymentMethod: " + paymentMethod); + return null; + } + } + + private void removeNewAccountForm() { + saveNewAccountButton.disableProperty().unbind(); + removeAccountRows(); + addAccountButton.setDisable(false); + } + + @Override + protected void removeSelectAccountForm() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + addAccountButton.setDisable(false); + paymentAccountsListView.getSelectionModel().clearSelection(); + } + + + private void removeAccountRows() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + } + + @Override + protected void copyAccount() { + var selectedAccount = paymentAccountsListView.getSelectionModel().getSelectedItem(); + if (selectedAccount == null) { + return; + } + Utilities.copyToClipboard(accountAgeWitnessService.getSignInfoFromAccount(selectedAccount)); + } + +} + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsViewModel.java new file mode 100644 index 0000000000..fc1526afc7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsViewModel.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.desktop.main.account.content.fiataccounts; + +import bisq.desktop.common.model.ActivatableWithDataModel; +import bisq.desktop.common.model.ViewModel; + +import bisq.core.payment.PaymentAccount; + +import com.google.inject.Inject; + +import javafx.collections.ObservableList; + +class FiatAccountsViewModel extends ActivatableWithDataModel implements ViewModel { + + @Inject + public FiatAccountsViewModel(FiatAccountsDataModel dataModel) { + super(dataModel); + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + dataModel.onSaveNewAccount(paymentAccount); + } + + public boolean onDeleteAccount(PaymentAccount paymentAccount) { + return dataModel.onDeleteAccount(paymentAccount); + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + dataModel.onSelectAccount(paymentAccount); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + ObservableList getPaymentAccounts() { + return dataModel.paymentAccounts; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/ManageMarketAlertsWindow.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/ManageMarketAlertsWindow.java new file mode 100644 index 0000000000..75493296d8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/ManageMarketAlertsWindow.java @@ -0,0 +1,206 @@ +/* + * 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 bisq.desktop.main.account.content.notifications; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.ImageUtil; + +import bisq.core.locale.Res; +import bisq.core.notifications.alerts.market.MarketAlertFilter; +import bisq.core.notifications.alerts.market.MarketAlerts; +import bisq.core.util.FormattingUtils; + +import bisq.common.UserThread; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.collections.FXCollections; + +import javafx.util.Callback; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ManageMarketAlertsWindow extends Overlay { + + private final MarketAlerts marketAlerts; + + ManageMarketAlertsWindow(MarketAlerts marketAlerts) { + this.marketAlerts = marketAlerts; + type = Type.Attention; + } + + @Override + public void show() { + if (headLine == null) + headLine = Res.get("account.notifications.marketAlert.manageAlerts.title"); + + width = 968; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + @Override + protected void applyStyles() { + super.applyStyles(); + gridPane.setId("popup-grid-pane-bg"); + } + + private void addContent() { + TableView tableView = new TableView<>(); + GridPane.setRowIndex(tableView, ++rowIndex); + GridPane.setColumnSpan(tableView, 2); + GridPane.setMargin(tableView, new Insets(10, 0, 0, 0)); + gridPane.getChildren().add(tableView); + Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noData")); + placeholder.setWrapText(true); + tableView.setPlaceholder(placeholder); + tableView.setPrefHeight(300); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + setColumns(tableView); + tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters())); + } + + private void removeMarketAlertFilter(MarketAlertFilter marketAlertFilter, TableView tableView) { + marketAlerts.removeMarketAlertFilter(marketAlertFilter); + UserThread.execute(() -> tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters()))); + } + + private void setColumns(TableView tableView) { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.paymentAccount")); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MarketAlertFilter item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPaymentAccount().getAccountName()); + } else { + setText(""); + } + } + }; + } + }); + tableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.trigger")); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MarketAlertFilter item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(FormattingUtils.formatPercentagePrice(item.getTriggerValue() / 10000d)); + } else { + setText(""); + } + } + }; + } + }); + tableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.offerType")); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MarketAlertFilter item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.isBuyOffer() ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin")); + } else { + setText(""); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new TableColumn<>(); + column.setMinWidth(40); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); + final Button removeButton = new AutoTooltipButton("", icon); + + { + removeButton.setId("icon-button"); + removeButton.setTooltip(new Tooltip(Res.get("shared.remove"))); + } + + @Override + public void updateItem(final MarketAlertFilter item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + removeButton.setOnAction(e -> removeMarketAlertFilter(item, tableView)); + setGraphic(removeButton); + } else { + setGraphic(null); + removeButton.setOnAction(null); + } + } + }; + } + }); + + tableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.fxml new file mode 100644 index 0000000000..2ac0b85773 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java new file mode 100644 index 0000000000..ca2bdec89d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java @@ -0,0 +1,721 @@ +/* + * 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 bisq.desktop.main.account.content.notifications; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.InfoInputTextField; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.PriceUtil; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AltcoinValidator; +import bisq.desktop.util.validation.FiatPriceValidator; +import bisq.desktop.util.validation.PercentageNumberValidator; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.notifications.MobileMessage; +import bisq.core.notifications.MobileNotificationService; +import bisq.core.notifications.alerts.DisputeMsgEvents; +import bisq.core.notifications.alerts.MyOfferTakenEvents; +import bisq.core.notifications.alerts.TradeEvents; +import bisq.core.notifications.alerts.market.MarketAlertFilter; +import bisq.core.notifications.alerts.market.MarketAlerts; +import bisq.core.notifications.alerts.price.PriceAlert; +import bisq.core.notifications.alerts.price.PriceAlertFilter; +import bisq.core.payment.PaymentAccount; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.validation.InputValidator; + +import bisq.common.UserThread; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.SetChangeListener; + +import javafx.util.StringConverter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static bisq.desktop.util.FormBuilder.*; + +@FxmlView +public class MobileNotificationsView extends ActivatableView { + private final Preferences preferences; + private final User user; + private final PriceFeedService priceFeedService; + private final MarketAlerts marketAlerts; + private final MobileNotificationService mobileNotificationService; + + private TextField tokenInputTextField; + private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField; + private ToggleButton useSoundToggleButton, tradeToggleButton, marketToggleButton, priceToggleButton; + private ComboBox currencyComboBox; + private ComboBox paymentAccountsComboBox; + private Button downloadButton, eraseButton, setPriceAlertButton, + removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/; + + private ChangeListener useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener, + priceCheckBoxListener, priceAlertHighFocusListener, priceAlertLowFocusListener, marketAlertTriggerFocusListener; + private ChangeListener tokenInputTextFieldListener, priceAlertHighListener, priceAlertLowListener, marketAlertTriggerListener; + private ChangeListener priceFeedServiceListener; + private SetChangeListener paymentAccountsChangeListener; + + private int gridRow = 0; + private int testMsgCounter = 0; + private RadioButton buyOffersRadioButton, sellOffersRadioButton; + private ToggleGroup offerTypeRadioButtonsToggleGroup; + private ChangeListener offerTypeListener; + private String selectedPriceAlertTradeCurrency; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private MobileNotificationsView(Preferences preferences, + User user, + PriceFeedService priceFeedService, + MarketAlerts marketAlerts, + MobileNotificationService mobileNotificationService) { + super(); + this.preferences = preferences; + this.user = user; + this.priceFeedService = priceFeedService; + this.marketAlerts = marketAlerts; + this.mobileNotificationService = mobileNotificationService; + } + + @Override + public void initialize() { + createListeners(); + createSetupFields(); + createSettingsFields(); + createMarketAlertFields(); + createPriceAlertFields(); + } + + @Override + protected void activate() { + addListeners(); + + // setup + tokenInputTextField.textProperty().addListener(tokenInputTextFieldListener); + downloadButton.setOnAction(e -> onDownload()); + // testMsgButton.setOnAction(e -> onSendTestMsg()); + eraseButton.setOnAction(e -> onErase()); + + // settings + useSoundToggleButton.selectedProperty().addListener(useSoundCheckBoxListener); + tradeToggleButton.selectedProperty().addListener(tradeCheckBoxListener); + marketToggleButton.selectedProperty().addListener(marketCheckBoxListener); + priceToggleButton.selectedProperty().addListener(priceCheckBoxListener); + + // market alert + marketAlertTriggerInputTextField.textProperty().addListener(marketAlertTriggerListener); + marketAlertTriggerInputTextField.focusedProperty().addListener(marketAlertTriggerFocusListener); + offerTypeRadioButtonsToggleGroup.selectedToggleProperty().addListener(offerTypeListener); + paymentAccountsComboBox.setOnAction(e -> onPaymentAccountSelected()); + addMarketAlertButton.setOnAction(e -> onAddMarketAlert()); + manageAlertsButton.setOnAction(e -> onManageMarketAlerts()); + + fillPaymentAccounts(); + + // price alert + priceAlertHighInputTextField.textProperty().addListener(priceAlertHighListener); + priceAlertLowInputTextField.textProperty().addListener(priceAlertLowListener); + priceAlertHighInputTextField.focusedProperty().addListener(priceAlertHighFocusListener); + priceAlertLowInputTextField.focusedProperty().addListener(priceAlertLowFocusListener); + priceFeedService.updateCounterProperty().addListener(priceFeedServiceListener); + currencyComboBox.setOnAction(e -> onSelectedTradeCurrency()); + setPriceAlertButton.setOnAction(e -> onSetPriceAlert()); + removePriceAlertButton.setOnAction(e -> onRemovePriceAlert()); + + currencyComboBox.setItems(preferences.getTradeCurrenciesAsObservable()); + + + if (preferences.getPhoneKeyAndToken() != null) { + tokenInputTextField.setText(preferences.getPhoneKeyAndToken()); + setPairingTokenFieldsVisible(); + } else { + eraseButton.setDisable(true); + //testMsgButton.setDisable(true); + } + setDisableForSetupFields(!mobileNotificationService.isSetupConfirmationSent()); + updateMarketAlertFields(); + fillPriceAlertFields(); + updatePriceAlertFields(); + } + + @Override + protected void deactivate() { + removeListeners(); + + // setup + tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener); + downloadButton.setOnAction(null); + //testMsgButton.setOnAction(null); + eraseButton.setOnAction(null); + + // settings + useSoundToggleButton.selectedProperty().removeListener(useSoundCheckBoxListener); + tradeToggleButton.selectedProperty().removeListener(tradeCheckBoxListener); + marketToggleButton.selectedProperty().removeListener(marketCheckBoxListener); + priceToggleButton.selectedProperty().removeListener(priceCheckBoxListener); + + // market alert + marketAlertTriggerInputTextField.textProperty().removeListener(marketAlertTriggerListener); + marketAlertTriggerInputTextField.focusedProperty().removeListener(marketAlertTriggerFocusListener); + offerTypeRadioButtonsToggleGroup.selectedToggleProperty().removeListener(offerTypeListener); + paymentAccountsComboBox.setOnAction(null); + addMarketAlertButton.setOnAction(null); + manageAlertsButton.setOnAction(null); + + // price alert + priceAlertHighInputTextField.textProperty().removeListener(priceAlertHighListener); + priceAlertLowInputTextField.textProperty().removeListener(priceAlertLowListener); + priceAlertHighInputTextField.focusedProperty().removeListener(priceAlertHighFocusListener); + priceAlertLowInputTextField.focusedProperty().removeListener(priceAlertLowFocusListener); + priceFeedService.updateCounterProperty().removeListener(priceFeedServiceListener); + currencyComboBox.setOnAction(null); + setPriceAlertButton.setOnAction(null); + removePriceAlertButton.setOnAction(null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI events + /////////////////////////////////////////////////////////////////////////////////////////// + + // Setup + private void onDownload() { + GUIUtil.openWebPage("https://bisq.network/downloads"); + } + + private void onErase() { + try { + mobileNotificationService.sendEraseMessage(); + reset(); + } catch (Exception e) { + new Popup().error(e.toString()).show(); + } + } + + //TODO: never used --> Do we really want to keep it here if we need it? + private void onSendTestMsg() { + MobileMessage message = null; + List messages = null; + switch (testMsgCounter) { + case 0: + message = MyOfferTakenEvents.getTestMsg(); + break; + case 1: + messages = TradeEvents.getTestMessages(); + break; + case 2: + message = DisputeMsgEvents.getTestMsg(); + break; + case 3: + message = PriceAlert.getTestMsg(); + break; + case 4: + default: + message = MarketAlerts.getTestMsg(); + break; + } + testMsgCounter++; + if (testMsgCounter > 4) + testMsgCounter = 0; + + try { + if (message != null) { + mobileNotificationService.sendMessage(message, useSoundToggleButton.isSelected()); + } else { + messages.forEach(msg -> { + try { + mobileNotificationService.sendMessage(msg, useSoundToggleButton.isSelected()); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + } catch (Exception e) { + new Popup().error(e.toString()).show(); + } + } + + + // Market alerts + private void onPaymentAccountSelected() { + marketAlertTriggerInputTextField.clear(); + marketAlertTriggerInputTextField.resetValidation(); + offerTypeRadioButtonsToggleGroup.selectToggle(null); + updateMarketAlertFields(); + } + + private void onAddMarketAlert() { + PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem(); + double percentAsDouble = ParsingUtils.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText()); + int triggerValue = (int) Math.round(percentAsDouble * 10000); + boolean isBuyOffer = offerTypeRadioButtonsToggleGroup.getSelectedToggle() == buyOffersRadioButton; + MarketAlertFilter marketAlertFilter = new MarketAlertFilter(paymentAccount, triggerValue, isBuyOffer); + marketAlerts.addMarketAlertFilter(marketAlertFilter); + paymentAccountsComboBox.getSelectionModel().clearSelection(); + } + + private void onManageMarketAlerts() { + new ManageMarketAlertsWindow(marketAlerts) + .onClose(this::updateMarketAlertFields) + .show(); + } + + // Price alerts + private void onSelectedTradeCurrency() { + TradeCurrency selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); + if (selectedItem != null) { + selectedPriceAlertTradeCurrency = selectedItem.getCode(); + boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(selectedPriceAlertTradeCurrency); + priceAlertHighInputTextField.setValidator(isCryptoCurrency ? new AltcoinValidator() : new FiatPriceValidator()); + priceAlertLowInputTextField.setValidator(isCryptoCurrency ? new AltcoinValidator() : new FiatPriceValidator()); + } else { + selectedPriceAlertTradeCurrency = null; + } + updatePriceAlertFields(); + } + + private void onSetPriceAlert() { + if (arePriceAlertInputsValid()) { + String code = selectedPriceAlertTradeCurrency; + long high = getPriceAsLong(priceAlertHighInputTextField); + long low = getPriceAsLong(priceAlertLowInputTextField); + if (high > 0 && low > 0) + user.setPriceAlertFilter(new PriceAlertFilter(code, high, low)); + updatePriceAlertFields(); + } + } + + private void onRemovePriceAlert() { + user.removePriceAlertFilter(); + fillPriceAlertFields(); + updatePriceAlertFields(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Create views + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createSetupFields() { + addTitledGroupBg(root, gridRow, 4, Res.get("account.notifications.setup.title")); + downloadButton = addButton(root, gridRow, + Res.get("account.notifications.download.label"), + Layout.TWICE_FIRST_ROW_DISTANCE); + + tokenInputTextField = addInputTextField(root, ++gridRow, + Res.get("account.notifications.email.label")); + tokenInputTextField.setPromptText(Res.get("account.notifications.email.prompt")); + tokenInputTextFieldListener = (observable, oldValue, newValue) -> applyKeyAndToken(newValue); + + /*testMsgButton = FormBuilder.addTopLabelButton(root, ++gridRow, Res.get("account.notifications.testMsg.label"), + Res.get("account.notifications.testMsg.title")).second; + testMsgButton.setDefaultButton(false);*/ + + eraseButton = addTopLabelButton(root, ++gridRow, + Res.get("account.notifications.erase.label"), + Res.get("account.notifications.erase.title")).second; + eraseButton.setId("notification-erase-button"); + } + + private void createSettingsFields() { + addTitledGroupBg(root, ++gridRow, 4, + Res.get("account.notifications.settings.title"), + Layout.GROUP_DISTANCE); + + useSoundToggleButton = addSlideToggleButton(root, gridRow, + Res.get("account.notifications.useSound.label"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + useSoundToggleButton.setSelected(preferences.isUseSoundForMobileNotifications()); + useSoundCheckBoxListener = (observable, oldValue, newValue) -> { + mobileNotificationService.getUseSoundProperty().set(newValue); + preferences.setUseSoundForMobileNotifications(newValue); + }; + + tradeToggleButton = addSlideToggleButton(root, ++gridRow, + Res.get("account.notifications.trade.label")); + tradeToggleButton.setSelected(preferences.isUseTradeNotifications()); + tradeCheckBoxListener = (observable, oldValue, newValue) -> { + mobileNotificationService.getUseTradeNotificationsProperty().set(newValue); + preferences.setUseTradeNotifications(newValue); + }; + + marketToggleButton = addSlideToggleButton(root, ++gridRow, + Res.get("account.notifications.market.label")); + marketToggleButton.setSelected(preferences.isUseMarketNotifications()); + marketCheckBoxListener = (observable, oldValue, newValue) -> { + mobileNotificationService.getUseMarketNotificationsProperty().set(newValue); + preferences.setUseMarketNotifications(newValue); + updateMarketAlertFields(); + }; + priceToggleButton = addSlideToggleButton(root, ++gridRow, + Res.get("account.notifications.price.label")); + priceToggleButton.setSelected(preferences.isUsePriceNotifications()); + priceCheckBoxListener = (observable, oldValue, newValue) -> { + mobileNotificationService.getUsePriceNotificationsProperty().set(newValue); + preferences.setUsePriceNotifications(newValue); + updatePriceAlertFields(); + }; + } + + private void createMarketAlertFields() { + addTitledGroupBg(root, ++gridRow, 4, Res.get("account.notifications.marketAlert.title"), + Layout.GROUP_DISTANCE); + paymentAccountsComboBox = FormBuilder.addComboBox(root, gridRow, + Res.get("account.notifications.marketAlert.selectPaymentAccount"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + paymentAccountsComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(PaymentAccount paymentAccount) { + return paymentAccount.getAccountName(); + } + + @Override + public PaymentAccount fromString(String string) { + return null; + } + }); + + offerTypeRadioButtonsToggleGroup = new ToggleGroup(); + Tuple3 tuple = FormBuilder.addTopLabelRadioButtonRadioButton(root, ++gridRow, + offerTypeRadioButtonsToggleGroup, Res.get("account.notifications.marketAlert.offerType.label"), + Res.get("account.notifications.marketAlert.offerType.buy"), + Res.get("account.notifications.marketAlert.offerType.sell"), 10); + buyOffersRadioButton = tuple.second; + sellOffersRadioButton = tuple.third; + offerTypeListener = (observable, oldValue, newValue) -> { + marketAlertTriggerInputTextField.clear(); + marketAlertTriggerInputTextField.resetValidation(); + updateMarketAlertFields(); + }; + InfoInputTextField infoInputTextField = FormBuilder.addTopLabelInfoInputTextField(root, ++gridRow, + Res.get("account.notifications.marketAlert.trigger"), 10).second; + marketAlertTriggerInputTextField = infoInputTextField.getInputTextField(); + marketAlertTriggerInputTextField.setPromptText(Res.get("account.notifications.marketAlert.trigger.prompt")); + PercentageNumberValidator validator = new PercentageNumberValidator(); + validator.setMaxValue(50D); + marketAlertTriggerInputTextField.setValidator(validator); + infoInputTextField.setContentForInfoPopOver(createMarketAlertPriceInfoPopupLabel(Res.get("account.notifications.marketAlert.trigger.info"))); + infoInputTextField.setIconsRightAligned(); + + marketAlertTriggerListener = (observable, oldValue, newValue) -> updateMarketAlertFields(); + marketAlertTriggerFocusListener = (observable, oldValue, newValue) -> { + if (oldValue && !newValue) { + try { + double percentAsDouble = ParsingUtils.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText()) * 100; + marketAlertTriggerInputTextField.setText(FormattingUtils.formatRoundedDoubleWithPrecision(percentAsDouble, 2) + "%"); + } catch (Throwable ignore) { + } + + updateMarketAlertFields(); + } + }; + + Tuple2 buttonTuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, + Res.get("account.notifications.marketAlert.addButton"), + Res.get("account.notifications.marketAlert.manageAlertsButton")); + addMarketAlertButton = buttonTuple.first; + manageAlertsButton = buttonTuple.second; + } + + private void createPriceAlertFields() { + addTitledGroupBg(root, ++gridRow, 4, + Res.get("account.notifications.priceAlert.title"), 20); + currencyComboBox = FormBuilder.addComboBox(root, gridRow, + Res.get("list.currency.select"), 40); + currencyComboBox.setPromptText(Res.get("list.currency.select")); + currencyComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(TradeCurrency currency) { + return currency.getNameAndCode(); + } + + @Override + public TradeCurrency fromString(String string) { + return null; + } + }); + + priceAlertHighInputTextField = addInputTextField(root, ++gridRow, + Res.get("account.notifications.priceAlert.high.label")); + priceAlertHighListener = (observable, oldValue, newValue) -> { + long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); + long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); + if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { + if (priceAlertHighTextFieldValue > priceAlertLowTextFieldValue) + updatePriceAlertFields(); + } + }; + priceAlertHighFocusListener = (observable, oldValue, newValue) -> { + if (oldValue && !newValue) { + applyPriceFormatting(priceAlertHighInputTextField); + long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); + long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); + if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { + if (priceAlertHighTextFieldValue <= priceAlertLowTextFieldValue) { + new Popup().warning(Res.get("account.notifications.priceAlert.warning.highPriceTooLow")).show(); + UserThread.execute(() -> { + priceAlertHighInputTextField.clear(); + updatePriceAlertFields(); + }); + } + } + } + }; + priceAlertLowInputTextField = addInputTextField(root, ++gridRow, + Res.get("account.notifications.priceAlert.low.label")); + priceAlertLowListener = (observable, oldValue, newValue) -> { + long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); + long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); + if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { + if (priceAlertLowTextFieldValue < priceAlertHighTextFieldValue) + updatePriceAlertFields(); + } + }; + priceAlertLowFocusListener = (observable, oldValue, newValue) -> { + applyPriceFormatting(priceAlertLowInputTextField); + long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); + long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); + if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { + if (priceAlertLowTextFieldValue >= priceAlertHighTextFieldValue) { + new Popup().warning(Res.get("account.notifications.priceAlert.warning.lowerPriceTooHigh")).show(); + UserThread.execute(() -> { + priceAlertLowInputTextField.clear(); + updatePriceAlertFields(); + }); + } + } + }; + + Tuple2 tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, + Res.get("account.notifications.priceAlert.setButton"), + Res.get("account.notifications.priceAlert.removeButton")); + setPriceAlertButton = tuple.first; + removePriceAlertButton = tuple.second; + + // When we get a price update an existing price alert might get removed. + // We get updated the view at each price update so we get aware of the removed PriceAlertFilter in the + // fillPriceAlertFields method. To be sure that we called after the PriceAlertFilter has been removed we delay + // to the next frame. The priceFeedServiceListener in the mobileNotificationService might get called before + // our listener here. + priceFeedServiceListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { + fillPriceAlertFields(); + updatePriceAlertFields(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // Setup/Settings + private void applyKeyAndToken(String keyAndToken) { + if (keyAndToken != null && !keyAndToken.isEmpty()) { + boolean isValid = mobileNotificationService.applyKeyAndToken(keyAndToken); + if (isValid) { + setDisableForSetupFields(false); + setPairingTokenFieldsVisible(); + updateMarketAlertFields(); + updatePriceAlertFields(); + } + } + } + + private void setDisableForSetupFields(boolean disable) { + // testMsgButton.setDisable(disable); + eraseButton.setDisable(disable); + + useSoundToggleButton.setDisable(disable); + tradeToggleButton.setDisable(disable); + marketToggleButton.setDisable(disable); + priceToggleButton.setDisable(disable); + } + + private void setPairingTokenFieldsVisible() { + tokenInputTextField.setManaged(true); + tokenInputTextField.setVisible(true); + } + + private void reset() { + mobileNotificationService.reset(); + tokenInputTextField.clear(); + setDisableForSetupFields(true); + eraseButton.setDisable(true); + //testMsgButton.setDisable(true); + onRemovePriceAlert(); + new ArrayList<>(marketAlerts.getMarketAlertFilters()).forEach(marketAlerts::removeMarketAlertFilter); + + } + + + // Market alerts + private Label createMarketAlertPriceInfoPopupLabel(String text) { + final Label label = new Label(text); + label.setPrefWidth(300); + label.setWrapText(true); + label.setPadding(new Insets(10)); + return label; + } + + private void updateMarketAlertFields() { + boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent(); + boolean selected = marketToggleButton.isSelected(); + boolean disabled = !selected || !setupConfirmationSent; + boolean isPaymentAccountSelected = paymentAccountsComboBox.getSelectionModel().getSelectedItem() != null; + boolean isOfferTypeSelected = offerTypeRadioButtonsToggleGroup.getSelectedToggle() != null; + boolean isTriggerValueValid = marketAlertTriggerInputTextField.getValidator() != null && + marketAlertTriggerInputTextField.getValidator().validate(marketAlertTriggerInputTextField.getText()).isValid; + boolean allInputsValid = isPaymentAccountSelected && isOfferTypeSelected && isTriggerValueValid; + + paymentAccountsComboBox.setDisable(disabled); + buyOffersRadioButton.setDisable(disabled); + sellOffersRadioButton.setDisable(disabled); + marketAlertTriggerInputTextField.setDisable(disabled); + addMarketAlertButton.setDisable(disabled || !allInputsValid); + manageAlertsButton.setDisable(disabled || marketAlerts.getMarketAlertFilters().isEmpty()); + } + + + // PriceAlert + private void fillPriceAlertFields() { + PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter(); + if (priceAlertFilter != null) { + String currencyCode = priceAlertFilter.getCurrencyCode(); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(currencyCode); + if (optionalTradeCurrency.isPresent()) { + currencyComboBox.getSelectionModel().select(optionalTradeCurrency.get()); + onSelectedTradeCurrency(); + + priceAlertHighInputTextField.setText(PriceUtil.formatMarketPrice(priceAlertFilter.getHigh(), currencyCode)); + priceAlertHighInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getHigh() / 10000d, currencyCode)); + priceAlertLowInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getLow() / 10000d, currencyCode)); + } else { + currencyComboBox.getSelectionModel().clearSelection(); + } + } else { + priceAlertHighInputTextField.clear(); + priceAlertLowInputTextField.clear(); + priceAlertHighInputTextField.resetValidation(); + priceAlertLowInputTextField.resetValidation(); + currencyComboBox.getSelectionModel().clearSelection(); + } + } + + private void updatePriceAlertFields() { + boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent(); + boolean selected = priceToggleButton.isSelected(); + boolean disable = !setupConfirmationSent || + !selected; + priceAlertHighInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable); + priceAlertLowInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable); + PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter(); + boolean valueSameAsFilter = false; + if (priceAlertFilter != null && + selectedPriceAlertTradeCurrency != null) { + valueSameAsFilter = priceAlertFilter.getHigh() == getPriceAsLong(priceAlertHighInputTextField) && + priceAlertFilter.getLow() == getPriceAsLong(priceAlertLowInputTextField) && + priceAlertFilter.getCurrencyCode().equals(selectedPriceAlertTradeCurrency); + } + setPriceAlertButton.setDisable(disable || !arePriceAlertInputsValid() || valueSameAsFilter); + removePriceAlertButton.setDisable(disable || priceAlertFilter == null); + currencyComboBox.setDisable(disable); + } + + private boolean arePriceAlertInputsValid() { + return selectedPriceAlertTradeCurrency != null && + isPriceInputValid(priceAlertHighInputTextField).isValid && + isPriceInputValid(priceAlertLowInputTextField).isValid; + } + + private InputValidator.ValidationResult isPriceInputValid(InputTextField inputTextField) { + InputValidator validator = inputTextField.getValidator(); + if (validator != null) + return validator.validate(inputTextField.getText()); + else + return new InputValidator.ValidationResult(false); + } + + private long getPriceAsLong(InputTextField inputTextField) { + return PriceUtil.getMarketPriceAsLong(inputTextField.getText(), selectedPriceAlertTradeCurrency); + } + + private void applyPriceFormatting(InputTextField inputTextField) { + try { + String reformattedPrice = PriceUtil.reformatMarketPrice(inputTextField.getText(), selectedPriceAlertTradeCurrency); + inputTextField.setText(reformattedPrice); + } catch (Throwable ignore) { + updatePriceAlertFields(); + } + } + + private void createListeners() { + paymentAccountsChangeListener = change -> fillPaymentAccounts(); + } + + private void addListeners() { + user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener); + } + + + private void removeListeners() { + user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener); + } + + private void fillPaymentAccounts() { + paymentAccountsComboBox.setItems(FXCollections.observableArrayList(user.getPaymentAccounts())); + } + +} + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java new file mode 100644 index 0000000000..25428cde1b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java @@ -0,0 +1,24 @@ +/* + * 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 bisq.desktop.main.account.content.notifications; + +public class NoWebCamFoundException extends Throwable { + public NoWebCamFoundException(String msg) { + super(msg); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.fxml new file mode 100644 index 0000000000..ca1a046b1e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java new file mode 100644 index 0000000000..3747ce3a0a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java @@ -0,0 +1,253 @@ +/* + * 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 bisq.desktop.main.account.content.password; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.PasswordTextField; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.MainView; +import bisq.desktop.main.account.AccountView; +import bisq.desktop.main.account.content.backup.BackupView; +import bisq.desktop.main.account.content.seedwords.SeedWordsView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.PasswordValidator; + +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.crypto.ScryptUtil; +import bisq.core.locale.Res; + +import bisq.common.util.Tuple4; + +import org.bitcoinj.crypto.KeyCrypterScrypt; + +import javax.inject.Inject; + +import com.jfoenix.validation.RequiredFieldValidator; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import javafx.beans.value.ChangeListener; + +import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabel; +import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addPasswordTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static com.google.common.base.Preconditions.checkArgument; + +@FxmlView +public class PasswordView extends ActivatableView { + + private final WalletsManager walletsManager; + private final PasswordValidator passwordValidator; + private final Navigation navigation; + + private PasswordTextField passwordField; + private PasswordTextField repeatedPasswordField; + private AutoTooltipButton pwButton; + private TitledGroupBg headline; + private int gridRow = 0; + private ChangeListener passwordFieldFocusChangeListener; + private ChangeListener passwordFieldTextChangeListener; + private ChangeListener repeatedPasswordFieldChangeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private PasswordView(WalletsManager walletsManager, PasswordValidator passwordValidator, Navigation navigation) { + this.walletsManager = walletsManager; + this.passwordValidator = passwordValidator; + this.navigation = navigation; + } + + @Override + public void initialize() { + headline = addTitledGroupBg(root, gridRow, 3, ""); + passwordField = addPasswordTextField(root, gridRow, Res.get("password.enterPassword"), Layout.TWICE_FIRST_ROW_DISTANCE); + final RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator(); + passwordField.getValidators().addAll(requiredFieldValidator, passwordValidator); + passwordFieldFocusChangeListener = (observable, oldValue, newValue) -> { + if (!newValue) validatePasswords(); + }; + + passwordFieldTextChangeListener = (observable, oldvalue, newValue) -> { + if (oldvalue != newValue) validatePasswords(); + }; + + repeatedPasswordField = addPasswordTextField(root, ++gridRow, Res.get("password.confirmPassword")); + requiredFieldValidator.setMessage(Res.get("validation.empty")); + repeatedPasswordField.getValidators().addAll(requiredFieldValidator, passwordValidator); + repeatedPasswordFieldChangeListener = (observable, oldValue, newValue) -> { + if (oldValue != newValue) validatePasswords(); + }; + + Tuple4 tuple = addButtonBusyAnimationLabel(root, ++gridRow, 0, "", 10); + pwButton = (AutoTooltipButton) tuple.first; + BusyAnimation busyAnimation = tuple.second; + Label deriveStatusLabel = tuple.third; + pwButton.setDisable(true); + + setText(); + + pwButton.setOnAction(e -> { + if (!walletsManager.areWalletsEncrypted()) { + new Popup().backgroundInfo(Res.get("password.backupReminder")) + .actionButtonText(Res.get("password.setPassword")) + .onAction(() -> onApplyPassword(busyAnimation, deriveStatusLabel)) + .secondaryActionButtonText(Res.get("password.makeBackup")) + .onSecondaryAction(() -> { + navigation.setReturnPath(navigation.getCurrentPath()); + navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); + }) + .width(800) + .show(); + } else { + onApplyPassword(busyAnimation, deriveStatusLabel); + } + }); + + addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.information"), Layout.GROUP_DISTANCE); + addMultilineLabel(root, gridRow, Res.get("account.password.info"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + } + + private void onApplyPassword(BusyAnimation busyAnimation, Label deriveStatusLabel) { + String password = passwordField.getText(); + checkArgument(password.length() < 500, Res.get("password.tooLong")); + + pwButton.setDisable(true); + deriveStatusLabel.setText(Res.get("password.deriveKey")); + busyAnimation.play(); + + KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); + ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> { + deriveStatusLabel.setText(""); + busyAnimation.stop(); + + if (walletsManager.areWalletsEncrypted()) { + if (walletsManager.checkAESKey(aesKey)) { + walletsManager.decryptWallets(aesKey); + new Popup() + .feedback(Res.get("password.walletDecrypted")) + .show(); + backupWalletAndResetFields(); + } else { + pwButton.setDisable(false); + new Popup() + .warning(Res.get("password.wrongPw")) + .show(); + } + } else { + try { + walletsManager.encryptWallets(keyCrypterScrypt, aesKey); + new Popup() + .feedback(Res.get("password.walletEncrypted")) + .show(); + backupWalletAndResetFields(); + walletsManager.clearBackup(); + } catch (Throwable t) { + new Popup() + .warning(Res.get("password.walletEncryptionFailed")) + .show(); + } + } + setText(); + updatePasswordListeners(); + }); + } + + private void backupWalletAndResetFields() { + passwordField.clear(); + repeatedPasswordField.clear(); + walletsManager.backupWallets(); + } + + private void setText() { + if (walletsManager.areWalletsEncrypted()) { + pwButton.updateText(Res.get("account.password.removePw.button")); + headline.setText(Res.get("account.password.removePw.headline")); + + repeatedPasswordField.setVisible(false); + repeatedPasswordField.setManaged(false); + } else { + pwButton.updateText(Res.get("account.password.setPw.button")); + headline.setText(Res.get("account.password.setPw.headline")); + + repeatedPasswordField.setVisible(true); + repeatedPasswordField.setManaged(true); + } + } + + @Override + protected void activate() { + updatePasswordListeners(); + + repeatedPasswordField.textProperty().addListener(repeatedPasswordFieldChangeListener); + } + + private void updatePasswordListeners() { + passwordField.focusedProperty().removeListener(passwordFieldFocusChangeListener); + passwordField.textProperty().removeListener(passwordFieldTextChangeListener); + + if (walletsManager.areWalletsEncrypted()) { + passwordField.textProperty().addListener(passwordFieldTextChangeListener); + } else { + passwordField.focusedProperty().addListener(passwordFieldFocusChangeListener); + } + } + + @Override + protected void deactivate() { + passwordField.focusedProperty().removeListener(passwordFieldFocusChangeListener); + passwordField.textProperty().removeListener(passwordFieldTextChangeListener); + repeatedPasswordField.textProperty().removeListener(repeatedPasswordFieldChangeListener); + + } + + private void validatePasswords() { + passwordValidator.setPasswordsMatch(true); + + if (passwordField.validate()) { + if (walletsManager.areWalletsEncrypted()) { + pwButton.setDisable(false); + return; + } else { + if (repeatedPasswordField.validate()) { + if (passwordField.getText().equals(repeatedPasswordField.getText())) { + pwButton.setDisable(false); + return; + } else { + passwordValidator.setPasswordsMatch(false); + repeatedPasswordField.validate(); + } + } + } + } + pwButton.setDisable(true); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.fxml new file mode 100644 index 0000000000..800f560fab --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java new file mode 100644 index 0000000000..ea3186a003 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java @@ -0,0 +1,312 @@ +/* + * 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 bisq.desktop.main.account.content.seedwords; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.SharedPresentation; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.WalletPasswordWindow; +import bisq.desktop.util.Layout; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; +import bisq.core.user.DontShowAgainLookup; + +import bisq.common.config.Config; + +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.crypto.MnemonicException; +import org.bitcoinj.wallet.DeterministicSeed; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; + +import javafx.scene.control.Button; +import javafx.scene.control.DatePicker; +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import java.io.File; +import java.io.IOException; + +import java.util.List; +import java.util.TimeZone; + +import static bisq.desktop.util.FormBuilder.*; +import static javafx.beans.binding.Bindings.createBooleanBinding; + +@FxmlView +public class SeedWordsView extends ActivatableView { + private final WalletsManager walletsManager; + private final OpenOfferManager openOfferManager; + private final BtcWalletService btcWalletService; + private final WalletPasswordWindow walletPasswordWindow; + private final File storageDir; + + private Button restoreButton; + private TextArea displaySeedWordsTextArea, seedWordsTextArea; + private DatePicker datePicker, restoreDatePicker; + + private int gridRow = 0; + private ChangeListener seedWordsValidChangeListener; + private final SimpleBooleanProperty seedWordsValid = new SimpleBooleanProperty(false); + private ChangeListener seedWordsTextAreaChangeListener; + private final BooleanProperty seedWordsEdited = new SimpleBooleanProperty(); + private String seedWordText; + private LocalDate walletCreationDate; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private SeedWordsView(WalletsManager walletsManager, + OpenOfferManager openOfferManager, + BtcWalletService btcWalletService, + WalletPasswordWindow walletPasswordWindow, + @Named(Config.STORAGE_DIR) File storageDir) { + this.walletsManager = walletsManager; + this.openOfferManager = openOfferManager; + this.btcWalletService = btcWalletService; + this.walletPasswordWindow = walletPasswordWindow; + this.storageDir = storageDir; + } + + @Override + protected void initialize() { + addTitledGroupBg(root, gridRow, 2, Res.get("account.seed.backup.title")); + displaySeedWordsTextArea = addTopLabelTextArea(root, gridRow, Res.get("seed.seedWords"), "", Layout.FIRST_ROW_DISTANCE).second; + displaySeedWordsTextArea.getStyleClass().add("wallet-seed-words"); + displaySeedWordsTextArea.setPrefHeight(40); + displaySeedWordsTextArea.setMaxHeight(40); + displaySeedWordsTextArea.setEditable(false); + + datePicker = addTopLabelDatePicker(root, ++gridRow, Res.get("seed.date"), 10).second; + datePicker.setMouseTransparent(true); + + addTitledGroupBg(root, ++gridRow, 3, Res.get("seed.restore.title"), Layout.GROUP_DISTANCE); + seedWordsTextArea = addTopLabelTextArea(root, gridRow, Res.get("seed.seedWords"), "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + seedWordsTextArea.getStyleClass().add("wallet-seed-words"); + seedWordsTextArea.setPrefHeight(40); + seedWordsTextArea.setMaxHeight(40); + + restoreDatePicker = addTopLabelDatePicker(root, ++gridRow, Res.get("seed.date"), 10).second; + restoreButton = addPrimaryActionButtonAFterGroup(root, ++gridRow, Res.get("seed.restore")); + + addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.information"), Layout.GROUP_DISTANCE); + addMultilineLabel(root, gridRow, Res.get("account.seed.info"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + + seedWordsValidChangeListener = (observable, oldValue, newValue) -> { + if (newValue) { + seedWordsTextArea.getStyleClass().remove("validation-error"); + } else { + seedWordsTextArea.getStyleClass().add("validation-error"); + } + }; + + seedWordsTextAreaChangeListener = (observable, oldValue, newValue) -> { + seedWordsEdited.set(true); + try { + MnemonicCode codec = new MnemonicCode(); + codec.check(Splitter.on(" ").splitToList(newValue)); + seedWordsValid.set(true); + } catch (IOException | MnemonicException e) { + seedWordsValid.set(false); + } + }; + } + + @Override + public void activate() { + seedWordsValid.addListener(seedWordsValidChangeListener); + seedWordsTextArea.textProperty().addListener(seedWordsTextAreaChangeListener); + restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !seedWordsEdited.get(), + seedWordsValid, seedWordsEdited)); + + restoreButton.setOnAction(e -> { + new Popup().information(Res.get("account.seed.restore.info")) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("account.seed.restore.ok")) + .onAction(this::onRestore) + .show(); + }); + + seedWordsTextArea.getStyleClass().remove("validation-error"); + restoreDatePicker.getStyleClass().remove("validation-error"); + + String key = "showBackupWarningAtSeedPhrase"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup().warning(Res.get("account.seed.backup.warning")) + .onAction(this::showSeedPhrase) + .actionButtonText(Res.get("shared.iUnderstand")) + .useIUnderstandButton() + .dontShowAgainId(key) + .hideCloseButton() + .show(); + } else { + showSeedPhrase(); + } + } + + private void showSeedPhrase() { + DeterministicSeed keyChainSeed = btcWalletService.getKeyChainSeed(); + // wallet creation date is not encrypted + walletCreationDate = Instant.ofEpochSecond(walletsManager.getChainSeedCreationTimeSeconds()).atZone(ZoneId.systemDefault()).toLocalDate(); + if (keyChainSeed.isEncrypted()) { + askForPassword(); + } else { + String key = "showSeedWordsWarning"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup().warning(Res.get("account.seed.warn.noPw.msg")) + .actionButtonText(Res.get("account.seed.warn.noPw.yes")) + .onAction(() -> { + DontShowAgainLookup.dontShowAgain(key, true); + initSeedWords(keyChainSeed); + showSeedScreen(); + }) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + initSeedWords(keyChainSeed); + showSeedScreen(); + } + } + } + + @Override + protected void deactivate() { + seedWordsValid.removeListener(seedWordsValidChangeListener); + seedWordsTextArea.textProperty().removeListener(seedWordsTextAreaChangeListener); + restoreButton.disableProperty().unbind(); + restoreButton.setOnAction(null); + + displaySeedWordsTextArea.setText(""); + seedWordsTextArea.setText(""); + + restoreDatePicker.setValue(null); + datePicker.setValue(null); + + seedWordsTextArea.getStyleClass().remove("validation-error"); + restoreDatePicker.getStyleClass().remove("validation-error"); + } + + private void askForPassword() { + walletPasswordWindow.headLine(Res.get("account.seed.enterPw")).onAesKey(aesKey -> { + initSeedWords(walletsManager.getDecryptedSeed(aesKey, btcWalletService.getKeyChainSeed(), btcWalletService.getKeyCrypter())); + showSeedScreen(); + }).hideForgotPasswordButton().show(); + } + + private void initSeedWords(DeterministicSeed seed) { + List mnemonicCode = seed.getMnemonicCode(); + if (mnemonicCode != null) { + seedWordText = Joiner.on(" ").join(mnemonicCode); + } + } + + private void showSeedScreen() { + displaySeedWordsTextArea.setText(seedWordText); + datePicker.setValue(walletCreationDate); + } + + private void onRestore() { + if (walletsManager.hasPositiveBalance()) { + new Popup().warning(Res.get("seed.warn.walletNotEmpty.msg")) + .actionButtonText(Res.get("seed.warn.walletNotEmpty.restore")) + .onAction(this::checkIfEncrypted) + .closeButtonText(Res.get("seed.warn.walletNotEmpty.emptyWallet")) + .show(); + } else { + checkIfEncrypted(); + } + } + + private void checkIfEncrypted() { + if (walletsManager.areWalletsEncrypted()) { + new Popup().information(Res.get("seed.warn.notEncryptedAnymore")) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::doRestoreDateCheck) + .show(); + } else { + doRestoreDateCheck(); + } + } + + private void doRestoreDateCheck() { + if (restoreDatePicker.getValue() == null) { + // Provide feedback when attempting to restore a wallet from seed words without specifying a date + new Popup().information(Res.get("seed.warn.walletDateEmpty")) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::doRestore) + .show(); + } else { + doRestore(); + } + } + + private LocalDate getWalletDate() { + LocalDate walletDate = restoreDatePicker.getValue(); + // Even though no current Bisq wallet could have been created before the v0.5 release date (2017.06.28), + // the user may want to import from a seed generated by another wallet. + // So use when the BIP39 standard was finalised (2013.10.09) as the oldest possible wallet date. + LocalDate oldestWalletDate = LocalDate.ofInstant( + Instant.ofEpochMilli(MnemonicCode.BIP39_STANDARDISATION_TIME_SECS * 1000), + TimeZone.getDefault().toZoneId()); + if (walletDate == null) { + // No date was specified, perhaps the user doesn't know the wallet date + walletDate = oldestWalletDate; + } else if (walletDate.isBefore(oldestWalletDate)) { + walletDate = oldestWalletDate; + } else if (walletDate.isAfter(LocalDate.now())) { + walletDate = LocalDate.now(); + } + return walletDate; + } + + private void doRestore() { + LocalDate walletDate = getWalletDate(); + // We subtract 1 day to be sure to not have any issues with timezones. Even if we can be sure that the timezone + // is handled correctly it could be that the user created the wallet in one timezone and make a restore at + // a different timezone which could lead in the worst case that he miss the first day of the wallet transactions. + LocalDateTime localDateTime = walletDate.atStartOfDay().minusDays(1); + long date = localDateTime.toEpochSecond(ZoneOffset.UTC); + + DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(seedWordsTextArea.getText()), null, "", date); + SharedPresentation.restoreSeedWords(walletsManager, openOfferManager, seed, storageDir); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.fxml b/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.fxml new file mode 100644 index 0000000000..2f00ffb216 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.java b/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.java new file mode 100644 index 0000000000..265992eb1a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/walletinfo/WalletInfoView.java @@ -0,0 +1,170 @@ +/* + * 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 bisq.desktop.main.account.content.walletinfo; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.ShowWalletDataWindow; +import bisq.desktop.util.Layout; + +import bisq.core.btc.listeners.BalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.DeterministicKeyChain; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE; + +@FxmlView +public class WalletInfoView extends ActivatableView { + + private final WalletsManager walletsManager; + private final BtcWalletService btcWalletService; + private final BsqWalletService bsqWalletService; + private final CoinFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + private int gridRow = 0; + private Button openDetailsButton; + private TextField btcTextField, bsqTextField; + private BalanceListener btcWalletBalanceListener; + private BalanceListener bsqWalletBalanceListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private WalletInfoView(WalletsManager walletsManager, + BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + BsqFormatter bsqFormatter) { + this.walletsManager = walletsManager; + this.btcWalletService = btcWalletService; + this.bsqWalletService = bsqWalletService; + this.btcFormatter = btcFormatter; + this.bsqFormatter = bsqFormatter; + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 3, Res.get("account.menu.walletInfo.balance.headLine")); + addMultilineLabel(root, gridRow, Res.get("account.menu.walletInfo.balance.info"), Layout.FIRST_ROW_DISTANCE, Double.MAX_VALUE); + btcTextField = addTopLabelTextField(root, ++gridRow, "BTC", -Layout.FLOATING_LABEL_DISTANCE).second; + bsqTextField = addTopLabelTextField(root, ++gridRow, "BSQ", -Layout.FLOATING_LABEL_DISTANCE).second; + + addTitledGroupBg(root, ++gridRow, 3, Res.get("account.menu.walletInfo.xpub.headLine"), Layout.GROUP_DISTANCE); + addXpubKeys(btcWalletService, "BTC", gridRow, Layout.FIRST_ROW_AND_GROUP_DISTANCE); + ++gridRow; // update gridRow + addXpubKeys(bsqWalletService, "BSQ", ++gridRow, -Layout.FLOATING_LABEL_DISTANCE); + + addTitledGroupBg(root, ++gridRow, 4, Res.get("account.menu.walletInfo.path.headLine"), Layout.GROUP_DISTANCE); + addMultilineLabel(root, gridRow, Res.get("account.menu.walletInfo.path.info"), Layout.FIRST_ROW_AND_GROUP_DISTANCE, Double.MAX_VALUE); + addTopLabelTextField(root, ++gridRow, Res.get("account.menu.walletInfo.walletSelector", "BTC", "legacy"), "44'/0'/0'", -Layout.FLOATING_LABEL_DISTANCE); + addTopLabelTextField(root, ++gridRow, Res.get("account.menu.walletInfo.walletSelector", "BTC", "segwit"), "44'/0'/1'", -Layout.FLOATING_LABEL_DISTANCE); + addTopLabelTextField(root, ++gridRow, Res.get("account.menu.walletInfo.walletSelector", "BSQ", ""), "44'/142'/0'", -Layout.FLOATING_LABEL_DISTANCE); + + openDetailsButton = addButtonAfterGroup(root, ++gridRow, Res.get("account.menu.walletInfo.openDetails")); + + btcWalletBalanceListener = new BalanceListener() { + @Override + public void onBalanceChanged(Coin balanceAsCoin, Transaction tx) { + updateBalances(btcWalletService); + } + }; + bsqWalletBalanceListener = new BalanceListener() { + @Override + public void onBalanceChanged(Coin balanceAsCoin, Transaction tx) { + updateBalances(bsqWalletService); + } + }; + } + + + @Override + protected void activate() { + btcWalletService.addBalanceListener(btcWalletBalanceListener); + bsqWalletService.addBalanceListener(bsqWalletBalanceListener); + updateBalances(btcWalletService); + updateBalances(bsqWalletService); + + openDetailsButton.setOnAction(e -> { + if (walletsManager.areWalletsAvailable()) { + new ShowWalletDataWindow(walletsManager).width(root.getWidth()).show(); + } else { + new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show(); + } + }); + } + + @Override + protected void deactivate() { + btcWalletService.removeBalanceListener(btcWalletBalanceListener); + bsqWalletService.removeBalanceListener(bsqWalletBalanceListener); + openDetailsButton.setOnAction(null); + } + + private void addXpubKeys(WalletService walletService, String currency, int gridRow, double top) { + int row = gridRow; + double topDist = top; + for (DeterministicKeyChain chain : walletService.getWallet().getActiveKeyChains()) { + Script.ScriptType outputScriptType = chain.getOutputScriptType(); + String type = outputScriptType == Script.ScriptType.P2WPKH ? "segwit" : "legacy"; + String key = chain.getWatchingKey().serializePubB58(Config.baseCurrencyNetworkParameters(), outputScriptType); + addTopLabelTextField(root, row, + Res.get("account.menu.walletInfo.walletSelector", currency, type), + key, topDist); + row++; + topDist = -Layout.FLOATING_LABEL_DISTANCE; + } + } + + private void updateBalances(WalletService walletService) { + if (walletService instanceof BtcWalletService) { + btcTextField.setText(btcFormatter.formatCoinWithCode(walletService.getBalance(ESTIMATED_SPENDABLE))); + } else { + bsqTextField.setText(bsqFormatter.formatCoinWithCode(walletService.getBalance(ESTIMATED_SPENDABLE))); + } + } + +} + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java new file mode 100644 index 0000000000..656d126f86 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java @@ -0,0 +1,262 @@ +/* + * 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 bisq.desktop.main.account.register; + + +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.UnlockDisputeAgentRegistrationWindow; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.ImageUtil; +import bisq.desktop.util.Layout; + +import bisq.core.locale.LanguageUtil; +import bisq.core.locale.Res; +import bisq.core.support.dispute.agent.DisputeAgent; + +import bisq.common.UserThread; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; +import javafx.geometry.VPos; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.ListChangeListener; + +import javafx.util.Callback; +import javafx.util.StringConverter; + +import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public abstract class AgentRegistrationView> + extends ActivatableViewAndModel { + + private final boolean useDevPrivilegeKeys; + private ListView languagesListView; + private ComboBox languageComboBox; + + private int gridRow = 0; + + private ChangeListener changeListener; + private UnlockDisputeAgentRegistrationWindow unlockDisputeAgentRegistrationWindow; + private ListChangeListener listChangeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public AgentRegistrationView(T model, boolean useDevPrivilegeKeys) { + super(model); + this.useDevPrivilegeKeys = useDevPrivilegeKeys; + } + + @Override + public void initialize() { + buildUI(); + + languageComboBox.setItems(model.allLanguageCodes); + + changeListener = (observable, oldValue, newValue) -> updateLanguageList(); + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + model.myDisputeAgentProperty.removeListener(changeListener); + languagesListView.getItems().removeListener(listChangeListener); + } + + public void onTabSelection(boolean isSelectedTab) { + if (isSelectedTab) { + model.myDisputeAgentProperty.addListener(changeListener); + updateLanguageList(); + + if (model.registrationPubKeyAsHex.get() == null && unlockDisputeAgentRegistrationWindow == null) { + unlockDisputeAgentRegistrationWindow = new UnlockDisputeAgentRegistrationWindow(useDevPrivilegeKeys); + unlockDisputeAgentRegistrationWindow.onClose(() -> unlockDisputeAgentRegistrationWindow = null) + .onKey(model::setPrivKeyAndCheckPubKey) + .width(700) + .show(); + } + } else { + model.myDisputeAgentProperty.removeListener(changeListener); + } + } + + private void updateLanguageList() { + languagesListView.setItems(model.languageCodes); + languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); + listChangeListener = c -> languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.getItems().addListener(listChangeListener); + } + + private void buildUI() { + GridPane gridPane = new GridPane(); + gridPane.setPadding(new Insets(30, 25, -1, 25)); + gridPane.setHgap(5); + gridPane.setVgap(5); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHgrow(Priority.SOMETIMES); + columnConstraints1.setMinWidth(200); + columnConstraints1.setMaxWidth(500); + gridPane.getColumnConstraints().addAll(columnConstraints1); + root.getChildren().add(gridPane); + + addTitledGroupBg(gridPane, gridRow, 4, Res.get("account.arbitratorRegistration.registration", getRole())); + TextField pubKeyTextField = addTopLabelTextField(gridPane, gridRow, Res.get("account.arbitratorRegistration.pubKey"), + model.registrationPubKeyAsHex.get(), Layout.FIRST_ROW_DISTANCE).second; + + pubKeyTextField.textProperty().bind(model.registrationPubKeyAsHex); + + Tuple3, VBox> tuple = FormBuilder.addTopLabelListView(gridPane, ++gridRow, Res.get("shared.yourLanguage")); + GridPane.setValignment(tuple.first, VPos.TOP); + languagesListView = tuple.second; + languagesListView.disableProperty().bind(model.registrationEditDisabled); + languagesListView.setMinHeight(3 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setMaxHeight(6 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView list) { + return new ListCell<>() { + final Label label = new AutoTooltipLabel(); + final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); + final Button removeButton = new AutoTooltipButton("", icon); + final AnchorPane pane = new AnchorPane(label, removeButton); + + { + label.setLayoutY(5); + removeButton.setId("icon-button"); + AnchorPane.setRightAnchor(removeButton, 0d); + } + + @Override + public void updateItem(final String item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(LanguageUtil.getDisplayName(item)); + removeButton.setOnAction(e -> onRemoveLanguage(item)); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + } + }); + + languageComboBox = FormBuilder.addComboBox(gridPane, ++gridRow); + languageComboBox.disableProperty().bind(model.registrationEditDisabled); + languageComboBox.setPromptText(Res.get("shared.addLanguage")); + languageComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(String code) { + return LanguageUtil.getDisplayName(code); + } + + @Override + public String fromString(String s) { + return null; + } + }); + languageComboBox.setOnAction(e -> onAddLanguage()); + + Tuple2 buttonButtonTuple2 = add2ButtonsAfterGroup(gridPane, ++gridRow, + Res.get("account.arbitratorRegistration.register"), Res.get("account.arbitratorRegistration.revoke")); + Button registerButton = buttonButtonTuple2.first; + registerButton.disableProperty().bind(model.registrationEditDisabled); + registerButton.setOnAction(e -> onRegister()); + + Button revokeButton = buttonButtonTuple2.second; + revokeButton.setDefaultButton(false); + revokeButton.disableProperty().bind(model.revokeButtonDisabled); + revokeButton.setOnAction(e -> onRevoke()); + + final TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, + Res.get("shared.information"), Layout.GROUP_DISTANCE); + + titledGroupBg.getStyleClass().add("last"); + + Label infoLabel = addMultilineLabel(gridPane, gridRow); + GridPane.setMargin(infoLabel, new Insets(Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + infoLabel.setText(Res.get("account.arbitratorRegistration.info.msg", getRole().toLowerCase())); + } + + protected abstract String getRole(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onAddLanguage() { + model.onAddLanguage(languageComboBox.getSelectionModel().getSelectedItem()); + UserThread.execute(() -> languageComboBox.getSelectionModel().clearSelection()); + } + + private void onRemoveLanguage(String locale) { + model.onRemoveLanguage(locale); + + if (languagesListView.getItems().size() == 0) { + new Popup().warning(Res.get("account.arbitratorRegistration.warn.min1Language")).show(); + model.onAddLanguage(LanguageUtil.getDefaultLanguageLocaleAsCode()); + } + } + + private void onRevoke() { + if (model.isBootstrappedOrShowPopup()) { + model.onRevoke( + () -> new Popup().feedback(Res.get("account.arbitratorRegistration.removedSuccess")).show(), + (errorMessage) -> new Popup().error(Res.get("account.arbitratorRegistration.removedFailed", + Res.get("shared.errorMessageInline", errorMessage))).show()); + } + } + + private void onRegister() { + if (model.isBootstrappedOrShowPopup()) { + model.onRegister( + () -> new Popup().feedback(Res.get("account.arbitratorRegistration.registerSuccess")).show(), + (errorMessage) -> new Popup().error(Res.get("account.arbitratorRegistration.registerFailed", + Res.get("shared.errorMessageInline", errorMessage))).show()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java new file mode 100644 index 0000000000..602b66949a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java @@ -0,0 +1,191 @@ +/* + * 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 bisq.desktop.main.account.register; + +import bisq.desktop.common.model.ActivatableViewModel; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.LanguageUtil; +import bisq.core.support.dispute.agent.DisputeAgent; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; + +public abstract class AgentRegistrationViewModel> extends ActivatableViewModel { + private final T disputeAgentManager; + protected final User user; + protected final P2PService p2PService; + protected final BtcWalletService walletService; + protected final KeyRing keyRing; + + final BooleanProperty registrationEditDisabled = new SimpleBooleanProperty(true); + final BooleanProperty revokeButtonDisabled = new SimpleBooleanProperty(true); + final ObjectProperty myDisputeAgentProperty = new SimpleObjectProperty<>(); + + protected final ObservableList languageCodes = FXCollections.observableArrayList(LanguageUtil.getDefaultLanguageLocaleAsCode()); + final ObservableList allLanguageCodes = FXCollections.observableArrayList(LanguageUtil.getAllLanguageCodes()); + private boolean allDataValid; + private final MapChangeListener mapChangeListener; + protected ECKey registrationKey; + final StringProperty registrationPubKeyAsHex = new SimpleStringProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + public AgentRegistrationViewModel(T disputeAgentManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + this.disputeAgentManager = disputeAgentManager; + this.user = user; + this.p2PService = p2PService; + this.walletService = walletService; + this.keyRing = keyRing; + + mapChangeListener = change -> { + R registeredDisputeAgentFromUser = getRegisteredDisputeAgentFromUser(); + myDisputeAgentProperty.set(registeredDisputeAgentFromUser); + + // We don't reset the languages in case of revocation, as its likely that the disputeAgent will use the + // same again when he re-activate registration later + if (registeredDisputeAgentFromUser != null) + languageCodes.setAll(registeredDisputeAgentFromUser.getLanguageCodes()); + + updateDisableStates(); + }; + } + + @Override + protected void activate() { + disputeAgentManager.getObservableMap().addListener(mapChangeListener); + myDisputeAgentProperty.set(getRegisteredDisputeAgentFromUser()); + updateDisableStates(); + } + + protected abstract R getRegisteredDisputeAgentFromUser(); + + @Override + protected void deactivate() { + disputeAgentManager.getObservableMap().removeListener(mapChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + void onAddLanguage(String code) { + if (code != null && !languageCodes.contains(code)) + languageCodes.add(code); + + updateDisableStates(); + } + + void onRemoveLanguage(String code) { + if (code != null && languageCodes.contains(code)) + languageCodes.remove(code); + + updateDisableStates(); + } + + boolean setPrivKeyAndCheckPubKey(String privKeyString) { + ECKey registrationKey = disputeAgentManager.getRegistrationKey(privKeyString); + if (registrationKey != null) { + String _registrationPubKeyAsHex = Utils.HEX.encode(registrationKey.getPubKey()); + boolean isKeyValid = disputeAgentManager.isPublicKeyInList(_registrationPubKeyAsHex); + if (isKeyValid) { + this.registrationKey = registrationKey; + registrationPubKeyAsHex.set(_registrationPubKeyAsHex); + } + updateDisableStates(); + return isKeyValid; + } else { + updateDisableStates(); + return false; + } + } + + protected abstract R getDisputeAgent(String registrationSignature, String emailAddress); + + void onRegister(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + updateDisableStates(); + if (allDataValid) { + String registrationSignature = disputeAgentManager.signStorageSignaturePubKey(registrationKey); + // TODO not impl in UI + String emailAddress = null; + @SuppressWarnings("ConstantConditions") + R disputeAgent = getDisputeAgent(registrationSignature, emailAddress); + + disputeAgentManager.addDisputeAgent(disputeAgent, + () -> { + updateDisableStates(); + resultHandler.handleResult(); + }, + (errorMessage) -> { + updateDisableStates(); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + } + + void onRevoke(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + disputeAgentManager.removeDisputeAgent( + () -> { + updateDisableStates(); + resultHandler.handleResult(); + }, + (errorMessage) -> { + updateDisableStates(); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + + private void updateDisableStates() { + allDataValid = languageCodes.size() > 0 && registrationKey != null && registrationPubKeyAsHex.get() != null; + registrationEditDisabled.set(!allDataValid || myDisputeAgentProperty.get() != null); + revokeButtonDisabled.set(!allDataValid || myDisputeAgentProperty.get() == null); + } + + boolean isBootstrappedOrShowPopup() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml new file mode 100644 index 0000000000..fbbe07b6ab --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java new file mode 100644 index 0000000000..05defe48bf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.desktop.main.account.register.arbitrator; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; + +import bisq.common.config.Config; + +import javax.inject.Named; + +import javax.inject.Inject; + +@FxmlView +public class ArbitratorRegistrationView extends AgentRegistrationView { + + @Inject + public ArbitratorRegistrationView(ArbitratorRegistrationViewModel model, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.arbitrator"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java new file mode 100644 index 0000000000..ec14062884 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java @@ -0,0 +1,71 @@ +/* + * 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 bisq.desktop.main.account.register.arbitrator; + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +public class ArbitratorRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public ArbitratorRegistrationViewModel(ArbitratorManager arbitratorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(arbitratorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected Arbitrator getDisputeAgent(String registrationSignature, + String emailAddress) { + AddressEntry arbitratorAddressEntry = walletService.getArbitratorAddressEntry(); + return new Arbitrator( + p2PService.getAddress(), + arbitratorAddressEntry.getPubKey(), + arbitratorAddressEntry.getAddressString(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected Arbitrator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredArbitrator(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml new file mode 100644 index 0000000000..0d180bee04 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java new file mode 100644 index 0000000000..d02ac46c2a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.desktop.main.account.register.mediator; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.mediation.mediator.Mediator; + +import bisq.common.config.Config; + +import javax.inject.Named; + +import javax.inject.Inject; + +@FxmlView +public class MediatorRegistrationView extends AgentRegistrationView { + + @Inject + public MediatorRegistrationView(MediatorRegistrationViewModel model, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.mediator"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java new file mode 100644 index 0000000000..d763c80261 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.desktop.main.account.register.mediator; + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +class MediatorRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public MediatorRegistrationViewModel(MediatorManager mediatorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(mediatorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected Mediator getDisputeAgent(String registrationSignature, + String emailAddress) { + return new Mediator( + p2PService.getAddress(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected Mediator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredMediator(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml new file mode 100644 index 0000000000..3ca8ce3c3b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java new file mode 100644 index 0000000000..070bf4d75c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java @@ -0,0 +1,46 @@ +/* + * 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 bisq.desktop.main.account.register.refundagent; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; + +import bisq.common.config.Config; + +import javax.inject.Named; + +import javax.inject.Inject; + +@FxmlView +public class RefundAgentRegistrationView extends AgentRegistrationView { + + @Inject + public RefundAgentRegistrationView(RefundAgentRegistrationViewModel model, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.refundAgentForSupportStaff"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java new file mode 100644 index 0000000000..f4b60f98bb --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java @@ -0,0 +1,68 @@ +/* + * 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 bisq.desktop.main.account.register.refundagent; + + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +public class RefundAgentRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public RefundAgentRegistrationViewModel(RefundAgentManager arbitratorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(arbitratorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected RefundAgent getDisputeAgent(String registrationSignature, + String emailAddress) { + return new RefundAgent( + p2PService.getAddress(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected RefundAgent getRegisteredDisputeAgentFromUser() { + return user.getRegisteredRefundAgent(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.fxml new file mode 100644 index 0000000000..6abee607e4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.fxml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.java b/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.java new file mode 100644 index 0000000000..6291b5c3fa --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/signing/SigningView.java @@ -0,0 +1,79 @@ +/* + * 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 bisq.desktop.main.account.register.signing; + + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.SignPaymentAccountsWindow; +import bisq.desktop.main.overlays.windows.SignSpecificWitnessWindow; +import bisq.desktop.main.overlays.windows.SignUnsignedPubKeysWindow; + +import bisq.common.util.Utilities; + +import javax.inject.Inject; + +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; + +import javafx.event.EventHandler; + +@FxmlView +public class SigningView extends ActivatableView { + + private final SignPaymentAccountsWindow signPaymentAccountsWindow; + private final SignSpecificWitnessWindow signSpecificWitnessWindow; + private final SignUnsignedPubKeysWindow signUnsignedPubKeysWindow; + private EventHandler keyEventEventHandler; + private Scene scene; + + @Inject + public SigningView(SignPaymentAccountsWindow signPaymentAccountsWindow, + SignSpecificWitnessWindow signSpecificWitnessWindow, + SignUnsignedPubKeysWindow signUnsignedPubKeysWindow) { + this.signPaymentAccountsWindow = signPaymentAccountsWindow; + this.signSpecificWitnessWindow = signSpecificWitnessWindow; + this.signUnsignedPubKeysWindow = signUnsignedPubKeysWindow; + keyEventEventHandler = this::handleKeyPressed; + } + + @Override + protected void activate() { + scene = root.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + @Override + protected void deactivate() { + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + protected void handleKeyPressed(KeyEvent event) { + if (Utilities.isAltOrCtrlPressed(KeyCode.S, event)) { + signPaymentAccountsWindow.show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.P, event)) { + signSpecificWitnessWindow.show(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.O, event)) { + signUnsignedPubKeysWindow.show(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.fxml new file mode 100644 index 0000000000..eccff618c9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.fxml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java new file mode 100644 index 0000000000..40b5f8082a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -0,0 +1,212 @@ +/* + * 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 bisq.desktop.main.dao; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.bonding.BondingView; +import bisq.desktop.main.dao.burnbsq.BurnBsqView; +import bisq.desktop.main.dao.economy.EconomyView; +import bisq.desktop.main.dao.governance.GovernanceView; +import bisq.desktop.main.dao.monitor.MonitorView; +import bisq.desktop.main.dao.news.NewsView; +import bisq.desktop.main.dao.wallet.BsqWalletView; +import bisq.desktop.main.dao.wallet.send.BsqSendView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.presentation.DaoPresentation; + +import bisq.core.dao.governance.votereveal.VoteRevealService; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import bisq.common.app.DevEnv; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; + +import javafx.beans.value.ChangeListener; + +@FxmlView +public class DaoView extends ActivatableView { + + @FXML + private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, daoNewsTab, monitorTab, factsAndFiguresTab; + + private Navigation.Listener navigationListener; + private ChangeListener tabChangeListener; + + private final ViewLoader viewLoader; + private final Navigation navigation; + private Preferences preferences; + private Tab selectedTab; + private BsqWalletView bsqWalletView; + + @Inject + private DaoView(CachingViewLoader viewLoader, VoteRevealService voteRevealService, Navigation navigation, + Preferences preferences) { + this.viewLoader = viewLoader; + this.navigation = navigation; + this.preferences = preferences; + + voteRevealService.addVoteRevealTxPublishedListener(txId -> { + new Popup().headLine(Res.get("dao.voteReveal.txPublished.headLine")) + .feedback(Res.get("dao.voteReveal.txPublished", txId)) + .show(); + }); + } + + @Override + public void initialize() { + factsAndFiguresTab = new Tab(Res.get("dao.tab.factsAndFigures").toUpperCase()); + bsqWalletTab = new Tab(Res.get("dao.tab.bsqWallet").toUpperCase()); + proposalsTab = new Tab(Res.get("dao.tab.proposals").toUpperCase()); + bondingTab = new Tab(Res.get("dao.tab.bonding").toUpperCase()); + burnBsqTab = new Tab(Res.get("dao.tab.proofOfBurn").toUpperCase()); + monitorTab = new Tab(Res.get("dao.tab.monitor").toUpperCase()); + + factsAndFiguresTab.setClosable(false); + bsqWalletTab.setClosable(false); + proposalsTab.setClosable(false); + bondingTab.setClosable(false); + burnBsqTab.setClosable(false); + monitorTab.setClosable(false); + + if (!DevEnv.isDaoActivated()) { + factsAndFiguresTab.setDisable(true); + bsqWalletTab.setDisable(true); + proposalsTab.setDisable(true); + bondingTab.setDisable(true); + burnBsqTab.setDisable(true); + monitorTab.setDisable(true); + + daoNewsTab = new Tab(Res.get("dao.tab.news").toUpperCase()); + + root.getTabs().add(daoNewsTab); + } else { + root.getTabs().addAll(factsAndFiguresTab, bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, monitorTab); + } + + navigationListener = (viewPath, data) -> { + if (viewPath.size() == 3 && viewPath.indexOf(DaoView.class) == 1) { + if (proposalsTab == null && viewPath.get(2).equals(EconomyView.class)) + navigation.navigateTo(MainView.class, DaoView.class, EconomyView.class); + else + loadView(viewPath.tip()); + } + }; + + tabChangeListener = (ov, oldValue, newValue) -> { + if (newValue == bsqWalletTab) { + Class selectedViewClass = bsqWalletView != null ? bsqWalletView.getSelectedViewClass() : null; + if (selectedViewClass == null) + navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, BsqSendView.class); + else + navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, selectedViewClass); + } else if (newValue == proposalsTab) { + navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); + } else if (newValue == bondingTab) { + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); + } else if (newValue == burnBsqTab) { + navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); + } else if (newValue == factsAndFiguresTab) { + navigation.navigateTo(MainView.class, DaoView.class, EconomyView.class); + } else if (newValue == monitorTab) { + navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class); + } + }; + } + + @Override + protected void activate() { + if (DevEnv.isDaoActivated()) { + + // Hide dao new badge if user saw this section + preferences.dontShowAgain(DaoPresentation.DAO_NEWS, true); + + navigation.addListener(navigationListener); + root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + + if (navigation.getCurrentPath().size() == 2 && navigation.getCurrentPath().get(1) == DaoView.class) { + Tab selectedItem = root.getSelectionModel().getSelectedItem(); + if (selectedItem == bsqWalletTab) + navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class); + else if (selectedItem == proposalsTab) + navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); + else if (selectedItem == bondingTab) + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); + else if (selectedItem == burnBsqTab) + navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); + else if (selectedItem == factsAndFiguresTab) + navigation.navigateTo(MainView.class, DaoView.class, EconomyView.class); + else if (selectedItem == monitorTab) + navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class); + } + } else { + loadView(NewsView.class); + } + } + + @Override + protected void deactivate() { + navigation.removeListener(navigationListener); + root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); + } + + private void loadView(Class viewClass) { + + if (selectedTab != null && selectedTab.getContent() != null) { + if (selectedTab.getContent() instanceof ScrollPane) { + ((ScrollPane) selectedTab.getContent()).setContent(null); + } else { + selectedTab.setContent(null); + } + } + + View view = viewLoader.load(viewClass); + if (view instanceof BsqWalletView) { + selectedTab = bsqWalletTab; + bsqWalletView = (BsqWalletView) view; + } else if (view instanceof GovernanceView) { + selectedTab = proposalsTab; + } else if (view instanceof BondingView) { + selectedTab = bondingTab; + } else if (view instanceof BurnBsqView) { + selectedTab = burnBsqTab; + } else if (view instanceof MonitorView) { + selectedTab = monitorTab; + } else if (view instanceof NewsView) { + selectedTab = daoNewsTab; + } else if (view instanceof EconomyView) { + selectedTab = factsAndFiguresTab; + } + + selectedTab.setContent(view.getRoot()); + root.getSelectionModel().select(selectedTab); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml new file mode 100644 index 0000000000..a4f49d59d3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java new file mode 100644 index 0000000000..cbeb133f9b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java @@ -0,0 +1,145 @@ +/* + * 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 bisq.desktop.main.dao.bonding; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.bonding.bonds.BondsView; +import bisq.desktop.main.dao.bonding.dashboard.BondingDashboardView; +import bisq.desktop.main.dao.bonding.reputation.MyReputationView; +import bisq.desktop.main.dao.bonding.roles.RolesView; + +import bisq.core.dao.governance.bond.Bond; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +@FxmlView +public class BondingView extends ActivatableView { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem dashboard, bondedRoles, reputation, bonds; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + private ToggleGroup toggleGroup; + + @Inject + private BondingView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = (viewPath, data) -> { + if (viewPath.size() != 4 || viewPath.indexOf(bisq.desktop.main.dao.bonding.BondingView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass, data); + }; + + toggleGroup = new ToggleGroup(); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, bisq.desktop.main.dao.bonding.BondingView.class); + dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), + BondingDashboardView.class, baseNavPath); + bondedRoles = new MenuItem(navigation, toggleGroup, Res.get("dao.bond.menuItem.bondedRoles"), + RolesView.class, baseNavPath); + reputation = new MenuItem(navigation, toggleGroup, Res.get("dao.bond.menuItem.reputation"), + MyReputationView.class, baseNavPath); + bonds = new MenuItem(navigation, toggleGroup, Res.get("dao.bond.menuItem.bonds"), + BondsView.class, baseNavPath); + + leftVBox.getChildren().addAll(dashboard, bondedRoles, reputation, bonds); + } + + @Override + protected void activate() { + dashboard.activate(); + bondedRoles.activate(); + reputation.activate(); + bonds.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(BondingView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = RolesView.class; + + loadView(selectedViewClass, null); + + } else if (viewPath.size() == 4 && viewPath.indexOf(BondingView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass, null); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + navigation.removeListener(listener); + + dashboard.deactivate(); + bondedRoles.deactivate(); + reputation.deactivate(); + bonds.deactivate(); + } + + private void loadView(Class viewClass, @Nullable Object data) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof BondingDashboardView) toggleGroup.selectToggle(dashboard); + else if (view instanceof RolesView) toggleGroup.selectToggle(bondedRoles); + else if (view instanceof MyReputationView) toggleGroup.selectToggle(reputation); + else if (view instanceof BondsView) { + toggleGroup.selectToggle(bonds); + if (data instanceof Bond) + ((BondsView) view).setSelectedBond((Bond) data); + } + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java new file mode 100644 index 0000000000..bd9743c018 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java @@ -0,0 +1,226 @@ +/* + * 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 bisq.desktop.main.dao.bonding; + +import bisq.desktop.Navigation; +import bisq.desktop.main.MainView; +import bisq.desktop.main.funds.FundsView; +import bisq.desktop.main.funds.deposit.DepositView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.lockup.LockupReason; +import bisq.core.dao.governance.bond.reputation.MyReputation; +import bisq.core.dao.governance.bond.reputation.MyReputationListService; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinUtil; +import bisq.core.util.FormattingUtils; + +import bisq.network.p2p.P2PService; + +import bisq.common.app.DevEnv; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +@Singleton +public class BondingViewUtils { + private final P2PService p2PService; + private final MyReputationListService myReputationListService; + private final BondedRolesRepository bondedRolesRepository; + private final WalletsSetup walletsSetup; + private final DaoFacade daoFacade; + private final Navigation navigation; + private final BsqFormatter bsqFormatter; + + @Inject + public BondingViewUtils(P2PService p2PService, + MyReputationListService myReputationListService, + BondedRolesRepository bondedRolesRepository, + WalletsSetup walletsSetup, + DaoFacade daoFacade, + Navigation navigation, + BsqFormatter bsqFormatter) { + this.p2PService = p2PService; + this.myReputationListService = myReputationListService; + this.bondedRolesRepository = bondedRolesRepository; + this.walletsSetup = walletsSetup; + this.daoFacade = daoFacade; + this.navigation = navigation; + this.bsqFormatter = bsqFormatter; + } + + public void lockupBondForBondedRole(Role role, Consumer resultHandler) { + Optional roleProposal = getAcceptedBondedRoleProposal(role); + checkArgument(roleProposal.isPresent(), "roleProposal must be present"); + + long requiredBond = daoFacade.getRequiredBond(roleProposal); + Coin lockupAmount = Coin.valueOf(requiredBond); + int lockupTime = roleProposal.get().getUnlockTime(); + if (!bondedRolesRepository.isBondedAssetAlreadyInBond(role)) { + lockupBond(role.getHash(), lockupAmount, lockupTime, LockupReason.BONDED_ROLE, resultHandler); + } else { + handleError(new RuntimeException("The role has been used already for a lockup tx.")); + } + } + + public void lockupBondForReputation(Coin lockupAmount, int lockupTime, byte[] salt, Consumer resultHandler) { + MyReputation myReputation = new MyReputation(salt); + lockupBond(myReputation.getHash(), lockupAmount, lockupTime, LockupReason.REPUTATION, resultHandler); + myReputationListService.addReputation(myReputation); + } + + private void lockupBond(byte[] hash, Coin lockupAmount, int lockupTime, LockupReason lockupReason, + Consumer resultHandler) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + if (!DevEnv.isDevMode()) { + try { + Tuple2 miningFeeAndTxVsize = daoFacade.getLockupTxMiningFeeAndTxVsize(lockupAmount, lockupTime, lockupReason, hash); + Coin miningFee = miningFeeAndTxVsize.first; + int txVsize = miningFeeAndTxVsize.second; + String duration = FormattingUtils.formatDurationAsWords(lockupTime * 10 * 60 * 1000L, false, false); + new Popup().headLine(Res.get("dao.bond.reputation.lockup.headline")) + .confirmation(Res.get("dao.bond.reputation.lockup.details", + bsqFormatter.formatCoinWithCode(lockupAmount), + lockupTime, + duration, + bsqFormatter.formatBTCWithCode(miningFee), + CoinUtil.getFeePerVbyte(miningFee, txVsize), + txVsize / 1000d + )) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> publishLockupTx(lockupAmount, lockupTime, lockupReason, hash, resultHandler)) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } catch (Throwable e) { + log.error(e.toString()); + e.printStackTrace(); + new Popup().warning(e.getMessage()).show(); + } + } else { + publishLockupTx(lockupAmount, lockupTime, lockupReason, hash, resultHandler); + } + } + } + + private void publishLockupTx(Coin lockupAmount, int lockupTime, LockupReason lockupReason, byte[] hash, Consumer resultHandler) { + daoFacade.publishLockupTx(lockupAmount, + lockupTime, + lockupReason, + hash, + txId -> { + if (!DevEnv.isDevMode()) + new Popup().feedback(Res.get("dao.tx.published.success")).show(); + + if (resultHandler != null) + resultHandler.accept(txId); + }, + this::handleError + ); + } + + public Optional getAcceptedBondedRoleProposal(Role role) { + return bondedRolesRepository.getAcceptedBondedRoleProposal(role); + } + + public void unLock(String lockupTxId, Consumer resultHandler) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + Optional lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId); + checkArgument(lockupTxOutput.isPresent(), "Lockup output must be present. TxId=" + lockupTxId); + Coin unlockAmount = Coin.valueOf(lockupTxOutput.get().getValue()); + Optional opLockTime = daoFacade.getLockTime(lockupTxId); + int lockTime = opLockTime.orElse(-1); + + try { + if (!DevEnv.isDevMode()) { + Tuple2 miningFeeAndTxVsize = daoFacade.getUnlockTxMiningFeeAndTxVsize(lockupTxId); + Coin miningFee = miningFeeAndTxVsize.first; + int txVsize = miningFeeAndTxVsize.second; + String duration = FormattingUtils.formatDurationAsWords(lockTime * 10 * 60 * 1000L, false, false); + new Popup().headLine(Res.get("dao.bond.reputation.unlock.headline")) + .confirmation(Res.get("dao.bond.reputation.unlock.details", + bsqFormatter.formatCoinWithCode(unlockAmount), + lockTime, + duration, + bsqFormatter.formatBTCWithCode(miningFee), + CoinUtil.getFeePerVbyte(miningFee, txVsize), + txVsize / 1000d + )) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> publishUnlockTx(lockupTxId, resultHandler)) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + publishUnlockTx(lockupTxId, resultHandler); + } + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + new Popup().warning(t.getMessage()).show(); + } + } + log.info("unlock tx: {}", lockupTxId); + } + + private void publishUnlockTx(String lockupTxId, Consumer resultHandler) { + daoFacade.publishUnlockTx(lockupTxId, + txId -> { + if (!DevEnv.isDevMode()) + new Popup().confirmation(Res.get("dao.tx.published.success")).show(); + + if (resultHandler != null) + resultHandler.accept(txId); + }, + errorMessage -> new Popup().warning(errorMessage.toString()).show() + ); + } + + private void handleError(Throwable throwable) { + if (throwable instanceof InsufficientMoneyException) { + final Coin missingCoin = ((InsufficientMoneyException) throwable).missing; + final String missing = missingCoin != null ? missingCoin.toFriendlyString() : "null"; + new Popup().warning(Res.get("popup.warning.insufficientBtcFundsForBsqTx", missing)) + .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) + .show(); + } else { + log.error(throwable.toString()); + throwable.printStackTrace(); + new Popup().warning(throwable.toString()).show(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java new file mode 100644 index 0000000000..6164a6a340 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java @@ -0,0 +1,67 @@ +/* + * 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 bisq.desktop.main.dao.bonding.bonds; + +import bisq.desktop.util.DisplayUtils; + +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Value +@Slf4j +class BondListItem { + private final Bond bond; + private final String bondType; + private final String lockupTxId; + private final String amount; + private final String lockupDateString; + private final String lockTime; + private final String bondDetails; + private final BondState bondState; + private final String bondStateString; + + BondListItem(Bond bond, BsqFormatter bsqFormatter) { + this.bond = bond; + + amount = bsqFormatter.formatCoin(Coin.valueOf(bond.getAmount())); + lockTime = Integer.toString(bond.getLockTime()); + if (bond instanceof BondedRole) { + bondType = Res.get("dao.bond.bondedRoles"); + bondDetails = bond.getBondedAsset().getDisplayString(); + } else { + bondType = Res.get("dao.bond.bondedReputation"); + bondDetails = Utilities.bytesAsHexString(bond.getBondedAsset().getHash()); + } + lockupTxId = bond.getLockupTxId(); + lockupDateString = bond.getLockupDate() > 0 ? DisplayUtils.formatDateTime(new Date(bond.getLockupDate())) : "-"; + bondState = bond.getBondState(); + bondStateString = Res.get("dao.bond.bondState." + bond.getBondState().name()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.fxml new file mode 100644 index 0000000000..7da6f9ec12 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java new file mode 100644 index 0000000000..9a32224107 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java @@ -0,0 +1,349 @@ +/* + * 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 bisq.desktop.main.dao.bonding.bonds; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.ExternalHyperlink; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InfoAutoTooltipLabel; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; + +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.bond.reputation.BondedReputation; +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.coin.BsqFormatter; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@FxmlView +public class BondsView extends ActivatableView { + private TableView tableView; + + private final BsqFormatter bsqFormatter; + private final BondedRolesRepository bondedRolesRepository; + private final BondedReputationRepository bondedReputationRepository; + private final Preferences preferences; + + private int gridRow = 0; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private ListChangeListener bondedRolesListener; + private ListChangeListener bondedReputationListener; + + private Bond selectedBond; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BondsView(BsqFormatter bsqFormatter, + BondedRolesRepository bondedRolesRepository, + BondedReputationRepository bondedReputationRepository, + Preferences preferences) { + this.bsqFormatter = bsqFormatter; + this.bondedRolesRepository = bondedRolesRepository; + this.bondedReputationRepository = bondedReputationRepository; + this.preferences = preferences; + } + + @Override + public void initialize() { + tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.allBonds.header"), "last"); + tableView.setItems(sortedList); + GridPane.setVgrow(tableView, Priority.ALWAYS); + addColumns(); + + bondedReputationListener = c -> updateList(); + bondedRolesListener = c -> updateList(); + } + + @Override + protected void activate() { + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + bondedReputationRepository.getBonds().addListener(bondedReputationListener); + bondedRolesRepository.getBonds().addListener(bondedRolesListener); + updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 37, 28, 2, 30); + } + + @Override + protected void deactivate() { + sortedList.comparatorProperty().unbind(); + bondedReputationRepository.getBonds().removeListener(bondedReputationListener); + bondedRolesRepository.getBonds().removeListener(bondedRolesListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setSelectedBond(Bond bond) { + // Set the selected bond if it's found in the tableView, which listens to sortedList. + // If this is called before the sortedList has been populated the selected bond is stored and + // we try to apply again after the next update. + tableView.getItems().stream() + .filter(item -> item.getBond() == bond) + .findFirst() + .ifPresentOrElse(item -> tableView.getSelectionModel().select(item), + () -> this.selectedBond = bond); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateList() { + List combined = new ArrayList<>(bondedReputationRepository.getBonds()); + combined.addAll(bondedRolesRepository.getBonds()); + observableList.setAll(combined.stream() + .map(bond -> new BondListItem(bond, bsqFormatter)) + .sorted(Comparator.comparing(BondListItem::getLockupDateString).reversed()) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(tableView, 37, 28, 2, 30); + if (selectedBond != null) { + Bond bond = selectedBond; + selectedBond = null; + setSelectedBond(bond); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", "BSQ")); + column.setMinWidth(80); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmount()); + } else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getBond().getAmount())); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockTime")); + column.setMinWidth(40); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLockTime()); + } else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getBond().getLockTime())); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.bondState")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + setText(item.getBondStateString()); + } else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(BondListItem::getBondStateString)); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.bondType")); + column.setMinWidth(100); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getBondType()); + } else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(BondListItem::getBondType)); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.details")); + column.setMinWidth(100); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private InfoAutoTooltipLabel infoTextField; + + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + String info = Res.get("shared.id") + ": " + item.getBond().getBondedAsset().getUid(); + + if (item.getBond() instanceof BondedRole) { + info = item.getBondDetails() + "\n" + info; + } + + infoTextField = new InfoAutoTooltipLabel(item.getBondDetails(), + AwesomeIcon.INFO_SIGN, + ContentDisplay.LEFT, + info, + 350 + ); + setGraphic(infoTextField); + } else + setGraphic(null); + } + }; + } + }); + column.setComparator(Comparator.comparing(BondListItem::getBondDetails)); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockupDate")); + column.setMinWidth(140); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLockupDateString()); + } else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getBond().getLockupDate())); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockupTxId")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.getStyleClass().add("last-column"); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final BondListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + String lockupTxId = item.getLockupTxId(); + hyperlinkWithIcon = new ExternalHyperlink(lockupTxId); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(lockupTxId, preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", lockupTxId))); + if (item.getLockupDateString().equals("-")) hyperlinkWithIcon.hideIcon(); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + column.setComparator(Comparator.comparing(BondListItem::getLockupTxId)); + tableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml new file mode 100644 index 0000000000..31a852c68f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java new file mode 100644 index 0000000000..32054288fd --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java @@ -0,0 +1,58 @@ +/* + * 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 bisq.desktop.main.dao.bonding.dashboard; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; + +import javax.inject.Inject; + +import javafx.scene.layout.GridPane; + +@FxmlView +public class BondingDashboardView extends ActivatableView { + private final BsqBalanceUtil bsqBalanceUtil; + + private int gridRow = 0; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BondingDashboardView(BsqBalanceUtil bsqBalanceUtil) { + this.bsqBalanceUtil = bsqBalanceUtil; + } + + public void initialize() { + gridRow = bsqBalanceUtil.addGroup(root, gridRow); + gridRow = bsqBalanceUtil.addBondBalanceGroup(root, gridRow, "last"); + } + + @Override + protected void activate() { + bsqBalanceUtil.activate(); + } + + @Override + protected void deactivate() { + bsqBalanceUtil.deactivate(); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationListItem.java new file mode 100644 index 0000000000..d3e75f1010 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationListItem.java @@ -0,0 +1,70 @@ +/* + * 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 bisq.desktop.main.dao.bonding.reputation; + +import bisq.desktop.util.DisplayUtils; + +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.governance.bond.reputation.MyBondedReputation; +import bisq.core.dao.governance.bond.reputation.MyReputation; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; + +import lombok.Value; + +@Value +class MyReputationListItem { + private final MyBondedReputation myBondedReputation; + private final String hash, salt; + private final String txId; + private final String amount; + private final String lockupDateString; + private final String lockTime; + + private final String buttonText; + private final boolean showButton; + private final BondState bondState; + private final String bondStateString; + private final String lockupTxId; + private final Date lockupDate; + + MyReputationListItem(MyBondedReputation myBondedReputation, + BsqFormatter bsqFormatter) { + this.myBondedReputation = myBondedReputation; + + MyReputation myReputation = myBondedReputation.getBondedAsset(); + hash = Utilities.bytesAsHexString(myReputation.getHash()); + salt = Utilities.bytesAsHexString(myReputation.getSalt()); + txId = myBondedReputation.getLockupTxId(); + amount = bsqFormatter.formatCoin(Coin.valueOf(myBondedReputation.getAmount())); + lockupDate = new Date(myBondedReputation.getLockupDate()); + lockupDateString = DisplayUtils.formatDateTime(lockupDate); + lockTime = Integer.toString(myBondedReputation.getLockTime()); + lockupTxId = myBondedReputation.getLockupTxId(); + bondState = myBondedReputation.getBondState(); + bondStateString = Res.get("dao.bond.bondState." + myBondedReputation.getBondState().name()); + showButton = myBondedReputation.getBondState() == BondState.LOCKUP_TX_CONFIRMED; + buttonText = Res.get("dao.bond.table.button.unlock"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.fxml new file mode 100644 index 0000000000..f0a618ed72 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java new file mode 100644 index 0000000000..764d073abb --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java @@ -0,0 +1,494 @@ +/* + * 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 bisq.desktop.main.dao.bonding.reputation; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.ExternalHyperlink; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.BondConsensus; +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.governance.bond.reputation.MyBondedReputation; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.ParsingUtils; +import bisq.core.util.validation.HexStringValidator; +import bisq.core.util.validation.IntegerValidator; + +import bisq.common.crypto.Hash; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import com.google.common.base.Charsets; + +import javafx.scene.control.Button; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.UUID; +import java.util.stream.Collectors; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class MyReputationView extends ActivatableView implements BsqBalanceListener { + private InputTextField amountInputTextField, timeInputTextField, saltInputTextField; + private Button lockupButton; + private TableView tableView; + + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final BondingViewUtils bondingViewUtils; + private final HexStringValidator hexStringValidator; + private final BsqValidator bsqValidator; + private final DaoFacade daoFacade; + private final Preferences preferences; + + private final IntegerValidator timeInputTextFieldValidator; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener, timeFocusOutListener, saltFocusOutListener; + private ChangeListener amountInputTextFieldListener, timeInputTextFieldListener, saltInputTextFieldListener; + private ListChangeListener myBondedReputationsChangeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private MyReputationView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BondingViewUtils bondingViewUtils, + HexStringValidator hexStringValidator, + BsqValidator bsqValidator, + DaoFacade daoFacade, + Preferences preferences) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bondingViewUtils = bondingViewUtils; + this.hexStringValidator = hexStringValidator; + this.bsqValidator = bsqValidator; + this.daoFacade = daoFacade; + this.preferences = preferences; + + timeInputTextFieldValidator = new IntegerValidator(); + timeInputTextFieldValidator.setMinValue(BondConsensus.getMinLockTime()); + timeInputTextFieldValidator.setMaxValue(BondConsensus.getMaxLockTime()); + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 3, Res.get("dao.bond.reputation.header")); + + amountInputTextField = addInputTextField(root, gridRow, Res.get("dao.bond.reputation.amount"), + Layout.FIRST_ROW_DISTANCE); + amountInputTextField.setValidator(bsqValidator); + + timeInputTextField = FormBuilder.addInputTextField(root, ++gridRow, Res.get("dao.bond.reputation.time")); + timeInputTextField.setValidator(timeInputTextFieldValidator); + + saltInputTextField = FormBuilder.addInputTextField(root, ++gridRow, Res.get("dao.bond.reputation.salt")); + saltInputTextField.setValidator(hexStringValidator); + + lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bond.reputation.lockupButton")); + + tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.reputation.table.header"), 20, "last"); + createColumns(); + tableView.setItems(sortedList); + GridPane.setVgrow(tableView, Priority.ALWAYS); + + createListeners(); + } + + @Override + protected void activate() { + amountInputTextField.textProperty().addListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + timeInputTextField.textProperty().addListener(timeInputTextFieldListener); + timeInputTextField.focusedProperty().addListener(timeFocusOutListener); + + saltInputTextField.textProperty().addListener(saltInputTextFieldListener); + saltInputTextField.focusedProperty().addListener(saltFocusOutListener); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + + daoFacade.getMyBondedReputations().addListener(myBondedReputationsChangeListener); + bsqWalletService.addBsqBalanceListener(this); + + lockupButton.setOnAction((event) -> { + Coin lockupAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); + int lockupTime = Integer.parseInt(timeInputTextField.getText()); + byte[] salt = Utilities.decodeFromHex(saltInputTextField.getText()); + bondingViewUtils.lockupBondForReputation(lockupAmount, + lockupTime, + salt, + txId -> { + }); + amountInputTextField.setText(""); + timeInputTextField.setText(""); + setNewRandomSalt(); + }); + + + amountInputTextField.resetValidation(); + timeInputTextField.resetValidation(); + + setNewRandomSalt(); + + updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); + } + + @Override + protected void deactivate() { + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + timeInputTextField.textProperty().removeListener(timeInputTextFieldListener); + timeInputTextField.focusedProperty().removeListener(timeFocusOutListener); + + saltInputTextField.textProperty().removeListener(saltInputTextFieldListener); + saltInputTextField.focusedProperty().removeListener(saltFocusOutListener); + + daoFacade.getMyBondedReputations().removeListener(myBondedReputationsChangeListener); + bsqWalletService.removeBsqBalanceListener(this); + + sortedList.comparatorProperty().unbind(); + + lockupButton.setOnAction(null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin availableConfirmedBalance, + Coin availableNonBsqBalance, + Coin unverifiedBalance, + Coin unconfirmedChangeBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + bsqValidator.setAvailableBalance(availableConfirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + amountFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + timeFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + saltFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + amountInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + timeInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + saltInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + + myBondedReputationsChangeListener = c -> updateList(); + } + + private void updateList() { + observableList.setAll(daoFacade.getMyBondedReputations().stream() + .map(myBondedReputation -> new MyReputationListItem(myBondedReputation, bsqFormatter)) + .sorted(Comparator.comparing(MyReputationListItem::getLockupDateString).reversed()) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); + } + + private void setNewRandomSalt() { + byte[] randomBytes = UUID.randomUUID().toString().getBytes(Charsets.UTF_8); + // We want to limit it to 20 bytes + byte[] hashOfRandomBytes = Hash.getSha256Ripemd160hash(randomBytes); + // bytesAsHexString results in 40 chars + String bytesAsHexString = Utilities.bytesAsHexString(hashOfRandomBytes); + saltInputTextField.setText(bytesAsHexString); + saltInputTextField.resetValidation(); + } + + private void updateButtonState() { + boolean isValid = bsqValidator.validate(amountInputTextField.getText()).isValid && + timeInputTextFieldValidator.validate(timeInputTextField.getText()).isValid && + hexStringValidator.validate(saltInputTextField.getText()).isValid; + lockupButton.setDisable(!isValid); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", "BSQ")); + column.setMinWidth(120); + column.setMaxWidth(column.getMinWidth()); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmount()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockTime")); + column.setMinWidth(60); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLockTime()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.bondState")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getBondStateString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockupDate")); + column.setMinWidth(140); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLockupDateString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockupTxId")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new ExternalHyperlink(transactionId); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(item.getTxId(), preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.reputation.salt")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getSalt()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.reputation.hash")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getHash()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new TableColumn<>(); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.getStyleClass().add("last-column"); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + AutoTooltipButton button; + + @Override + public void updateItem(final MyReputationListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty && item.isShowButton()) { + button = new AutoTooltipButton(item.getButtonText()); + button.setOnAction(e -> { + if (item.getBondState() == BondState.LOCKUP_TX_CONFIRMED) { + bondingViewUtils.unLock(item.getLockupTxId(), + txId -> { + }); + } + }); + setGraphic(button); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + tableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RoleDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RoleDetailsWindow.java new file mode 100644 index 0000000000..b23e6066d4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RoleDetailsWindow.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.desktop.main.dao.bonding.roles; + +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.model.governance.BondedRoleType; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import org.bitcoinj.core.Coin; + +import javafx.geometry.Insets; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class RoleDetailsWindow extends Overlay { + private final BondedRoleType bondedRoleType; + private final Optional roleProposal; + private final DaoFacade daoFacade; + private final BsqFormatter bsqFormatter; + + + RoleDetailsWindow(BondedRoleType bondedRoleType, Optional roleProposal, DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.bondedRoleType = bondedRoleType; + this.roleProposal = roleProposal; + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + width = 968; + type = Type.Confirmation; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void show() { + headLine = Res.get("dao.bond.details.header"); + + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(70, 80, 60, 80)); + gridPane.getStyleClass().add("grid-pane"); + } + + private void addContent() { + FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.bond.details.role"), + bondedRoleType.getDisplayString()); + + long requiredBond = daoFacade.getRequiredBond(roleProposal); + int unlockTime = roleProposal.map(RoleProposal::getUnlockTime).orElse(bondedRoleType.getUnlockTimeInBlocks()); + FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.bond.details.requiredBond"), + bsqFormatter.formatCoinWithCode(Coin.valueOf(requiredBond))); + + FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.bond.details.unlockTime"), + Res.get("dao.bond.details.blocks", unlockTime)); + + FormBuilder.addTopLabelHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("dao.bond.details.link"), + bondedRoleType.getLink(), bondedRoleType.getLink(), 0); + + FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.bond.details.isSingleton"), + DisplayUtils.booleanToYesNo(bondedRoleType.isAllowMultipleHolders())); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesListItem.java new file mode 100644 index 0000000000..40bb7f668b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesListItem.java @@ -0,0 +1,66 @@ +/* + * 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 bisq.desktop.main.dao.bonding.roles; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.locale.Res; + +import java.util.Date; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +class RolesListItem { + private final BondedRole bondedRole; + private final Role role; + private final String buttonText; + private final boolean isButtonVisible; + private final BondState bondState; + private final String bondStateString; + private final String lockupTxId; + private final Date lockupDate; + + RolesListItem(BondedRole bondedRole, + DaoFacade daoFacade) { + this.bondedRole = bondedRole; + + role = bondedRole.getBondedAsset(); + boolean isMyRole = daoFacade.isMyRole(role); + bondState = bondedRole.getBondState(); + lockupTxId = bondedRole.getLockupTxId(); + lockupDate = new Date(bondedRole.getLockupDate()); + bondStateString = Res.get("dao.bond.bondState." + bondedRole.getBondState().name()); + + boolean showLockup = bondedRole.getBondState() == BondState.READY_FOR_LOCKUP; + boolean showRevoke = bondedRole.getBondState() == BondState.LOCKUP_TX_CONFIRMED; + if (showLockup) { + buttonText = Res.get("dao.bond.table.button.lockup"); + } else if (showRevoke) { + buttonText = Res.get("dao.bond.table.button.revoke"); + } else { + buttonText = ""; + } + + isButtonVisible = isMyRole && (showLockup || showRevoke); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.fxml new file mode 100644 index 0000000000..456483f9e6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java new file mode 100644 index 0000000000..febf166e44 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java @@ -0,0 +1,335 @@ +/* + * 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 bisq.desktop.main.dao.bonding.roles; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.ExternalHyperlink; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.BondState; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.state.model.governance.BondedRoleType; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.coin.BsqFormatter; + +import javax.inject.Inject; + +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Collectors; + +@FxmlView +public class RolesView extends ActivatableView { + private TableView tableView; + + private final BondingViewUtils bondingViewUtils; + private final BsqFormatter bsqFormatter; + private final DaoFacade daoFacade; + private final Preferences preferences; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private ListChangeListener bondedRoleListChangeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private RolesView(BsqFormatter bsqFormatter, + BondingViewUtils bondingViewUtils, + DaoFacade daoFacade, + Preferences preferences) { + this.bsqFormatter = bsqFormatter; + this.bondingViewUtils = bondingViewUtils; + this.daoFacade = daoFacade; + this.preferences = preferences; + } + + @Override + public void initialize() { + int gridRow = 0; + tableView = FormBuilder.addTableViewWithHeader(root, gridRow, Res.get("dao.bond.bondedRoles"), "last"); + createColumns(); + tableView.setItems(sortedList); + GridPane.setVgrow(tableView, Priority.ALWAYS); + bondedRoleListChangeListener = c -> updateList(); + } + + @Override + protected void activate() { + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + daoFacade.getBondedRoles().addListener(bondedRoleListChangeListener); + updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); + } + + @Override + protected void deactivate() { + sortedList.comparatorProperty().unbind(); + daoFacade.getBondedRoles().removeListener(bondedRoleListChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateList() { + observableList.setAll(daoFacade.getAcceptedBondedRoles().stream() + .map(bond -> new RolesListItem(bond, daoFacade)) + .sorted(Comparator.comparing(RolesListItem::getLockupDate).reversed()) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.name")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.getStyleClass().add("first-column"); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getRole().getName()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.link")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + String link = item.getRole().getLink(); + hyperlinkWithIcon = new ExternalHyperlink(link); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openWebPage(link)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("shared.openURL", link))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.bondType")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private Hyperlink hyperlink; + + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + BondedRoleType bondedRoleType = item.getRole().getBondedRoleType(); + String type = bondedRoleType.getDisplayString(); + hyperlink = new Hyperlink(type); + hyperlink.setOnAction(event -> { + Optional roleProposal = bondingViewUtils.getAcceptedBondedRoleProposal(item.getRole()); + new RoleDetailsWindow(bondedRoleType, roleProposal, daoFacade, bsqFormatter).show(); + }); + hyperlink.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails", type))); + setGraphic(hyperlink); + } else { + setGraphic(null); + if (hyperlink != null) + hyperlink.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.lockupTxId")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + private Label label; + + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + String transactionId = item.getBondedRole().getLockupTxId(); + if (transactionId != null) { + hyperlinkWithIcon = new ExternalHyperlink(transactionId); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(transactionId, preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + label = new Label("-"); + setGraphic(label); + } + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + if (label != null) + label = null; + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.bondState")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getBondStateString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new TableColumn<>(); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.getStyleClass().add("last-column"); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + AutoTooltipButton button; + + @Override + public void updateItem(final RolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty && item.isButtonVisible()) { + if (button == null) { + button = new AutoTooltipButton(item.getButtonText()); + button.setMinWidth(70); + button.setOnAction(e -> { + if (item.getBondState() == BondState.READY_FOR_LOCKUP) { + bondingViewUtils.lockupBondForBondedRole(item.getRole(), + txId -> { + }); + } else if (item.getBondState() == BondState.LOCKUP_TX_CONFIRMED) { + bondingViewUtils.unLock(item.getLockupTxId(), + txId -> { + }); + } + }); + setGraphic(button); + } + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + tableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml new file mode 100644 index 0000000000..3a5230561f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java new file mode 100644 index 0000000000..882da4aedf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java @@ -0,0 +1,125 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView; +import bisq.desktop.main.dao.burnbsq.proofofburn.ProofOfBurnView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class BurnBsqView extends ActivatableView { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem assetFee, proofOfBurn; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + private ToggleGroup toggleGroup; + + @Inject + private BurnBsqView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = (viewPath, data) -> { + if (viewPath.size() != 4 || viewPath.indexOf(BurnBsqView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + toggleGroup = new ToggleGroup(); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class); + assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"), + AssetFeeView.class, baseNavPath); + proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"), + ProofOfBurnView.class, baseNavPath); + + leftVBox.getChildren().addAll(assetFee, proofOfBurn); + } + + @Override + protected void activate() { + assetFee.activate(); + proofOfBurn.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = AssetFeeView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(BurnBsqView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + navigation.removeListener(listener); + + assetFee.deactivate(); + proofOfBurn.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee); + else if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml new file mode 100644 index 0000000000..bbb0b8ae4d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java new file mode 100644 index 0000000000..0d95572770 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -0,0 +1,471 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.assetfee; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.dao.governance.asset.StatefulAsset; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; +import javafx.util.StringConverter; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class AssetFeeView extends ActivatableView implements BsqBalanceListener, DaoStateListener { + private ComboBox assetComboBox; + private InputTextField feeAmountInputTextField; + private TextField trialPeriodTextField; + private Button payFeeButton; + private TableView tableView; + + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final BsqValidator bsqValidator; + private final AssetService assetService; + private final DaoStateService daoStateService; + private final CoinFormatter btcFormatter; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener; + private ChangeListener amountInputTextFieldListener; + @Nullable + private StatefulAsset selectedAsset; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AssetFeeView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BsqValidator bsqValidator, + AssetService assetService, + DaoStateService daoStateService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bsqValidator = bsqValidator; + this.assetService = assetService; + this.daoStateService = daoStateService; + this.btcFormatter = btcFormatter; + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 3, Res.get("dao.burnBsq.header")); + + assetComboBox = FormBuilder.addComboBox(root, gridRow, + Res.get("dao.burnBsq.selectAsset"), Layout.FIRST_ROW_DISTANCE); + assetComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(StatefulAsset statefulAsset) { + return CurrencyUtil.getNameAndCode(statefulAsset.getAsset().getTickerSymbol()); + } + + @Override + public StatefulAsset fromString(String string) { + return null; + } + }); + + feeAmountInputTextField = addInputTextField(root, ++gridRow, Res.get("dao.burnBsq.fee")); + feeAmountInputTextField.setValidator(bsqValidator); + + trialPeriodTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, Res.get("dao.burnBsq.trialPeriod")).second; + + payFeeButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.burnBsq.payFee")); + + tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.burnBsq.allAssets"), 20, "last"); + createColumns(); + tableView.setItems(sortedList); + + createListeners(); + } + + @Override + protected void activate() { + assetComboBox.setOnAction(e -> { + selectedAsset = assetComboBox.getSelectionModel().getSelectedItem(); + }); + + feeAmountInputTextField.textProperty().addListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + + daoStateService.addDaoStateListener(this); + + bsqWalletService.addBsqBalanceListener(this); + + assetService.updateAssetStates(); + updateList(); + + onUpdateAvailableConfirmedBalance(bsqWalletService.getAvailableConfirmedBalance()); + + payFeeButton.setOnAction((event) -> { + Coin listingFee = getListingFee(); + long days = getDays(); + // We don't allow shorter periods as it would allow an attacker to try to deactivate other coins by making a + // small fee payment to reduce the trial period and look back period. + // Still not a perfect solution but should be good enough for now. + long minDays = 30; + if (days >= minDays) { + try { + Transaction transaction = assetService.payFee(selectedAsset, listingFee.value); + Coin miningFee = transaction.getFee(); + int txVsize = transaction.getVsize(); + + if (!DevEnv.isDevMode()) { + GUIUtil.showBsqFeeInfoPopup(listingFee, miningFee, txVsize, bsqFormatter, btcFormatter, + Res.get("dao.burnBsq.assetFee"), () -> doPublishFeeTx(transaction)); + } else { + doPublishFeeTx(transaction); + } + } catch (InsufficientMoneyException | TxException e) { + e.printStackTrace(); + new Popup().error(e.toString()).show(); + } + } else { + new Popup().warning(Res.get("dao.burnBsq.assets.toFewDays", minDays)).show(); + } + }); + + + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 100); + updateButtonState(); + + feeAmountInputTextField.resetValidation(); + } + + @Override + protected void deactivate() { + assetComboBox.setOnAction(null); + + feeAmountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + daoStateService.removeDaoStateListener(this); + + bsqWalletService.removeBsqBalanceListener(this); + + sortedList.comparatorProperty().unbind(); + + payFeeButton.setOnAction(null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin availableConfirmedBalance, + Coin availableNonBsqBalance, + Coin unverifiedBalance, + Coin unconfirmedChangeBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + + onUpdateAvailableConfirmedBalance(availableConfirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + // Delay a bit to reduce load at onParseBlockCompleteAfterBatchProcessing event + UserThread.runAfter(() -> { + assetService.updateAssetStates(); + updateList(); + }, 300, TimeUnit.MILLISECONDS); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + amountFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + amountInputTextFieldListener = (observable, oldValue, newValue) -> { + trialPeriodTextField.setText(Res.get("dao.burnBsq.assets.days", getDays())); + updateButtonState(); + }; + } + + private void onUpdateAvailableConfirmedBalance(Coin availableConfirmedBalance) { + bsqValidator.setAvailableBalance(availableConfirmedBalance); + updateButtonState(); + } + + private long getDays() { + return getListingFee().value / assetService.getFeePerDay().value; + } + + // We only update on new BSQ blocks and at view activation. We do not update at each trade statistics change as + // that would cause too much CPU load. The assetService.updateAssetStates() call takes about 22 ms. + private void updateList() { + // Here we exclude the assets which have been removed by voting. Paying a fee would not change the state. + List statefulAssets = assetService.getStatefulAssets(); + ObservableList nonRemovedStatefulAssets = FXCollections.observableArrayList(statefulAssets.stream() + .filter(e -> !e.wasRemovedByVoting()) + .collect(Collectors.toList())); + assetComboBox.setItems(nonRemovedStatefulAssets); + + // In the table we want to show all including removed assets. + observableList.setAll(statefulAssets.stream() + .map(statefulAsset -> new AssetListItem(statefulAsset, bsqFormatter)) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 100); + } + + private void updateButtonState() { + boolean isValid = bsqValidator.validate(feeAmountInputTextField.getText()).isValid && + selectedAsset != null; + payFeeButton.setDisable(!isValid); + } + + private Coin getListingFee() { + return ParsingUtils.parseToCoin(feeAmountInputTextField.getText(), bsqFormatter); + } + + private void doPublishFeeTx(Transaction transaction) { + assetService.publishTransaction(transaction, + () -> { + assetComboBox.getSelectionModel().clearSelection(); + if (!DevEnv.isDevMode()) + new Popup().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup().warning(errorMessage).show()); + + feeAmountInputTextField.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.nameAndCode")); + column.setMinWidth(120); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getNameAndCode()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getNameAndCode)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.state")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAssetStateString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getAssetStateString)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.tradeVolume")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getTradedVolumeAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getTradedVolume)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.lookBackPeriod")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLookBackPeriodInDays()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getLookBackPeriodInDays)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.trialFee")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getFeeOfTrialPeriodAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getFeeOfTrialPeriod)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.totalFee")); + column.setMinWidth(120); + column.getStyleClass().add("last-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getTotalFeesPaidAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getTotalFeesPaid)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java new file mode 100644 index 0000000000..3f49e0edab --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java @@ -0,0 +1,57 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.assetfee; + +import bisq.core.dao.governance.asset.StatefulAsset; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import lombok.Value; + +@Value +class AssetListItem { + private final StatefulAsset statefulAsset; + private final String tickerSymbol; + private final String assetStateString; + private final int trialPeriodInBlocks; + private final String nameAndCode; + private final long totalFeesPaid; + private final String totalFeesPaidAsString; + private final long feeOfTrialPeriod; + private final String feeOfTrialPeriodAsString; + private final String tradedVolumeAsString; + private final String lookBackPeriodInDays; + private final long tradedVolume; + + AssetListItem(StatefulAsset statefulAsset, + BsqFormatter bsqFormatter) { + this.statefulAsset = statefulAsset; + + tickerSymbol = statefulAsset.getTickerSymbol(); + nameAndCode = statefulAsset.getNameAndCode(); + assetStateString = Res.get("dao.assetState." + statefulAsset.getAssetState()); + feeOfTrialPeriod = statefulAsset.getFeeOfTrialPeriod(); + feeOfTrialPeriodAsString = bsqFormatter.formatCoinWithCode(feeOfTrialPeriod); + totalFeesPaid = statefulAsset.getTotalFeesPaid(); + totalFeesPaidAsString = bsqFormatter.formatCoinWithCode(totalFeesPaid); + trialPeriodInBlocks = (int) totalFeesPaid * 144; + tradedVolume = statefulAsset.getTradeVolume(); + tradedVolumeAsString = bsqFormatter.formatBTCWithCode(tradedVolume); + lookBackPeriodInDays = Res.get("dao.burnBsq.assets.days", statefulAsset.getLookBackPeriodInDays()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java new file mode 100644 index 0000000000..0fe3afe189 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java @@ -0,0 +1,74 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.util.DisplayUtils; + +import bisq.core.dao.governance.proofofburn.MyProofOfBurn; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; +import java.util.Optional; + +import lombok.Value; + +@Value +class MyProofOfBurnListItem { + private final MyProofOfBurn myProofOfBurn; + private final long amount; + private final String amountAsString; + private final String txId; + private final String hashAsHex; + private final String preImage; + private final String pubKey; + private final Date date; + private final String dateAsString; + + MyProofOfBurnListItem(MyProofOfBurn myProofOfBurn, ProofOfBurnService proofOfBurnService, BsqFormatter bsqFormatter) { + this.myProofOfBurn = myProofOfBurn; + + preImage = myProofOfBurn.getPreImage(); + Optional optionalTx = proofOfBurnService.getTx(myProofOfBurn.getTxId()); + if (optionalTx.isPresent()) { + Tx tx = optionalTx.get(); + date = new Date(tx.getTime()); + dateAsString = DisplayUtils.formatDateTime(date); + amount = proofOfBurnService.getAmount(tx); + amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); + txId = tx.getId(); + hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); + } else { + amount = 0; + amountAsString = Res.get("shared.na"); + txId = Res.get("shared.na"); + hashAsHex = Res.get("shared.na"); + pubKey = Res.get("shared.na"); + dateAsString = Res.get("shared.na"); + date = new Date(0); + } + } + +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java new file mode 100644 index 0000000000..5dfe0fbdf8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java @@ -0,0 +1,53 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.util.DisplayUtils; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; + +import lombok.Value; + +@Value +class ProofOfBurnListItem { + private final long amount; + private final String amountAsString; + private final String txId; + private final String hashAsHex; + private final String pubKey; + private final Date date; + private final String dateAsString; + + ProofOfBurnListItem(Tx tx, ProofOfBurnService proofOfBurnService, BsqFormatter bsqFormatter) { + amount = proofOfBurnService.getAmount(tx); + amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); + txId = tx.getId(); + hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); + date = new Date(tx.getTime()); + dateAsString = DisplayUtils.formatDateTime(date); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java new file mode 100644 index 0000000000..d62a428817 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.locale.Res; + +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import java.util.Optional; + +import static bisq.desktop.util.FormBuilder.addInputTextField; + +class ProofOfBurnSignatureWindow extends Overlay { + private final ProofOfBurnService proofOfBurnService; + private final String proofOfBurnTxId; + private final String pubKey; + + private TextField sigTextField; + private VBox sigTextFieldBox; + + ProofOfBurnSignatureWindow(ProofOfBurnService proofOfBurnService, String proofOfBurnTxId) { + this.proofOfBurnService = proofOfBurnService; + this.proofOfBurnTxId = proofOfBurnTxId; + this.pubKey = proofOfBurnService.getPubKeyAsHex(proofOfBurnTxId); + type = Type.Attention; + } + + public void show() { + if (headLine == null) + headLine = Res.get("dao.proofOfBurn.signature.window.title"); + + width = 800; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + @SuppressWarnings("Duplicates") + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(100); + gridPane.getColumnConstraints().add(columnConstraints); + } + + private void addContent() { + FormBuilder.addTopLabelTextField(gridPane, rowIndex, Res.get("dao.proofOfBurn.pubKey"), pubKey, 40); + + InputTextField messageInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.message")); + + Button signButton = FormBuilder.addButton(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sign"), 10); + signButton.setOnAction(e -> { + proofOfBurnService.sign(proofOfBurnTxId, messageInputTextField.getText()).ifPresent(sig -> { + sigTextFieldBox.setVisible(true); + sigTextField.setText(sig); + }); + }); + Tuple3 tuple = FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + sigTextFieldBox = tuple.third; + sigTextField = tuple.second; + sigTextFieldBox.setVisible(false); + + actionHandlerOptional = Optional.of(() -> { + Utilities.copyToClipboard(sigTextField.getText()); + }); + actionButtonText = Res.get("dao.proofOfBurn.copySig"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java new file mode 100644 index 0000000000..5737854f0b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java @@ -0,0 +1,103 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.locale.Res; + +import bisq.common.util.Tuple3; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import java.security.SignatureException; + +import static bisq.desktop.util.FormBuilder.addInputTextField; + +class ProofOfBurnVerificationWindow extends Overlay { + private final ProofOfBurnService proofOfBurnService; + private final String pubKey; + + private TextField verificationResultTextField; + private VBox verificationResultBox; + + ProofOfBurnVerificationWindow(ProofOfBurnService proofOfBurnService, String proofOfBurnTxId) { + this.proofOfBurnService = proofOfBurnService; + this.pubKey = proofOfBurnService.getPubKeyAsHex(proofOfBurnTxId); + type = Type.Attention; + } + + @SuppressWarnings("Duplicates") + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(100); + gridPane.getColumnConstraints().add(columnConstraints); + } + + public void show() { + if (headLine == null) + headLine = Res.get("dao.proofOfBurn.verify.window.title"); + + width = 800; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + private void addContent() { + FormBuilder.addTopLabelTextField(gridPane, rowIndex, Res.get("dao.proofOfBurn.pubKey"), pubKey, 40); + InputTextField messageInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.message")); + InputTextField signatureInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + Button verifyButton = FormBuilder.addButton(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.verify"), 10); + + verifyButton.setOnAction(e -> { + try { + verificationResultBox.setVisible(true); + proofOfBurnService.verify(messageInputTextField.getText(), pubKey, signatureInputTextField.getText()); + verificationResultTextField.setText(Res.get("dao.proofOfBurn.verificationResult.ok")); + } catch (SignatureException e1) { + verificationResultTextField.setText(Res.get("dao.proofOfBurn.verificationResult.failed")); + } + }); + + Tuple3 tuple = FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + verificationResultBox = tuple.third; + verificationResultTextField = tuple.second; + verificationResultBox.setVisible(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml new file mode 100644 index 0000000000..498a89f2a6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java new file mode 100644 index 0000000000..d1dbc15d75 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java @@ -0,0 +1,652 @@ +/* + * 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 bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.ExternalHyperlink; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.stream.Collectors; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@FxmlView +public class ProofOfBurnView extends ActivatableView implements BsqBalanceListener { + private final ProofOfBurnService proofOfBurnService; + private final MyProofOfBurnListService myProofOfBurnListService; + private final Preferences preferences; + private final CoinFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final BsqValidator bsqValidator; + + private InputTextField amountInputTextField, preImageTextField; + private TextField hashTextField; + private Button burnButton; + private TableView myItemsTableView; + private TableView allTxsTableView; + + private final ObservableList myItemsObservableList = FXCollections.observableArrayList(); + private final SortedList myItemsSortedList = new SortedList<>(myItemsObservableList); + + private final ObservableList allItemsObservableList = FXCollections.observableArrayList(); + private final SortedList allItemsSortedList = new SortedList<>(allItemsObservableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener, preImageFocusOutListener; + private ChangeListener amountInputTextFieldListener, preImageInputTextFieldListener; + private InvalidationListener updateListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ProofOfBurnView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BsqValidator bsqValidator, + ProofOfBurnService proofOfBurnService, + MyProofOfBurnListService myProofOfBurnListService, + Preferences preferences, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bsqValidator = bsqValidator; + this.proofOfBurnService = proofOfBurnService; + this.myProofOfBurnListService = myProofOfBurnListService; + this.preferences = preferences; + this.btcFormatter = btcFormatter; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 4, Res.get("dao.proofOfBurn.header")); + amountInputTextField = addInputTextField(root, ++gridRow, Res.get("dao.proofOfBurn.amount"), Layout.FIRST_ROW_DISTANCE); + preImageTextField = addInputTextField(root, ++gridRow, Res.get("dao.proofOfBurn.preImage")); + hashTextField = addTopLabelTextField(root, ++gridRow, Res.get("dao.proofOfBurn.hash")).second; + burnButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.proofOfBurn.burn")); + + myItemsTableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.proofOfBurn.myItems"), 30); + createColumnsForMyItems(); + myItemsTableView.setItems(myItemsSortedList); + + allTxsTableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.proofOfBurn.allTxs"), 30, "last"); + createColumnsForAllTxs(); + allTxsTableView.setItems(allItemsSortedList); + + createListeners(); + } + + @Override + protected void activate() { + amountInputTextField.textProperty().addListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + preImageTextField.textProperty().addListener(preImageInputTextFieldListener); + preImageTextField.focusedProperty().addListener(preImageFocusOutListener); + + allItemsSortedList.comparatorProperty().bind(allTxsTableView.comparatorProperty()); + + proofOfBurnService.getUpdateFlag().addListener(updateListener); + bsqWalletService.addBsqBalanceListener(this); + onUpdateAvailableConfirmedBalance(bsqWalletService.getAvailableConfirmedBalance()); + + burnButton.setOnAction((event) -> { + Coin amount = getAmountFee(); + try { + String preImageAsString = preImageTextField.getText(); + Transaction transaction = proofOfBurnService.burn(preImageAsString, amount.value); + Coin miningFee = transaction.getFee(); + int txVsize = transaction.getVsize(); + + if (!DevEnv.isDevMode()) { + GUIUtil.showBsqFeeInfoPopup(amount, miningFee, txVsize, bsqFormatter, btcFormatter, + Res.get("dao.proofOfBurn.header"), () -> doPublishFeeTx(transaction, preImageAsString)); + } else { + doPublishFeeTx(transaction, preImageAsString); + } + } catch (InsufficientMoneyException | TxException e) { + e.printStackTrace(); + new Popup().error(e.toString()).show(); + } + }); + + amountInputTextField.setValidator(bsqValidator); + preImageTextField.setValidator(new InputValidator()); + + updateList(); + GUIUtil.setFitToRowsForTableView(myItemsTableView, 41, 28, 4, 6); + GUIUtil.setFitToRowsForTableView(allTxsTableView, 41, 28, 2, 10); + updateButtonState(); + } + + @Override + protected void deactivate() { + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + allItemsSortedList.comparatorProperty().unbind(); + + proofOfBurnService.getUpdateFlag().removeListener(updateListener); + bsqWalletService.removeBsqBalanceListener(this); + + burnButton.setOnAction(null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin availableConfirmedBalance, + Coin availableNonBsqBalance, + Coin unverifiedBalance, + Coin unconfirmedChangeBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + onUpdateAvailableConfirmedBalance(availableConfirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + amountFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + amountInputTextFieldListener = (observable, oldValue, newValue) -> { + updateButtonState(); + }; + preImageFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + preImageInputTextFieldListener = (observable, oldValue, newValue) -> { + hashTextField.setText(proofOfBurnService.getHashAsString(newValue)); + updateButtonState(); + }; + + updateListener = observable -> updateList(); + } + + private void onUpdateAvailableConfirmedBalance(Coin availableConfirmedBalance) { + bsqValidator.setAvailableBalance(availableConfirmedBalance); + updateButtonState(); + } + + private void updateList() { + myItemsObservableList.setAll(myProofOfBurnListService.getMyProofOfBurnList().stream() + .map(myProofOfBurn -> new MyProofOfBurnListItem(myProofOfBurn, proofOfBurnService, bsqFormatter)) + .sorted(Comparator.comparing(MyProofOfBurnListItem::getDate).reversed()) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(myItemsTableView, 41, 28, 4, 6); + + allItemsObservableList.setAll(proofOfBurnService.getProofOfBurnTxList().stream() + .map(tx -> new ProofOfBurnListItem(tx, proofOfBurnService, bsqFormatter)) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(allTxsTableView, 41, 28, 2, 10); + } + + private void updateButtonState() { + boolean isValid = bsqValidator.validate(amountInputTextField.getText()).isValid && + preImageTextField.validate(); + burnButton.setDisable(!isValid); + } + + private Coin getAmountFee() { + return ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); + } + + private void doPublishFeeTx(Transaction transaction, String preImageAsString) { + proofOfBurnService.publishTransaction(transaction, preImageAsString, + () -> { + if (!DevEnv.isDevMode()) + new Popup().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup().warning(errorMessage).show()); + + amountInputTextField.clear(); + preImageTextField.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + + private void createColumnsForMyItems() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.amount")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.getStyleClass().add("first-column"); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmountAsString()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getAmount)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.date")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getDateAsString()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getDate)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.preImage")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPreImage()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getPreImage)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.hash")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getHashAsHex()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getHashAsHex)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.txs")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new ExternalHyperlink(transactionId); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(item.getTxId(), preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getTxId)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.pubKey")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPubKey()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getPubKey)); + + column = new AutoTooltipTableColumn<>(""); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.getStyleClass().add("last-column"); + column.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (button == null) { + button = new AutoTooltipButton(Res.get("dao.proofOfBurn.sign")); + setGraphic(button); + } + button.setOnAction(e -> new ProofOfBurnSignatureWindow(proofOfBurnService, item.getTxId()).show()); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setSortable(false); + } + + private void createColumnsForAllTxs() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.amount")); + column.setMinWidth(80); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmountAsString()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getAmount)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.date")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getDateAsString()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getDate)); + + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.hash")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getHashAsHex()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getHashAsHex)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.txs")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new ExternalHyperlink(transactionId); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(item.getTxId(), preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getTxId)); + + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.pubKey")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPubKey()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getPubKey)); + + + column = new AutoTooltipTableColumn<>(""); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.getStyleClass().add("last-column"); + column.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (button == null) { + button = new AutoTooltipButton(Res.get("dao.proofOfBurn.verify")); + setGraphic(button); + } + button.setOnAction(e -> new ProofOfBurnVerificationWindow(proofOfBurnService, item.getTxId()).show()); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setSortable(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.fxml new file mode 100644 index 0000000000..1f1c5c527b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.java new file mode 100644 index 0000000000..f21e6b62d3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/EconomyView.java @@ -0,0 +1,135 @@ +/* + * 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 supply. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.economy.dashboard.BsqDashboardView; +import bisq.desktop.main.dao.economy.supply.SupplyView; +import bisq.desktop.main.dao.economy.transactions.BSQTransactionsView; + +import bisq.core.locale.Res; + +import bisq.common.app.DevEnv; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class EconomyView extends ActivatableView { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem dashboard, supply, transactions; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + private ToggleGroup toggleGroup; + + @Inject + private EconomyView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = (viewPath, data) -> { + if (viewPath.size() != 4 || viewPath.indexOf(EconomyView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + toggleGroup = new ToggleGroup(); + List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, EconomyView.class); + dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), BsqDashboardView.class, baseNavPath); + supply = new MenuItem(navigation, toggleGroup, Res.get("dao.factsAndFigures.menuItem.supply"), SupplyView.class, baseNavPath); + transactions = new MenuItem(navigation, toggleGroup, Res.get("dao.factsAndFigures.menuItem.transactions"), BSQTransactionsView.class, baseNavPath); + + leftVBox.getChildren().addAll(dashboard, supply, transactions); + + if (!DevEnv.isDaoActivated()) { + dashboard.setDisable(true); + supply.setDisable(true); + transactions.setDisable(true); + } + } + + @Override + protected void activate() { + dashboard.activate(); + supply.activate(); + transactions.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(EconomyView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = BsqDashboardView.class; + + loadView(selectedViewClass); + } else if (viewPath.size() == 4 && viewPath.indexOf(EconomyView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + navigation.removeListener(listener); + + dashboard.deactivate(); + supply.deactivate(); + transactions.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof BsqDashboardView) toggleGroup.selectToggle(dashboard); + else if (view instanceof SupplyView) toggleGroup.selectToggle(supply); + else if (view instanceof BSQTransactionsView) toggleGroup.selectToggle(transactions); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.fxml new file mode 100644 index 0000000000..3a541a621a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java new file mode 100644 index 0000000000..b4d4c9dd89 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -0,0 +1,366 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.TextFieldWithIcon; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.dao.economy.dashboard.price.PriceChartView; +import bisq.desktop.main.dao.economy.dashboard.volume.VolumeChartView; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.Res; +import bisq.core.monetary.Price; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; + +import javafx.collections.ObservableList; + +import java.text.DecimalFormat; + +import java.util.Optional; + +import static bisq.desktop.util.FormBuilder.addLabelWithSubText; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithIcon; + + +@FxmlView +public class BsqDashboardView extends ActivatableView implements DaoStateListener { + + private final PriceChartView priceChartView; + private final VolumeChartView volumeChartView; + private final DaoFacade daoFacade; + private final TradeStatisticsManager tradeStatisticsManager; + private final PriceFeedService priceFeedService; + private final Preferences preferences; + private final BsqFormatter bsqFormatter; + + private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField, + usdVolumeTextField, btcVolumeTextField, averageBsqUsdPriceTextField, averageBsqBtcPriceTextField; + private TextFieldWithIcon avgPrice30TextField, avgUSDPrice30TextField; + private Label marketPriceLabel; + + private ChangeListener priceChangeListener; + private int gridRow = 0; + private Coin availableAmount; + private Price avg30DayUSDPrice; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BsqDashboardView(PriceChartView priceChartView, + VolumeChartView volumeChartView, + DaoFacade daoFacade, + TradeStatisticsManager tradeStatisticsManager, + PriceFeedService priceFeedService, + Preferences preferences, + BsqFormatter bsqFormatter) { + this.priceChartView = priceChartView; + this.volumeChartView = volumeChartView; + this.daoFacade = daoFacade; + this.tradeStatisticsManager = tradeStatisticsManager; + this.priceFeedService = priceFeedService; + this.preferences = preferences; + this.bsqFormatter = bsqFormatter; + } + + @Override + public void initialize() { + createTextFields(); + createPriceChart(); + createTradeChart(); + + priceChangeListener = (observable, oldValue, newValue) -> { + updatePrice(); + updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false); + updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true); + updateMarketCap(); + }; + } + + @Override + protected void activate() { + daoFacade.addBsqStateListener(this); + priceFeedService.updateCounterProperty().addListener(priceChangeListener); + + updateWithBsqBlockChainData(); + updatePrice(); + updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false); + updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true); + updateMarketCap(); + + averageBsqUsdPriceTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + priceFormat.setMaximumFractionDigits(4); + return priceFormat.format(priceChartView.averageBsqUsdPriceProperty().get()) + " BSQ/USD"; + }, + priceChartView.averageBsqUsdPriceProperty())); + averageBsqBtcPriceTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + priceFormat.setMaximumFractionDigits(8); + /* yAxisFormatter = value -> { + value = MathUtils.scaleDownByPowerOf10(value.longValue(), 8); + return priceFormat.format(value) + " BSQ/BTC"; + };*/ + + double scaled = MathUtils.scaleDownByPowerOf10(priceChartView.averageBsqBtcPriceProperty().get(), 8); + return priceFormat.format(scaled) + " BSQ/BTC"; + }, + priceChartView.averageBsqBtcPriceProperty())); + + usdVolumeTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + volumeFormat.setMaximumFractionDigits(0); + double scaled = MathUtils.scaleDownByPowerOf10(volumeChartView.usdVolumeProperty().get(), 4); + return volumeFormat.format(scaled) + " USD"; + }, + volumeChartView.usdVolumeProperty())); + btcVolumeTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + volumeFormat.setMaximumFractionDigits(4); + double scaled = MathUtils.scaleDownByPowerOf10(volumeChartView.btcVolumeProperty().get(), 8); + return volumeFormat.format(scaled) + " BTC"; + }, + volumeChartView.btcVolumeProperty())); + } + + + @Override + protected void deactivate() { + daoFacade.removeBsqStateListener(this); + priceFeedService.updateCounterProperty().removeListener(priceChangeListener); + + averageBsqUsdPriceTextField.textProperty().unbind(); + averageBsqBtcPriceTextField.textProperty().unbind(); + usdVolumeTextField.textProperty().unbind(); + btcVolumeTextField.textProperty().unbind(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + updateWithBsqBlockChainData(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createTextFields() { + Tuple3 marketPriceBox = addLabelWithSubText(root, gridRow++, "", ""); + marketPriceLabel = marketPriceBox.first; + marketPriceLabel.getStyleClass().add("dao-kpi-big"); + + marketPriceBox.second.getStyleClass().add("dao-kpi-subtext"); + + avgUSDPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90"), -20).second; + + avgUSDPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -35).second; + AnchorPane.setRightAnchor(avgUSDPrice30TextField.getIconLabel(), 10d); + + avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second; + + avgPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.avgPrice30"), -15).second; + AnchorPane.setRightAnchor(avgPrice30TextField.getIconLabel(), 10d); + + marketCapTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.marketCap")).second; + + availableAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.availableAmount")).second; + } + + private void createPriceChart() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, + Res.get("dao.factsAndFigures.supply.priceChat"), Layout.FLOATING_LABEL_DISTANCE); + titledGroupBg.getStyleClass().add("last"); + + priceChartView.initialize(); + VBox chartContainer = priceChartView.getRoot(); + + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 0d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); + GridPane.setColumnSpan(chartPane, 2); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); + + root.getChildren().add(chartPane); + + averageBsqUsdPriceTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection")).second; + averageBsqBtcPriceTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection")).second; + + } + + private void createTradeChart() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, + Res.get("dao.factsAndFigures.supply.volumeChat"), Layout.FLOATING_LABEL_DISTANCE); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg + + volumeChartView.initialize(); + VBox chartContainer = volumeChartView.getRoot(); + + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 0d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); + GridPane.setColumnSpan(chartPane, 2); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); + + root.getChildren().add(chartPane); + + usdVolumeTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.volumeUsd")).second; + btcVolumeTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.volumeBtc")).second; + } + + private void updateWithBsqBlockChainData() { + Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); + Coin issuedAmountFromCompRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.COMPENSATION)); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); + Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); + Coin totalAmountOfBurntBsq = Coin.valueOf(daoFacade.getTotalAmountOfBurntBsq()); + + availableAmount = issuedAmountFromGenesis + .add(issuedAmountFromCompRequests) + .add(issuedAmountFromReimbursementRequests) + .subtract(totalAmountOfBurntBsq) + .subtract(totalConfiscatedAmount); + + availableAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(availableAmount)); + } + + private void updatePrice() { + Optional optionalBsqPrice = priceFeedService.getBsqPrice(); + if (optionalBsqPrice.isPresent()) { + Price bsqPrice = optionalBsqPrice.get(); + marketPriceLabel.setText(FormattingUtils.formatPrice(bsqPrice) + " BSQ/BTC"); + } else { + marketPriceLabel.setText(Res.get("shared.na")); + } + } + + private void updateMarketCap() { + if (avg30DayUSDPrice != null) { + marketCapTextField.setText(bsqFormatter.formatMarketCap(avg30DayUSDPrice, availableAmount)); + } else { + marketCapTextField.setText(Res.get("shared.na")); + } + } + + private void updateAveragePriceFields(TextField field90, TextFieldWithIcon field30, boolean isUSDField) { + long average90 = updateAveragePriceField(field90, 90, isUSDField); + long average30 = updateAveragePriceField(field30.getTextField(), 30, isUSDField); + boolean trendUp = average30 > average90; + boolean trendDown = average30 < average90; + + Label iconLabel = field30.getIconLabel(); + ObservableList styleClass = iconLabel.getStyleClass(); + if (trendUp) { + field30.setVisible(true); + field30.setIcon(AwesomeIcon.CIRCLE_ARROW_UP); + styleClass.remove("price-trend-down"); + styleClass.add("price-trend-up"); + } else if (trendDown) { + field30.setVisible(true); + field30.setIcon(AwesomeIcon.CIRCLE_ARROW_DOWN); + styleClass.remove("price-trend-up"); + styleClass.add("price-trend-down"); + } else { + iconLabel.setVisible(false); + } + } + + private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) { + Tuple2 tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, days); + Price usdPrice = tuple.first; + Price bsqPrice = tuple.second; + + if (isUSDField) { + textField.setText(usdPrice + " BSQ/USD"); + if (days == 30) { + avg30DayUSDPrice = usdPrice; + } + } else { + textField.setText(bsqPrice + " BSQ/BTC"); + } + + Price average = isUSDField ? usdPrice : bsqPrice; + return average.getValue(); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java new file mode 100644 index 0000000000..49b2a0a7b4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java @@ -0,0 +1,223 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartDataModel; + +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import java.time.Instant; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartDataModel extends ChartDataModel { + private final TradeStatisticsManager tradeStatisticsManager; + private Map bsqUsdPriceByInterval, bsqBtcPriceByInterval, btcUsdPriceByInterval; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PriceChartDataModel(TradeStatisticsManager tradeStatisticsManager) { + super(); + + this.tradeStatisticsManager = tradeStatisticsManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void invalidateCache() { + bsqUsdPriceByInterval = null; + bsqBtcPriceByInterval = null; + btcUsdPriceByInterval = null; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price from timeline selection + /////////////////////////////////////////////////////////////////////////////////////////// + + double averageBsqUsdPrice() { + return getAveragePriceFromDateFilter(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ") || + tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBsqUsdPrice); + } + + double averageBsqBtcPrice() { + return getAveragePriceFromDateFilter(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ"), + PriceChartDataModel::getAverageBsqBtcPrice); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + Map getBsqUsdPriceByInterval() { + if (bsqUsdPriceByInterval != null) { + return bsqUsdPriceByInterval; + } + bsqUsdPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ") || + tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBsqUsdPrice); + return bsqUsdPriceByInterval; + } + + Map getBsqBtcPriceByInterval() { + if (bsqBtcPriceByInterval != null) { + return bsqBtcPriceByInterval; + } + + bsqBtcPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ"), + PriceChartDataModel::getAverageBsqBtcPrice); + return bsqBtcPriceByInterval; + } + + Map getBtcUsdPriceByInterval() { + if (btcUsdPriceByInterval != null) { + return btcUsdPriceByInterval; + } + + btcUsdPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBtcUsdPrice); + return btcUsdPriceByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price functions + /////////////////////////////////////////////////////////////////////////////////////////// + + private static double getAverageBsqUsdPrice(List list) { + double sumBsq = 0; + double sumBtcFromBsqTrades = 0; + double sumBtcFromUsdTrades = 0; + double sumUsd = 0; + for (TradeStatistics3 tradeStatistics : list) { + if (tradeStatistics.getCurrency().equals("BSQ")) { + sumBsq += getBsqAmount(tradeStatistics); + sumBtcFromBsqTrades += getBtcAmount(tradeStatistics); + } else if (tradeStatistics.getCurrency().equals("USD")) { + sumUsd += getUsdAmount(tradeStatistics); + sumBtcFromUsdTrades += getBtcAmount(tradeStatistics); + } + } + if (sumBsq == 0 || sumBtcFromBsqTrades == 0 || sumBtcFromUsdTrades == 0 || sumUsd == 0) { + return 0d; + } + double averageUsdPrice = sumUsd / sumBtcFromUsdTrades; + return sumBtcFromBsqTrades * averageUsdPrice / sumBsq; + } + + private static double getAverageBsqBtcPrice(List list) { + double sumBsq = 0; + double sumBtc = 0; + for (TradeStatistics3 tradeStatistics : list) { + sumBsq += getBsqAmount(tradeStatistics); + sumBtc += getBtcAmount(tradeStatistics); + } + if (sumBsq == 0 || sumBtc == 0) { + return 0d; + } + return MathUtils.scaleUpByPowerOf10(sumBtc / sumBsq, 8); + } + + private static double getAverageBtcUsdPrice(List list) { + double sumUsd = 0; + double sumBtc = 0; + for (TradeStatistics3 tradeStatistics : list) { + sumUsd += getUsdAmount(tradeStatistics); + sumBtc += getBtcAmount(tradeStatistics); + } + if (sumUsd == 0 || sumBtc == 0) { + return 0d; + } + return sumUsd / sumBtc; + } + + private static long getBtcAmount(TradeStatistics3 tradeStatistics) { + return tradeStatistics.getAmount(); + } + + private static double getUsdAmount(TradeStatistics3 tradeStatistics) { + return MathUtils.scaleUpByPowerOf10(tradeStatistics.getTradeVolume().getValue(), 4); + } + + private static long getBsqAmount(TradeStatistics3 tradeStatistics) { + return tradeStatistics.getTradeVolume().getValue(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getPriceByInterval(Predicate collectionFilter, + Function, Double> getAveragePriceFunction) { + return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), + collectionFilter, + tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), + dateFilter, + getAveragePriceFunction); + } + + private Map getPriceByInterval(Collection collection, + Predicate collectionFilter, + Function groupByDateFunction, + Predicate dateFilter, + Function, Double> getAveragePriceFunction) { + return collection.stream() + .filter(collectionFilter) + .collect(Collectors.groupingBy(groupByDateFunction)) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .map(entry -> new AbstractMap.SimpleEntry<>( + entry.getKey(), + getAveragePriceFunction.apply(entry.getValue()))) + .filter(e -> e.getValue() > 0d) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private double getAveragePriceFromDateFilter(Predicate collectionFilter, + Function, Double> getAveragePriceFunction) { + return getAveragePriceFunction.apply(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(collectionFilter) + .filter(tradeStatistics -> dateFilter.test(tradeStatistics.getDateAsLong() / 1000)) + .collect(Collectors.toList())); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java new file mode 100644 index 0000000000..0245b028a0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java @@ -0,0 +1,158 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartView extends ChartView { + private XYChart.Series seriesBsqUsdPrice, seriesBsqBtcPrice, seriesBtcUsdPrice; + private DoubleProperty averageBsqUsdPriceProperty = new SimpleDoubleProperty(); + private DoubleProperty averageBsqBtcPriceProperty = new SimpleDoubleProperty(); + + @Inject + public PriceChartView(PriceChartViewModel model) { + super(model); + + setRadioButtonBehaviour(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyDoubleProperty averageBsqUsdPriceProperty() { + return averageBsqUsdPriceProperty; + } + + public ReadOnlyDoubleProperty averageBsqBtcPriceProperty() { + return averageBsqBtcPriceProperty; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onSetYAxisFormatter(XYChart.Series series) { + if (series == seriesBsqUsdPrice) { + model.setBsqUsdPriceFormatter(); + } else if (series == seriesBsqBtcPrice) { + model.setBsqBtcPriceFormatter(); + } else { + model.setBtcUsdPriceFormatter(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesBsqUsdPrice, seriesBsqBtcPrice, seriesBtcUsdPrice); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesBsqUsdPrice.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesBsqUsdPrice = new XYChart.Series<>(); + seriesBsqUsdPrice.setName(Res.get("dao.factsAndFigures.supply.bsqUsdPrice")); + seriesIndexMap.put(getSeriesId(seriesBsqUsdPrice), 0); + + seriesBsqBtcPrice = new XYChart.Series<>(); + seriesBsqBtcPrice.setName(Res.get("dao.factsAndFigures.supply.bsqBtcPrice")); + seriesIndexMap.put(getSeriesId(seriesBsqBtcPrice), 1); + + seriesBtcUsdPrice = new XYChart.Series<>(); + seriesBtcUsdPrice.setName(Res.get("dao.factsAndFigures.supply.btcUsdPrice")); + seriesIndexMap.put(getSeriesId(seriesBtcUsdPrice), 2); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesBsqUsdPrice); + onSetYAxisFormatter(seriesBsqUsdPrice); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + String seriesId = getSeriesId(series); + if (seriesId.equals(getSeriesId(seriesBsqUsdPrice))) { + seriesBsqUsdPrice.getData().setAll(model.getBsqUsdPriceChartData()); + } else if (seriesId.equals(getSeriesId(seriesBsqBtcPrice))) { + seriesBsqBtcPrice.getData().setAll(model.getBsqBtcPriceChartData()); + } else if (seriesId.equals(getSeriesId(seriesBtcUsdPrice))) { + seriesBtcUsdPrice.getData().setAll(model.getBtcUsdPriceChartData()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesBsqUsdPrice)) { + seriesBsqUsdPrice.getData().setAll(model.getBsqUsdPriceChartData()); + } + if (activeSeries.contains(seriesBsqBtcPrice)) { + seriesBsqBtcPrice.getData().setAll(model.getBsqBtcPriceChartData()); + } + if (activeSeries.contains(seriesBtcUsdPrice)) { + seriesBtcUsdPrice.getData().setAll(model.getBtcUsdPriceChartData()); + } + + averageBsqBtcPriceProperty.set(model.averageBsqBtcPrice()); + averageBsqUsdPriceProperty.set(model.averageBsqUsdPrice()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java new file mode 100644 index 0000000000..6074a279d3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java @@ -0,0 +1,118 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartViewModel extends ChartViewModel { + private Function yAxisFormatter = value -> value + " BSQ/USD"; + private final DecimalFormat priceFormat; + + @Inject + public PriceChartViewModel(PriceChartDataModel dataModel) { + super(dataModel); + + priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price from timeline selection + /////////////////////////////////////////////////////////////////////////////////////////// + + double averageBsqUsdPrice() { + return dataModel.averageBsqUsdPrice(); + } + + double averageBsqBtcPrice() { + return dataModel.averageBsqBtcPrice(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getBsqUsdPriceChartData() { + return toChartDoubleData(dataModel.getBsqUsdPriceByInterval()); + } + + List> getBsqBtcPriceChartData() { + return toChartDoubleData(dataModel.getBsqBtcPriceByInterval()); + } + + List> getBtcUsdPriceChartData() { + return toChartDoubleData(dataModel.getBtcUsdPriceByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return yAxisFormatter.apply(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + void setBsqUsdPriceFormatter() { + priceFormat.setMaximumFractionDigits(4); + yAxisFormatter = value -> priceFormat.format(value) + " BSQ/USD"; + } + + void setBsqBtcPriceFormatter() { + priceFormat.setMaximumFractionDigits(8); + yAxisFormatter = value -> { + value = MathUtils.scaleDownByPowerOf10(value.longValue(), 8); + return priceFormat.format(value) + " BSQ/BTC"; + }; + } + + void setBtcUsdPriceFormatter() { + priceFormat.setMaximumFractionDigits(0); + yAxisFormatter = value -> priceFormat.format(value) + " BTC/USD"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java new file mode 100644 index 0000000000..e147ce6977 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java @@ -0,0 +1,158 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartDataModel; + +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import javax.inject.Inject; + +import java.time.Instant; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartDataModel extends ChartDataModel { + private final TradeStatisticsManager tradeStatisticsManager; + private Map usdVolumeByInterval, btcVolumeByInterval; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public VolumeChartDataModel(TradeStatisticsManager tradeStatisticsManager) { + super(); + + this.tradeStatisticsManager = tradeStatisticsManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getUsdVolume() { + return getUsdVolumeByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + long getBtcVolume() { + return getBtcVolumeByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void invalidateCache() { + usdVolumeByInterval = null; + btcVolumeByInterval = null; + } + + public Map getUsdVolumeByInterval() { + if (usdVolumeByInterval != null) { + return usdVolumeByInterval; + } + + usdVolumeByInterval = getVolumeByInterval(VolumeChartDataModel::getVolumeInUsd); + return usdVolumeByInterval; + } + + public Map getBtcVolumeByInterval() { + if (btcVolumeByInterval != null) { + return btcVolumeByInterval; + } + + btcVolumeByInterval = getVolumeByInterval(VolumeChartDataModel::getVolumeInBtc); + return btcVolumeByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Get volume functions + /////////////////////////////////////////////////////////////////////////////////////////// + + private static long getVolumeInUsd(List list) { + double sumBtcFromAllTrades = 0; + double sumBtcFromUsdTrades = 0; + double sumUsd = 0; + for (TradeStatistics3 tradeStatistics : list) { + long amount = tradeStatistics.getAmount(); + if (tradeStatistics.getCurrency().equals("USD")) { + sumUsd += tradeStatistics.getTradeVolume().getValue(); + sumBtcFromUsdTrades += amount; + } + sumBtcFromAllTrades += amount; + } + if (sumBtcFromAllTrades == 0 || sumBtcFromUsdTrades == 0 || sumUsd == 0) { + return 0L; + } + double averageUsdPrice = sumUsd / sumBtcFromUsdTrades; + // We truncate to 4 decimals + return (long) (sumBtcFromAllTrades * averageUsdPrice); + } + + private static long getVolumeInBtc(List list) { + return list.stream().mapToLong(TradeStatistics3::getAmount).sum(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getVolumeByInterval(Function, Long> getVolumeFunction) { + return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), + tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), + dateFilter, + getVolumeFunction); + } + + private Map getVolumeByInterval(Collection collection, + Function groupByDateFunction, + Predicate dateFilter, + Function, Long> getVolumeFunction) { + return collection.stream() + .collect(Collectors.groupingBy(groupByDateFunction)) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .map(entry -> new AbstractMap.SimpleEntry<>( + entry.getKey(), + getVolumeFunction.apply(entry.getValue()))) + .filter(e -> e.getValue() > 0L) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java new file mode 100644 index 0000000000..2dff0a73ea --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.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 bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartView extends ChartView { + private XYChart.Series seriesUsdVolume, seriesBtcVolume; + + private LongProperty usdVolumeProperty = new SimpleLongProperty(); + private LongProperty btcVolumeProperty = new SimpleLongProperty(); + + @Inject + public VolumeChartView(VolumeChartViewModel model) { + super(model); + + setRadioButtonBehaviour(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyLongProperty usdVolumeProperty() { + return usdVolumeProperty; + } + + public ReadOnlyLongProperty btcVolumeProperty() { + return btcVolumeProperty; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onSetYAxisFormatter(XYChart.Series series) { + if (series == seriesUsdVolume) { + model.setUsdVolumeFormatter(); + } else { + model.setBtcVolumeFormatter(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesUsdVolume, seriesBtcVolume); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesUsdVolume.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesUsdVolume = new XYChart.Series<>(); + seriesUsdVolume.setName(Res.get("dao.factsAndFigures.supply.tradeVolumeInUsd")); + seriesIndexMap.put(getSeriesId(seriesUsdVolume), 0); + + seriesBtcVolume = new XYChart.Series<>(); + seriesBtcVolume.setName(Res.get("dao.factsAndFigures.supply.tradeVolumeInBtc")); + seriesIndexMap.put(getSeriesId(seriesBtcVolume), 1); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesUsdVolume); + onSetYAxisFormatter(seriesUsdVolume); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + if (getSeriesId(series).equals(getSeriesId(seriesUsdVolume))) { + seriesUsdVolume.getData().setAll(model.getUsdVolumeChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesBtcVolume))) { + seriesBtcVolume.getData().setAll(model.getBtcVolumeChartData()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesUsdVolume)) { + seriesUsdVolume.getData().setAll(model.getUsdVolumeChartData()); + } + if (activeSeries.contains(seriesBtcVolume)) { + seriesBtcVolume.getData().setAll(model.getBtcVolumeChartData()); + } + + usdVolumeProperty.set(model.getUsdVolume()); + btcVolumeProperty.set(model.getBtcVolume()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java new file mode 100644 index 0000000000..142d2f3912 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java @@ -0,0 +1,107 @@ +/* + * 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 bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartViewModel extends ChartViewModel { + private Function yAxisFormatter = value -> value + " USD"; + private final DecimalFormat volumeFormat; + + + @Inject + public VolumeChartViewModel(VolumeChartDataModel dataModel) { + super(dataModel); + + volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getUsdVolume() { + return dataModel.getUsdVolume(); + } + + long getBtcVolume() { + return dataModel.getBtcVolume(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getUsdVolumeChartData() { + return toChartLongData(dataModel.getUsdVolumeByInterval()); + } + + List> getBtcVolumeChartData() { + return toChartLongData(dataModel.getBtcVolumeByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return yAxisFormatter.apply(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + void setUsdVolumeFormatter() { + volumeFormat.setMaximumFractionDigits(0); + yAxisFormatter = value -> volumeFormat.format(MathUtils.scaleDownByPowerOf10(value.longValue(), 4)) + " USD"; + } + + void setBtcVolumeFormatter() { + volumeFormat.setMaximumFractionDigits(4); + yAxisFormatter = value -> volumeFormat.format(MathUtils.scaleDownByPowerOf10(value.longValue(), 8)) + " BTC"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.fxml new file mode 100644 index 0000000000..561c3c2121 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java new file mode 100644 index 0000000000..7c8816942b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -0,0 +1,213 @@ +/* + * 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 bisq.desktop.main.dao.economy.supply; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.dao.economy.supply.dao.DaoChartView; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyLongProperty; + +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; + +@FxmlView +public class SupplyView extends ActivatableView implements DaoStateListener { + private final DaoFacade daoFacade; + private final DaoChartView daoChartView; + private final BsqFormatter bsqFormatter; + + private TextField genesisIssueAmountTextField, compensationAmountTextField, reimbursementAmountTextField, + bsqTradeFeeAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalUnlockedAmountTextField, totalConfiscatedAmountTextField, proofOfBurnAmountTextField; + private int gridRow = 0; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private SupplyView(DaoFacade daoFacade, + DaoChartView daoChartView, + BsqFormatter bsqFormatter) { + this.daoFacade = daoFacade; + this.daoChartView = daoChartView; + this.bsqFormatter = bsqFormatter; + } + + @Override + public void initialize() { + createDaoChart(); + createIssuedAndBurnedFields(); + createLockedBsqFields(); + } + + @Override + protected void activate() { + daoFacade.addBsqStateListener(this); + + compensationAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.compensationAmountProperty()), + daoChartView.compensationAmountProperty())); + reimbursementAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.reimbursementAmountProperty()), + daoChartView.reimbursementAmountProperty())); + bsqTradeFeeAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.bsqTradeFeeAmountProperty()), + daoChartView.bsqTradeFeeAmountProperty())); + proofOfBurnAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.proofOfBurnAmountProperty()), + daoChartView.proofOfBurnAmountProperty())); + + Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); + genesisIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromGenesis)); + updateWithBsqBlockChainData(); + } + + @Override + protected void deactivate() { + daoFacade.removeBsqStateListener(this); + + compensationAmountTextField.textProperty().unbind(); + reimbursementAmountTextField.textProperty().unbind(); + bsqTradeFeeAmountTextField.textProperty().unbind(); + proofOfBurnAmountTextField.textProperty().unbind(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + updateWithBsqBlockChainData(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createDaoChart() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg + + daoChartView.initialize(); + VBox chartContainer = daoChartView.getRoot(); + + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 0d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); + GridPane.setColumnSpan(chartPane, 2); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); + + root.getChildren().add(chartPane); + } + + private void createIssuedAndBurnedFields() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.issued"), Layout.FLOATING_LABEL_DISTANCE); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg + + Tuple3 genesisAmountTuple = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.factsAndFigures.supply.genesisIssueAmount"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); + genesisIssueAmountTextField = genesisAmountTuple.second; + GridPane.setColumnSpan(genesisAmountTuple.third, 2); + + compensationAmountTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.supply.compRequestIssueAmount")).second; + reimbursementAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.reimbursementAmount")).second; + + addTitledGroupBg(root, ++gridRow, 1, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); + + bsqTradeFeeAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.factsAndFigures.supply.bsqTradeFee"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; + + proofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.proofOfBurn"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; + } + + private void createLockedBsqFields() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.factsAndFigures.supply.locked"), Layout.GROUP_DISTANCE); + titledGroupBg.getStyleClass().add("last"); + + totalLockedUpAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.factsAndFigures.supply.totalLockedUpAmount"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + totalUnlockingAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.totalUnlockingAmount"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + + totalUnlockedAmountTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.supply.totalUnlockedAmount")).second; + totalConfiscatedAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.totalConfiscatedAmount")).second; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateWithBsqBlockChainData() { + Coin totalLockedUpAmount = Coin.valueOf(daoFacade.getTotalLockupAmount()); + totalLockedUpAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalLockedUpAmount)); + + Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs()); + totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount)); + + Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs()); + totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount)); + + Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); + totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); + } + + private String getFormattedValue(ReadOnlyLongProperty property) { + return bsqFormatter.formatAmountWithGroupSeparatorAndCode(Coin.valueOf(property.get())); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java new file mode 100644 index 0000000000..52b90f72ba --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java @@ -0,0 +1,276 @@ +/* + * 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 bisq.desktop.main.dao.economy.supply.dao; + +import bisq.desktop.components.chart.ChartDataModel; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.time.Instant; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class DaoChartDataModel extends ChartDataModel { + private final DaoStateService daoStateService; + private final Function blockTimeOfIssuanceFunction; + private Map totalIssuedByInterval, compensationByInterval, reimbursementByInterval, + totalBurnedByInterval, bsqTradeFeeByInterval, proofOfBurnByInterval; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoChartDataModel(DaoStateService daoStateService) { + super(); + + this.daoStateService = daoStateService; + + // TODO getBlockTime is the bottleneck. Add a lookup map to daoState to fix that in a dedicated PR. + blockTimeOfIssuanceFunction = memoize(issuance -> { + int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0); + return daoStateService.getBlockTime(height); + }); + } + + @Override + protected void invalidateCache() { + totalIssuedByInterval = null; + compensationByInterval = null; + reimbursementByInterval = null; + totalBurnedByInterval = null; + bsqTradeFeeByInterval = null; + proofOfBurnByInterval = null; + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getCompensationAmount() { + return getCompensationByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + long getReimbursementAmount() { + return getReimbursementByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + long getBsqTradeFeeAmount() { + return getBsqTradeFeeByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + long getProofOfBurnAmount() { + return getProofOfBurnByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data for chart + /////////////////////////////////////////////////////////////////////////////////////////// + + Map getTotalIssuedByInterval() { + if (totalIssuedByInterval != null) { + return totalIssuedByInterval; + } + + Map compensationMap = getCompensationByInterval(); + Map reimbursementMap = getReimbursementByInterval(); + totalIssuedByInterval = getMergedMap(compensationMap, reimbursementMap, Long::sum); + return totalIssuedByInterval; + } + + Map getCompensationByInterval() { + if (compensationByInterval != null) { + return compensationByInterval; + } + + Set issuanceSetForType = daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION); + Map issuedBsqByInterval = getIssuedBsqByInterval(issuanceSetForType, getDateFilter()); + Map historicalIssuanceByInterval = getHistoricalIssuedBsqByInterval(DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, getDateFilter()); + compensationByInterval = getMergedMap(issuedBsqByInterval, historicalIssuanceByInterval, (daoDataValue, staticDataValue) -> staticDataValue); + return compensationByInterval; + } + + Map getReimbursementByInterval() { + if (reimbursementByInterval != null) { + return reimbursementByInterval; + } + + Map issuedBsqByInterval = getIssuedBsqByInterval(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), getDateFilter()); + Map historicalIssuanceByInterval = getHistoricalIssuedBsqByInterval(DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, getDateFilter()); + reimbursementByInterval = getMergedMap(issuedBsqByInterval, historicalIssuanceByInterval, (daoDataValue, staticDataValue) -> staticDataValue); + return reimbursementByInterval; + } + + Map getTotalBurnedByInterval() { + if (totalBurnedByInterval != null) { + return totalBurnedByInterval; + } + + Map tradeFee = getBsqTradeFeeByInterval(); + Map proofOfBurn = getProofOfBurnByInterval(); + totalBurnedByInterval = getMergedMap(tradeFee, proofOfBurn, Long::sum); + return totalBurnedByInterval; + } + + Map getBsqTradeFeeByInterval() { + if (bsqTradeFeeByInterval != null) { + return bsqTradeFeeByInterval; + } + + bsqTradeFeeByInterval = getBurntBsqByInterval(daoStateService.getTradeFeeTxs(), getDateFilter()); + return bsqTradeFeeByInterval; + } + + Map getProofOfBurnByInterval() { + if (proofOfBurnByInterval != null) { + return proofOfBurnByInterval; + } + + proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs(), getDateFilter()); + return proofOfBurnByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getIssuedBsqByInterval(Set issuanceSet, Predicate dateFilter) { + return issuanceSet.stream() + .collect(Collectors.groupingBy(issuance -> + toTimeInterval(Instant.ofEpochMilli(blockTimeOfIssuanceFunction.apply(issuance))))) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().stream() + .mapToLong(Issuance::getAmount) + .sum())); + } + + private Map getHistoricalIssuedBsqByInterval(Map historicalData, + Predicate dateFilter) { + + return historicalData.entrySet().stream() + .filter(e -> dateFilter.test(e.getKey())) + .collect(Collectors.toMap(e -> toTimeInterval(Instant.ofEpochSecond(e.getKey())), + Map.Entry::getValue, + (a, b) -> a + b)); + } + + private Map getBurntBsqByInterval(Collection txs, Predicate dateFilter) { + return txs.stream() + .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().stream() + .mapToLong(Tx::getBurntBsq) + .sum())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private static Function memoize(Function fn) { + Map map = new ConcurrentHashMap<>(); + return x -> map.computeIfAbsent(x, fn); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Historical data + /////////////////////////////////////////////////////////////////////////////////////////// + + // We did not use the reimbursement requests initially (but the compensation requests) because the limits + // have been too low. Over time it got mixed in compensation requests and reimbursement requests. + // To reflect that we use static data derived from the Github data. For new data we do not need that anymore + // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. + private static class DaoEconomyHistoricalData { + // Key is start date of the cycle in epoch seconds, value is reimbursement amount + public final static Map REIMBURSEMENTS_BY_CYCLE_DATE = new HashMap<>(); + public final static Map COMPENSATIONS_BY_CYCLE_DATE = new HashMap<>(); + + static { + REIMBURSEMENTS_BY_CYCLE_DATE.put(1571349571L, 60760L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1574180991L, 2621000L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1576966522L, 4769100L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1579613568L, 0L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1582399054L, 9186600L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1585342220L, 12089400L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1588025030L, 5420700L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1591004931L, 9138760L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1593654027L, 10821807L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1596407074L, 2160157L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1599175867L, 8769408L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1601861442L, 4956585L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1604845863L, 2121664L); + + COMPENSATIONS_BY_CYCLE_DATE.put(1555340856L, 6931863L); + COMPENSATIONS_BY_CYCLE_DATE.put(1558083590L, 2287000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1560771266L, 2273000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1563347672L, 2943772L); + COMPENSATIONS_BY_CYCLE_DATE.put(1566009595L, 10040170L); + COMPENSATIONS_BY_CYCLE_DATE.put(1568643566L, 8685115L); + COMPENSATIONS_BY_CYCLE_DATE.put(1571349571L, 7315879L); + COMPENSATIONS_BY_CYCLE_DATE.put(1574180991L, 12508300L); + COMPENSATIONS_BY_CYCLE_DATE.put(1576966522L, 5884500L); + COMPENSATIONS_BY_CYCLE_DATE.put(1579613568L, 8206000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1582399054L, 3518364L); + COMPENSATIONS_BY_CYCLE_DATE.put(1585342220L, 6231700L); + COMPENSATIONS_BY_CYCLE_DATE.put(1588025030L, 4391400L); + COMPENSATIONS_BY_CYCLE_DATE.put(1591004931L, 3636463L); + COMPENSATIONS_BY_CYCLE_DATE.put(1593654027L, 6156631L); + COMPENSATIONS_BY_CYCLE_DATE.put(1596407074L, 5838368L); + COMPENSATIONS_BY_CYCLE_DATE.put(1599175867L, 6086442L); + COMPENSATIONS_BY_CYCLE_DATE.put(1601861442L, 5615973L); + COMPENSATIONS_BY_CYCLE_DATE.put(1604845863L, 7782667L); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java new file mode 100644 index 0000000000..8cf2e7cc88 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -0,0 +1,211 @@ +/* + * 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 bisq.desktop.main.dao.economy.supply.dao; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoChartView extends ChartView { + private LongProperty compensationAmountProperty = new SimpleLongProperty(); + private LongProperty reimbursementAmountProperty = new SimpleLongProperty(); + private LongProperty bsqTradeFeeAmountProperty = new SimpleLongProperty(); + private LongProperty proofOfBurnAmountProperty = new SimpleLongProperty(); + + private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, + seriesReimbursement, seriesTotalIssued, seriesTotalBurned; + + + @Inject + public DaoChartView(DaoChartViewModel model) { + super(model); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyLongProperty compensationAmountProperty() { + return compensationAmountProperty; + } + + public ReadOnlyLongProperty reimbursementAmountProperty() { + return reimbursementAmountProperty; + } + + public ReadOnlyLongProperty bsqTradeFeeAmountProperty() { + return bsqTradeFeeAmountProperty; + } + + public ReadOnlyLongProperty proofOfBurnAmountProperty() { + return proofOfBurnAmountProperty; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesTotalIssued, seriesCompensation, seriesReimbursement); + } + + @Override + protected Collection> getSeriesForLegend2() { + return List.of(seriesTotalBurned, seriesBsqTradeFee, seriesProofOfBurn); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesTotalBurned.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesTotalIssued = new XYChart.Series<>(); + seriesTotalIssued.setName(Res.get("dao.factsAndFigures.supply.totalIssued")); + seriesIndexMap.put(getSeriesId(seriesTotalIssued), 0); + + seriesTotalBurned = new XYChart.Series<>(); + seriesTotalBurned.setName(Res.get("dao.factsAndFigures.supply.totalBurned")); + seriesIndexMap.put(getSeriesId(seriesTotalBurned), 1); + + seriesCompensation = new XYChart.Series<>(); + seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); + seriesIndexMap.put(getSeriesId(seriesCompensation), 2); + + seriesReimbursement = new XYChart.Series<>(); + seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); + seriesIndexMap.put(getSeriesId(seriesReimbursement), 3); + + seriesBsqTradeFee = new XYChart.Series<>(); + seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); + seriesIndexMap.put(getSeriesId(seriesBsqTradeFee), 4); + + seriesProofOfBurn = new XYChart.Series<>(); + seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); + seriesIndexMap.put(getSeriesId(seriesProofOfBurn), 5); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesTotalIssued); + activateSeries(seriesTotalBurned); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { + applyTotalIssued(); + } else if (getSeriesId(series).equals(getSeriesId(seriesCompensation))) { + applyCompensation(); + } else if (getSeriesId(series).equals(getSeriesId(seriesReimbursement))) { + applyReimbursement(); + } else if (getSeriesId(series).equals(getSeriesId(seriesTotalBurned))) { + applyTotalBurned(); + } else if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { + applyBsqTradeFee(); + } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { + applyProofOfBurn(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesTotalIssued)) { + applyTotalIssued(); + } + if (activeSeries.contains(seriesCompensation)) { + applyCompensation(); + } + if (activeSeries.contains(seriesReimbursement)) { + applyReimbursement(); + } + if (activeSeries.contains(seriesTotalBurned)) { + applyTotalBurned(); + } + if (activeSeries.contains(seriesBsqTradeFee)) { + applyBsqTradeFee(); + } + if (activeSeries.contains(seriesProofOfBurn)) { + applyProofOfBurn(); + } + + compensationAmountProperty.set(model.getCompensationAmount()); + reimbursementAmountProperty.set(model.getReimbursementAmount()); + bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); + proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); + } + + private void applyTotalIssued() { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); + } + + private void applyCompensation() { + seriesCompensation.getData().setAll(model.getCompensationChartData()); + } + + private void applyReimbursement() { + seriesReimbursement.getData().setAll(model.getReimbursementChartData()); + } + + private void applyTotalBurned() { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + } + + private void applyBsqTradeFee() { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + } + + private void applyProofOfBurn() { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java new file mode 100644 index 0000000000..6acca876a1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java @@ -0,0 +1,125 @@ +/* + * 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 bisq.desktop.main.dao.economy.supply.dao; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; +import bisq.core.util.coin.BsqFormatter; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoChartViewModel extends ChartViewModel { + private final DecimalFormat priceFormat; + private final BsqFormatter bsqFormatter; + + + @Inject + public DaoChartViewModel(DaoChartDataModel dataModel, BsqFormatter bsqFormatter) { + super(dataModel); + + this.bsqFormatter = bsqFormatter; + priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getTotalIssuedChartData() { + return toChartData(dataModel.getTotalIssuedByInterval()); + } + + List> getCompensationChartData() { + return toChartData(dataModel.getCompensationByInterval()); + } + + List> getReimbursementChartData() { + return toChartData(dataModel.getReimbursementByInterval()); + } + + List> getTotalBurnedChartData() { + return toChartData(dataModel.getTotalBurnedByInterval()); + } + + List> getBsqTradeFeeChartData() { + return toChartData(dataModel.getBsqTradeFeeByInterval()); + } + + List> getProofOfBurnChartData() { + return toChartData(dataModel.getProofOfBurnByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return priceFormat.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(value.longValue()))) + " BSQ"; + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + @Override + protected String getTooltipValueConverter(Number value) { + return bsqFormatter.formatBSQSatoshisWithCode(value.longValue()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoChartDataModel delegates + /////////////////////////////////////////////////////////////////////////////////////////// + + long getCompensationAmount() { + return dataModel.getCompensationAmount(); + } + + long getReimbursementAmount() { + return dataModel.getReimbursementAmount(); + } + + long getBsqTradeFeeAmount() { + return dataModel.getBsqTradeFeeAmount(); + } + + long getProofOfBurnAmount() { + return dataModel.getProofOfBurnAmount(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.fxml new file mode 100644 index 0000000000..bb89c7c4b2 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java new file mode 100644 index 0000000000..36cd962320 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java @@ -0,0 +1,155 @@ +/* + * 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 bisq.desktop.main.dao.economy.transactions; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import bisq.common.util.Tuple3; + +import javax.inject.Inject; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelHyperlinkWithIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; + +@FxmlView +public class BSQTransactionsView extends ActivatableView implements DaoStateListener { + + private final DaoFacade daoFacade; + private final Preferences preferences; + + private int gridRow = 0; + private TextField allTxTextField, burntFeeTxsTextField, + utxoTextField, compensationIssuanceTxTextField, + reimbursementIssuanceTxTextField, invalidTxsTextField, irregularTxsTextField; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BSQTransactionsView(DaoFacade daoFacade, + Preferences preferences) { + this.daoFacade = daoFacade; + this.preferences = preferences; + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.transactions.genesis")); + String genTxHeight = String.valueOf(daoFacade.getGenesisBlockHeight()); + String genesisTxId = daoFacade.getGenesisTxId(); + String url = preferences.getBsqBlockChainExplorer().txUrl + genesisTxId; + + GridPane.setColumnSpan(addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.factsAndFigures.transactions.genesisBlockHeight"), + genTxHeight, Layout.FIRST_ROW_DISTANCE).third, 2); + + // TODO use addTopLabelTxIdTextField + Tuple3 tuple = addTopLabelHyperlinkWithIcon(root, ++gridRow, + Res.get("dao.factsAndFigures.transactions.genesisTxId"), genesisTxId, url, 0); + HyperlinkWithIcon hyperlinkWithIcon = tuple.second; + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", genesisTxId))); + + GridPane.setColumnSpan(tuple.third, 2); + + + int startRow = ++gridRow; + + TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, 3, Res.get("dao.factsAndFigures.transactions.txDetails"), Layout.GROUP_DISTANCE); + titledGroupBg.getStyleClass().add("last"); + + allTxTextField = addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.factsAndFigures.transactions.allTx"), + genTxHeight, Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + utxoTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.factsAndFigures.transactions.utxo")).second; + compensationIssuanceTxTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.transactions.compensationIssuanceTx")).second; + reimbursementIssuanceTxTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.transactions.reimbursementIssuanceTx")).second; + + int columnIndex = 1; + gridRow = startRow; + + titledGroupBg = addTitledGroupBg(root, startRow, columnIndex, 3, "", Layout.GROUP_DISTANCE); + titledGroupBg.getStyleClass().add("last"); + + burntFeeTxsTextField = addTopLabelReadOnlyTextField(root, gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.burntTx"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + invalidTxsTextField = addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.invalidTx")).second; + irregularTxsTextField = addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.irregularTx")).second; + gridRow++; + + } + + @Override + protected void activate() { + daoFacade.addBsqStateListener(this); + + updateWithBsqBlockChainData(); + } + + @Override + protected void deactivate() { + daoFacade.removeBsqStateListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + updateWithBsqBlockChainData(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateWithBsqBlockChainData() { + allTxTextField.setText(String.valueOf(daoFacade.getNumTxs())); + utxoTextField.setText(String.valueOf(daoFacade.getUnspentTxOutputs().size())); + compensationIssuanceTxTextField.setText(String.valueOf(daoFacade.getNumIssuanceTransactions(IssuanceType.COMPENSATION))); + reimbursementIssuanceTxTextField.setText(String.valueOf(daoFacade.getNumIssuanceTransactions(IssuanceType.REIMBURSEMENT))); + burntFeeTxsTextField.setText(String.valueOf(daoFacade.getBurntFeeTxs().size())); + invalidTxsTextField.setText(String.valueOf(daoFacade.getInvalidTxs().size())); + irregularTxsTextField.setText(String.valueOf(daoFacade.getIrregularTxs().size())); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml new file mode 100644 index 0000000000..11370e0ee4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java new file mode 100644 index 0000000000..6d0c6eaa56 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java @@ -0,0 +1,182 @@ +/* + * 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 bisq.desktop.main.dao.governance; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.governance.dashboard.GovernanceDashboardView; +import bisq.desktop.main.dao.governance.make.MakeProposalView; +import bisq.desktop.main.dao.governance.proposals.ProposalsView; +import bisq.desktop.main.dao.governance.result.VoteResultView; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import javafx.beans.value.ChangeListener; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class GovernanceView extends ActivatableView implements DaoStateListener { + private final ViewLoader viewLoader; + private final Navigation navigation; + private final DaoFacade daoFacade; + private final DaoStateService daoStateService; + + private MenuItem dashboard, make, open, result; + private Navigation.Listener navigationListener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + private ChangeListener phaseChangeListener; + private ToggleGroup toggleGroup; + + @Inject + private GovernanceView(CachingViewLoader viewLoader, Navigation navigation, DaoFacade daoFacade, + DaoStateService daoStateService) { + this.viewLoader = viewLoader; + this.navigation = navigation; + this.daoFacade = daoFacade; + this.daoStateService = daoStateService; + } + + @Override + public void initialize() { + navigationListener = (viewPath, data) -> { + if (viewPath.size() != 4 || viewPath.indexOf(GovernanceView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + phaseChangeListener = (observable, oldValue, newValue) -> { + if (newValue == DaoPhase.Phase.BLIND_VOTE) + open.setLabelText(Res.get("dao.proposal.menuItem.vote")); + else + open.setLabelText(Res.get("dao.proposal.menuItem.browse")); + }; + + toggleGroup = new ToggleGroup(); + List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, GovernanceView.class); + dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), + GovernanceDashboardView.class, baseNavPath); + make = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.make"), + MakeProposalView.class, baseNavPath); + open = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.browse"), + ProposalsView.class, baseNavPath); + result = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.result"), + VoteResultView.class, baseNavPath); + + leftVBox.getChildren().addAll(dashboard, make, open, result); + } + + @Override + protected void activate() { + if (daoStateService.isParseBlockChainComplete()) { + daoFacade.phaseProperty().addListener(phaseChangeListener); + } else { + daoStateService.addDaoStateListener(this); + } + + + dashboard.activate(); + make.activate(); + open.activate(); + result.activate(); + + navigation.addListener(navigationListener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(GovernanceView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = MakeProposalView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(GovernanceView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + daoFacade.phaseProperty().removeListener(phaseChangeListener); + daoStateService.removeDaoStateListener(this); + + navigation.removeListener(navigationListener); + + dashboard.deactivate(); + make.deactivate(); + open.deactivate(); + result.deactivate(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockChainComplete() { + daoFacade.phaseProperty().addListener(phaseChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof GovernanceDashboardView) toggleGroup.selectToggle(dashboard); + else if (view instanceof MakeProposalView) toggleGroup.selectToggle(make); + else if (view instanceof ProposalsView) toggleGroup.selectToggle(open); + else if (view instanceof VoteResultView) toggleGroup.selectToggle(result); + } +} + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java new file mode 100644 index 0000000000..20b33ddaf6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java @@ -0,0 +1,136 @@ +/* + * 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 bisq.desktop.main.dao.governance; + +import bisq.desktop.components.SeparatedPhaseBars; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@Slf4j +public class PhasesView implements DaoStateListener { + private final DaoFacade daoFacade; + private final PeriodService periodService; + private SeparatedPhaseBars separatedPhaseBars; + private List phaseBarsItems; + + @Inject + private PhasesView(DaoFacade daoFacade, PeriodService periodService) { + this.daoFacade = daoFacade; + this.periodService = periodService; + } + + public int addGroup(GridPane gridPane, int gridRow) { + addTitledGroupBg(gridPane, gridRow, 1, Res.get("dao.cycle.headline")); + separatedPhaseBars = createSeparatedPhaseBars(); + GridPane.setMargin(separatedPhaseBars, new Insets(Layout.FIRST_ROW_DISTANCE + 5, 0, 0, 0)); + GridPane.setRowIndex(separatedPhaseBars, gridRow); + gridPane.getChildren().add(separatedPhaseBars); + return gridRow; + } + + public void activate() { + daoFacade.addBsqStateListener(this); + + applyData(daoFacade.getChainHeight()); + } + + public void deactivate() { + daoFacade.removeBsqStateListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + applyData(block.getHeight()); + + phaseBarsItems.forEach(item -> { + DaoPhase.Phase phase = item.getPhase(); + // Last block is considered for the break as we must not publish a tx there (would get confirmed in next + // block which would be a break). Only at result phase we don't have that situation ans show the last block + // as valid block in the phase. + if (periodService.isInPhaseButNotLastBlock(phase) || + (phase == DaoPhase.Phase.RESULT && periodService.isInPhase(block.getHeight(), phase))) { + item.setActive(); + } else { + item.setInActive(); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private SeparatedPhaseBars createSeparatedPhaseBars() { + phaseBarsItems = Arrays.asList( + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.PROPOSAL, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK1, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BLIND_VOTE, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK2, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.VOTE_REVEAL, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK3, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.RESULT, false)); + return new SeparatedPhaseBars(phaseBarsItems); + } + + private void applyData(int height) { + if (height > 0) { + phaseBarsItems.forEach(item -> { + int firstBlock = daoFacade.getFirstBlockOfPhaseForDisplay(height, item.getPhase()); + int lastBlock = daoFacade.getLastBlockOfPhaseForDisplay(height, item.getPhase()); + int duration = daoFacade.getDurationForPhaseForDisplay(item.getPhase()); + item.setPeriodRange(firstBlock, lastBlock, duration); + double progress = 0; + if (height >= firstBlock && height <= lastBlock) { + progress = (double) (height - firstBlock + 1) / (double) duration; + } else if (height < firstBlock) { + progress = 0; + } else if (height > lastBlock) { + progress = 1; + } + + item.getProgressProperty().set(progress); + }); + separatedPhaseBars.updateWidth(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java new file mode 100644 index 0000000000..edf96413c8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -0,0 +1,711 @@ +/* + * 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 bisq.desktop.main.dao.governance; + +import bisq.desktop.Navigation; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.bonding.BondingView; +import bisq.desktop.main.dao.bonding.bonds.BondsView; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.bond.role.BondedRole; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.governance.proposal.param.ChangeParamInputValidator; +import bisq.core.dao.governance.proposal.param.ChangeParamValidator; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.BondedRoleType; +import bisq.core.dao.state.model.governance.ChangeParamProposal; +import bisq.core.dao.state.model.governance.CompensationProposal; +import bisq.core.dao.state.model.governance.ConfiscateBondProposal; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.GenericProposal; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.ProposalVoteResult; +import bisq.core.dao.state.model.governance.ReimbursementProposal; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.dao.state.model.governance.RoleProposal; +import bisq.core.dao.state.model.governance.Vote; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.RegexValidator; + +import bisq.asset.Asset; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Coin; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.*; +import static com.google.common.base.Preconditions.checkNotNull; + +@SuppressWarnings({"ConstantConditions", "StatementWithEmptyBody"}) +@Slf4j +public class ProposalDisplay { + private final GridPane gridPane; + private final BsqFormatter bsqFormatter; + private final DaoFacade daoFacade; + + // Nullable because if we are in result view mode (readonly) we don't need to set the input validator) + @Nullable + private final ChangeParamValidator changeParamValidator; + private final Navigation navigation; + private final Preferences preferences; + + @Nullable + private TextField proposalFeeTextField, comboBoxValueTextField, requiredBondForRoleTextField; + private TextField proposalTypeTextField, myVoteTextField, voteResultTextField; + public InputTextField nameTextField; + public InputTextField linkInputTextField; + @Nullable + public InputTextField requestedBsqTextField, paramValueTextField; + @Nullable + public ComboBox paramComboBox; + @Nullable + public ComboBox confiscateBondComboBox; + @Nullable + public ComboBox bondedRoleTypeComboBox; + @Nullable + public ComboBox assetComboBox; + + @Getter + private int gridRow; + private HyperlinkWithIcon linkHyperlinkWithIcon; + private HyperlinkWithIcon txHyperlinkWithIcon; + private int gridRowStartIndex; + private final List inputChangedListeners = new ArrayList<>(); + @Getter + private List inputControls = new ArrayList<>(); + @Getter + private List> comboBoxes = new ArrayList<>(); + private final ChangeListener focusOutListener; + private final ChangeListener inputListener; + private ChangeListener paramChangeListener; + private ChangeListener requiredBondForRoleListener; + private TitledGroupBg myVoteTitledGroup; + private VBox linkWithIconContainer, comboBoxValueContainer, myVoteBox, voteResultBox; + private int votingBoxRowSpan; + + private Optional navigateHandlerOptional = Optional.empty(); + + public ProposalDisplay(GridPane gridPane, + BsqFormatter bsqFormatter, + DaoFacade daoFacade, + @Nullable ChangeParamValidator changeParamValidator, + Navigation navigation, + @Nullable Preferences preferences) { + this.gridPane = gridPane; + this.bsqFormatter = bsqFormatter; + this.daoFacade = daoFacade; + this.changeParamValidator = changeParamValidator; + this.navigation = navigation; + this.preferences = preferences; + + // focusOutListener = observable -> inputChangedListeners.forEach(Runnable::run); + + focusOutListener = (observable, oldValue, newValue) -> { + if (oldValue && !newValue) + inputChangedListeners.forEach(Runnable::run); + }; + inputListener = (observable, oldValue, newValue) -> inputChangedListeners.forEach(Runnable::run); + } + + public void addInputChangedListener(Runnable listener) { + inputChangedListeners.add(listener); + } + + public void removeInputChangedListener(Runnable listener) { + inputChangedListeners.remove(listener); + } + + public void createAllFields(String title, int gridRowStartIndex, double top, ProposalType proposalType, + boolean isMakeProposalScreen) { + createAllFields(title, gridRowStartIndex, top, proposalType, isMakeProposalScreen, null); + } + + public void createAllFields(String title, int gridRowStartIndex, double top, ProposalType proposalType, + boolean isMakeProposalScreen, String titledGroupStyle) { + removeAllFields(); + this.gridRowStartIndex = gridRowStartIndex; + this.gridRow = gridRowStartIndex; + int titledGroupBgRowSpan = 5; + + switch (proposalType) { + case COMPENSATION_REQUEST: + case REIMBURSEMENT_REQUEST: + case CONFISCATE_BOND: + case REMOVE_ASSET: + break; + case CHANGE_PARAM: + case BONDED_ROLE: + titledGroupBgRowSpan = 6; + break; + case GENERIC: + titledGroupBgRowSpan = 4; + break; + } + + TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, gridRow, titledGroupBgRowSpan, title, top); + + if (titledGroupStyle != null) titledGroupBg.getStyleClass().add(titledGroupStyle); + + double proposalTypeTop; + + if (top == Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR) { + proposalTypeTop = Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE_WITHOUT_SEPARATOR; + } else if (top == Layout.GROUP_DISTANCE) { + proposalTypeTop = Layout.FIRST_ROW_AND_GROUP_DISTANCE; + } else if (top == 0) { + proposalTypeTop = Layout.FIRST_ROW_DISTANCE; + } else { + proposalTypeTop = Layout.FIRST_ROW_DISTANCE + top; + } + + proposalTypeTextField = addTopLabelTextField(gridPane, gridRow, + Res.get("dao.proposal.display.type"), proposalType.getDisplayName(), proposalTypeTop).second; + + nameTextField = addInputTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.name")); + if (isMakeProposalScreen) + nameTextField.setValidator(new InputValidator()); + inputControls.add(nameTextField); + + linkInputTextField = addInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.link")); + linkInputTextField.setPromptText(Res.get("dao.proposal.display.link.prompt")); + if (isMakeProposalScreen) { + RegexValidator validator = new RegexValidator(); + if (proposalType == ProposalType.COMPENSATION_REQUEST) { + validator.setPattern("https://bisq.network/dao-compensation/\\d+"); + linkInputTextField.setText("https://bisq.network/dao-compensation/#"); + } else if (proposalType == ProposalType.REIMBURSEMENT_REQUEST) { + validator.setPattern("https://bisq.network/dao-reimbursement/\\d+"); + linkInputTextField.setText("https://bisq.network/dao-reimbursement/#"); + } else { + validator.setPattern("https://bisq.network/dao-proposals/\\d+"); + linkInputTextField.setText("https://bisq.network/dao-proposals/#"); + } + linkInputTextField.setValidator(validator); + } + inputControls.add(linkInputTextField); + + Tuple3 tuple = addTopLabelHyperlinkWithIcon(gridPane, gridRow, + Res.get("dao.proposal.display.link"), "", "", 0); + linkHyperlinkWithIcon = tuple.second; + linkWithIconContainer = tuple.third; + // TODO HyperlinkWithIcon does not scale automatically (button base, -> make anchorpane as base) + linkHyperlinkWithIcon.prefWidthProperty().bind(nameTextField.widthProperty()); + + linkWithIconContainer.setVisible(false); + linkWithIconContainer.setManaged(false); + + if (!isMakeProposalScreen) { + Tuple3 uidTuple = addTopLabelHyperlinkWithIcon(gridPane, ++gridRow, + Res.get("dao.proposal.display.txId"), "", "", 0); + txHyperlinkWithIcon = uidTuple.second; + // TODO HyperlinkWithIcon does not scale automatically (button base, -> make anchorPane as base) + txHyperlinkWithIcon.prefWidthProperty().bind(nameTextField.widthProperty()); + } + + int comboBoxValueTextFieldIndex = -1; + switch (proposalType) { + case COMPENSATION_REQUEST: + case REIMBURSEMENT_REQUEST: + requestedBsqTextField = addInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.requestedBsq")); + checkNotNull(requestedBsqTextField, "requestedBsqTextField must not be null"); + inputControls.add(requestedBsqTextField); + + if (isMakeProposalScreen) { + BsqValidator bsqValidator = new BsqValidator(bsqFormatter); + if (proposalType == ProposalType.COMPENSATION_REQUEST) { + bsqValidator.setMinValue(daoFacade.getMinCompensationRequestAmount()); + bsqValidator.setMaxValue(daoFacade.getMaxCompensationRequestAmount()); + } else if (proposalType == ProposalType.REIMBURSEMENT_REQUEST) { + bsqValidator.setMinValue(daoFacade.getMinReimbursementRequestAmount()); + bsqValidator.setMaxValue(daoFacade.getMaxReimbursementRequestAmount()); + } + requestedBsqTextField.setValidator(bsqValidator); + } + break; + case CHANGE_PARAM: + checkNotNull(gridPane, "gridPane must not be null"); + paramComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, + Res.get("dao.proposal.display.paramComboBox.label")); + comboBoxValueTextFieldIndex = gridRow; + checkNotNull(paramComboBox, "paramComboBox must not be null"); + List list = Arrays.stream(Param.values()) + .filter(e -> e != Param.UNDEFINED && e != Param.PHASE_UNDEFINED) + .collect(Collectors.toList()); + paramComboBox.setItems(FXCollections.observableArrayList(list)); + paramComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Param param) { + return param != null ? param.getDisplayString() : ""; + } + + @Override + public Param fromString(String string) { + return null; + } + }); + comboBoxes.add(paramComboBox); + paramValueTextField = addInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.paramValue")); + + inputControls.add(paramValueTextField); + + paramChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + paramValueTextField.clear(); + String currentValue = bsqFormatter.formatParamValue(newValue, daoFacade.getParamValue(newValue)); + paramValueTextField.setPromptText(Res.get("dao.param.currentValue", currentValue)); + if (changeParamValidator != null && isMakeProposalScreen) { + ChangeParamInputValidator validator = new ChangeParamInputValidator(newValue, changeParamValidator); + paramValueTextField.setValidator(validator); + } + } + }; + paramComboBox.getSelectionModel().selectedItemProperty().addListener(paramChangeListener); + break; + case BONDED_ROLE: + bondedRoleTypeComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, + Res.get("dao.proposal.display.bondedRoleComboBox.label")); + comboBoxValueTextFieldIndex = gridRow; + checkNotNull(bondedRoleTypeComboBox, "bondedRoleTypeComboBox must not be null"); + List bondedRoleTypes = Arrays.stream(BondedRoleType.values()) + .filter(e -> e != BondedRoleType.UNDEFINED) + .collect(Collectors.toList()); + bondedRoleTypeComboBox.setItems(FXCollections.observableArrayList(bondedRoleTypes)); + bondedRoleTypeComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(BondedRoleType bondedRoleType) { + return bondedRoleType != null ? bondedRoleType.getDisplayString() : ""; + } + + @Override + public BondedRoleType fromString(String string) { + return null; + } + }); + comboBoxes.add(bondedRoleTypeComboBox); + requiredBondForRoleTextField = addTopLabelReadOnlyTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.requiredBondForRole.label")).second; + + requiredBondForRoleListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + requiredBondForRoleTextField.setText(bsqFormatter.formatCoinWithCode(Coin.valueOf(daoFacade.getRequiredBond(newValue)))); + } + }; + bondedRoleTypeComboBox.getSelectionModel().selectedItemProperty().addListener(requiredBondForRoleListener); + + break; + case CONFISCATE_BOND: + confiscateBondComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, + Res.get("dao.proposal.display.confiscateBondComboBox.label")); + comboBoxValueTextFieldIndex = gridRow; + checkNotNull(confiscateBondComboBox, "confiscateBondComboBox must not be null"); + + confiscateBondComboBox.setItems(FXCollections.observableArrayList(daoFacade.getAllActiveBonds())); + confiscateBondComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Bond bond) { + String details = " (" + Res.get("dao.bond.table.column.lockupTxId") + ": " + bond.getLockupTxId() + ")"; + if (bond instanceof BondedRole) { + return bond.getBondedAsset().getDisplayString() + details; + } else { + return Res.get("dao.bond.bondedReputation") + details; + } + } + + @Override + public Bond fromString(String string) { + return null; + } + }); + comboBoxes.add(confiscateBondComboBox); + break; + case GENERIC: + break; + case REMOVE_ASSET: + assetComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, + Res.get("dao.proposal.display.assetComboBox.label")); + comboBoxValueTextFieldIndex = gridRow; + checkNotNull(assetComboBox, "assetComboBox must not be null"); + List assetList = CurrencyUtil.getSortedAssetStream() + .filter(e -> !e.getTickerSymbol().equals("BSQ")) + .collect(Collectors.toList()); + assetComboBox.setItems(FXCollections.observableArrayList(assetList)); + assetComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Asset asset) { + return asset != null ? CurrencyUtil.getNameAndCode(asset.getTickerSymbol()) : ""; + } + + @Override + public Asset fromString(String string) { + return null; + } + }); + comboBoxes.add(assetComboBox); + break; + } + + if (comboBoxValueTextFieldIndex > -1) { + Tuple3 tuple3 = addTopLabelReadOnlyTextField(gridPane, comboBoxValueTextFieldIndex, + Res.get("dao.proposal.display.option")); + comboBoxValueTextField = tuple3.second; + comboBoxValueContainer = tuple3.third; + comboBoxValueContainer.setVisible(false); + comboBoxValueContainer.setManaged(false); + } + + if (isMakeProposalScreen) { + proposalFeeTextField = addTopLabelTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.proposalFee")).second; + proposalFeeTextField.setText(bsqFormatter.formatCoinWithCode(daoFacade.getProposalFee(daoFacade.getChainHeight()))); + } + + votingBoxRowSpan = 4; + + myVoteTitledGroup = addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("dao.proposal.myVote.title"), Layout.COMPACT_FIRST_ROW_DISTANCE); + + Tuple3 tuple3 = addTopLabelTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.myVote"), Layout.COMPACT_FIRST_ROW_DISTANCE); + + myVoteBox = tuple3.third; + setMyVoteBoxVisibility(false); + + myVoteTextField = tuple3.second; + + tuple3 = addTopLabelReadOnlyTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.voteResult")); + + voteResultBox = tuple3.third; + voteResultBox.setVisible(false); + voteResultBox.setManaged(false); + + voteResultTextField = tuple3.second; + + addListeners(); + } + + public void applyBallot(@Nullable Ballot ballot) { + String myVote = Res.get("dao.proposal.display.myVote.ignored"); + boolean isNotNull = ballot != null; + Vote vote = isNotNull ? ballot.getVote() : null; + if (vote != null) { + myVote = vote.isAccepted() ? Res.get("dao.proposal.display.myVote.accepted") : + Res.get("dao.proposal.display.myVote.rejected"); + } + myVoteTextField.setText(myVote); + + setMyVoteBoxVisibility(isNotNull); + } + + public void applyEvaluatedProposal(@Nullable EvaluatedProposal evaluatedProposal) { + + boolean isEvaluatedProposalNotNull = evaluatedProposal != null; + if (isEvaluatedProposalNotNull) { + String result = evaluatedProposal.isAccepted() ? Res.get("dao.proposal.voteResult.success") : + Res.get("dao.proposal.voteResult.failed"); + ProposalVoteResult proposalVoteResult = evaluatedProposal.getProposalVoteResult(); + String threshold = (proposalVoteResult.getThreshold() / 100D) + "%"; + String requiredThreshold = (daoFacade.getRequiredThreshold(evaluatedProposal.getProposal()) * 100D) + "%"; + String quorum = bsqFormatter.formatCoinWithCode(Coin.valueOf(proposalVoteResult.getQuorum())); + String requiredQuorum = bsqFormatter.formatCoinWithCode(daoFacade.getRequiredQuorum(evaluatedProposal.getProposal())); + String summary = Res.get("dao.proposal.voteResult.summary", result, + threshold, requiredThreshold, quorum, requiredQuorum); + voteResultTextField.setText(summary); + } + voteResultBox.setVisible(isEvaluatedProposalNotNull); + voteResultBox.setManaged(isEvaluatedProposalNotNull); + } + + public void applyBallotAndVoteWeight(@Nullable Ballot ballot, long merit, long stake) { + applyBallotAndVoteWeight(ballot, merit, stake, true); + } + + public void applyBallotAndVoteWeight(@Nullable Ballot ballot, long merit, long stake, boolean ballotIncluded) { + boolean ballotIsNotNull = ballot != null; + boolean hasVoted = stake > 0; + if (hasVoted) { + String myVote = Res.get("dao.proposal.display.myVote.ignored"); + Vote vote = ballotIsNotNull ? ballot.getVote() : null; + if (vote != null) { + myVote = vote.isAccepted() ? Res.get("dao.proposal.display.myVote.accepted") : + Res.get("dao.proposal.display.myVote.rejected"); + } + + String voteIncluded = ballotIncluded ? "" : " - " + Res.get("dao.proposal.display.myVote.unCounted"); + String meritString = bsqFormatter.formatCoinWithCode(Coin.valueOf(merit)); + String stakeString = bsqFormatter.formatCoinWithCode(Coin.valueOf(stake)); + String weight = bsqFormatter.formatCoinWithCode(Coin.valueOf(merit + stake)); + String myVoteSummary = Res.get("dao.proposal.myVote.summary", myVote, + weight, meritString, stakeString, voteIncluded); + myVoteTextField.setText(myVoteSummary); + + GridPane.setRowSpan(myVoteTitledGroup, votingBoxRowSpan - 1); + } + + boolean show = ballotIsNotNull && hasVoted; + setMyVoteBoxVisibility(show); + } + + public void setIsVoteIncludedInResult(boolean isVoteIncludedInResult) { + if (!isVoteIncludedInResult && myVoteTextField != null && !myVoteTextField.getText().isEmpty()) { + String text = myVoteTextField.getText(); + myVoteTextField.setText(Res.get("dao.proposal.myVote.invalid") + " - " + text); + myVoteTextField.getStyleClass().add("error-text"); + } + } + + public void applyProposalPayload(Proposal proposal) { + proposalTypeTextField.setText(proposal.getType().getDisplayName()); + nameTextField.setText(proposal.getName()); + linkInputTextField.setVisible(false); + linkInputTextField.setManaged(false); + if (linkWithIconContainer != null) { + linkWithIconContainer.setVisible(true); + linkWithIconContainer.setManaged(true); + linkHyperlinkWithIcon.setText(proposal.getLink()); + linkHyperlinkWithIcon.setOnAction(e -> GUIUtil.openWebPage(proposal.getLink())); + } + + if (txHyperlinkWithIcon != null) { + txHyperlinkWithIcon.setText(proposal.getTxId()); + txHyperlinkWithIcon.setOnAction(e -> + GUIUtil.openTxInBsqBlockExplorer(proposal.getTxId(), preferences)); + } + + if (proposal instanceof CompensationProposal) { + CompensationProposal compensationProposal = (CompensationProposal) proposal; + checkNotNull(requestedBsqTextField, "requestedBsqTextField must not be null"); + requestedBsqTextField.setText(bsqFormatter.formatCoinWithCode(compensationProposal.getRequestedBsq())); + } else if (proposal instanceof ReimbursementProposal) { + ReimbursementProposal reimbursementProposal = (ReimbursementProposal) proposal; + checkNotNull(requestedBsqTextField, "requestedBsqTextField must not be null"); + requestedBsqTextField.setText(bsqFormatter.formatCoinWithCode(reimbursementProposal.getRequestedBsq())); + } else if (proposal instanceof ChangeParamProposal) { + ChangeParamProposal changeParamProposal = (ChangeParamProposal) proposal; + checkNotNull(paramComboBox, "paramComboBox must not be null"); + paramComboBox.getSelectionModel().select(changeParamProposal.getParam()); + comboBoxValueTextField.setText(paramComboBox.getConverter().toString(changeParamProposal.getParam())); + checkNotNull(paramValueTextField, "paramValueTextField must not be null"); + paramValueTextField.setText(bsqFormatter.formatParamValue(changeParamProposal.getParam(), changeParamProposal.getParamValue())); + String currentValue = bsqFormatter.formatParamValue(changeParamProposal.getParam(), + daoFacade.getParamValue(changeParamProposal.getParam())); + int height = daoFacade.getTx(changeParamProposal.getTxId()) + .map(BaseTx::getBlockHeight) + .orElse(daoFacade.getGenesisBlockHeight()); + String valueAtProposal = bsqFormatter.formatParamValue(changeParamProposal.getParam(), + daoFacade.getParamValue(changeParamProposal.getParam(), height)); + paramValueTextField.setPromptText(Res.get("dao.param.currentAndPastValue", currentValue, valueAtProposal)); + } else if (proposal instanceof RoleProposal) { + RoleProposal roleProposal = (RoleProposal) proposal; + checkNotNull(bondedRoleTypeComboBox, "bondedRoleComboBox must not be null"); + Role role = roleProposal.getRole(); + bondedRoleTypeComboBox.getSelectionModel().select(role.getBondedRoleType()); + comboBoxValueTextField.setText(bondedRoleTypeComboBox.getConverter().toString(role.getBondedRoleType())); + requiredBondForRoleTextField.setText(bsqFormatter.formatCoin(Coin.valueOf(daoFacade.getRequiredBond(roleProposal)))); + // TODO maybe show also unlock time? + } else if (proposal instanceof ConfiscateBondProposal) { + ConfiscateBondProposal confiscateBondProposal = (ConfiscateBondProposal) proposal; + checkNotNull(confiscateBondComboBox, "confiscateBondComboBox must not be null"); + daoFacade.getBondByLockupTxId(confiscateBondProposal.getLockupTxId()) + .ifPresent(bond -> { + confiscateBondComboBox.getSelectionModel().select(bond); + comboBoxValueTextField.setText(confiscateBondComboBox.getConverter().toString(bond)); + comboBoxValueTextField.setOnMouseClicked(e -> { + navigateHandlerOptional.ifPresent(Runnable::run); + navigation.navigateToWithData(bond, MainView.class, DaoView.class, BondingView.class, + BondsView.class); + }); + + comboBoxValueTextField.getStyleClass().addAll("hyperlink", "force-underline", "show-hand"); + }); + } else if (proposal instanceof GenericProposal) { + // do nothing + } else if (proposal instanceof RemoveAssetProposal) { + RemoveAssetProposal removeAssetProposal = (RemoveAssetProposal) proposal; + checkNotNull(assetComboBox, "assetComboBox must not be null"); + CurrencyUtil.findAsset(removeAssetProposal.getTickerSymbol(), BaseCurrencyNetwork.BTC_MAINNET) + .ifPresent(asset -> { + assetComboBox.getSelectionModel().select(asset); + comboBoxValueTextField.setText(assetComboBox.getConverter().toString(asset)); + }); + } + int chainHeight = daoFacade.getTx(proposal.getTxId()).map(Tx::getBlockHeight).orElse(daoFacade.getChainHeight()); + if (proposalFeeTextField != null) + proposalFeeTextField.setText(bsqFormatter.formatCoinWithCode(daoFacade.getProposalFee(chainHeight))); + } + + private void addListeners() { + inputControls.stream() + .filter(Objects::nonNull).forEach(inputControl -> { + inputControl.textProperty().addListener(inputListener); + inputControl.focusedProperty().addListener(focusOutListener); + }); + comboBoxes.stream() + .filter(Objects::nonNull) + .forEach(comboBox -> comboBox.getSelectionModel().selectedItemProperty().addListener(inputListener)); + } + + public void removeListeners() { + inputControls.stream() + .filter(Objects::nonNull).forEach(inputControl -> { + inputControl.textProperty().removeListener(inputListener); + inputControl.focusedProperty().removeListener(focusOutListener); + }); + comboBoxes.stream() + .filter(Objects::nonNull) + .forEach(comboBox -> comboBox.getSelectionModel().selectedItemProperty().removeListener(inputListener)); + + if (paramComboBox != null && paramChangeListener != null) + paramComboBox.getSelectionModel().selectedItemProperty().removeListener(paramChangeListener); + + if (bondedRoleTypeComboBox != null && requiredBondForRoleListener != null) + bondedRoleTypeComboBox.getSelectionModel().selectedItemProperty().removeListener(requiredBondForRoleListener); + } + + public void clearForm() { + inputControls.stream().filter(Objects::nonNull).forEach(TextInputControl::clear); + + if (linkHyperlinkWithIcon != null) + linkHyperlinkWithIcon.clear(); + + comboBoxes.stream().filter(Objects::nonNull).forEach(comboBox -> comboBox.getSelectionModel().clearSelection()); + } + + public void setEditable(boolean isEditable) { + inputControls.stream().filter(Objects::nonNull).forEach(e -> e.setEditable(isEditable)); + comboBoxes.stream().filter(Objects::nonNull).forEach(comboBox -> { + comboBox.setVisible(isEditable); + comboBox.setManaged(isEditable); + + if (comboBoxValueContainer != null) { + comboBoxValueContainer.setVisible(!isEditable); + comboBoxValueContainer.setManaged(!isEditable); + } + }); + + linkInputTextField.setVisible(true); + linkInputTextField.setManaged(true); + + if (linkWithIconContainer != null) { + linkWithIconContainer.setVisible(false); + linkWithIconContainer.setManaged(false); + linkHyperlinkWithIcon.setOnAction(null); + } + } + + public void removeAllFields() { + if (gridRow > 0) { + clearForm(); + GUIUtil.removeChildrenFromGridPaneRows(gridPane, gridRowStartIndex, gridRow); + gridRow = gridRowStartIndex; + } + + if (linkHyperlinkWithIcon != null) + linkHyperlinkWithIcon.prefWidthProperty().unbind(); + + inputControls.clear(); + comboBoxes.clear(); + } + + public void onNavigate(Runnable navigateHandler) { + navigateHandlerOptional = Optional.of(navigateHandler); + } + + public int incrementAndGetGridRow() { + return ++gridRow; + } + + @SuppressWarnings("Duplicates") + public ScrollPane getView() { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + + AnchorPane anchorPane = new AnchorPane(); + scrollPane.setContent(anchorPane); + + gridPane.setHgap(5); + gridPane.setVgap(5); + + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setPercentWidth(100); + + gridPane.getColumnConstraints().addAll(columnConstraints1); + + AnchorPane.setBottomAnchor(gridPane, 10d); + AnchorPane.setRightAnchor(gridPane, 10d); + AnchorPane.setLeftAnchor(gridPane, 10d); + AnchorPane.setTopAnchor(gridPane, 10d); + anchorPane.getChildren().add(gridPane); + + return scrollPane; + } + + private void setMyVoteBoxVisibility(boolean visibility) { + myVoteTitledGroup.setVisible(visibility); + myVoteTitledGroup.setManaged(visibility); + myVoteBox.setVisible(visibility); + myVoteBox.setManaged(visibility); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.fxml new file mode 100644 index 0000000000..a71c978980 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java new file mode 100644 index 0000000000..d6926cb24d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java @@ -0,0 +1,130 @@ +/* + * 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 bisq.desktop.main.dao.governance.dashboard; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.presentation.DaoUtil; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; + +// We use here ChainHeightListener because we are interested in period changes not in the result of a completed +// block. The event from the ChainHeightListener is sent before parsing starts. +// The event from the ChainHeightListener would notify after parsing a new block. +@FxmlView +public class GovernanceDashboardView extends ActivatableView implements DaoStateListener { + private final DaoFacade daoFacade; + private final PeriodService periodService; + private final PhasesView phasesView; + + private int gridRow = 0; + private TextField currentPhaseTextField, currentBlockHeightTextField, proposalTextField, blindVoteTextField, voteRevealTextField, voteResultTextField; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public GovernanceDashboardView(DaoFacade daoFacade, PeriodService periodService, PhasesView phasesView) { + this.daoFacade = daoFacade; + this.periodService = periodService; + this.phasesView = phasesView; + } + + @Override + public void initialize() { + gridRow = phasesView.addGroup(root, gridRow); + + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 6, Res.get("dao.cycle.overview.headline"), Layout.GROUP_DISTANCE); + titledGroupBg.getStyleClass().add("last"); + currentBlockHeightTextField = addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.cycle.currentBlockHeight"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + currentPhaseTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.cycle.currentPhase")).second; + proposalTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.cycle.proposal")).second; + blindVoteTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.cycle.blindVote")).second; + voteRevealTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.cycle.voteReveal")).second; + voteResultTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.cycle.voteResult")).second; + } + + @Override + protected void activate() { + super.activate(); + + phasesView.activate(); + + daoFacade.addBsqStateListener(this); + + applyData(daoFacade.getChainHeight()); + } + + @Override + protected void deactivate() { + super.deactivate(); + + phasesView.deactivate(); + + daoFacade.removeBsqStateListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + applyData(block.getHeight()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyData(int height) { + currentBlockHeightTextField.setText(String.valueOf(daoFacade.getChainHeight())); + DaoPhase.Phase phase = daoFacade.phaseProperty().get(); + // If we are in last block of proposal, blindVote or voteReveal phase we show following break. + if (!periodService.isInPhaseButNotLastBlock(phase) && + (phase == DaoPhase.Phase.PROPOSAL || phase == DaoPhase.Phase.BLIND_VOTE || phase == DaoPhase.Phase.VOTE_REVEAL)) { + phase = periodService.getPhaseForHeight(height + 1); + } + currentPhaseTextField.setText(Res.get("dao.phase." + phase.name())); + proposalTextField.setText(DaoUtil.getPhaseDuration(height, DaoPhase.Phase.PROPOSAL, daoFacade)); + blindVoteTextField.setText(DaoUtil.getPhaseDuration(height, DaoPhase.Phase.BLIND_VOTE, daoFacade)); + voteRevealTextField.setText(DaoUtil.getPhaseDuration(height, DaoPhase.Phase.VOTE_REVEAL, daoFacade)); + voteResultTextField.setText(DaoUtil.getPhaseDuration(height, DaoPhase.Phase.RESULT, daoFacade)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml new file mode 100644 index 0000000000..d5d6c8d287 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java new file mode 100644 index 0000000000..5b1a467968 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -0,0 +1,525 @@ +/* + * 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 bisq.desktop.main.dao.governance.make; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.main.dao.governance.ProposalDisplay; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; + +import bisq.core.btc.exceptions.InsufficientBsqException; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.bond.Bond; +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.governance.proposal.IssuanceProposal; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.governance.proposal.ProposalValidationException; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.governance.proposal.param.ChangeParamValidator; +import bisq.core.dao.governance.voteresult.VoteResultException; +import bisq.core.dao.presentation.DaoUtil; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.BondedRoleType; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Role; +import bisq.core.locale.Res; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.asset.Asset; + +import bisq.network.p2p.P2PService; + +import bisq.common.app.DevEnv; +import bisq.common.util.Tuple3; +import bisq.common.util.Tuple4; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; +import static bisq.desktop.util.FormBuilder.addComboBox; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; +import static com.google.common.base.Preconditions.checkNotNull; + +@FxmlView +public class MakeProposalView extends ActivatableView implements DaoStateListener { + private final DaoFacade daoFacade; + private final WalletsSetup walletsSetup; + private final P2PService p2PService; + private final PhasesView phasesView; + private final ChangeParamValidator changeParamValidator; + private final CoinFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + private final Navigation navigation; + private final BsqWalletService bsqWalletService; + + @Nullable + private ProposalDisplay proposalDisplay; + private Button makeProposalButton; + private ComboBox proposalTypeComboBox; + private ChangeListener proposalTypeChangeListener; + private TextField nextProposalTextField; + private TitledGroupBg proposalTitledGroup; + private VBox nextProposalBox; + private BusyAnimation busyAnimation; + private Label busyLabel; + + private final BooleanProperty isProposalPhase = new SimpleBooleanProperty(false); + private final StringProperty proposalGroupTitle = new SimpleStringProperty(Res.get("dao.proposal.create.phase.inactive")); + + @Nullable + private ProposalType selectedProposalType; + private int gridRow; + private int alwaysVisibleGridRowIndex; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private MakeProposalView(DaoFacade daoFacade, + WalletsSetup walletsSetup, + P2PService p2PService, + BsqWalletService bsqWalletService, + PhasesView phasesView, + ChangeParamValidator changeParamValidator, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + BsqFormatter bsqFormatter, + Navigation navigation) { + this.daoFacade = daoFacade; + this.walletsSetup = walletsSetup; + this.p2PService = p2PService; + this.bsqWalletService = bsqWalletService; + this.phasesView = phasesView; + this.changeParamValidator = changeParamValidator; + this.btcFormatter = btcFormatter; + this.bsqFormatter = bsqFormatter; + this.navigation = navigation; + } + + @Override + public void initialize() { + gridRow = phasesView.addGroup(root, gridRow); + + proposalTitledGroup = addTitledGroupBg(root, ++gridRow, 2, proposalGroupTitle.get(), Layout.GROUP_DISTANCE); + proposalTitledGroup.getStyleClass().add("last"); + final Tuple3 nextProposalPhaseTuple = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.cycle.proposal.next"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + nextProposalBox = nextProposalPhaseTuple.third; + nextProposalTextField = nextProposalPhaseTuple.second; + proposalTypeComboBox = addComboBox(root, gridRow, + Res.get("dao.proposal.create.proposalType"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + proposalTypeComboBox.setMaxWidth(300); + proposalTypeComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(ProposalType proposalType) { + return proposalType.getDisplayName(); + } + + @Override + public ProposalType fromString(String string) { + return null; + } + }); + proposalTypeChangeListener = (observable, oldValue, newValue) -> { + selectedProposalType = newValue; + removeProposalDisplay(); + addProposalDisplay(); + }; + alwaysVisibleGridRowIndex = gridRow + 1; + + List proposalTypes = Arrays.stream(ProposalType.values()) + .filter(e -> e != ProposalType.UNDEFINED) + .collect(Collectors.toList()); + proposalTypeComboBox.setItems(FXCollections.observableArrayList(proposalTypes)); + } + + @Override + protected void activate() { + addBindings(); + + phasesView.activate(); + + daoFacade.addBsqStateListener(this); + + proposalTypeComboBox.getSelectionModel().selectedItemProperty().addListener(proposalTypeChangeListener); + if (makeProposalButton != null) + setMakeProposalButtonHandler(); + + Optional blockAtChainHeight = daoFacade.getBlockAtChainHeight(); + + blockAtChainHeight.ifPresent(this::onParseBlockCompleteAfterBatchProcessing); + } + + @Override + protected void deactivate() { + removeBindings(); + + phasesView.deactivate(); + + daoFacade.removeBsqStateListener(this); + + proposalTypeComboBox.getSelectionModel().selectedItemProperty().removeListener(proposalTypeChangeListener); + if (makeProposalButton != null) + makeProposalButton.setOnAction(null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings, Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBindings() { + proposalTypeComboBox.managedProperty().bind(isProposalPhase); + proposalTypeComboBox.visibleProperty().bind(isProposalPhase); + nextProposalBox.managedProperty().bind(isProposalPhase.not()); + nextProposalBox.visibleProperty().bind(isProposalPhase.not()); + proposalTitledGroup.textProperty().bind(proposalGroupTitle); + } + + private void removeBindings() { + proposalTypeComboBox.managedProperty().unbind(); + proposalTypeComboBox.visibleProperty().unbind(); + nextProposalBox.managedProperty().unbind(); + nextProposalBox.visibleProperty().unbind(); + proposalTitledGroup.textProperty().unbind(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + isProposalPhase.set(daoFacade.isInPhaseButNotLastBlock(DaoPhase.Phase.PROPOSAL)); + if (isProposalPhase.get()) { + proposalGroupTitle.set(Res.get("dao.proposal.create.selectProposalType")); + } else { + proposalGroupTitle.set(Res.get("dao.proposal.create.phase.inactive")); + proposalTypeComboBox.getSelectionModel().clearSelection(); + updateTimeUntilNextProposalPhase(block.getHeight()); + } + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateTimeUntilNextProposalPhase(int height) { + nextProposalTextField.setText(DaoUtil.getNextPhaseDuration(height, DaoPhase.Phase.PROPOSAL, daoFacade)); + } + + private void publishMyProposal(ProposalType type) { + try { + ProposalWithTransaction proposalWithTransaction = getProposalWithTransaction(type); + if (proposalWithTransaction == null) + return; + + Proposal proposal = proposalWithTransaction.getProposal(); + Transaction transaction = proposalWithTransaction.getTransaction(); + Coin miningFee = transaction.getFee(); + int txVsize = transaction.getVsize(); + Coin fee = daoFacade.getProposalFee(daoFacade.getChainHeight()); + + if (type.equals(ProposalType.BONDED_ROLE)) { + checkNotNull(proposalDisplay, "proposalDisplay must not be null"); + checkNotNull(proposalDisplay.bondedRoleTypeComboBox, "proposalDisplay.bondedRoleTypeComboBox must not be null"); + BondedRoleType bondedRoleType = proposalDisplay.bondedRoleTypeComboBox.getSelectionModel().getSelectedItem(); + long requiredBond = daoFacade.getRequiredBond(bondedRoleType); + long availableBalance = bsqWalletService.getAvailableConfirmedBalance().value; + + if (requiredBond > availableBalance) { + long missing = requiredBond - availableBalance; + new Popup().warning(Res.get("dao.proposal.create.missingBsqFundsForBond", + bsqFormatter.formatCoinWithCode(missing))) + .actionButtonText(Res.get("dao.proposal.create.publish")) + .onAction(() -> showFeeInfoAndPublishMyProposal(proposal, transaction, miningFee, txVsize, fee)) + .show(); + } else { + showFeeInfoAndPublishMyProposal(proposal, transaction, miningFee, txVsize, fee); + } + } else { + showFeeInfoAndPublishMyProposal(proposal, transaction, miningFee, txVsize, fee); + } + } catch (InsufficientMoneyException e) { + if (e instanceof InsufficientBsqException) { + new Popup().warning(Res.get("dao.proposal.create.missingBsqFunds", + bsqFormatter.formatCoinWithCode(e.missing))).show(); + } else { + if (type.equals(ProposalType.COMPENSATION_REQUEST) || type.equals(ProposalType.REIMBURSEMENT_REQUEST)) { + new Popup().warning(Res.get("dao.proposal.create.missingIssuanceFunds", + 100, + btcFormatter.formatCoinWithCode(e.missing))).show(); + } else { + new Popup().warning(Res.get("dao.proposal.create.missingMinerFeeFunds", + btcFormatter.formatCoinWithCode(e.missing))).show(); + } + } + } catch (ProposalValidationException e) { + String message; + if (e.getMinRequestAmount() != null) { + message = Res.get("validation.bsq.amountBelowMinAmount", + bsqFormatter.formatCoinWithCode(e.getMinRequestAmount())); + } else { + message = e.getMessage(); + } + new Popup().warning(message).show(); + } catch (IllegalArgumentException e) { + log.error(e.toString()); + e.printStackTrace(); + new Popup().warning(e.getMessage()).show(); + } catch (Throwable e) { + log.error(e.toString()); + e.printStackTrace(); + new Popup().warning(e.toString()).show(); + } + } + + private void showFeeInfoAndPublishMyProposal(Proposal proposal, Transaction transaction, Coin miningFee, int txVsize, Coin fee) { + if (!DevEnv.isDevMode()) { + Coin btcForIssuance = null; + + if (proposal instanceof IssuanceProposal) btcForIssuance = ((IssuanceProposal) proposal).getRequestedBsq(); + + GUIUtil.showBsqFeeInfoPopup(fee, miningFee, btcForIssuance, txVsize, bsqFormatter, btcFormatter, + Res.get("dao.proposal"), () -> doPublishMyProposal(proposal, transaction)); + } else { + doPublishMyProposal(proposal, transaction); + } + } + + private void doPublishMyProposal(Proposal proposal, Transaction transaction) { + //TODO it still happens that the user can click twice. Not clear why that can happen. Maybe we get updateButtonState + // called in between which re-enables the button? + makeProposalButton.setDisable(true); + busyLabel.setVisible(true); + busyAnimation.play(); + + daoFacade.publishMyProposal(proposal, + transaction, + () -> { + if (!DevEnv.isDevMode()) + new Popup().feedback(Res.get("dao.tx.published.success")).show(); + + if (proposalDisplay != null) + proposalDisplay.clearForm(); + proposalTypeComboBox.getSelectionModel().clearSelection(); + busyAnimation.stop(); + busyLabel.setVisible(false); + makeProposalButton.setDisable(false); + }, + errorMessage -> { + new Popup().warning(errorMessage).show(); + busyAnimation.stop(); + busyLabel.setVisible(false); + makeProposalButton.setDisable(false); + }); + + } + + @Nullable + private ProposalWithTransaction getProposalWithTransaction(ProposalType proposalType) + throws InsufficientMoneyException, ProposalValidationException, TxException, VoteResultException.ValidationException { + + checkNotNull(proposalDisplay, "proposalDisplay must not be null"); + + String link = proposalDisplay.linkInputTextField.getText(); + String name = proposalDisplay.nameTextField.getText(); + switch (proposalType) { + case COMPENSATION_REQUEST: + checkNotNull(proposalDisplay.requestedBsqTextField, + "proposalDisplay.requestedBsqTextField must not be null"); + return daoFacade.getCompensationProposalWithTransaction(name, + link, + ParsingUtils.parseToCoin(proposalDisplay.requestedBsqTextField.getText(), bsqFormatter)); + case REIMBURSEMENT_REQUEST: + checkNotNull(proposalDisplay.requestedBsqTextField, + "proposalDisplay.requestedBsqTextField must not be null"); + return daoFacade.getReimbursementProposalWithTransaction(name, + link, + ParsingUtils.parseToCoin(proposalDisplay.requestedBsqTextField.getText(), bsqFormatter)); + case CHANGE_PARAM: + checkNotNull(proposalDisplay.paramComboBox, + "proposalDisplay.paramComboBox must not be null"); + checkNotNull(proposalDisplay.paramValueTextField, + "proposalDisplay.paramValueTextField must not be null"); + Param selectedParam = proposalDisplay.paramComboBox.getSelectionModel().getSelectedItem(); + if (selectedParam == null) + throw new ProposalValidationException("selectedParam is null"); + String paramValueAsString = proposalDisplay.paramValueTextField.getText(); + if (paramValueAsString == null || paramValueAsString.isEmpty()) + throw new ProposalValidationException("paramValue is null or empty"); + + try { + String paramValue = bsqFormatter.parseParamValueToString(selectedParam, paramValueAsString); + proposalDisplay.paramValueTextField.setText(paramValue); + log.info("Change param: paramValue={}, paramValueAsString={}", paramValue, paramValueAsString); + + changeParamValidator.validateParamValue(selectedParam, paramValue); + return daoFacade.getParamProposalWithTransaction(name, + link, + selectedParam, + paramValue); + } catch (Throwable e) { + new Popup().warning(e.getMessage()).show(); + return null; + } + case BONDED_ROLE: + checkNotNull(proposalDisplay.bondedRoleTypeComboBox, + "proposalDisplay.bondedRoleTypeComboBox must not be null"); + Role role = new Role(name, + link, + proposalDisplay.bondedRoleTypeComboBox.getSelectionModel().getSelectedItem()); + return daoFacade.getBondedRoleProposalWithTransaction(role); + case CONFISCATE_BOND: + checkNotNull(proposalDisplay.confiscateBondComboBox, + "proposalDisplay.confiscateBondComboBox must not be null"); + Bond bond = proposalDisplay.confiscateBondComboBox.getSelectionModel().getSelectedItem(); + + if (!bond.isActive()) + throw new VoteResultException.ValidationException("Bond is not locked and can't be confiscated"); + + return daoFacade.getConfiscateBondProposalWithTransaction(name, link, bond.getLockupTxId()); + case GENERIC: + return daoFacade.getGenericProposalWithTransaction(name, link); + case REMOVE_ASSET: + checkNotNull(proposalDisplay.assetComboBox, + "proposalDisplay.assetComboBox must not be null"); + Asset asset = proposalDisplay.assetComboBox.getSelectionModel().getSelectedItem(); + return daoFacade.getRemoveAssetProposalWithTransaction(name, link, asset); + default: + final String msg = "Undefined ProposalType " + selectedProposalType; + log.error(msg); + throw new RuntimeException(msg); + } + } + + private void addProposalDisplay() { + if (selectedProposalType != null) { + proposalDisplay = new ProposalDisplay(root, bsqFormatter, daoFacade, changeParamValidator, navigation, null); + + proposalDisplay.createAllFields(Res.get("dao.proposal.create.new"), alwaysVisibleGridRowIndex, Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR, + selectedProposalType, true); + + final Tuple4 makeProposalTuple = addButtonBusyAnimationLabelAfterGroup(root, + proposalDisplay.getGridRow(), 0, Res.get("dao.proposal.create.button")); + makeProposalButton = makeProposalTuple.first; + + busyAnimation = makeProposalTuple.second; + busyLabel = makeProposalTuple.third; + busyLabel.setVisible(false); + busyLabel.setText(Res.get("dao.proposal.create.publishing")); + + setMakeProposalButtonHandler(); + proposalDisplay.addInputChangedListener(this::updateButtonState); + updateButtonState(); + } + } + + private void removeProposalDisplay() { + if (proposalDisplay != null) { + proposalDisplay.removeAllFields(); + GUIUtil.removeChildrenFromGridPaneRows(root, alwaysVisibleGridRowIndex, proposalDisplay.getGridRow()); + proposalDisplay.removeInputChangedListener(this::updateButtonState); + proposalDisplay.removeListeners(); + proposalDisplay = null; + } + } + + private void setMakeProposalButtonHandler() { + makeProposalButton.setOnAction(event -> { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + publishMyProposal(selectedProposalType); + } + }); + } + + private void updateButtonState() { + AtomicBoolean inputsValid = new AtomicBoolean(true); + if (proposalDisplay != null) { + proposalDisplay.getInputControls().stream() + .filter(Objects::nonNull).forEach(e -> { + if (e instanceof InputTextField) { + InputTextField inputTextField = (InputTextField) e; + inputsValid.set(inputsValid.get() && + inputTextField.getValidator() != null && + inputTextField.getValidator().validate(e.getText()).isValid); + } + }); + proposalDisplay.getComboBoxes().stream() + .filter(Objects::nonNull).forEach(comboBox -> inputsValid.set(inputsValid.get() && + comboBox.getSelectionModel().getSelectedItem() != null)); + + InputTextField linkInputTextField = proposalDisplay.linkInputTextField; + inputsValid.set(inputsValid.get() && + linkInputTextField.getValidator().validate(linkInputTextField.getText()).isValid); + } + + makeProposalButton.setDisable(!inputsValid.get()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java new file mode 100644 index 0000000000..78cb1a0a73 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java @@ -0,0 +1,187 @@ +/* + * 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 bisq.desktop.main.dao.governance.proposals; + +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Vote; +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXButton; + +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; + +import javafx.beans.value.ChangeListener; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@ToString +@Slf4j +@EqualsAndHashCode +//TODO merge with vote result ProposalListItem +public class ProposalsListItem { + + enum IconButtonType { + REMOVE_PROPOSAL(Res.get("dao.proposal.table.icon.tooltip.removeProposal")), + ACCEPT(Res.get("dao.proposal.display.myVote.accepted")), + REJECT(Res.get("dao.proposal.display.myVote.rejected")), + IGNORE(Res.get("dao.proposal.display.myVote.ignored")); + @Getter + private String title; + + IconButtonType(String title) { + this.title = title; + } + } + + @Getter + private final Proposal proposal; + private final DaoFacade daoFacade; + private final BsqFormatter bsqFormatter; + + @Getter + @Nullable + private Ballot ballot; + + @Getter + private JFXButton iconButton; + + private ChangeListener phaseChangeListener; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + ProposalsListItem(Proposal proposal, + DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.proposal = proposal; + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + init(); + } + + ProposalsListItem(Ballot ballot, + DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.ballot = ballot; + this.proposal = ballot.getProposal(); + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + init(); + } + + private void init() { + phaseChangeListener = (observable, oldValue, newValue) -> onPhaseChanged(newValue); + + daoFacade.phaseProperty().addListener(phaseChangeListener); + + onPhaseChanged(daoFacade.phaseProperty().get()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void cleanup() { + daoFacade.phaseProperty().removeListener(phaseChangeListener); + } + + public void onPhaseChanged(DaoPhase.Phase phase) { + //noinspection IfCanBeSwitch + Label icon; + if (phase == DaoPhase.Phase.PROPOSAL) { + icon = FormBuilder.getIcon(AwesomeIcon.TRASH); + + icon.getStyleClass().addAll("icon", "dao-remove-proposal-icon"); + iconButton = new JFXButton("", icon); + boolean isMyProposal = daoFacade.isMyProposal(proposal); + if (isMyProposal) { + iconButton.setUserData(IconButtonType.REMOVE_PROPOSAL); + } + iconButton.setVisible(isMyProposal); + iconButton.setManaged(isMyProposal); + iconButton.getStyleClass().add("hidden-icon-button"); + iconButton.setTooltip(new Tooltip(Res.get("dao.proposal.table.icon.tooltip.removeProposal"))); + } else if (iconButton != null) { + iconButton.setVisible(true); + iconButton.setManaged(true); + } + + // ballot + if (ballot != null) { + Vote vote = ballot.getVote(); + + if (vote != null) { + if ((vote).isAccepted()) { + icon = FormBuilder.getIcon(AwesomeIcon.THUMBS_UP); + icon.getStyleClass().addAll("icon", "dao-accepted-icon"); + iconButton = new JFXButton("", icon); + iconButton.setUserData(IconButtonType.ACCEPT); + } else { + icon = FormBuilder.getIcon(AwesomeIcon.THUMBS_DOWN); + icon.getStyleClass().addAll("icon", "dao-rejected-icon"); + iconButton = new JFXButton("", icon); + iconButton.setUserData(IconButtonType.REJECT); + } + } else { + icon = FormBuilder.getIcon(AwesomeIcon.MINUS); + icon.getStyleClass().addAll("icon", "dao-ignored-icon"); + iconButton = new JFXButton("", icon); + iconButton.setUserData(IconButtonType.IGNORE); + } + iconButton.setTooltip(new Tooltip(Res.get("dao.proposal.table.icon.tooltip.changeVote", + ((IconButtonType) iconButton.getUserData()).getTitle(), + getNext(((IconButtonType) iconButton.getUserData())) + ))); + iconButton.getStyleClass().add("hidden-icon-button"); + iconButton.layout(); + } + } + + public String getProposalTypeAsString() { + return Res.get("dao.proposal.type." + proposal.getType().name()); + } + + private String getNext(IconButtonType iconButtonType) { + switch (iconButtonType) { + case ACCEPT: + return IconButtonType.REJECT.getTitle(); + case REJECT: + return IconButtonType.IGNORE.getTitle(); + default: + return IconButtonType.ACCEPT.getTitle(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml new file mode 100644 index 0000000000..c056ac3b60 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java new file mode 100644 index 0000000000..d8763a94c6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -0,0 +1,893 @@ +/* + * 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 bisq.desktop.main.dao.governance.proposals; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.TxIdTextField; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.SelectProposalWindow; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; +import bisq.core.dao.governance.myvote.MyVote; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.EvaluatedProposal; +import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Vote; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; +import bisq.common.util.Tuple4; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.jfoenix.controls.JFXButton; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.*; +import static bisq.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; + +@FxmlView +public class ProposalsView extends ActivatableView implements BsqBalanceListener, DaoStateListener { + private final DaoFacade daoFacade; + private final BsqWalletService bsqWalletService; + private final PhasesView phasesView; + private final DaoStateService daoStateService; + private final MyBlindVoteListService myBlindVoteListService; + private final Preferences preferences; + private final BsqFormatter bsqFormatter; + private final CoinFormatter btcFormatter; + private final SelectProposalWindow selectProposalWindow; + + private final ObservableList listItems = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(listItems); + private final List